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