001/** 002 * Copyright 2010 The Kuali Foundation Licensed under the 003 * Educational Community License, Version 2.0 (the "License"); you may 004 * not use this file except in compliance with the License. You may 005 * obtain a copy of the License at 006 * 007 * http://www.osedu.org/licenses/ECL-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, 010 * software distributed under the License is distributed on an "AS IS" 011 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 012 * or implied. See the License for the specific language governing 013 * permissions and limitations under the License. 014 */ 015 016package org.kuali.student.common.ui.client.mvc; 017 018import com.google.gwt.core.client.GWT; 019import com.google.gwt.event.shared.GwtEvent.Type; 020import com.google.gwt.event.shared.HandlerManager; 021import com.google.gwt.event.shared.HandlerRegistration; 022import com.google.gwt.user.client.Window; 023import com.google.gwt.user.client.ui.Composite; 024import com.google.gwt.user.client.ui.Widget; 025import org.kuali.student.common.ui.client.application.KSAsyncCallback; 026import org.kuali.student.common.ui.client.application.ViewContext; 027import org.kuali.student.common.ui.client.configurable.mvc.views.SectionView; 028import org.kuali.student.common.ui.client.mvc.breadcrumb.BreadcrumbSupport; 029import org.kuali.student.common.ui.client.mvc.history.HistoryManager; 030import org.kuali.student.common.ui.client.mvc.history.HistorySupport; 031import org.kuali.student.common.ui.client.mvc.history.NavigationEvent; 032import org.kuali.student.common.ui.client.reporting.ReportExport; 033import org.kuali.student.common.ui.client.security.AuthorizationCallback; 034import org.kuali.student.common.ui.client.security.RequiresAuthorization; 035import org.kuali.student.common.ui.client.service.GwtExportRpcService; 036import org.kuali.student.common.ui.client.service.GwtExportRpcServiceAsync; 037import org.kuali.student.common.ui.client.util.ExportElement; 038import org.kuali.student.common.ui.client.util.ExportUtils; 039import org.kuali.student.common.ui.client.widgets.progress.BlockingTask; 040import org.kuali.student.common.ui.client.widgets.progress.KSBlockingProgressIndicator; 041import org.kuali.student.r1.common.assembly.data.Data; 042 043import java.util.*; 044 045/** 046 * Abstract Controller composite. Provides basic controller operations, and defines abstract methods that a composite must 047 * implement in order to be a controller. 048 * 049 * @author Kuali Student Team 050 */ 051public abstract class Controller extends Composite implements HistorySupport, BreadcrumbSupport, ReportExport{ 052 public static final Callback<Boolean> NO_OP_CALLBACK = new Callback<Boolean>() { 053 @Override 054 public void exec(Boolean result) { 055 // do nothing 056 } 057 }; 058 059 protected Controller parentController = null; 060 private View currentView = null; 061 private Enum<?> currentViewEnum = null; 062 private String defaultModelId = null; 063 protected ViewContext context = new ViewContext(); 064 private final Map<String, ModelProvider<? extends Model>> models = new HashMap<String, ModelProvider<? extends Model>>(); 065 private boolean fireNavEvents = true; 066 private HandlerManager applicationEventHandlers = new HandlerManager(this); 067 private GwtExportRpcServiceAsync reportExportRpcService = GWT.create(GwtExportRpcService.class); 068 069 protected Controller() { 070 } 071 072 /** 073 * Simple Version of showView, no callback 074 * @param <V> 075 * view enum type 076 * @param viewType 077 * enum value representing the view to show 078 */ 079 public <V extends Enum<?>> void showView(final V viewType){ 080 this.showView(viewType, NO_OP_CALLBACK); 081 } 082 083 084 085 /** 086 * Directs the controller to display the specified view. The parameter must be an enum value, based on an enum defined in 087 * the controller implementation. For example, a "Search" controller might have an enumeration of: <code> 088 * public enum SearchViews { 089 * SIMPLE_SEARCH, 090 * ADVANCED_SEARCH, 091 * SEARCH_RESULTS 092 * } 093 * </code> The implementing class must define a getView(V viewType) method that will cast the generic enum to the view 094 * specific enum. 095 * 096 * @param <V> 097 * view enum type 098 * @param viewType 099 * enum value representing the view to show 100 * @param onReadyCallback the callback to invoke when the method has completed execution 101 * @return false if the current view cancels the operation 102 */ 103 public <V extends Enum<?>> void showView(final V viewType, final Callback<Boolean> onReadyCallback) { 104 GWT.log("showView " + viewType.toString(), null); 105 getView(viewType, new Callback<View>(){ 106 107 @Override 108 public void exec(View result) { 109 View view = result; 110 if (view == null) { 111 onReadyCallback.exec(false); 112 //throw new ControllerException("View not registered: " + viewType.toString()); 113 } 114 beginShowView(view, viewType, onReadyCallback); 115 116 }},null); 117 } 118 119 private <V extends Enum<?>> void beginShowView(final View view, final V viewType, final Callback<Boolean> onReadyCallback){ 120 beforeViewChange(viewType, new Callback<Boolean>(){ 121 122 @Override 123 public void exec(Boolean result) { 124 if(result){ 125 boolean requiresAuthz = (view instanceof RequiresAuthorization) && ((RequiresAuthorization)view).isAuthorizationRequired(); 126 127 if (requiresAuthz){ 128// GWT.log("Checking permission type '" + getViewContext().getPermissionType().getPermissionTemplateName() + "' for viewType '" + viewType.toString() + "'", null); 129 130 //A callback is required if async rpc call is required for authz check 131 ((RequiresAuthorization)view).checkAuthorization(new AuthorizationCallback(){ 132 public void isAuthorized() { 133 finalizeShowView(view, viewType, onReadyCallback); 134 } 135 136 public void isNotAuthorized(String msg) { 137 Window.alert(msg); 138 onReadyCallback.exec(false); 139 } 140 }); 141 } else { 142 GWT.log("Not Requiring Auth.", null); 143 finalizeShowView(view, viewType, onReadyCallback); 144 } 145 } 146 else{ 147 onReadyCallback.exec(false); 148 } 149 150 } 151 }); 152 } 153 154 private <V extends Enum<?>> void finalizeShowView(final View view, final V viewType, final Callback<Boolean> onReadyCallback){ 155 if (((currentView == null) || currentView.beforeHide()) && view != null) { 156 view.beforeShow(new Callback<Boolean>() { 157 @Override 158 public void exec(Boolean result) { 159 if (!result) { 160 GWT.log("showView: beforeShow yielded false " + viewType, null); 161 onReadyCallback.exec(false); 162 } else { 163 if (currentView != null) { 164 hideView(currentView); 165 } 166 167 currentViewEnum = viewType; 168 currentView = view; 169 GWT.log("renderView " + viewType.toString(), null); 170 if(fireNavEvents){ 171 fireNavigationEvent(); 172 } 173 renderView(view); 174 onReadyCallback.exec(true); 175 176 } 177 } 178 }); 179 } else { 180 onReadyCallback.exec(false); 181 GWT.log("Current view canceled hide action", null); 182 } 183 } 184 185 protected void fireNavigationEvent() { 186 //DeferredCommand.addCommand(new Command() { 187 // @Override 188 //public void execute() { 189 fireApplicationEvent(new NavigationEvent(Controller.this)); 190 //} 191 //}); 192 } 193 194 /** 195 * Returns the currently displayed view 196 * 197 * @return the currently displayed view 198 */ 199 public View getCurrentView() { 200 return currentView; 201 } 202 203 public Enum<?> getCurrentViewEnum() { 204 return currentViewEnum; 205 } 206 207 public void setCurrentViewEnum(Enum<?> currentViewEnum) { 208 this.currentViewEnum = currentViewEnum; 209 } 210 211 /** 212 * Sets the controller's parent controller. In most cases, this can be omitted as the controller will be automatically 213 * detected via the DOM in cases where it is not specified. The only time that the controller needs to be manually set is 214 * in cases where the logical controller hierarchy differs from the physical DOM hierarchy. For example, if a nested 215 * controller is rendered in a PopupPanel, then the parent controller must be set manually using this method 216 * 217 * @param controller 218 * the parent controller 219 */ 220 public void setParentController(Controller controller) { 221 parentController = controller; 222 } 223 224 /** 225 * Returns the parent controller. If the current parent controller is not set, then the controller will attempt to 226 * automatically locate the parent controller via the DOM. 227 * 228 * @return 229 */ 230 public Controller getParentController() { 231 if (parentController == null) { 232 parentController = Controller.findController(this); 233 } 234 return parentController; 235 } 236 237 /** 238 * Attempts to find the parent controller of a given widget via the DOM 239 * 240 * @param w 241 * the widget for which to find the parent controller 242 * @return the controller, or null if not found 243 */ 244 public static Controller findController(Widget w) { 245 Controller result = null; 246 while (true) { 247 w = w.getParent(); 248 if (w == null) { 249 break; 250 } else if (w instanceof Controller) { 251 result = (Controller) w; 252 break; 253 } else if (w instanceof View) { 254 // this is in the event that a parent/child relationship is broken by a view being rendered in a lightbox, 255 // etc 256 result = ((View) w).getController(); 257 break; 258 } 259 } 260 return result; 261 } 262 263 /** 264 * Called by child views and controllers to request a model reference. By default it delegates calls to the parent 265 * controller if one is found. Override this method to declare a model local to the controller. Always make sure to 266 * delegate the call to the superclass if the requested type is not one which is defined locally. For example: <code> 267 * 268 * @Override 269 * @SuppressWarnings("unchecked") public void requestModel(Class<? extends Idable> modelType, ModelRequestCallback 270 * callback) { if (modelType.equals(Address.class)) { callback.onModelReady(addresses); } 271 * else { super.requestModel(modelType, callback); } } </code> 272 * @param modelType 273 * @param callback 274 */ 275 @SuppressWarnings("unchecked") 276 public void requestModel(final Class modelType, final ModelRequestCallback callback) { 277 requestModel((modelType == null) ? null : modelType.getName(), callback); 278 } 279 280 @SuppressWarnings("unchecked") 281 public void requestModel(final String modelId, final ModelRequestCallback callback) { 282 String id = (modelId == null) ? defaultModelId : modelId; 283 284 ModelProvider<? extends Model> p = models.get(id); 285 if (p != null) { 286 p.requestModel(callback); 287 } else if (getParentController() != null) { 288 parentController.requestModel(modelId, callback); 289 } else { 290 if (callback != null) { 291 callback.onRequestFail(new RuntimeException("The requested model was not found: " + modelId)); 292 } 293 } 294 } 295 296 @SuppressWarnings("rawtypes") 297 public void requestModel(final ModelRequestCallback callback) { 298 requestModel((String)null, callback); 299 } 300 301 public <T extends Model> void registerModel(String modelId, ModelProvider<T> provider) { 302 models.put(modelId, provider); 303 } 304 305 public String getDefaultModelId() { 306 return defaultModelId; 307 } 308 public void setDefaultModelId(String defaultModelId) { 309 this.defaultModelId = defaultModelId; 310 } 311 312 /** 313 * Registers an application eventhandler. The controller will try to propagate "unchecked" handlers to the parent 314 * controller if a parent controller exists. This method can be overridden to handle unchecked locally if they are fired 315 * locally. 316 * 317 * @param type 318 * @param handler 319 * @return 320 */ 321 @SuppressWarnings("unchecked") 322 public HandlerRegistration addApplicationEventHandler(Type type, ApplicationEventHandler handler) { 323 if ((handler instanceof UncheckedApplicationEventHandler) && (getParentController() != null)) { 324 return parentController.addApplicationEventHandler(type, handler); 325 } 326 return applicationEventHandlers.addHandler(type, handler); 327 } 328 329 /** 330 * Fires an application event. 331 * 332 * @param event 333 */ 334 @SuppressWarnings("unchecked") 335 public void fireApplicationEvent(ApplicationEvent event) { 336 // TODO this logic needs to be reworked a bit... if an unchecked event has been bound locally, do we want to still 337 // fire it externally as well? 338 if ((event instanceof UncheckedApplicationEvent) && (getParentController() != null)) { 339 parentController.fireApplicationEvent(event); 340 } 341 // dispatch to local "checked" handlers, and to any unchecked handlers that have been bound to local 342 applicationEventHandlers.fireEvent(event); 343 344 } 345 346 /** 347 * Must be implemented by the subclass to render the view. 348 * 349 * @param view 350 */ 351 protected abstract void renderView(View view); 352 353 /** 354 * Must be implemented by the subclass to hide the view. 355 * 356 * @param view 357 */ 358 protected abstract void hideView(View view); 359 360 /** 361 * Returns the view associated with the specified enum value. See showView(V viewType) above for a full description 362 * defaults to the abstract get view method unless overridden 363 * @param <V> 364 * @param viewType 365 * @param callback 366 * @param tokenMap optionally passed in token map if you need tokens from the history manager 367 */ 368 protected <V extends Enum<?>> void getView(V viewType, Callback<View> callback, Map<String, String> tokenMap){ 369 getView(viewType, callback); 370 } 371 372 /** 373 * Returns the view associated with the specified enum value. See showView(V viewType) above for a full description 374 * 375 * @param <V> 376 * @param viewType 377 * @return 378 */ 379 protected abstract <V extends Enum<?>> void getView(V viewType, Callback<View> callback); 380 381 /** 382 * If a controller which extends this class must perform some action or check before a view 383 * is changed, then override this method. Do not call super() in the override, as it will 384 * allow the view to continue to change. 385 * @param okToChangeCallback 386 */ 387 public void beforeViewChange(Enum<?> viewChangingTo, Callback<Boolean> okToChangeCallback) { 388 okToChangeCallback.exec(true); 389 } 390 391 /** 392 * Shows the default view. Must be implemented by subclass, in order to define the default view. 393 */ 394 public abstract void showDefaultView(Callback<Boolean> onReadyCallback); 395 396 public abstract Enum<?> getViewEnumValue(String enumValue); 397 398 /** 399 * This particular implementation appends to the history stack the name of the current view shown by 400 * this controller and view context (in string format) to that historyStack and passes the stack to 401 * be processed to the currentView. 402 * @see org.kuali.student.common.ui.client.mvc.history.HistorySupport#collectHistory(java.lang.String) 403 */ 404 @Override 405 public String collectHistory(String historyStack) { 406 String token = getHistoryToken(); 407 historyStack = historyStack + "/" + token; 408 409 if(currentView != null){ 410 String tempHistoryStack = historyStack; 411 historyStack = currentView.collectHistory(historyStack); 412 413 //Sanity check, if collectHistory returns null or empty string, restore 414 if(historyStack == null){ 415 historyStack = tempHistoryStack; 416 } 417 else if(historyStack != null && historyStack.isEmpty()){ 418 historyStack = tempHistoryStack; 419 } 420 } 421 return historyStack; 422 } 423 424 protected String getHistoryToken() { 425 String historyToken = ""; 426 if (currentViewEnum != null) { 427 historyToken = currentViewEnum.toString(); 428 if(currentView != null && currentView instanceof Controller 429 && ((Controller)currentView).getViewContext() != null){ 430 ViewContext context = ((Controller) currentView).getViewContext(); 431 historyToken = HistoryManager.appendContext(historyToken, context); 432 } 433 434 } 435 return historyToken; 436 } 437 438 /** 439 * The onHistoryEvent implementation in controller reads the history stack it receives and determines 440 * if the next token/view to be processed is a controller, if it is, it hands off the rest of the history stack 441 * to that controller after showing it. Otherwise, it shows the view 442 * and allows that view to perform any onHistoryEvent actions it may need to take. 443 * <br><br>For example the historyStack /HOME/CURRICULUM_HOME/COURSE_PROPOSAL would start at the root controller, 444 * and hand it off to the home controller, then the curriculum home controller, then the course proposal controller 445 * and stop there. Along the way each of those controller would show themselves visually in the UI, 446 * if they contain any layout (some do not). 447 * 448 * @see org.kuali.student.common.ui.client.mvc.history.HistorySupport#onHistoryEvent(java.lang.String) 449 */ 450 @Override 451 public void onHistoryEvent(String historyStack) { 452 final String nextHistoryStack = HistoryManager.nextHistoryStack(historyStack); 453 String[] tokens = HistoryManager.splitHistoryStack(nextHistoryStack); 454 if (tokens.length >= 1 && tokens[0] != null && !tokens[0].isEmpty()) { 455 final Map<String, String> tokenMap = HistoryManager.getTokenMap(tokens[0]); 456 //TODO add some automatic view context setting here, get and set 457 String viewEnumString = tokenMap.get("view"); 458 if (viewEnumString != null) { 459 final Enum<?> viewEnum = getViewEnumValue(viewEnumString); 460 461 if (viewEnum != null) { 462 getView(viewEnum, new Callback<View>(){ 463 464 @Override 465 public void exec(View result) { 466 View theView = result; 467 boolean sameContext = true; 468 if(theView instanceof Controller){ 469 470 ViewContext newContext = new ViewContext(); 471 Iterator<String> tokenIt = tokenMap.keySet().iterator(); 472 while(tokenIt.hasNext()){ 473 String key = tokenIt.next(); 474 if(key.equals(ViewContext.ID_ATR)){ 475 newContext.setId(tokenMap.get(ViewContext.ID_ATR)); 476 } 477 else if(key.equals(ViewContext.ID_TYPE_ATR)){ 478 newContext.setIdType(tokenMap.get(ViewContext.ID_TYPE_ATR)); 479 } 480 //do not add view attribute from the token map to the context 481 else if(!key.equals("view")){ 482 newContext.setAttribute(key, tokenMap.get(key)); 483 } 484 } 485 486 ViewContext viewContext = ((Controller) theView).getViewContext(); 487 if(viewContext.compareTo(newContext) != 0){ 488 ((Controller) theView).setViewContext(newContext); 489 sameContext = false; 490 } 491 } 492 if (currentViewEnum == null || !viewEnum.equals(currentViewEnum) 493 || !sameContext) { 494 beginShowView(theView, viewEnum, new Callback<Boolean>() { 495 @Override 496 public void exec(Boolean result) { 497 if (result) { 498 currentView.onHistoryEvent(nextHistoryStack); 499 } 500 } 501 }); 502 } else if (currentView != null) { 503 currentView.onHistoryEvent(nextHistoryStack); 504 } 505 } 506 },tokenMap); 507 508 } 509 } 510 } 511 else{ 512 this.showDefaultView(new Callback<Boolean>(){ 513 514 @Override 515 public void exec(Boolean result) { 516 if(result){ 517 currentView.onHistoryEvent(nextHistoryStack); 518 } 519 520 } 521 }); 522 } 523 524 } 525 526 /** 527 * Sets the view context. This is important for determining the permission for seeing views under 528 * this controllers scope, what the id and id type of the model the controller handles are defined here. 529 * Additional attributes that the controller and it's views need to know about are also defined in the 530 * viewContext. 531 * @param viewContext 532 */ 533 public void setViewContext(ViewContext viewContext){ 534 this.context = viewContext; 535 } 536 537 public ViewContext getViewContext() { 538 return this.context; 539 } 540 541 public void resetCurrentView(){ 542 currentView = null; 543 } 544 545 /** 546 * 547 * This method implement the "Generic Export" of a windows content to Jasper based on the format the user selected. 548 * This method can be overwritten on a subclass to do specific export to the specific view 549 * 550 * @see org.kuali.student.common.ui.client.reporting.ReportExport#doReportExport(java.util.List, String format, String reportTitle) 551 */ 552 @Override 553 public void doReportExport(List<ExportElement> exportElements, final String format, final String reportTitle) { 554 // Service call... 555 final BlockingTask loadDataTask = new BlockingTask("Generating Export File"); 556 557 DataModel dataModel = getExportDataModel(); 558 Data modelDataObject = null; 559 if (dataModel != null) { 560 modelDataObject = dataModel.getRoot(); 561 } 562 563 564 // we want to show that something is happening while the files are generated. 565 KSBlockingProgressIndicator.addTask(loadDataTask); 566 567 reportExportRpcService.reportExport(exportElements, modelDataObject, getExportTemplateName(), format, reportTitle, new KSAsyncCallback<String>() { 568 @Override 569 public void onSuccess(String result) { 570 // On success get documentID back from GWT Servlet// 571 572 // We need to get the base url and strip the gwt module name . 573 String baseUrl = GWT.getHostPageBaseURL(); 574 baseUrl = baseUrl.replaceFirst(GWT.getModuleName() + "/", ""); 575 576 KSBlockingProgressIndicator.removeTask(loadDataTask); 577 578 Window.open(baseUrl + "exportDownloadHTTPServlet?exportId="+result + "&format=" + format, "", ""); 579 } 580 581 @Override 582 public void handleFailure(Throwable caught) { 583 KSBlockingProgressIndicator.removeTask(loadDataTask); 584 super.handleFailure(caught); 585 } 586 587 }); 588 589 590 591 } 592 593 // TODO Nina ??? Do we want to keep this seen in the light of the exportElements parameter 594 @Override 595 public DataModel getExportDataModel() { 596 return null; 597 } 598 599 /** 600 * 601 * @see org.kuali.student.common.ui.client.reporting.ReportExport#getExportTemplateName() 602 */ 603 @Override 604 public String getExportTemplateName() { 605 return exportTemplateName; 606 } 607 608 @Override 609 public List<ExportElement> getExportElementsFromView() { 610 String viewName = null; 611 View currentView = this.getCurrentView(); 612 if (currentView != null) { 613 614 ArrayList<ExportElement> exportElements = null; 615 616 if (currentView != null && currentView instanceof SectionView) { 617 viewName = currentView.getName(); 618 exportElements = ExportUtils.getExportElementsFromView((SectionView)currentView, exportElements, viewName, "Sectionname"); 619 return exportElements; 620 } 621 } 622 return null; 623 } 624}