View Javadoc

1   /**
2    * Copyright 2005-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.service.impl;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.core.api.exception.RiceRuntimeException;
20  import org.kuali.rice.kim.api.identity.Person;
21  import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
22  import org.kuali.rice.krad.datadictionary.AttributeDefinition;
23  import org.kuali.rice.krad.inquiry.Inquirable;
24  import org.kuali.rice.krad.service.DataDictionaryService;
25  import org.kuali.rice.krad.service.KRADServiceLocator;
26  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
27  import org.kuali.rice.krad.service.ModuleService;
28  import org.kuali.rice.krad.uif.UifConstants;
29  import org.kuali.rice.krad.uif.component.ComponentSecurity;
30  import org.kuali.rice.krad.uif.container.Group;
31  import org.kuali.rice.krad.uif.field.ActionField;
32  import org.kuali.rice.krad.uif.field.FieldGroup;
33  import org.kuali.rice.krad.uif.view.ViewAuthorizer;
34  import org.kuali.rice.krad.uif.view.ViewPresentationController;
35  import org.kuali.rice.krad.uif.component.BindingInfo;
36  import org.kuali.rice.krad.uif.component.ClientSideState;
37  import org.kuali.rice.krad.uif.component.Component;
38  import org.kuali.rice.krad.uif.component.DataBinding;
39  import org.kuali.rice.krad.uif.component.PropertyReplacer;
40  import org.kuali.rice.krad.uif.component.RequestParameter;
41  import org.kuali.rice.krad.uif.container.CollectionGroup;
42  import org.kuali.rice.krad.uif.container.Container;
43  import org.kuali.rice.krad.uif.control.Control;
44  import org.kuali.rice.krad.uif.field.DataField;
45  import org.kuali.rice.krad.uif.field.InputField;
46  import org.kuali.rice.krad.uif.field.Field;
47  import org.kuali.rice.krad.uif.field.RemoteFieldsHolder;
48  import org.kuali.rice.krad.uif.layout.LayoutManager;
49  import org.kuali.rice.krad.uif.modifier.ComponentModifier;
50  import org.kuali.rice.krad.uif.service.ExpressionEvaluatorService;
51  import org.kuali.rice.krad.uif.service.ViewDictionaryService;
52  import org.kuali.rice.krad.uif.service.ViewHelperService;
53  import org.kuali.rice.krad.uif.util.BooleanMap;
54  import org.kuali.rice.krad.uif.util.CloneUtils;
55  import org.kuali.rice.krad.uif.util.ComponentFactory;
56  import org.kuali.rice.krad.uif.util.ComponentUtils;
57  import org.kuali.rice.krad.uif.util.ExpressionUtils;
58  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
59  import org.kuali.rice.krad.uif.util.ScriptUtils;
60  import org.kuali.rice.krad.uif.util.ViewModelUtils;
61  import org.kuali.rice.krad.uif.view.View;
62  import org.kuali.rice.krad.uif.view.ViewModel;
63  import org.kuali.rice.krad.uif.widget.Inquiry;
64  import org.kuali.rice.krad.uif.widget.Widget;
65  import org.kuali.rice.krad.util.GlobalVariables;
66  import org.kuali.rice.krad.util.KRADConstants;
67  import org.kuali.rice.krad.util.ObjectUtils;
68  import org.kuali.rice.krad.valuefinder.ValueFinder;
69  import org.kuali.rice.krad.web.form.UifFormBase;
70  import org.springframework.util.MethodInvoker;
71  
72  import java.io.Serializable;
73  import java.lang.annotation.Annotation;
74  import java.util.ArrayList;
75  import java.util.Collection;
76  import java.util.Collections;
77  import java.util.HashMap;
78  import java.util.HashSet;
79  import java.util.List;
80  import java.util.Map;
81  import java.util.Map.Entry;
82  import java.util.Set;
83  
84  /**
85   * Default Implementation of <code>ViewHelperService</code>
86   *
87   * @author Kuali Rice Team (rice.collab@kuali.org)
88   */
89  public class ViewHelperServiceImpl implements ViewHelperService, Serializable {
90      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ViewHelperServiceImpl.class);
91  
92      private transient DataDictionaryService dataDictionaryService;
93      private transient ExpressionEvaluatorService expressionEvaluatorService;
94      private transient ViewDictionaryService viewDictionaryService;
95  
96      /**
97       * Uses reflection to find all fields defined on the <code>View</code> instance that have
98       * the <code>RequestParameter</code> annotation (which indicates the field may be populated by the request).
99       *
100      * <p>
101      * For each field found, if there is a corresponding key/value pair in the request parameters,
102      * the value is used to populate the field. In addition, any conditional properties of
103      * <code>PropertyReplacers</code> configured for the field are cleared so that the request parameter
104      * value does not get overridden by the dictionary conditional logic
105      * </p>
106      *
107      * @see org.kuali.rice.krad.uif.service.ViewHelperService#populateViewFromRequestParameters(org.kuali.rice.krad.uif.view.View,
108      *      java.util.Map)
109      */
110     @Override
111     public void populateViewFromRequestParameters(View view, Map<String, String> parameters) {
112         // build Map of property replacers by property name so that we can remove them
113         // if the property was set by a request parameter
114         Map<String, Set<PropertyReplacer>> viewPropertyReplacers = new HashMap<String, Set<PropertyReplacer>>();
115         for (PropertyReplacer replacer : view.getPropertyReplacers()) {
116             Set<PropertyReplacer> propertyReplacers = new HashSet<PropertyReplacer>();
117             if (viewPropertyReplacers.containsKey(replacer.getPropertyName())) {
118                 propertyReplacers = viewPropertyReplacers.get(replacer.getPropertyName());
119             }
120             propertyReplacers.add(replacer);
121 
122             viewPropertyReplacers.put(replacer.getPropertyName(), propertyReplacers);
123         }
124 
125         Map<String, Annotation> annotatedFields = CloneUtils.getFieldsWithAnnotation(view.getClass(),
126                 RequestParameter.class);
127 
128         // for each request parameter allowed on the view, if the request contains a value use
129         // to set on View, and clear and conditional expressions or property replacers for that field
130         Map<String, String> viewRequestParameters = new HashMap<String, String>();
131         for (String fieldToPopulate : annotatedFields.keySet()) {
132             RequestParameter requestParameter = (RequestParameter) annotatedFields.get(fieldToPopulate);
133 
134             // use specified parameter name if given, else use field name to retrieve parameter value
135             String requestParameterName = requestParameter.parameterName();
136             if (StringUtils.isBlank(requestParameterName)) {
137                 requestParameterName = fieldToPopulate;
138             }
139 
140             if (!parameters.containsKey(requestParameterName)) {
141                 continue;
142             }
143 
144             String fieldValue = parameters.get(requestParameterName);
145             if (StringUtils.isNotBlank(fieldValue)) {
146                 viewRequestParameters.put(requestParameterName, fieldValue);
147                 ObjectPropertyUtils.setPropertyValue(view, fieldToPopulate, fieldValue);
148 
149                 // remove any conditional configuration so value is not
150                 // overridden later during the apply model phase
151                 if (view.getPropertyExpressions().containsKey(fieldToPopulate)) {
152                     view.getPropertyExpressions().remove(fieldToPopulate);
153                 }
154 
155                 if (viewPropertyReplacers.containsKey(fieldToPopulate)) {
156                     Set<PropertyReplacer> propertyReplacers = viewPropertyReplacers.get(fieldToPopulate);
157                     for (PropertyReplacer replacer : propertyReplacers) {
158                         view.getPropertyReplacers().remove(replacer);
159                     }
160                 }
161             }
162         }
163 
164         view.setViewRequestParameters(viewRequestParameters);
165     }
166 
167     /**
168      * @see org.kuali.rice.krad.uif.service.ViewHelperService#performInitialization(org.kuali.rice.krad.uif.view.View,
169      *      java.lang.Object)
170      */
171     @Override
172     public void performInitialization(View view, Object model) {
173         view.assignComponentIds(view);
174         performComponentInitialization(view, model, view);
175     }
176 
177     /**
178      * Performs the complete component lifecycle on the component passed in, in this order:
179      * performComponentInitialization, performComponentApplyModel, and performComponentFinalize.
180      *
181      * @see {@link org.kuali.rice.krad.uif.service.ViewHelperService#performComponentLifecycle(
182      *org.kuali.rice.krad.uif.view.View, java.lang.Object, org.kuali.rice.krad.uif.component.Component,
183      *      java.lang.String)
184      * @see {@link #performComponentInitialization(org.kuali.rice.krad.uif.view.View, Object,
185      *      org.kuali.rice.krad.uif.component.Component)}
186      * @see {@link #performComponentApplyModel(View, Component, Object)}
187      * @see {@link #performComponentFinalize(View, Component, Object, Component, Map)}
188      */
189     public void performComponentLifecycle(View view, Object model, Component component, String origId) {
190         Component origComponent = view.getViewIndex().getComponentById(origId);
191 
192         Component parent = (Component) origComponent.getContext().get(UifConstants.ContextVariableNames.PARENT);
193         component.pushAllToContext(origComponent.getContext());
194 
195         // adjust IDs for suffixes that might have been added by a parent component during the full view lifecycle
196         String suffix = StringUtils.replaceOnce(origComponent.getId(), origComponent.getFactoryId(), "");
197         // remove attribute suffix since that gets added in lifecycle
198         if (suffix.endsWith(UifConstants.IdSuffixes.ATTRIBUTE)) {
199             suffix = StringUtils.removeEnd(suffix, UifConstants.IdSuffixes.ATTRIBUTE);
200         }
201         ComponentUtils.updateIdWithSuffix(component, suffix);
202 
203         // binding path should stay the same
204         if (component instanceof DataBinding) {
205             ((DataBinding) component).setBindingInfo(((DataBinding) origComponent).getBindingInfo());
206             ((DataBinding) component).getBindingInfo().setBindingPath(
207                     ((DataBinding) origComponent).getBindingInfo().getBindingPath());
208         }
209 
210         // copy properties that are set by parent components in the full view lifecycle
211         if (component instanceof Field) {
212             ((Field) component).setLabelFieldRendered(((Field) origComponent).isLabelFieldRendered());
213         } else if (component instanceof CollectionGroup) {
214             ((CollectionGroup) component).setSubCollectionSuffix(
215                     ((CollectionGroup) origComponent).getSubCollectionSuffix());
216         }
217 
218         // reset data if needed
219         if (component.isResetDataOnRefresh()) {
220             // TODO: this should handle groups as well, going through nested data fields
221             if (component instanceof DataField) {
222                 // TODO: should check default value
223 
224                 // clear value
225                 ObjectPropertyUtils.initializeProperty(model,
226                         ((DataField) component).getBindingInfo().getBindingPath());
227             }
228         }
229 
230         performComponentInitialization(view, model, component);
231         performComponentApplyModel(view, component, model);
232 
233         // make sure id, binding, and label settings stay the same as initial
234         if (component instanceof Group || component instanceof FieldGroup) {
235             List<Component> nestedComponents = ComponentUtils.getAllNestedComponents(component);
236             for (Component nestedComponent : nestedComponents) {
237                 Component origNestedComponent = null;
238                 if (nestedComponent instanceof DataField) {
239                     origNestedComponent = view.getViewIndex().getComponentById(
240                             nestedComponent.getId() + suffix + UifConstants.IdSuffixes.ATTRIBUTE);
241                 } else {
242                     origNestedComponent = view.getViewIndex().getComponentById(nestedComponent.getId() + suffix);
243                 }
244 
245                 if (origNestedComponent != null) {
246                     // update binding
247                     if (nestedComponent instanceof DataBinding) {
248                         ((DataBinding) nestedComponent).setBindingInfo(
249                                 ((DataBinding) origNestedComponent).getBindingInfo());
250                         ((DataBinding) nestedComponent).getBindingInfo().setBindingPath(
251                                 ((DataBinding) origNestedComponent).getBindingInfo().getBindingPath());
252                     }
253 
254                     // update label rendered flag
255                     if (nestedComponent instanceof Field) {
256                         ((Field) nestedComponent).setLabelFieldRendered(
257                                 ((Field) origNestedComponent).isLabelFieldRendered());
258                     }
259 
260                     // update id
261                     ComponentUtils.updateIdWithSuffix(nestedComponent, suffix);
262                 }
263             }
264         }
265 
266         // TODO: need to handle updating client state for component refresh
267         Map<String, Object> clientState = new HashMap<String, Object>();
268         performComponentFinalize(view, component, model, parent, clientState);
269 
270         // get client state for component and build update script for on load
271         String clientStateScript = buildClientSideStateScript(view, clientState, true);
272         String onLoadScript = component.getOnLoadScript();
273         if (StringUtils.isNotBlank(onLoadScript)) {
274             clientStateScript = onLoadScript + clientStateScript;
275         }
276         component.setOnLoadScript(clientStateScript);
277 
278         // update index for component
279         view.getViewIndex().indexComponent(component);
280     }
281 
282     /**
283      * Performs initialization of a component by these steps:
284      *
285      * <ul>
286      * <li>If component id not set, assigns to next available int for view</li>
287      * <li>For <code>InputField</code> instances, set defaults from the data
288      * dictionary.</li>
289      * <li>Invoke the initialize method on the component. Here the component can
290      * setup defaults and do other initialization that is specific to that
291      * component.</li>
292      * <li>Invoke any configured <code>ComponentModifier</code> instances for
293      * the component.</li>
294      * <li>Call the component to get the List of components that are nested
295      * within and recursively call this method to initialize those components.</li>
296      * <li>Call custom initialize hook for service overrides</li>
297      * </ul>
298      *
299      * <p>
300      * Note the order various initialize points are called, this can sometimes
301      * be an important factor to consider when initializing a component
302      * </p>
303      *
304      * @see org.kuali.rice.krad.uif.service.ViewHelperService#performComponentInitialization(org.kuali.rice.krad.uif.view.View,
305      *      java.lang.Object, org.kuali.rice.krad.uif.component.Component)
306      */
307     public void performComponentInitialization(View view, Object model, Component component) {
308         if (component == null) {
309             return;
310         }
311 
312         if (StringUtils.isBlank(component.getId()) || StringUtils.isBlank(component.getFactoryId())) {
313             throw new RiceRuntimeException("Ids are not set, this should happen unless a component is misconfigured");
314         }
315 
316         // TODO: duplicate ID check
317 
318         LOG.debug("Initializing component: " + component.getId() + " with type: " + component.getClass());
319 
320         // invoke component to initialize itself after properties have been set
321         component.performInitialization(view, model);
322 
323         // for attribute fields, set defaults from dictionary entry
324         if (component instanceof DataField) {
325             initializeDataFieldFromDataDictionary(view, (DataField) component);
326         }
327 
328         // add initial state to the view index for component refreshes
329         view.getViewIndex().addInitialComponentState(component);
330 
331         if (component instanceof Container) {
332             LayoutManager layoutManager = ((Container) component).getLayoutManager();
333 
334             // invoke hook point for adding components through code
335             addCustomContainerComponents(view, model, (Container) component);
336 
337             // process any remote fields holder that might be in the containers items, collection items will get
338             // processed as the lines are being built
339             if (!(component instanceof CollectionGroup)) {
340                 processAnyRemoteFieldsHolder(view, model, (Container) component);
341             }
342         }
343 
344         // for collection groups set defaults from dictionary entry
345         if (component instanceof CollectionGroup) {
346             // TODO: initialize from dictionary
347         }
348 
349         // invoke component modifiers setup to run in the initialize phase
350         runComponentModifiers(view, component, null, UifConstants.ViewPhases.INITIALIZE);
351 
352         // initialize nested components
353         for (Component nestedComponent : component.getComponentsForLifecycle()) {
354             performComponentInitialization(view, model, nestedComponent);
355         }
356 
357         // initialize nested components in property replacements
358         for (PropertyReplacer replacer : component.getPropertyReplacers()) {
359             for (Component replacerComponent : replacer.getNestedComponents()) {
360                 performComponentInitialization(view, model, replacerComponent);
361             }
362         }
363 
364         // invoke initialize service hook
365         performCustomInitialization(view, component);
366     }
367 
368     /**
369      * Iterates through the containers configured items checking for <code>RemotableFieldsHolder</code>, if found
370      * the holder is invoked to retrieved the remotable fields and translate to attribute fields. The translated list
371      * is then inserted into the container item list at the position of the holder
372      *
373      * @param view - view instance containing the container
374      * @param model - object instance containing the view data
375      * @param container - container instance to check for any remotable fields holder
376      */
377     protected void processAnyRemoteFieldsHolder(View view, Object model, Container container) {
378         List<Component> processedItems = new ArrayList<Component>();
379 
380         // check for holders and invoke to retrieve the remotable fields and translate
381         // translated fields are placed into the container item list at the position of the holder
382         for (Component item : container.getItems()) {
383             if (item instanceof RemoteFieldsHolder) {
384                 List<InputField> translatedFields = ((RemoteFieldsHolder) item).fetchAndTranslateRemoteFields(view,
385                         model, container);
386                 processedItems.addAll(translatedFields);
387             } else {
388                 processedItems.add(item);
389             }
390         }
391 
392         // updated container items
393         container.setItems(processedItems);
394     }
395 
396     /**
397      * Sets properties of the <code>InputField</code> (if blank) to the
398      * corresponding attribute entry in the data dictionary
399      *
400      * @param view - view instance containing the field
401      * @param field - data field instance to initialize
402      */
403     protected void initializeDataFieldFromDataDictionary(View view, DataField field) {
404         AttributeDefinition attributeDefinition = null;
405 
406         String dictionaryAttributeName = field.getDictionaryAttributeName();
407         String dictionaryObjectEntry = field.getDictionaryObjectEntry();
408 
409         // if entry given but not attribute name, use field name as attribute
410         // name
411         if (StringUtils.isNotBlank(dictionaryObjectEntry) && StringUtils.isBlank(dictionaryAttributeName)) {
412             dictionaryAttributeName = field.getPropertyName();
413         }
414 
415         // if dictionary entry and attribute set, attempt to find definition
416         if (StringUtils.isNotBlank(dictionaryAttributeName) && StringUtils.isNotBlank(dictionaryObjectEntry)) {
417             attributeDefinition = getDataDictionaryService().getAttributeDefinition(dictionaryObjectEntry,
418                     dictionaryAttributeName);
419         }
420 
421         // if definition not found, recurse through path
422         if (attributeDefinition == null) {
423             String propertyPath = field.getBindingInfo().getBindingPath();
424             if (StringUtils.isNotBlank(field.getBindingInfo().getCollectionPath())) {
425                 propertyPath = field.getBindingInfo().getCollectionPath();
426                 if (StringUtils.isNotBlank(field.getBindingInfo().getBindByNamePrefix())) {
427                     propertyPath += "." + field.getBindingInfo().getBindByNamePrefix();
428                 }
429                 propertyPath += "." + field.getBindingInfo().getBindingName();
430             }
431 
432             attributeDefinition = findNestedDictionaryAttribute(view, field, null, propertyPath);
433         }
434 
435         // if a definition was found, initialize field from definition
436         if (attributeDefinition != null) {
437             field.copyFromAttributeDefinition(view, attributeDefinition);
438         }
439 
440         // if control still null, assign default
441         if (field instanceof InputField) {
442             InputField inputField = (InputField) field;
443             if (inputField.getControl() == null) {
444                 Control control = ComponentFactory.getTextControl();
445                 control.setId(view.getNextId());
446                 control.setFactoryId(control.getId());
447 
448                 inputField.setControl(control);
449             }
450         }
451     }
452 
453     /**
454      * Recursively drills down the property path (if nested) to find an
455      * AttributeDefinition, the first attribute definition found will be
456      * returned
457      *
458      * <p>
459      * e.g. suppose parentPath is 'document' and propertyPath is
460      * 'account.subAccount.name', first the property type for document will be
461      * retrieved using the view metadata and used as the dictionary entry, with
462      * the propertyPath as the dictionary attribute, if an attribute definition
463      * exists it will be returned. Else, the first part of the property path is
464      * added to the parent, making the parentPath 'document.account' and the
465      * propertyPath 'subAccount.name', the method is then called again to
466      * perform the process with those parameters. The recursion continues until
467      * an attribute field is found, or the propertyPath is no longer nested
468      * </p>
469      *
470      * @param view - view instance containing the field
471      * @param field - field we are attempting to find a supporting attribute
472      * definition for
473      * @param parentPath - parent path to use for getting the dictionary entry
474      * @param propertyPath - path of the property relative to the parent, to use as
475      * dictionary attribute and to drill down on
476      * @return AttributeDefinition if found, or Null
477      */
478     protected AttributeDefinition findNestedDictionaryAttribute(View view, DataField field, String parentPath,
479             String propertyPath) {
480         AttributeDefinition attributeDefinition = null;
481 
482         // attempt to find definition for parent and property
483         String dictionaryAttributeName = propertyPath;
484         String dictionaryObjectEntry = null;
485 
486         if (field.getBindingInfo().isBindToMap()) {
487             parentPath = "";
488             if (!field.getBindingInfo().isBindToForm() && StringUtils.isNotBlank(
489                     field.getBindingInfo().getBindingObjectPath())) {
490                 parentPath = field.getBindingInfo().getBindingObjectPath();
491             }
492             if (StringUtils.isNotBlank(field.getBindingInfo().getBindByNamePrefix())) {
493                 if (StringUtils.isNotBlank(parentPath)) {
494                     parentPath += "." + field.getBindingInfo().getBindByNamePrefix();
495                 } else {
496                     parentPath = field.getBindingInfo().getBindByNamePrefix();
497                 }
498             }
499 
500             dictionaryAttributeName = field.getBindingInfo().getBindingName();
501         }
502 
503         if (StringUtils.isNotBlank(parentPath)) {
504             Class<?> dictionaryModelClass = ViewModelUtils.getPropertyTypeByClassAndView(view, parentPath);
505             if (dictionaryModelClass != null) {
506                 dictionaryObjectEntry = dictionaryModelClass.getName();
507 
508                 attributeDefinition = getDataDictionaryService().getAttributeDefinition(dictionaryObjectEntry,
509                         dictionaryAttributeName);
510             }
511         }
512 
513         // if definition not found and property is still nested, recurse down
514         // one level
515         if ((attributeDefinition == null) && StringUtils.contains(propertyPath, ".")) {
516             String nextParentPath = StringUtils.substringBefore(propertyPath, ".");
517             if (StringUtils.isNotBlank(parentPath)) {
518                 nextParentPath = parentPath + "." + nextParentPath;
519             }
520             String nextPropertyPath = StringUtils.substringAfter(propertyPath, ".");
521 
522             return findNestedDictionaryAttribute(view, field, nextParentPath, nextPropertyPath);
523         }
524 
525         // if a definition was found, update the fields dictionary properties
526         if (attributeDefinition != null) {
527             field.setDictionaryAttributeName(dictionaryAttributeName);
528             field.setDictionaryObjectEntry(dictionaryObjectEntry);
529         }
530 
531         return attributeDefinition;
532     }
533 
534     /**
535      * @see org.kuali.rice.krad.uif.service.ViewHelperService#performApplyModel(org.kuali.rice.krad.uif.view.View,
536      *      java.lang.Object)
537      */
538     @Override
539     public void performApplyModel(View view, Object model) {
540         // get action flag and edit modes from authorizer/presentation controller
541         retrieveEditModesAndActionFlags(view, (UifFormBase) model);
542 
543         // set view context for conditional expressions
544         setViewContext(view, model);
545 
546         performComponentApplyModel(view, view, model);
547     }
548 
549     /**
550      * Invokes the configured <code>PresentationController</code> and
551      * </code>Authorizer</code> for the view to get the exported action flags
552      * and edit modes that can be used in conditional logic
553      *
554      * @param view - view instance that is being built and presentation/authorizer pulled for
555      * @param model - Object that contains the model data
556      */
557     protected void retrieveEditModesAndActionFlags(View view, UifFormBase model) {
558         ViewPresentationController presentationController = view.getPresentationController();
559         ViewAuthorizer authorizer = view.getAuthorizer();
560 
561         Person user = GlobalVariables.getUserSession().getPerson();
562 
563         Set<String> actionFlags = presentationController.getActionFlags(view, model);
564         actionFlags = authorizer.getActionFlags(view, model, user, actionFlags);
565 
566         view.setActionFlags(new BooleanMap(actionFlags));
567 
568         Set<String> editModes = presentationController.getEditModes(view, model);
569         editModes = authorizer.getEditModes(view, model, user, editModes);
570 
571         view.setEditModes(new BooleanMap(editModes));
572     }
573 
574     /**
575      * Sets up the view context which will be available to other components
576      * through their context for conditional logic evaluation
577      *
578      * @param view - view instance to set context for
579      * @param model - object containing the view data
580      */
581     protected void setViewContext(View view, Object model) {
582         view.pushAllToContext(getPreModelContext(view));
583 
584         // evaluate view expressions for further context
585         for (Entry<String, String> variableExpression : view.getExpressionVariables().entrySet()) {
586             String variableName = variableExpression.getKey();
587             Object value = getExpressionEvaluatorService().evaluateExpression(model, view.getContext(),
588                     variableExpression.getValue());
589             view.pushObjectToContext(variableName, value);
590         }
591     }
592 
593     /**
594      * Returns the general context that is available before the apply model
595      * phase (during the initialize phase)
596      *
597      * @param view - view instance for context
598      * @return Map<String, Object> context map
599      */
600     protected Map<String, Object> getPreModelContext(View view) {
601         Map<String, Object> context = new HashMap<String, Object>();
602 
603         context.put(UifConstants.ContextVariableNames.VIEW, view);
604         context.put(UifConstants.ContextVariableNames.VIEW_HELPER, this);
605 
606         Map<String, String> properties = KRADServiceLocator.getKualiConfigurationService().getAllProperties();
607         context.put(UifConstants.ContextVariableNames.CONFIG_PROPERTIES, properties);
608         context.put(UifConstants.ContextVariableNames.CONSTANTS, KRADConstants.class);
609 
610         return context;
611     }
612 
613     /**
614      * Applies the model data to a component of the View instance
615      *
616      * <p>
617      * The component is invoked to to apply the model data. Here the component
618      * can generate any additional fields needed or alter the configured fields.
619      * After the component is invoked a hook for custom helper service
620      * processing is invoked. Finally the method is recursively called for all
621      * the component children
622      * </p>
623      *
624      * @param view - view instance the component belongs to
625      * @param component - the component instance the model should be applied to
626      * @param model - top level object containing the data
627      */
628     protected void performComponentApplyModel(View view, Component component, Object model) {
629         if (component == null) {
630             return;
631         }
632 
633         // evaluate expressions on component properties
634         component.pushAllToContext(getCommonContext(view, component));
635         ExpressionUtils.adjustPropertyExpressions(view, component);
636         getExpressionEvaluatorService().evaluateObjectExpressions(component, model, component.getContext());
637 
638         // evaluate expressions on component security
639         ComponentSecurity componentSecurity = component.getComponentSecurity();
640         ExpressionUtils.adjustPropertyExpressions(view, componentSecurity);
641         getExpressionEvaluatorService().evaluateObjectExpressions(componentSecurity, model, component.getContext());
642 
643         // evaluate expressions on the binding info object
644         if (component instanceof DataBinding) {
645             BindingInfo bindingInfo = ((DataBinding) component).getBindingInfo();
646             ExpressionUtils.adjustPropertyExpressions(view, bindingInfo);
647             getExpressionEvaluatorService().evaluateObjectExpressions(bindingInfo, model, component.getContext());
648         }
649 
650         // evaluate expressions on the layout manager
651         if (component instanceof Container) {
652             LayoutManager layoutManager = ((Container) component).getLayoutManager();
653 
654             if (layoutManager != null) {
655                 layoutManager.getContext().putAll(getCommonContext(view, component));
656                 layoutManager.pushObjectToContext(UifConstants.ContextVariableNames.PARENT, component);
657                 layoutManager.pushObjectToContext(UifConstants.ContextVariableNames.MANAGER, layoutManager);
658 
659                 ExpressionUtils.adjustPropertyExpressions(view, layoutManager);
660                 getExpressionEvaluatorService().evaluateObjectExpressions(layoutManager, model,
661                         layoutManager.getContext());
662             }
663         }
664 
665         // sync the component with previous client side state
666         syncClientSideStateForComponent(component, ((ViewModel) model).getClientStateForSyncing());
667 
668         // invoke authorizer and presentation controller to set component state
669         applyAuthorizationAndPresentationLogic(view, component, (ViewModel) model);
670 
671         // invoke component to perform its conditional logic
672         Component parent = (Component) component.getContext().get(UifConstants.ContextVariableNames.PARENT);
673         component.performApplyModel(view, model, parent);
674 
675         // invoke service override hook
676         performCustomApplyModel(view, component, model);
677 
678         // invoke component modifiers configured to run in the apply model phase
679         runComponentModifiers(view, component, model, UifConstants.ViewPhases.APPLY_MODEL);
680 
681         // get children and recursively perform conditional logic
682         for (Component nestedComponent : component.getComponentsForLifecycle()) {
683             if (nestedComponent != null) {
684                 nestedComponent.pushObjectToContext(UifConstants.ContextVariableNames.PARENT, component);
685             }
686 
687             performComponentApplyModel(view, nestedComponent, model);
688         }
689     }
690 
691     /**
692      * Invokes the view's configured {@link ViewAuthorizer} and {@link ViewPresentationController} to set state of
693      * the component
694      *
695      * <p>
696      * The following authorization is done here:
697      * Fields: edit, view, required, mask, and partial mask
698      * Groups: edit and view
699      * Actions: take action
700      * </p>
701      *
702      * <p>
703      * Note additional checks are also done for fields that are part of a collection group. This authorization is
704      * found in {@link org.kuali.rice.krad.uif.container.CollectionGroupBuilder}
705      * </p>
706      *
707      * @param view - view instance the component belongs to and from which the authorizer and presentation controller
708      * will be pulled
709      * @param component - component instance to authorize
710      * @param model - model object containing the data for the view
711      */
712     protected void applyAuthorizationAndPresentationLogic(View view, Component component, ViewModel model) {
713         ViewPresentationController presentationController = view.getPresentationController();
714         ViewAuthorizer authorizer = view.getAuthorizer();
715 
716         Person user = GlobalVariables.getUserSession().getPerson();
717 
718         // if component not flagged for render no need to check auth and controller logic
719         if (!component.isRender()) {
720             return;
721         }
722 
723         // check top level view edit authorization
724         if (component instanceof View) {
725             if (!view.isReadOnly()) {
726                 boolean canEditView = authorizer.canEditView(view, model, user);
727                 if (canEditView) {
728                     canEditView = presentationController.canEditView(view, model);
729                 }
730                 view.setReadOnly(!canEditView);
731             }
732         }
733 
734         // perform group authorization and presentation logic
735         else if (component instanceof Group) {
736             Group group = (Group) component;
737 
738             // if group is not hidden, do authorization for viewing the group
739             if (!group.isHidden()) {
740                 boolean canViewGroup = authorizer.canViewGroup(view, model, group, group.getId(), user);
741                 if (canViewGroup) {
742                     canViewGroup = presentationController.canViewGroup(view, model, group, group.getId());
743                 }
744                 group.setHidden(!canViewGroup);
745                 group.setRender(canViewGroup);
746             }
747 
748             // if group is editable, do authorization for editing the group
749             if (!group.isReadOnly()) {
750                 boolean canEditGroup = authorizer.canEditGroup(view, model, group, group.getId(), user);
751                 if (canEditGroup) {
752                     canEditGroup = presentationController.canEditGroup(view, model, group, group.getId());
753                 }
754                 group.setReadOnly(!canEditGroup);
755             }
756         }
757 
758         // perform field authorization and presentation logic
759         else if (component instanceof Field) {
760             Field field = (Field) component;
761 
762             String propertyName = null;
763             if (field instanceof DataBinding) {
764                 propertyName = ((DataBinding) field).getPropertyName();
765             }
766 
767             // if field is not hidden, do authorization for viewing the field
768             if (!field.isHidden()) {
769                 boolean canViewField = authorizer.canViewField(view, model, field, propertyName, user);
770                 if (canViewField) {
771                     canViewField = presentationController.canViewField(view, model, field, propertyName);
772                 }
773                 field.setHidden(!canViewField);
774                 field.setRender(canViewField);
775             }
776 
777             // if field is not readOnly, check edit authorization
778             if (!field.isReadOnly()) {
779                 // check field edit authorization
780                 boolean canEditField = authorizer.canEditField(view, model, field, propertyName, user);
781                 if (canEditField) {
782                     canEditField = presentationController.canEditField(view, model, field, propertyName);
783                 }
784                 field.setReadOnly(!canEditField);
785             }
786 
787             // if field is not already required, invoke presentation logic to determine if it should be
788             if ((field.getRequired() == null) || !field.getRequired().booleanValue()) {
789                 boolean fieldIsRequired = presentationController.fieldIsRequired(view, model, field, propertyName);
790             }
791 
792             if (field instanceof DataField) {
793                 DataField dataField = (DataField) field;
794 
795                 // check mask authorization
796                 boolean canUnmaskValue = authorizer.canUnmaskField(view, model, dataField, dataField.getPropertyName(),
797                         user);
798                 if (!canUnmaskValue) {
799                     dataField.setApplyValueMask(true);
800                     dataField.setMaskFormatter(dataField.getComponentSecurity().getAttributeSecurity().
801                             getMaskFormatter());
802                 } else {
803                     // check partial mask authorization
804                     boolean canPartiallyUnmaskValue = authorizer.canPartialUnmaskField(view, model, dataField,
805                             dataField.getPropertyName(), user);
806                     if (!canPartiallyUnmaskValue) {
807                         dataField.setApplyValueMask(true);
808                         dataField.setMaskFormatter(
809                                 dataField.getComponentSecurity().getAttributeSecurity().getPartialMaskFormatter());
810                     }
811                 }
812             }
813 
814             // check authorization for actions
815             if (field instanceof ActionField) {
816                 ActionField actionField = (ActionField) field;
817 
818                 boolean canTakeAction = authorizer.canPerformAction(view, model, actionField,
819                         actionField.getActionEvent(), actionField.getId(), user);
820                 if (canTakeAction) {
821                     canTakeAction = presentationController.canPerformAction(view, model, actionField,
822                             actionField.getActionEvent(), actionField.getId());
823                 }
824                 actionField.setRender(canTakeAction);
825             }
826         }
827 
828         // perform widget authorization and presentation logic
829         else if (component instanceof Widget) {
830             Widget widget = (Widget) component;
831 
832             // if widget is not hidden, do authorization for viewing the widget
833             if (!widget.isHidden()) {
834                 boolean canViewWidget = authorizer.canViewWidget(view, model, widget, widget.getId(), user);
835                 if (canViewWidget) {
836                     canViewWidget = presentationController.canViewWidget(view, model, widget, widget.getId());
837                 }
838                 widget.setHidden(!canViewWidget);
839                 widget.setRender(canViewWidget);
840             }
841 
842             // if widget is not readOnly, check edit authorization
843             if (!widget.isReadOnly()) {
844                 boolean canEditWidget = authorizer.canEditWidget(view, model, widget, widget.getId(), user);
845                 if (canEditWidget) {
846                     canEditWidget = presentationController.canEditWidget(view, model, widget, widget.getId());
847                 }
848                 widget.setReadOnly(!canEditWidget);
849             }
850         }
851     }
852 
853     /**
854      * Runs any configured <code>ComponentModifiers</code> for the given
855      * component that match the given run phase and who run condition evaluation
856      * succeeds
857      *
858      * <p>
859      * If called during the initialize phase, the performInitialization method will be invoked on
860      * the <code>ComponentModifier</code> before running
861      * </p>
862      *
863      * @param view - view instance for context
864      * @param component - component instance whose modifiers should be run
865      * @param model - model object for context
866      * @param runPhase - current phase to match on
867      */
868     protected void runComponentModifiers(View view, Component component, Object model, String runPhase) {
869         for (ComponentModifier modifier : component.getComponentModifiers()) {
870             // if run phase is initialize, invoke initialize method on modifier first
871             if (StringUtils.equals(runPhase, UifConstants.ViewPhases.INITIALIZE)) {
872                 modifier.performInitialization(view, model, component);
873             }
874 
875             // check run phase matches
876             if (StringUtils.equals(modifier.getRunPhase(), runPhase)) {
877                 // check condition (if set) evaluates to true
878                 boolean runModifier = true;
879                 if (StringUtils.isNotBlank(modifier.getRunCondition())) {
880                     Map<String, Object> context = new HashMap<String, Object>();
881                     context.put(UifConstants.ContextVariableNames.COMPONENT, component);
882                     context.put(UifConstants.ContextVariableNames.VIEW, view);
883 
884                     String conditionEvaluation = getExpressionEvaluatorService().evaluateExpressionTemplate(model,
885                             context, modifier.getRunCondition());
886                     runModifier = Boolean.parseBoolean(conditionEvaluation);
887                 }
888 
889                 if (runModifier) {
890                     modifier.performModification(view, model, component);
891                 }
892             }
893         }
894     }
895 
896     /**
897      * Gets global objects for the context map and pushes them to the context
898      * for the component
899      *
900      * @param view - view instance for component
901      * @param component - component instance to push context to
902      */
903     protected Map<String, Object> getCommonContext(View view, Component component) {
904         Map<String, Object> context = new HashMap<String, Object>();
905 
906         context.putAll(view.getContext());
907         context.put(UifConstants.ContextVariableNames.COMPONENT, component);
908 
909         return context;
910     }
911 
912     /**
913      * @see org.kuali.rice.krad.uif.service.ViewHelperService#performFinalize(org.kuali.rice.krad.uif.view.View,
914      *      java.lang.Object)
915      */
916     @Override
917     public void performFinalize(View view, Object model) {
918         Map<String, Object> clientState = new HashMap<String, Object>();
919         performComponentFinalize(view, view, model, null, clientState);
920 
921         String clientStateScript = buildClientSideStateScript(view, clientState, false);
922         String viewPreLoadScript = view.getPreLoadScript();
923         if (StringUtils.isNotBlank(viewPreLoadScript)) {
924             clientStateScript = viewPreLoadScript + clientStateScript;
925         }
926         view.setPreLoadScript(clientStateScript);
927 
928         // apply default values if they have not been applied yet
929         if (!((ViewModel) model).isDefaultsApplied()) {
930             applyDefaultValues(view, view, model);
931             ((ViewModel) model).setDefaultsApplied(true);
932         }
933     }
934 
935     /**
936      * Builds script that will initialize configuration parameters and component state on the client
937      *
938      * <p>
939      * Here client side state is initialized along with configuration variables that need exposed to script
940      * </p>
941      *
942      * @param view - view instance that is being built
943      * @param clientSideState - map of key/value pairs that should be exposed as client side state
944      * @param updateOnly - boolean that indicates whether we are just updating a component (true), or the full view
945      */
946     protected String buildClientSideStateScript(View view, Map<String, Object> clientSideState, boolean updateOnly) {
947         // merge any additional client side state added to the view during processing
948         // state from view will override in all cases except when both values are maps, in which the maps
949         // be combined for the new value
950         for (Entry<String, Object> additionalState : view.getClientSideState().entrySet()) {
951             if (!clientSideState.containsKey(additionalState.getKey())) {
952                 clientSideState.put(additionalState.getKey(), additionalState.getValue());
953             } else {
954                 Object state = clientSideState.get(additionalState.getKey());
955                 Object mergeState = additionalState.getValue();
956                 if ((state instanceof Map) && (mergeState instanceof Map)) {
957                     ((Map) state).putAll((Map) mergeState);
958                 } else {
959                     clientSideState.put(additionalState.getKey(), additionalState.getValue());
960                 }
961             }
962         }
963 
964         // script for initializing client side state on load
965         String clientStateScript = "";
966         if (!clientSideState.isEmpty()) {
967             if (updateOnly) {
968                 clientStateScript = "updateViewState({";
969             } else {
970                 clientStateScript = "initializeViewState({";
971             }
972 
973             for (Entry<String, Object> stateEntry : clientSideState.entrySet()) {
974                 clientStateScript += "'" + stateEntry.getKey() + "':";
975                 clientStateScript += ScriptUtils.translateValue(stateEntry.getValue());
976                 clientStateScript += ",";
977             }
978             clientStateScript = StringUtils.removeEnd(clientStateScript, ",");
979             clientStateScript += "});";
980         }
981 
982         // add necessary configuration parameters
983         if (!updateOnly) {
984             String kradImageLocation = KRADServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
985                     "krad.externalizable.images.url");
986             clientStateScript += "setConfigParam('"
987                     + UifConstants.ClientSideVariables.KRAD_IMAGE_LOCATION
988                     + "','"
989                     + kradImageLocation
990                     + "');";
991         }
992 
993         return clientStateScript;
994     }
995 
996     /**
997      * Update state of the given component and does final preparation for
998      * rendering
999      *
1000      * @param view - view instance the component belongs to
1001      * @param component - the component instance that should be updated
1002      * @param model - top level object containing the data
1003      * @param parent - Parent component for the component being finalized
1004      * @param clientSideState - map to add client state to
1005      */
1006     protected void performComponentFinalize(View view, Component component, Object model, Component parent,
1007             Map<String, Object> clientSideState) {
1008         if (component == null) {
1009             return;
1010         }
1011 
1012         // implement readonly request overrides
1013         ViewModel viewModel = (ViewModel) model;
1014         if ((component instanceof DataBinding) && view.isSupportsReadOnlyFieldsOverride() && !viewModel
1015                 .getReadOnlyFieldsList().isEmpty()) {
1016             String propertyName = ((DataBinding) component).getPropertyName();
1017             if (viewModel.getReadOnlyFieldsList().contains(propertyName)) {
1018                 component.setReadOnly(true);
1019             }
1020         }
1021 
1022         // invoke configured method finalizers
1023         invokeMethodFinalizer(view, component, model);
1024 
1025         // invoke component to update its state
1026         component.performFinalize(view, model, parent);
1027 
1028         // add client side state for annotated component properties
1029         addClientSideStateForComponent(component, clientSideState);
1030 
1031         // invoke service override hook
1032         performCustomFinalize(view, component, model, parent);
1033 
1034         // invoke component modifiers setup to run in the finalize phase
1035         runComponentModifiers(view, component, model, UifConstants.ViewPhases.FINALIZE);
1036 
1037         // get components children and recursively update state
1038         for (Component nestedComponent : component.getComponentsForLifecycle()) {
1039             performComponentFinalize(view, nestedComponent, model, component, clientSideState);
1040         }
1041     }
1042 
1043     /**
1044      * Reflects the class for the given component to find any fields that are annotated with
1045      * <code>ClientSideState</code> and adds the corresponding property name/value pair to the client side state
1046      * map
1047      *
1048      * <p>
1049      * Note if the component is the <code>View</code, state is added directly to the client side state map, while
1050      * for other components a nested Map is created to hold the state, which is then placed into the client side
1051      * state map with the component id as the key
1052      * </p>
1053      *
1054      * @param component - component instance to get client state for
1055      * @param clientSideState - map to add client side variable name/values to
1056      */
1057     protected void addClientSideStateForComponent(Component component, Map<String, Object> clientSideState) {
1058         Map<String, Annotation> annotatedFields = CloneUtils.getFieldsWithAnnotation(component.getClass(),
1059                 ClientSideState.class);
1060 
1061         if (!annotatedFields.isEmpty()) {
1062             Map<String, Object> componentClientState = null;
1063             if (component instanceof View) {
1064                 componentClientState = clientSideState;
1065             } else {
1066                 if (clientSideState.containsKey(component.getId())) {
1067                     componentClientState = (Map<String, Object>) clientSideState.get(component.getId());
1068                 } else {
1069                     componentClientState = new HashMap<String, Object>();
1070                     clientSideState.put(component.getId(), componentClientState);
1071                 }
1072             }
1073 
1074             for (Entry<String, Annotation> annotatedField : annotatedFields.entrySet()) {
1075                 ClientSideState clientSideStateAnnot = (ClientSideState) annotatedField.getValue();
1076 
1077                 String variableName = clientSideStateAnnot.variableName();
1078                 if (StringUtils.isBlank(variableName)) {
1079                     variableName = annotatedField.getKey();
1080                 }
1081 
1082                 Object value = ObjectPropertyUtils.getPropertyValue(component, annotatedField.getKey());
1083                 componentClientState.put(variableName, value);
1084             }
1085         }
1086     }
1087 
1088     /**
1089      * Updates the properties of the given component instance with the value found from the corresponding map of
1090      * client state (if found)
1091      *
1092      * @param component - component instance to update
1093      * @param clientSideState - map of state to sync with
1094      */
1095     protected void syncClientSideStateForComponent(Component component, Map<String, Object> clientSideState) {
1096         // find the map of state that was sent for component (if any)
1097         Map<String, Object> componentState = null;
1098         if (component instanceof View) {
1099             componentState = clientSideState;
1100         } else {
1101             if (clientSideState.containsKey(component.getId())) {
1102                 componentState = (Map<String, Object>) clientSideState.get(component.getId());
1103             }
1104         }
1105 
1106         // if state was sent, match with fields on the component that are annotated to have client state
1107         if ((componentState != null) && (!componentState.isEmpty())) {
1108             Map<String, Annotation> annotatedFields = CloneUtils.getFieldsWithAnnotation(component.getClass(),
1109                     ClientSideState.class);
1110 
1111             for (Entry<String, Annotation> annotatedField : annotatedFields.entrySet()) {
1112                 ClientSideState clientSideStateAnnot = (ClientSideState) annotatedField.getValue();
1113 
1114                 String variableName = clientSideStateAnnot.variableName();
1115                 if (StringUtils.isBlank(variableName)) {
1116                     variableName = annotatedField.getKey();
1117                 }
1118 
1119                 if (componentState.containsKey(variableName)) {
1120                     Object value = componentState.get(variableName);
1121                     ObjectPropertyUtils.setPropertyValue(component, annotatedField.getKey(), value);
1122                 }
1123             }
1124         }
1125     }
1126 
1127     /**
1128      * Invokes the finalize method for the component (if configured) and sets
1129      * the render output for the component to the returned method string (if
1130      * method is not a void type)
1131      *
1132      * @param view - view instance that contains the component
1133      * @param component - component to run finalize method for
1134      * @param model - top level object containing the data
1135      */
1136     protected void invokeMethodFinalizer(View view, Component component, Object model) {
1137         String finalizeMethodToCall = component.getFinalizeMethodToCall();
1138         MethodInvoker finalizeMethodInvoker = component.getFinalizeMethodInvoker();
1139 
1140         if (StringUtils.isBlank(finalizeMethodToCall) && (finalizeMethodInvoker == null)) {
1141             return;
1142         }
1143 
1144         if (finalizeMethodInvoker == null) {
1145             finalizeMethodInvoker = new MethodInvoker();
1146         }
1147 
1148         // if method not set on invoker, use renderingMethodToCall, note staticMethod could be set(don't know since
1149         // there is not a getter), if so it will override the target method in prepare
1150         if (StringUtils.isBlank(finalizeMethodInvoker.getTargetMethod())) {
1151             finalizeMethodInvoker.setTargetMethod(finalizeMethodToCall);
1152         }
1153 
1154         // if target class or object not set, use view helper service
1155         if ((finalizeMethodInvoker.getTargetClass() == null) && (finalizeMethodInvoker.getTargetObject() == null)) {
1156             finalizeMethodInvoker.setTargetObject(view.getViewHelperService());
1157         }
1158 
1159         // setup arguments for method
1160         List<Object> additionalArguments = component.getFinalizeMethodAdditionalArguments();
1161         if (additionalArguments == null) {
1162             additionalArguments = new ArrayList<Object>();
1163         }
1164 
1165         Object[] arguments = new Object[2 + additionalArguments.size()];
1166         arguments[0] = component;
1167         arguments[1] = model;
1168 
1169         int argumentIndex = 1;
1170         for (Object argument : additionalArguments) {
1171             argumentIndex++;
1172             arguments[argumentIndex] = argument;
1173         }
1174         finalizeMethodInvoker.setArguments(arguments);
1175 
1176         // invoke method and get render output
1177         try {
1178             LOG.debug("Invoking render method: "
1179                     + finalizeMethodInvoker.getTargetMethod()
1180                     + " for component: "
1181                     + component.getId());
1182             finalizeMethodInvoker.prepare();
1183 
1184             Class<?> methodReturnType = finalizeMethodInvoker.getPreparedMethod().getReturnType();
1185             if (StringUtils.equals("void", methodReturnType.getName())) {
1186                 finalizeMethodInvoker.invoke();
1187             } else {
1188                 String renderOutput = (String) finalizeMethodInvoker.invoke();
1189 
1190                 component.setSelfRendered(true);
1191                 component.setRenderOutput(renderOutput);
1192             }
1193         } catch (Exception e) {
1194             LOG.error("Error invoking rendering method for component: " + component.getId(), e);
1195             throw new RuntimeException("Error invoking rendering method for component: " + component.getId(), e);
1196         }
1197     }
1198 
1199     /**
1200      * @see org.kuali.rice.krad.uif.service.ViewHelperService#processCollectionAddLine(org.kuali.rice.krad.uif.view.View,
1201      *      java.lang.Object, java.lang.String)
1202      */
1203     @Override
1204     public void processCollectionAddLine(View view, Object model, String collectionPath) {
1205         // get the collection group from the view
1206         CollectionGroup collectionGroup = view.getViewIndex().getCollectionGroupByPath(collectionPath);
1207         if (collectionGroup == null) {
1208             logAndThrowRuntime("Unable to get collection group component for path: " + collectionPath);
1209         }
1210 
1211         // get the collection instance for adding the new line
1212         Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model, collectionPath);
1213         if (collection == null) {
1214             logAndThrowRuntime("Unable to get collection property from model for path: " + collectionPath);
1215         }
1216 
1217         // now get the new line we need to add
1218         String addLinePath = collectionGroup.getAddLineBindingInfo().getBindingPath();
1219         Object addLine = ObjectPropertyUtils.getPropertyValue(model, addLinePath);
1220         if (addLine == null) {
1221             logAndThrowRuntime("Add line instance not found for path: " + addLinePath);
1222         }
1223 
1224         processBeforeAddLine(view, collectionGroup, model, addLine);
1225 
1226         // validate the line to make sure it is ok to add
1227         boolean isValidLine = performAddLineValidation(view, collectionGroup, model, addLine);
1228         if (isValidLine) {
1229             // TODO: should check to see if there is an add line method on the
1230             // collection parent and if so call that instead of just adding to
1231             // the collection (so that sequence can be set)
1232             if (collection instanceof List) {
1233                 ((List) collection).add(0, addLine);
1234             } else {
1235                 collection.add(addLine);
1236             }
1237 
1238             // make a new instance for the add line
1239             collectionGroup.initializeNewCollectionLine(view, model, collectionGroup, true);
1240         }
1241 
1242         processAfterAddLine(view, collectionGroup, model, addLine);
1243     }
1244 
1245     /**
1246      * Performs validation on the new collection line before it is added to the
1247      * corresponding collection
1248      *
1249      * @param view - view instance that the action was taken on
1250      * @param collectionGroup - collection group component for the collection
1251      * @param addLine - new line instance to validate
1252      * @param model - object instance that contain's the views data
1253      * @return boolean true if the line is valid and it should be added to the
1254      *         collection, false if it was not valid and should not be added to
1255      *         the collection
1256      */
1257     protected boolean performAddLineValidation(View view, CollectionGroup collectionGroup, Object model,
1258             Object addLine) {
1259         boolean isValid = true;
1260 
1261         // TODO: this should invoke rules, subclasses like the document view
1262         // should create the document add line event
1263 
1264         return isValid;
1265     }
1266 
1267     /**
1268      * @see org.kuali.rice.krad.uif.service.ViewHelperService#processCollectionDeleteLine(org.kuali.rice.krad.uif.view.View,
1269      *      java.lang.Object, java.lang.String, int)
1270      */
1271     public void processCollectionDeleteLine(View view, Object model, String collectionPath, int lineIndex) {
1272         // get the collection group from the view
1273         CollectionGroup collectionGroup = view.getViewIndex().getCollectionGroupByPath(collectionPath);
1274         if (collectionGroup == null) {
1275             logAndThrowRuntime("Unable to get collection group component for path: " + collectionPath);
1276         }
1277 
1278         // get the collection instance for adding the new line
1279         Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model, collectionPath);
1280         if (collection == null) {
1281             logAndThrowRuntime("Unable to get collection property from model for path: " + collectionPath);
1282         }
1283 
1284         // TODO: look into other ways of identifying a line so we can deal with
1285         // unordered collections
1286         if (collection instanceof List) {
1287             Object deleteLine = ((List<Object>) collection).get(lineIndex);
1288 
1289             // validate the delete action is allowed for this line
1290             boolean isValid = performDeleteLineValidation(view, collectionGroup, deleteLine);
1291             if (isValid) {
1292                 ((List<Object>) collection).remove(lineIndex);
1293                 processAfterDeleteLine(view, collectionGroup, model, lineIndex);
1294             }
1295         } else {
1296             logAndThrowRuntime("Only List collection implementations are supported for the delete by index method");
1297         }
1298     }
1299 
1300     /**
1301      * Performs validation on the collection line before it is removed from the
1302      * corresponding collection
1303      *
1304      * @param view - view instance that the action was taken on
1305      * @param collectionGroup - collection group component for the collection
1306      * @param deleteLine - line that will be removed
1307      * @return boolean true if the action is allowed and the line should be
1308      *         removed, false if the line should not be removed
1309      */
1310     protected boolean performDeleteLineValidation(View view, CollectionGroup collectionGroup, Object deleteLine) {
1311         boolean isValid = true;
1312 
1313         // TODO: this should invoke rules, sublclasses like the document view
1314         // should create the document delete line event
1315 
1316         return isValid;
1317     }
1318 
1319     /**
1320      * @see org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl#processMultipleValueLookupResults
1321      */
1322     public void processMultipleValueLookupResults(View view, Object model, String collectionPath,
1323             String lookupResultValues) {
1324         // if no line values returned, no population is needed
1325         if (StringUtils.isBlank(lookupResultValues)) {
1326             return;
1327         }
1328 
1329         // retrieve the collection group so we can get the collection class and collection lookup
1330         CollectionGroup collectionGroup = view.getViewIndex().getCollectionGroupByPath(collectionPath);
1331         if (collectionGroup == null) {
1332             throw new RuntimeException("Unable to find collection group for path: " + collectionPath);
1333         }
1334 
1335         Class<?> collectionObjectClass = collectionGroup.getCollectionObjectClass();
1336         Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model,
1337                 collectionGroup.getBindingInfo().getBindingPath());
1338         if (collection == null) {
1339             Class<?> collectionClass = ObjectPropertyUtils.getPropertyType(model,
1340                     collectionGroup.getBindingInfo().getBindingPath());
1341             collection = (Collection<Object>) ObjectUtils.newInstance(collectionClass);
1342             ObjectPropertyUtils.setPropertyValue(model, collectionGroup.getBindingInfo().getBindingPath(), collection);
1343         }
1344 
1345         Map<String, String> fieldConversions = collectionGroup.getCollectionLookup().getFieldConversions();
1346         List<String> toFieldNamesColl = new ArrayList(fieldConversions.values());
1347         Collections.sort(toFieldNamesColl);
1348         String[] toFieldNames = new String[toFieldNamesColl.size()];
1349         toFieldNamesColl.toArray(toFieldNames);
1350 
1351         // first split to get the line value sets
1352         String[] lineValues = StringUtils.split(lookupResultValues, ",");
1353 
1354         // for each returned set create a new instance of collection class and populate with returned line values
1355         for (String lineValue : lineValues) {
1356             Object lineDataObject = null;
1357 
1358             // TODO: need to put this in data object service so logic can be reused
1359             ModuleService moduleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(
1360                     collectionObjectClass);
1361             if (moduleService != null && moduleService.isExternalizable(collectionObjectClass)) {
1362                 lineDataObject = moduleService.createNewObjectFromExternalizableClass(collectionObjectClass.asSubclass(
1363                         ExternalizableBusinessObject.class));
1364             } else {
1365                 lineDataObject = ObjectUtils.newInstance(collectionObjectClass);
1366             }
1367 
1368             // apply default values to new line
1369             applyDefaultValuesForCollectionLine(view, model, collectionGroup, lineDataObject);
1370 
1371             String[] fieldValues = StringUtils.split(lineValue, ":");
1372             if (fieldValues.length != toFieldNames.length) {
1373                 throw new RuntimeException(
1374                         "Value count passed back from multi-value lookup does not match field conversion count");
1375             }
1376 
1377             // set each field value on the line
1378             for (int i = 0; i < fieldValues.length; i++) {
1379                 String fieldName = toFieldNames[i];
1380                 ObjectPropertyUtils.setPropertyValue(lineDataObject, fieldName, fieldValues[i]);
1381             }
1382 
1383             // TODO: duplicate identifier check
1384 
1385             collection.add(lineDataObject);
1386         }
1387     }
1388 
1389     /**
1390      * Finds the <code>Inquirable</code> configured for the given data object
1391      * class and delegates to it for building the inquiry URL
1392      *
1393      * @see org.kuali.rice.krad.uif.service.ViewHelperService#buildInquiryLink(java.lang.Object,
1394      *      java.lang.String, org.kuali.rice.krad.uif.widget.Inquiry)
1395      */
1396     public void buildInquiryLink(Object dataObject, String propertyName, Inquiry inquiry) {
1397         Inquirable inquirable = getViewDictionaryService().getInquirable(dataObject.getClass(), inquiry.getViewName());
1398         if (inquirable != null) {
1399             inquirable.buildInquirableLink(dataObject, propertyName, inquiry);
1400         } else {
1401             // inquirable not found, no inquiry link can be set
1402             inquiry.setRender(false);
1403         }
1404     }
1405 
1406     /**
1407      * @see org.kuali.rice.krad.uif.service.ViewHelperService#applyDefaultValuesForCollectionLine(org.kuali.rice.krad.uif.view.View,
1408      *      java.lang.Object, org.kuali.rice.krad.uif.container.CollectionGroup,
1409      *      java.lang.Object)
1410      */
1411     public void applyDefaultValuesForCollectionLine(View view, Object model, CollectionGroup collectionGroup,
1412             Object line) {
1413         // retrieve all data fields for the collection line
1414         List<DataField> dataFields = ComponentUtils.getComponentsOfTypeDeep(collectionGroup.getAddLineFields(),
1415                 DataField.class);
1416         for (DataField dataField : dataFields) {
1417             String bindingPath = "";
1418             if (StringUtils.isNotBlank(dataField.getBindingInfo().getBindByNamePrefix())) {
1419                 bindingPath = dataField.getBindingInfo().getBindByNamePrefix() + ".";
1420             }
1421             bindingPath += dataField.getBindingInfo().getBindingName();
1422 
1423             populateDefaultValueForField(view, line, dataField, bindingPath);
1424         }
1425     }
1426 
1427     /**
1428      * Iterates through the view components picking up data fields and applying an default value configured
1429      *
1430      * @param view - view instance we are applying default values for
1431      * @param component - component that should be checked for default values
1432      * @param model - model object that values should be set on
1433      */
1434     protected void applyDefaultValues(View view, Component component, Object model) {
1435         if (component == null) {
1436             return;
1437         }
1438 
1439         // if component is a data field apply default value
1440         if (component instanceof DataField) {
1441             DataField dataField = ((DataField) component);
1442 
1443             // need to make sure binding is initialized since this could be on a page we have not initialized yet
1444             dataField.getBindingInfo().setDefaults(view, dataField.getPropertyName());
1445 
1446             populateDefaultValueForField(view, model, dataField, dataField.getBindingInfo().getBindingPath());
1447         }
1448 
1449         List<Component> nestedComponents = component.getComponentsForLifecycle();
1450 
1451         // if view, need to add all pages since only one will be on the lifecycle
1452         if (component instanceof View) {
1453             nestedComponents.addAll(((View) component).getItems());
1454         }
1455 
1456         for (Component nested : nestedComponents) {
1457             applyDefaultValues(view, nested, model);
1458         }
1459     }
1460 
1461     /**
1462      * Applies the default value configured for the given field (if any) to the
1463      * line given object property that is determined by the given binding path
1464      *
1465      * <p>
1466      * Checks for a configured default value or default value class for the
1467      * field. If both are given, the configured static default value will win.
1468      * In addition, if the default value contains an el expression it is
1469      * evaluated against the initial context
1470      * </p>
1471      *
1472      * @param view - view instance the field belongs to
1473      * @param object - object that should be populated
1474      * @param dataField - field to check for configured default value
1475      * @param bindingPath - path to the property on the object that should be populated
1476      */
1477     protected void populateDefaultValueForField(View view, Object object, DataField dataField, String bindingPath) {
1478         // check for configured default value
1479         String defaultValue = dataField.getDefaultValue();
1480         if (StringUtils.isBlank(defaultValue) && (dataField.getDefaultValueFinderClass() != null)) {
1481             ValueFinder defaultValueFinder = ObjectUtils.newInstance(dataField.getDefaultValueFinderClass());
1482             defaultValue = defaultValueFinder.getValue();
1483         }
1484 
1485         // populate default value if given and path is valid
1486         if (StringUtils.isNotBlank(defaultValue) && ObjectPropertyUtils.isWritableProperty(object, bindingPath)) {
1487             if (getExpressionEvaluatorService().containsElPlaceholder(defaultValue)) {
1488                 Map<String, Object> context = getPreModelContext(view);
1489                 defaultValue = getExpressionEvaluatorService().evaluateExpressionTemplate(null, context, defaultValue);
1490             }
1491 
1492             // TODO: this should go through our formatters
1493             ObjectPropertyUtils.setPropertyValue(object, bindingPath, defaultValue);
1494         }
1495     }
1496 
1497     /**
1498      * Hook for creating new components with code and adding them to a container
1499      *
1500      * <p>
1501      * Subclasses can override this method to check for one or more containers by id and then adding components
1502      * created in code. This is invoked before the initialize method on the container component, so the full
1503      * lifecycle will be run on the components returned.
1504      * </p>
1505      *
1506      * <p>
1507      * New components instances can be retrieved using {@link ComponentFactory}
1508      * </p>
1509      *
1510      * @param view - view instance the container belongs to
1511      * @param model - object containing the view data
1512      * @param container - container instance to add components to
1513      */
1514     protected void addCustomContainerComponents(View view, Object model, Container container) {
1515 
1516     }
1517 
1518     /**
1519      * Hook for service overrides to perform custom initialization on the
1520      * component
1521      *
1522      * @param view - view instance containing the component
1523      * @param component - component instance to initialize
1524      */
1525     protected void performCustomInitialization(View view, Component component) {
1526 
1527     }
1528 
1529     /**
1530      * Hook for service overrides to perform custom apply model logic on the
1531      * component
1532      *
1533      * @param view - view instance containing the component
1534      * @param component - component instance to apply model to
1535      * @param model - Top level object containing the data (could be the form or a
1536      * top level business object, dto)
1537      */
1538     protected void performCustomApplyModel(View view, Component component, Object model) {
1539 
1540     }
1541 
1542     /**
1543      * Hook for service overrides to perform custom component finalization
1544      *
1545      * @param view - view instance containing the component
1546      * @param component - component instance to update
1547      * @param model - Top level object containing the data
1548      * @param parent - Parent component for the component being finalized
1549      */
1550     protected void performCustomFinalize(View view, Component component, Object model, Component parent) {
1551 
1552     }
1553 
1554     /**
1555      * Hook for service overrides to process the new collection line before it
1556      * is added to the collection
1557      *
1558      * @param view - view instance that is being presented (the action was taken
1559      * on)
1560      * @param collectionGroup - collection group component for the collection the line will
1561      * be added to
1562      * @param model - object instance that contain's the views data
1563      * @param addLine - the new line instance to be processed
1564      */
1565     protected void processBeforeAddLine(View view, CollectionGroup collectionGroup, Object model, Object addLine) {
1566 
1567     }
1568 
1569     /**
1570      * Hook for service overrides to process the new collection line after it
1571      * has been added to the collection
1572      *
1573      * @param view - view instance that is being presented (the action was taken
1574      * on)
1575      * @param collectionGroup - collection group component for the collection the line that
1576      * was added
1577      * @param model - object instance that contain's the views data
1578      * @param addLine - the new line that was added
1579      */
1580     protected void processAfterAddLine(View view, CollectionGroup collectionGroup, Object model, Object addLine) {
1581 
1582     }
1583 
1584     /**
1585      * Hook for service overrides to process the collection line after it has been deleted
1586      *
1587      * @param view - view instance that is being presented (the action was taken on)
1588      * @param collectionGroup - collection group component for the collection the line that
1589      * was added
1590      * @param model - object instance that contains the views data
1591      * @param lineIndex - index of the line that was deleted
1592      */
1593     protected void processAfterDeleteLine(View view, CollectionGroup collectionGroup, Object model, int lineIndex) {
1594 
1595     }
1596 
1597     protected void logAndThrowRuntime(String message) {
1598         LOG.error(message);
1599         throw new RuntimeException(message);
1600     }
1601 
1602     protected DataDictionaryService getDataDictionaryService() {
1603         if (this.dataDictionaryService == null) {
1604             this.dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
1605         }
1606 
1607         return this.dataDictionaryService;
1608     }
1609 
1610     public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
1611         this.dataDictionaryService = dataDictionaryService;
1612     }
1613 
1614     protected ExpressionEvaluatorService getExpressionEvaluatorService() {
1615         if (this.expressionEvaluatorService == null) {
1616             this.expressionEvaluatorService = KRADServiceLocatorWeb.getExpressionEvaluatorService();
1617         }
1618 
1619         return this.expressionEvaluatorService;
1620     }
1621 
1622     public void setExpressionEvaluatorService(ExpressionEvaluatorService expressionEvaluatorService) {
1623         this.expressionEvaluatorService = expressionEvaluatorService;
1624     }
1625 
1626     public ViewDictionaryService getViewDictionaryService() {
1627         if (this.viewDictionaryService == null) {
1628             this.viewDictionaryService = KRADServiceLocatorWeb.getViewDictionaryService();
1629         }
1630         return this.viewDictionaryService;
1631     }
1632 
1633     public void setViewDictionaryService(ViewDictionaryService viewDictionaryService) {
1634         this.viewDictionaryService = viewDictionaryService;
1635     }
1636 }