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}