View Javadoc

1   /**
2    * Copyright 2010 The Kuali Foundation Licensed under the
3    * Educational Community License, Version 2.0 (the "License"); you may
4    * not use this file except in compliance with the License. You may
5    * obtain a copy of the License at
6    *
7    * http://www.osedu.org/licenses/ECL-2.0
8    *
9    * Unless required by applicable law or agreed to in writing,
10   * software distributed under the License is distributed on an "AS IS"
11   * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12   * or implied. See the License for the specific language governing
13   * permissions and limitations under the License.
14   */
15  
16  package org.kuali.student.common.ui.client.mvc;
17  
18  import java.util.HashMap;
19  import java.util.Iterator;
20  import java.util.Map;
21  
22  import org.kuali.student.common.rice.authorization.PermissionType;
23  import org.kuali.student.common.ui.client.application.ViewContext;
24  import org.kuali.student.common.ui.client.configurable.mvc.LayoutController;
25  import org.kuali.student.common.ui.client.mvc.breadcrumb.BreadcrumbSupport;
26  import org.kuali.student.common.ui.client.mvc.history.HistoryManager;
27  import org.kuali.student.common.ui.client.mvc.history.HistorySupport;
28  import org.kuali.student.common.ui.client.mvc.history.NavigationEvent;
29  import org.kuali.student.common.ui.client.security.AuthorizationCallback;
30  import org.kuali.student.common.ui.client.security.RequiresAuthorization;
31  
32  import com.google.gwt.core.client.GWT;
33  import com.google.gwt.event.shared.GwtEvent.Type;
34  import com.google.gwt.event.shared.HandlerManager;
35  import com.google.gwt.event.shared.HandlerRegistration;
36  import com.google.gwt.user.client.Window;
37  import com.google.gwt.user.client.ui.Composite;
38  import com.google.gwt.user.client.ui.Widget;
39  
40  /**
41   * Abstract Controller composite. Provides basic controller operations, and defines abstract methods that a composite must
42   * implement in order to be a controller.
43   * 
44   * @author Kuali Student Team
45   */
46  public abstract class Controller extends Composite implements HistorySupport, BreadcrumbSupport{
47  	public static final Callback<Boolean> NO_OP_CALLBACK = new Callback<Boolean>() {
48  		@Override
49  		public void exec(Boolean result) {
50  			// do nothing
51  		}
52  	};
53  	
54      protected Controller parentController = null;
55      private View currentView = null;
56      private Enum<?> currentViewEnum = null;
57      private String defaultModelId = null;
58      protected ViewContext context = new ViewContext();
59      private final Map<String, ModelProvider<? extends Model>> models = new HashMap<String, ModelProvider<? extends Model>>();
60      private boolean fireNavEvents = true;
61      private HandlerManager applicationEventHandlers = new HandlerManager(this);
62      
63      protected Controller() {
64      }
65      
66      /**
67       * Simple Version of showView, no callback
68       * @param <V>
69       * 			view enum type
70       * @param viewType
71       * 			enum value representing the view to show
72       */
73      public <V extends Enum<?>> void showView(final V viewType){
74      	this.showView(viewType, NO_OP_CALLBACK);
75      }
76      
77  
78      
79      /**
80       * Directs the controller to display the specified view. The parameter must be an enum value, based on an enum defined in
81       * the controller implementation. For example, a "Search" controller might have an enumeration of: <code>
82       *  public enum SearchViews {
83       *      SIMPLE_SEARCH,
84       *      ADVANCED_SEARCH,
85       *      SEARCH_RESULTS
86       *  }
87       * </code> The implementing class must define a getView(V viewType) method that will cast the generic enum to the view
88       * specific enum.
89       * 
90       * @param <V>
91       *            view enum type
92       * @param viewType
93       *            enum value representing the view to show
94       * @param onReadyCallback the callback to invoke when the method has completed execution
95       * @return false if the current view cancels the operation
96       */
97      public <V extends Enum<?>> void showView(final V viewType, final Callback<Boolean> onReadyCallback) {
98          GWT.log("showView " + viewType.toString(), null);
99          getView(viewType, new Callback<View>(){
100 
101 			@Override
102 			public void exec(View result) {
103 				View view = result;
104 				if (view == null) {
105 		        	onReadyCallback.exec(false);
106 		            //throw new ControllerException("View not registered: " + viewType.toString());
107 		        }
108 		        beginShowView(view, viewType, onReadyCallback);
109 				
110 			}});
111     }
112     
113     private <V extends Enum<?>> void beginShowView(final View view, final V viewType, final Callback<Boolean> onReadyCallback){
114     	beforeViewChange(viewType, new Callback<Boolean>(){
115 
116 			@Override
117 			public void exec(Boolean result) {
118 				if(result){
119 					 boolean requiresAuthz = (view instanceof RequiresAuthorization) && ((RequiresAuthorization)view).isAuthorizationRequired(); 
120 						
121 				        if (requiresAuthz){
122 				        	ViewContext tempContext = new ViewContext();
123 				        	if(view instanceof LayoutController){
124 				        		tempContext = ((LayoutController) view).getViewContext();
125 				        	}                 
126 				        	else{
127 				        		tempContext = view.getController().getViewContext();
128 				        	}
129 				        	
130 				        	PermissionType permType = (tempContext != null) ? tempContext.getPermissionType() : null;
131 				        	if (permType != null) {
132 				        		GWT.log("Checking permission type '" + permType.getPermissionTemplateName() + "' for view '" + view.toString() + "'", null);
133 				            	//A callback is required if async rpc call is required for authz check
134 					        	((RequiresAuthorization)view).checkAuthorization(permType, new AuthorizationCallback(){
135 									public void isAuthorized() {
136 										finalizeShowView(view, viewType, onReadyCallback);
137 									}
138 				
139 									public void isNotAuthorized(String msg) {
140 										Window.alert(msg);
141 										onReadyCallback.exec(false);					
142 									}        		
143 					        	});
144 				        	}
145 				        	else {
146 				        		GWT.log("Cannot find PermissionType for view '" + view.toString() + "' which requires authorization", null);
147 				            	finalizeShowView(view, viewType, onReadyCallback);
148 				        	}
149 				        } else {
150 				    		GWT.log("Not Requiring Auth.", null);
151 				        	finalizeShowView(view, viewType, onReadyCallback);
152 				        }
153 				}
154 				else{
155 					onReadyCallback.exec(false);
156 				}
157 				
158 			}
159 		});
160     }
161     
162     private <V extends Enum<?>> void finalizeShowView(final View view, final V viewType, final Callback<Boolean> onReadyCallback){
163         if ((currentView == null) || currentView.beforeHide()) {
164 			view.beforeShow(new Callback<Boolean>() {
165 				@Override
166 				public void exec(Boolean result) {
167 					if (!result) {
168 						GWT.log("showView: beforeShow yielded false " + viewType, null);
169 			        	onReadyCallback.exec(false);
170 					} else {
171 			        	if (currentView != null) {
172 			                hideView(currentView);
173 			            }
174 			            
175 			            currentViewEnum = viewType;
176 			            currentView = view;
177 			            GWT.log("renderView " + viewType.toString(), null);
178 			            if(fireNavEvents){
179 			            	fireNavigationEvent();
180 			            }
181 			            renderView(view);
182 			        	onReadyCallback.exec(true);
183 			        	
184 					}
185 				}
186 			});
187         } else {
188         	onReadyCallback.exec(false);
189             GWT.log("Current view canceled hide action", null);
190         }    	
191     }
192 
193     protected void fireNavigationEvent() {
194         //DeferredCommand.addCommand(new Command() {
195            // @Override
196             //public void execute() {
197                 fireApplicationEvent(new NavigationEvent(Controller.this));
198             //}
199         //});
200     }
201     
202     /**
203      * Returns the currently displayed view
204      * 
205      * @return the currently displayed view
206      */
207     public View getCurrentView() {
208         return currentView;
209     }
210     
211     public Enum<?> getCurrentViewEnum() {
212         return currentViewEnum;
213     }
214 
215     public void setCurrentViewEnum(Enum<?> currentViewEnum) {
216         this.currentViewEnum = currentViewEnum;
217     }
218 
219     /**
220      * Sets the controller's parent controller. In most cases, this can be omitted as the controller will be automatically
221      * detected via the DOM in cases where it is not specified. The only time that the controller needs to be manually set is
222      * in cases where the logical controller hierarchy differs from the physical DOM hierarchy. For example, if a nested
223      * controller is rendered in a PopupPanel, then the parent controller must be set manually using this method
224      * 
225      * @param controller
226      *            the parent controller
227      */
228     public void setParentController(Controller controller) {
229         parentController = controller;
230     }
231 
232     /**
233      * Returns the parent controller. If the current parent controller is not set, then the controller will attempt to
234      * automatically locate the parent controller via the DOM.
235      * 
236      * @return
237      */
238     public Controller getParentController() {
239         if (parentController == null) {
240             parentController = Controller.findController(this);
241         }
242         return parentController;
243     }
244 
245     /**
246      * Attempts to find the parent controller of a given widget via the DOM
247      * 
248      * @param w
249      *            the widget for which to find the parent controller
250      * @return the controller, or null if not found
251      */
252     public static Controller findController(Widget w) {
253         Controller result = null;
254         while (true) {
255             w = w.getParent();
256             if (w == null) {
257                 break;
258             } else if (w instanceof Controller) {
259                 result = (Controller) w;
260                 break;
261             } else if (w instanceof View) {
262                 // this is in the event that a parent/child relationship is broken by a view being rendered in a lightbox,
263                 // etc
264                 result = ((View) w).getController();
265                 break;
266             }
267         }
268         return result;
269     }
270 
271     /**
272      * Called by child views and controllers to request a model reference. By default it delegates calls to the parent
273      * controller if one is found. Override this method to declare a model local to the controller. Always make sure to
274      * delegate the call to the superclass if the requested type is not one which is defined locally. For example: <code>
275      * 
276      * @Override
277      * @SuppressWarnings("unchecked") public void requestModel(Class<? extends Idable> modelType, ModelRequestCallback
278      *                                callback) { if (modelType.equals(Address.class)) { callback.onModelReady(addresses); }
279      *                                else { super.requestModel(modelType, callback); } } </code>
280      * @param modelType
281      * @param callback
282      */
283     @SuppressWarnings("unchecked")
284     public void requestModel(final Class modelType, final ModelRequestCallback callback) {
285         requestModel((modelType == null) ? null : modelType.getName(), callback);
286     }
287     
288     @SuppressWarnings("unchecked")
289     public void requestModel(final String modelId, final ModelRequestCallback callback) {
290         String id = (modelId == null) ? defaultModelId : modelId;
291 
292         ModelProvider<? extends Model> p = models.get(id);
293         if (p != null) {
294             p.requestModel(callback);
295         } else if (getParentController() != null) {
296             parentController.requestModel(modelId, callback);
297         } else {
298             if (callback != null) {
299                 callback.onRequestFail(new RuntimeException("The requested model was not found: " + modelId));
300             }
301         }
302     }
303 
304     @SuppressWarnings("unchecked")
305     public void requestModel(final ModelRequestCallback callback) {
306         requestModel((String)null, callback);
307     }
308 
309     public <T extends Model> void registerModel(String modelId, ModelProvider<T> provider) {
310         models.put(modelId, provider);
311     }
312     
313     public String getDefaultModelId() {
314         return defaultModelId;
315     }
316     public void setDefaultModelId(String defaultModelId) {
317         this.defaultModelId = defaultModelId;
318     }
319     
320     /**
321      * Registers an application eventhandler. The controller will try to propagate "unchecked" handlers to the parent
322      * controller if a parent controller exists. This method can be overridden to handle unchecked locally if they are fired
323      * locally.
324      * 
325      * @param type
326      * @param handler
327      * @return
328      */
329     @SuppressWarnings("unchecked")
330     public HandlerRegistration addApplicationEventHandler(Type type, ApplicationEventHandler handler) {
331         if ((handler instanceof UncheckedApplicationEventHandler) && (getParentController() != null)) {
332             return parentController.addApplicationEventHandler(type, handler);
333         }
334         return applicationEventHandlers.addHandler(type, handler);
335     }
336 
337     /**
338      * Fires an application event.
339      * 
340      * @param event
341      */
342     @SuppressWarnings("unchecked")
343     public void fireApplicationEvent(ApplicationEvent event) {
344         // TODO this logic needs to be reworked a bit... if an unchecked event has been bound locally, do we want to still
345         // fire it externally as well?
346         if ((event instanceof UncheckedApplicationEvent) && (getParentController() != null)) {
347             parentController.fireApplicationEvent(event);
348         }
349         // dispatch to local "checked" handlers, and to any unchecked handlers that have been bound to local
350         applicationEventHandlers.fireEvent(event);
351 
352     }
353 
354     /**
355      * Must be implemented by the subclass to render the view.
356      * 
357      * @param view
358      */
359     protected abstract void renderView(View view);
360 
361     /**
362      * Must be implemented by the subclass to hide the view.
363      * 
364      * @param view
365      */
366     protected abstract void hideView(View view);
367 
368     /**
369      * Returns the view associated with the specified enum value. See showView(V viewType) above for a full description
370      * 
371      * @param <V>
372      * @param viewType
373      * @return
374      */
375     protected abstract <V extends Enum<?>> void getView(V viewType, Callback<View> callback);
376     
377     /**
378      * If a controller which extends this class must perform some action or check before a view
379      * is changed, then override this method.  Do not call super() in the override, as it will
380      * allow the view to continue to change.
381      * @param okToChangeCallback
382      */
383     public void beforeViewChange(Enum<?> viewChangingTo, Callback<Boolean> okToChangeCallback) {
384     	okToChangeCallback.exec(true);
385     }
386 
387     /**
388      * Shows the default view. Must be implemented by subclass, in order to define the default view.
389      */
390     public abstract void showDefaultView(Callback<Boolean> onReadyCallback);
391     
392     public abstract Enum<?> getViewEnumValue(String enumValue);
393     
394     /**
395      * This particular implementation appends to the history stack the name of the current view shown by
396      * this controller and view context (in string format) to that historyStack and passes the stack to
397      * be processed to the currentView.
398      * @see org.kuali.student.common.ui.client.mvc.history.HistorySupport#collectHistory(java.lang.String)
399      */
400     @Override
401     public String collectHistory(String historyStack) {
402     	String token = getHistoryToken();
403     	historyStack = historyStack + "/" + token;
404     	
405     	if(currentView != null){
406     		String tempHistoryStack = historyStack;
407     		historyStack = currentView.collectHistory(historyStack);
408     		
409     		//Sanity check, if collectHistory returns null or empty string, restore
410     		if(historyStack == null){
411     			historyStack = tempHistoryStack;
412     		}
413     		else if(historyStack != null && historyStack.isEmpty()){
414     			historyStack = tempHistoryStack;
415     		}
416     	}
417     	return historyStack;
418     }
419     
420     protected String getHistoryToken() {
421     	String historyToken = "";
422         if (currentViewEnum != null) {
423             historyToken = currentViewEnum.toString();
424             if(currentView != null && currentView instanceof Controller 
425             		&& ((Controller)currentView).getViewContext() != null){
426             	ViewContext context = ((Controller) currentView).getViewContext();
427             	historyToken = HistoryManager.appendContext(historyToken, context);
428             }
429              
430         }
431         return historyToken;
432     }
433 
434     /**
435      * The onHistoryEvent implementation in controller reads the history stack it receives and determines
436      * if the next token/view to be processed is a controller, if it is, it hands off the rest of the history stack
437      * to that controller after showing it.  Otherwise, it shows the view
438      * and allows that view to perform any onHistoryEvent actions it may need to take.
439      * <br><br>For example the historyStack /HOME/CURRICULUM_HOME/COURSE_PROPOSAL would start at the root controller,
440      * and hand it off to the home controller, then the curriculum home controller, then the course proposal controller
441      * and stop there.  Along the way each of those controller would show themselves visually in the UI, 
442      * if they contain any layout (some do not).
443      * 
444      * @see org.kuali.student.common.ui.client.mvc.history.HistorySupport#onHistoryEvent(java.lang.String)
445      */
446     @Override
447     public void onHistoryEvent(String historyStack) {
448     	final String nextHistoryStack = HistoryManager.nextHistoryStack(historyStack);
449         String[] tokens = HistoryManager.splitHistoryStack(nextHistoryStack);
450         if (tokens.length >= 1 && tokens[0] != null && !tokens[0].isEmpty()) {
451             final Map<String, String> tokenMap = HistoryManager.getTokenMap(tokens[0]);
452             //TODO add some automatic view context setting here, get and set
453             String viewEnumString = tokenMap.get("view");
454             if (viewEnumString != null) {
455                 final Enum<?> viewEnum = getViewEnumValue(viewEnumString);
456                 
457                 if (viewEnum != null) {
458                 	getView(viewEnum, new Callback<View>(){
459 
460 						@Override
461 						public void exec(View result) {
462 							View theView = result;
463 			            	boolean sameContext = true;
464 		                	if(theView instanceof Controller){
465 		                		
466 		                		ViewContext newContext = new ViewContext();
467 		                		Iterator<String> tokenIt = tokenMap.keySet().iterator();
468 		                		while(tokenIt.hasNext()){
469 		                			String key = tokenIt.next();
470 		                			if(key.equals(ViewContext.ID_ATR)){
471 		                				newContext.setId(tokenMap.get(ViewContext.ID_ATR));
472 		                			}
473 		                			else if(key.equals(ViewContext.ID_TYPE_ATR)){
474 		                				newContext.setIdType(tokenMap.get(ViewContext.ID_TYPE_ATR));
475 		                			}
476 		                			//do not add view attribute from the token map to the context
477 		                			else if(!key.equals("view")){
478 		                				newContext.setAttribute(key, tokenMap.get(key));
479 		                			}
480 		                		}
481 		                		
482 		                		ViewContext viewContext = ((Controller) theView).getViewContext();
483 		                		if(viewContext.compareTo(newContext) != 0){
484 		                			((Controller) theView).setViewContext(newContext);
485 		                			sameContext = false;
486 		                		}
487 		                	}
488 		                    if (currentViewEnum == null || !viewEnum.equals(currentViewEnum) 
489 		                    		|| !sameContext) {
490 		                        beginShowView(theView, viewEnum, new Callback<Boolean>() {
491 		                            @Override
492 		                            public void exec(Boolean result) {
493 		                                if (result) {
494 		                                    currentView.onHistoryEvent(nextHistoryStack);
495 		                                }
496 		                            }
497 		                        });
498 		                    } else if (currentView != null) {
499 		                    	currentView.onHistoryEvent(nextHistoryStack);
500 		                    }
501 						}
502 					});
503     
504                 }
505             }
506         }
507         else{
508     		this.showDefaultView(new Callback<Boolean>(){
509 
510 				@Override
511 				public void exec(Boolean result) {
512 					if(result){
513 						currentView.onHistoryEvent(nextHistoryStack);
514 					}
515 					
516 				}
517 			});
518     	}
519         
520     }
521 
522     /**
523      * Sets the view context.  This is important for determining the permission for seeing views under
524      * this controllers scope, what the id and id type of the model the controller handles are defined here.
525      * Additional attributes that the controller and it's views need to know about are also defined in the
526      * viewContext.
527      * @param viewContext
528      */
529     public void setViewContext(ViewContext viewContext){
530     	this.context = viewContext;
531     }
532 
533     public ViewContext getViewContext() {
534     	return this.context;
535     }
536     
537     public void resetCurrentView(){
538     	currentView = null;
539     }
540     
541 }