View Javadoc

1   /**
2    * Copyright 2005-2013 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.config.property.ConfigurationService;
20  import org.kuali.rice.core.api.exception.RiceRuntimeException;
21  import org.kuali.rice.kim.api.identity.Person;
22  import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
23  import org.kuali.rice.krad.datadictionary.AttributeDefinition;
24  import org.kuali.rice.krad.inquiry.Inquirable;
25  import org.kuali.rice.krad.messages.MessageService;
26  import org.kuali.rice.krad.service.DataDictionaryService;
27  import org.kuali.rice.krad.service.KRADServiceLocator;
28  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
29  import org.kuali.rice.krad.service.ModuleService;
30  import org.kuali.rice.krad.uif.UifConstants;
31  import org.kuali.rice.krad.uif.component.ComponentSecurity;
32  import org.kuali.rice.krad.uif.container.Group;
33  import org.kuali.rice.krad.uif.element.Action;
34  import org.kuali.rice.krad.uif.field.ActionField;
35  import org.kuali.rice.krad.uif.field.FieldGroup;
36  import org.kuali.rice.krad.uif.util.ViewCleaner;
37  import org.kuali.rice.krad.uif.view.ViewAuthorizer;
38  import org.kuali.rice.krad.uif.view.ViewPresentationController;
39  import org.kuali.rice.krad.uif.component.BindingInfo;
40  import org.kuali.rice.krad.uif.component.ClientSideState;
41  import org.kuali.rice.krad.uif.component.Component;
42  import org.kuali.rice.krad.uif.component.DataBinding;
43  import org.kuali.rice.krad.uif.component.PropertyReplacer;
44  import org.kuali.rice.krad.uif.component.RequestParameter;
45  import org.kuali.rice.krad.uif.container.CollectionGroup;
46  import org.kuali.rice.krad.uif.container.Container;
47  import org.kuali.rice.krad.uif.control.Control;
48  import org.kuali.rice.krad.uif.field.DataField;
49  import org.kuali.rice.krad.uif.field.InputField;
50  import org.kuali.rice.krad.uif.field.Field;
51  import org.kuali.rice.krad.uif.field.RemoteFieldsHolder;
52  import org.kuali.rice.krad.uif.layout.LayoutManager;
53  import org.kuali.rice.krad.uif.modifier.ComponentModifier;
54  import org.kuali.rice.krad.uif.service.ExpressionEvaluatorService;
55  import org.kuali.rice.krad.uif.service.ViewDictionaryService;
56  import org.kuali.rice.krad.uif.service.ViewHelperService;
57  import org.kuali.rice.krad.uif.util.BooleanMap;
58  import org.kuali.rice.krad.uif.util.CloneUtils;
59  import org.kuali.rice.krad.uif.util.ComponentFactory;
60  import org.kuali.rice.krad.uif.util.ComponentUtils;
61  import org.kuali.rice.krad.uif.util.ExpressionUtils;
62  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
63  import org.kuali.rice.krad.uif.util.ScriptUtils;
64  import org.kuali.rice.krad.uif.util.ViewModelUtils;
65  import org.kuali.rice.krad.uif.view.View;
66  import org.kuali.rice.krad.uif.view.ViewModel;
67  import org.kuali.rice.krad.uif.widget.Inquiry;
68  import org.kuali.rice.krad.uif.widget.Widget;
69  import org.kuali.rice.krad.util.ErrorMessage;
70  import org.kuali.rice.krad.util.GlobalVariables;
71  import org.kuali.rice.krad.util.GrowlMessage;
72  import org.kuali.rice.krad.util.KRADConstants;
73  import org.kuali.rice.krad.util.MessageMap;
74  import org.kuali.rice.krad.util.ObjectUtils;
75  import org.kuali.rice.krad.valuefinder.ValueFinder;
76  import org.kuali.rice.krad.web.form.UifFormBase;
77  import org.springframework.util.ClassUtils;
78  import org.springframework.util.MethodInvoker;
79  
80  import java.io.Serializable;
81  import java.lang.annotation.Annotation;
82  import java.text.MessageFormat;
83  import java.util.ArrayList;
84  import java.util.Collection;
85  import java.util.Collections;
86  import java.util.HashMap;
87  import java.util.HashSet;
88  import java.util.List;
89  import java.util.Map;
90  import java.util.Map.Entry;
91  import java.util.Set;
92  
93  /**
94   * Default Implementation of <code>ViewHelperService</code>
95   *
96   * @author Kuali Rice Team (rice.collab@kuali.org)
97   */
98  public class ViewHelperServiceImpl implements ViewHelperService, Serializable {
99      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ViewHelperServiceImpl.class);
100 
101     private transient DataDictionaryService dataDictionaryService;
102     private transient ExpressionEvaluatorService expressionEvaluatorService;
103     private transient ViewDictionaryService viewDictionaryService;
104     private transient ConfigurationService configurationService;
105 
106     /**
107      * Uses reflection to find all fields defined on the <code>View</code> instance that have
108      * the <code>RequestParameter</code> annotation (which indicates the field may be populated by the request).
109      *
110      * <p>
111      * For each field found, if there is a corresponding key/value pair in the request parameters,
112      * the value is used to populate the field. In addition, any conditional properties of
113      * <code>PropertyReplacers</code> configured for the field are cleared so that the request parameter
114      * value does not get overridden by the dictionary conditional logic
115      * </p>
116      *
117      * @see org.kuali.rice.krad.uif.service.ViewHelperService#populateViewFromRequestParameters(org.kuali.rice.krad.uif.view.View,
118      *      java.util.Map)
119      */
120     @Override
121     public void populateViewFromRequestParameters(View view, Map<String, String> parameters) {
122         // build Map of property replacers by property name so that we can remove them
123         // if the property was set by a request parameter
124         Map<String, Set<PropertyReplacer>> viewPropertyReplacers = new HashMap<String, Set<PropertyReplacer>>();
125         for (PropertyReplacer replacer : view.getPropertyReplacers()) {
126             Set<PropertyReplacer> propertyReplacers = new HashSet<PropertyReplacer>();
127             if (viewPropertyReplacers.containsKey(replacer.getPropertyName())) {
128                 propertyReplacers = viewPropertyReplacers.get(replacer.getPropertyName());
129             }
130             propertyReplacers.add(replacer);
131 
132             viewPropertyReplacers.put(replacer.getPropertyName(), propertyReplacers);
133         }
134 
135         Map<String, Annotation> annotatedFields = CloneUtils.getFieldsWithAnnotation(view.getClass(),
136                 RequestParameter.class);
137 
138         // for each request parameter allowed on the view, if the request contains a value use
139         // to set on View, and clear and conditional expressions or property replacers for that field
140         Map<String, String> viewRequestParameters = new HashMap<String, String>();
141         for (String fieldToPopulate : annotatedFields.keySet()) {
142             RequestParameter requestParameter = (RequestParameter) annotatedFields.get(fieldToPopulate);
143 
144             // use specified parameter name if given, else use field name to retrieve parameter value
145             String requestParameterName = requestParameter.parameterName();
146             if (StringUtils.isBlank(requestParameterName)) {
147                 requestParameterName = fieldToPopulate;
148             }
149 
150             if (!parameters.containsKey(requestParameterName)) {
151                 continue;
152             }
153 
154             String fieldValue = parameters.get(requestParameterName);
155             if (StringUtils.isNotBlank(fieldValue)) {
156                 viewRequestParameters.put(requestParameterName, fieldValue);
157                 ObjectPropertyUtils.setPropertyValue(view, fieldToPopulate, fieldValue);
158 
159                 // remove any conditional configuration so value is not
160                 // overridden later during the apply model phase
161                 if (view.getPropertyExpressions().containsKey(fieldToPopulate)) {
162                     view.getPropertyExpressions().remove(fieldToPopulate);
163                 }
164 
165                 if (viewPropertyReplacers.containsKey(fieldToPopulate)) {
166                     Set<PropertyReplacer> propertyReplacers = viewPropertyReplacers.get(fieldToPopulate);
167                     for (PropertyReplacer replacer : propertyReplacers) {
168                         view.getPropertyReplacers().remove(replacer);
169                     }
170                 }
171             }
172         }
173 
174         view.setViewRequestParameters(viewRequestParameters);
175     }
176 
177     /**
178      * @see org.kuali.rice.krad.uif.service.ViewHelperService#performInitialization(org.kuali.rice.krad.uif.view.View,
179      *      java.lang.Object)
180      */
181     @Override
182     public void performInitialization(View view, Object model) {
183         view.assignComponentIds(view);
184 
185         // increment the id sequence so components added later to the static view components
186         // will not conflict with components on the page when navigation happens
187         view.setIdSequence(100000);
188         performComponentInitialization(view, model, view);
189 
190         // get the list of dialogs from the view and then set the refreshedByAction on the dialog to true.
191         // This will leave the component in the viewIndex to be updated using an AJAX call
192         // TODO: Figure out a better way to store dialogs only if it is rendered using an ajax request
193         for (Component dialog : view.getDialogs()) {
194             dialog.setRefreshedByAction(true);
195         }
196     }
197 
198     /**
199      * Performs the complete component lifecycle on the component passed in, in this order:
200      * performComponentInitialization, performComponentApplyModel, and performComponentFinalize.
201      *
202      * @see {@link org.kuali.rice.krad.uif.service.ViewHelperService#performComponentLifecycle(
203      *org.kuali.rice.krad.uif.view.View, java.lang.Object, org.kuali.rice.krad.uif.component.Component,
204      *      java.lang.String)
205      * @see {@link #performComponentInitialization(org.kuali.rice.krad.uif.view.View, Object,
206      *      org.kuali.rice.krad.uif.component.Component)}
207      * @see {@link #performComponentApplyModel(View, Component, Object)}
208      * @see {@link #performComponentFinalize(View, Component, Object, Component, Map)}
209      */
210     public void performComponentLifecycle(View view, Object model, Component component, String origId) {
211         Component origComponent = view.getViewIndex().getComponentById(origId);
212 
213         // run through and assign any ids starting with the id for the refreshed component (this might be
214         // necessary if we are getting a new component instance from the bean factory)
215         Integer currentSequenceVal = view.getIdSequence();
216         Integer startingSequenceVal = view.getViewIndex().getIdSequenceSnapshot().get(component.getId());
217         // if the component was retrieved from the initial states map in ViewIndex, startingSequenceVal is null
218         if (startingSequenceVal != null) {
219             view.setIdSequence(startingSequenceVal);
220         }
221 
222         view.assignComponentIds(component);
223 
224         // now set back from the ending view sequence so IDs for any dynamically created (newly) will not stomp
225         // on existing components
226         view.setIdSequence(currentSequenceVal);
227 
228         String suffix = StringUtils.replaceOnce(origComponent.getId(), origComponent.getBaseId(), "");
229         if (StringUtils.isNotBlank(suffix)) {
230             ComponentUtils.updateIdWithSuffix(component, suffix);
231         }
232 
233         Component parent = (Component) origComponent.getContext().get(UifConstants.ContextVariableNames.PARENT);
234 
235         // update context on all components within the refresh component to catch context set by parent
236         component.pushAllToContext(origComponent.getContext());
237         List<Component> nestedComponents = ComponentUtils.getAllNestedComponents(component);
238         for (Component nestedComponent : nestedComponents) {
239             nestedComponent.pushAllToContext(origComponent.getContext());
240         }
241 
242         // the expression graph for refreshed components is captured in the view index (initially it might expressions
243         // might have come from a parent), after getting the expression graph then we need to populate the expressions
244         // on the configurable for which they apply
245         Map<String, String> expressionGraph = view.getViewIndex().getComponentExpressionGraphs().get(
246                 component.getBaseId());
247         component.setExpressionGraph(expressionGraph);
248         ExpressionUtils.populatePropertyExpressionsFromGraph(component, false);
249 
250         // binding path should stay the same
251         if (component instanceof DataBinding) {
252             ((DataBinding) component).setBindingInfo(((DataBinding) origComponent).getBindingInfo());
253             ((DataBinding) component).getBindingInfo().setBindingPath(
254                     ((DataBinding) origComponent).getBindingInfo().getBindingPath());
255         }
256 
257         // copy properties that are set by parent components in the full view lifecycle
258         if (component instanceof Field) {
259             ((Field) component).setLabelRendered(((Field) origComponent).isLabelRendered());
260         } else if (component instanceof CollectionGroup) {
261             String subCollectionSuffix = ((CollectionGroup) origComponent).getSubCollectionSuffix();
262             ((CollectionGroup) component).setSubCollectionSuffix(subCollectionSuffix);
263 
264             suffix = StringUtils.removeStart(suffix, subCollectionSuffix);
265         }
266 
267         if (origComponent.isRefreshedByAction()) {
268             component.setRefreshedByAction(true);
269         }
270 
271         // reset data if needed
272         if (component.isResetDataOnRefresh()) {
273             // TODO: this should handle groups as well, going through nested data fields
274             if (component instanceof DataField) {
275                 // TODO: should check default value
276 
277                 // clear value
278                 ObjectPropertyUtils.initializeProperty(model,
279                         ((DataField) component).getBindingInfo().getBindingPath());
280             }
281         }
282 
283         performComponentInitialization(view, model, component);
284         view.getViewIndex().indexComponent(component);
285 
286         Map<String, Integer> visitedIds = new HashMap<String, Integer>();
287         performComponentApplyModel(view, component, model, visitedIds);
288         view.getViewIndex().indexComponent(component);
289 
290         // adjust IDs for suffixes that might have been added by a parent component during the full view lifecycle
291         if (StringUtils.isNotBlank(suffix)) {
292             ComponentUtils.updateChildIdsWithSuffixNested(component, suffix);
293         }
294 
295         // if disclosed by action and request was made, make sure the component will display
296         if (component.isDisclosedByAction()) {
297             component.setRender(true);
298             component.setHidden(false);
299         }
300 
301         // TODO: need to handle updating client state for component refresh
302         Map<String, Object> clientState = new HashMap<String, Object>();
303         performComponentFinalize(view, component, model, parent, clientState);
304 
305         // make sure id, binding, and label settings stay the same as initial
306         if (component instanceof Group || component instanceof FieldGroup) {
307             List<Component> nestedGroupComponents = ComponentUtils.getAllNestedComponents(component);
308             List<Component> originalNestedGroupComponents = ComponentUtils.getAllNestedComponents(origComponent);
309 
310             for (Component nestedComponent : nestedGroupComponents) {
311                 Component origNestedComponent = ComponentUtils.findComponentInList(originalNestedGroupComponents,
312                         nestedComponent.getId());
313 
314                 if (origNestedComponent != null) {
315                     // update binding
316                     if (nestedComponent instanceof DataBinding) {
317                         ((DataBinding) nestedComponent).setBindingInfo(
318                                 ((DataBinding) origNestedComponent).getBindingInfo());
319                         ((DataBinding) nestedComponent).getBindingInfo().setBindingPath(
320                                 ((DataBinding) origNestedComponent).getBindingInfo().getBindingPath());
321                     }
322 
323                     // update label rendered flag
324                     if (nestedComponent instanceof Field) {
325                         ((Field) nestedComponent).setLabelRendered(((Field) origNestedComponent).isLabelRendered());
326                     }
327 
328                     if (origNestedComponent.isRefreshedByAction()) {
329                         nestedComponent.setRefreshedByAction(true);
330                     }
331                 }
332             }
333         }
334 
335         // get client state for component and build update script for on load
336         String clientStateScript = buildClientSideStateScript(view, clientState, true);
337         String onLoadScript = component.getOnLoadScript();
338         if (StringUtils.isNotBlank(onLoadScript)) {
339             clientStateScript = onLoadScript + clientStateScript;
340         }
341         component.setOnLoadScript(clientStateScript);
342 
343         // get script for generating growl messages
344         String growlScript = buildGrowlScript(view);
345         ((ViewModel) model).setGrowlScript(growlScript);
346 
347         view.getViewIndex().indexComponent(component);
348     }
349 
350     /**
351      * Performs initialization of a component by these steps:
352      *
353      * <ul>
354      * <li>For <code>DataField</code> instances, set defaults from the data
355      * dictionary.</li>
356      * <li>Invoke the initialize method on the component. Here the component can
357      * setup defaults and do other initialization that is specific to that
358      * component.</li>
359      * <li>Invoke any configured <code>ComponentModifier</code> instances for
360      * the component.</li>
361      * <li>Call the component to get the List of components that are nested
362      * within and recursively call this method to initialize those components.</li>
363      * <li>Call custom initialize hook for service overrides</li>
364      * </ul>
365      *
366      * <p>
367      * Note the order various initialize points are called, this can sometimes
368      * be an important factor to consider when initializing a component
369      * </p>
370      *
371      * @throws RiceRuntimeException if the component id or factoryId is not specified
372      * @see org.kuali.rice.krad.uif.service.ViewHelperService#performComponentInitialization(org.kuali.rice.krad.uif.view.View,
373      *      java.lang.Object, org.kuali.rice.krad.uif.component.Component)
374      */
375     public void performComponentInitialization(View view, Object model, Component component) {
376         if (component == null) {
377             return;
378         }
379 
380         if (StringUtils.isBlank(component.getId())) {
381             throw new RiceRuntimeException("Id is not set, this should not happen unless a component is misconfigured");
382         }
383 
384         // TODO: duplicate ID check
385 
386         LOG.debug("Initializing component: " + component.getId() + " with type: " + component.getClass());
387 
388         // add initial state to the view index for component refreshes
389         if (!(component instanceof View)) {
390             view.getViewIndex().addInitialComponentStateIfNeeded(component);
391         }
392 
393         // the component can have an expression graph for which the expressions need pulled to
394         // the list the expression service will evaluate
395         ExpressionUtils.populatePropertyExpressionsFromGraph(component, true);
396 
397         // invoke component to initialize itself after properties have been set
398         component.performInitialization(view, model);
399 
400         // move expressions on property replacers and component modifiers
401         for (PropertyReplacer replacer : component.getPropertyReplacers()) {
402             ExpressionUtils.populatePropertyExpressionsFromGraph(replacer, true);
403         }
404 
405         for (ComponentModifier modifier : component.getComponentModifiers()) {
406             ExpressionUtils.populatePropertyExpressionsFromGraph(modifier, true);
407         }
408 
409         // for attribute fields, set defaults from dictionary entry
410         if (component instanceof DataField) {
411             initializeDataFieldFromDataDictionary(view, (DataField) component);
412         }
413 
414         if (component instanceof Container) {
415             LayoutManager layoutManager = ((Container) component).getLayoutManager();
416 
417             // invoke hook point for adding components through code
418             addCustomContainerComponents(view, model, (Container) component);
419 
420             // process any remote fields holder that might be in the containers items, collection items will get
421             // processed as the lines are being built
422             if (!(component instanceof CollectionGroup)) {
423                 processAnyRemoteFieldsHolder(view, model, (Container) component);
424             }
425         }
426 
427         // for collection groups set defaults from dictionary entry
428         if (component instanceof CollectionGroup) {
429             // TODO: initialize from dictionary
430         }
431 
432         // invoke initialize service hook
433         performCustomInitialization(view, component);
434 
435         // invoke component modifiers setup to run in the initialize phase
436         runComponentModifiers(view, component, null, UifConstants.ViewPhases.INITIALIZE);
437 
438         // initialize nested components
439         for (Component nestedComponent : component.getComponentsForLifecycle()) {
440             performComponentInitialization(view, model, nestedComponent);
441         }
442 
443         // initialize component prototypes
444         for (Component nestedComponent : component.getComponentPrototypes()) {
445             performComponentInitialization(view, model, nestedComponent);
446         }
447     }
448 
449     /**
450      * Iterates through the containers configured items checking for <code>RemotableFieldsHolder</code>, if found
451      * the holder is invoked to retrieved the remotable fields and translate to attribute fields. The translated list
452      * is then inserted into the container item list at the position of the holder
453      *
454      * @param view - view instance containing the container
455      * @param model - object instance containing the view data
456      * @param container - container instance to check for any remotable fields holder
457      */
458     protected void processAnyRemoteFieldsHolder(View view, Object model, Container container) {
459         List<Component> processedItems = new ArrayList<Component>();
460 
461         // check for holders and invoke to retrieve the remotable fields and translate
462         // translated fields are placed into the container item list at the position of the holder
463         for (Component item : container.getItems()) {
464             if (item instanceof RemoteFieldsHolder) {
465                 List<InputField> translatedFields = ((RemoteFieldsHolder) item).fetchAndTranslateRemoteFields(view,
466                         model, container);
467                 processedItems.addAll(translatedFields);
468             } else {
469                 processedItems.add(item);
470             }
471         }
472 
473         // updated container items
474         container.setItems(processedItems);
475     }
476 
477     /**
478      * Sets properties of the <code>InputField</code> (if blank) to the
479      * corresponding attribute entry in the data dictionary
480      *
481      * @param view - view instance containing the field
482      * @param field - data field instance to initialize
483      */
484     protected void initializeDataFieldFromDataDictionary(View view, DataField field) {
485         AttributeDefinition attributeDefinition = null;
486 
487         String dictionaryAttributeName = field.getDictionaryAttributeName();
488         String dictionaryObjectEntry = field.getDictionaryObjectEntry();
489 
490         // if entry given but not attribute name, use field name as attribute
491         // name
492         if (StringUtils.isNotBlank(dictionaryObjectEntry) && StringUtils.isBlank(dictionaryAttributeName)) {
493             dictionaryAttributeName = field.getPropertyName();
494         }
495 
496         // if dictionary entry and attribute set, attempt to find definition
497         if (StringUtils.isNotBlank(dictionaryAttributeName) && StringUtils.isNotBlank(dictionaryObjectEntry)) {
498             attributeDefinition = getDataDictionaryService().getAttributeDefinition(dictionaryObjectEntry,
499                     dictionaryAttributeName);
500         }
501 
502         // if definition not found, recurse through path
503         if (attributeDefinition == null) {
504             String propertyPath = field.getBindingInfo().getBindingPath();
505             if (StringUtils.isNotBlank(field.getBindingInfo().getCollectionPath())) {
506                 propertyPath = field.getBindingInfo().getCollectionPath();
507                 if (StringUtils.isNotBlank(field.getBindingInfo().getBindByNamePrefix())) {
508                     propertyPath += "." + field.getBindingInfo().getBindByNamePrefix();
509                 }
510                 propertyPath += "." + field.getBindingInfo().getBindingName();
511             }
512 
513             attributeDefinition = findNestedDictionaryAttribute(view, field, null, propertyPath);
514         }
515 
516         // if a definition was found, initialize field from definition
517         if (attributeDefinition != null) {
518             field.copyFromAttributeDefinition(view, attributeDefinition);
519         }
520 
521         // if control still null, assign default
522         if (field instanceof InputField) {
523             InputField inputField = (InputField) field;
524             if (inputField.getControl() == null) {
525                 Control control = ComponentFactory.getTextControl();
526                 view.assignComponentIds(control);
527 
528                 inputField.setControl(control);
529             }
530         }
531     }
532 
533     /**
534      * Recursively drills down the property path (if nested) to find an
535      * AttributeDefinition, the first attribute definition found will be
536      * returned
537      *
538      * <p>
539      * e.g. suppose parentPath is 'document' and propertyPath is
540      * 'account.subAccount.name', first the property type for document will be
541      * retrieved using the view metadata and used as the dictionary entry, with
542      * the propertyPath as the dictionary attribute, if an attribute definition
543      * exists it will be returned. Else, the first part of the property path is
544      * added to the parent, making the parentPath 'document.account' and the
545      * propertyPath 'subAccount.name', the method is then called again to
546      * perform the process with those parameters. The recursion continues until
547      * an attribute field is found, or the propertyPath is no longer nested
548      * </p>
549      *
550      * @param view - view instance containing the field
551      * @param field - field we are attempting to find a supporting attribute
552      * definition for
553      * @param parentPath - parent path to use for getting the dictionary entry
554      * @param propertyPath - path of the property relative to the parent, to use as
555      * dictionary attribute and to drill down on
556      * @return AttributeDefinition if found, or Null
557      */
558     protected AttributeDefinition findNestedDictionaryAttribute(View view, DataField field, String parentPath,
559             String propertyPath) {
560         AttributeDefinition attributeDefinition = null;
561 
562         // attempt to find definition for parent and property
563         String dictionaryAttributeName = propertyPath;
564         String dictionaryObjectEntry = null;
565 
566         if (field.getBindingInfo().isBindToMap()) {
567             parentPath = "";
568             if (!field.getBindingInfo().isBindToForm() && StringUtils.isNotBlank(
569                     field.getBindingInfo().getBindingObjectPath())) {
570                 parentPath = field.getBindingInfo().getBindingObjectPath();
571             }
572             if (StringUtils.isNotBlank(field.getBindingInfo().getBindByNamePrefix())) {
573                 if (StringUtils.isNotBlank(parentPath)) {
574                     parentPath += "." + field.getBindingInfo().getBindByNamePrefix();
575                 } else {
576                     parentPath = field.getBindingInfo().getBindByNamePrefix();
577                 }
578             }
579 
580             dictionaryAttributeName = field.getBindingInfo().getBindingName();
581         }
582 
583         if (StringUtils.isNotBlank(parentPath)) {
584             Class<?> dictionaryModelClass = ViewModelUtils.getPropertyTypeByClassAndView(view, parentPath);
585             if (dictionaryModelClass != null) {
586                 dictionaryObjectEntry = dictionaryModelClass.getName();
587 
588                 attributeDefinition = getDataDictionaryService().getAttributeDefinition(dictionaryObjectEntry,
589                         dictionaryAttributeName);
590             }
591         }
592 
593         // if definition not found and property is still nested, recurse down
594         // one level
595         if ((attributeDefinition == null) && StringUtils.contains(propertyPath, ".")) {
596             String nextParentPath = StringUtils.substringBefore(propertyPath, ".");
597             if (StringUtils.isNotBlank(parentPath)) {
598                 nextParentPath = parentPath + "." + nextParentPath;
599             }
600             String nextPropertyPath = StringUtils.substringAfter(propertyPath, ".");
601 
602             return findNestedDictionaryAttribute(view, field, nextParentPath, nextPropertyPath);
603         }
604 
605         // if a definition was found, update the fields dictionary properties
606         if (attributeDefinition != null) {
607             field.setDictionaryAttributeName(dictionaryAttributeName);
608             field.setDictionaryObjectEntry(dictionaryObjectEntry);
609         }
610 
611         return attributeDefinition;
612     }
613 
614     /**
615      * @see org.kuali.rice.krad.uif.service.ViewHelperService#performApplyModel(org.kuali.rice.krad.uif.view.View,
616      *      java.lang.Object)
617      */
618     @Override
619     public void performApplyModel(View view, Object model) {
620         // get action flag and edit modes from authorizer/presentation controller
621         retrieveEditModesAndActionFlags(view, (UifFormBase) model);
622 
623         // set view context for conditional expressions
624         setViewContext(view, model);
625 
626         Map<String, Integer> visitedIds = new HashMap<String, Integer>();
627         performComponentApplyModel(view, view, model, visitedIds);
628     }
629 
630     /**
631      * Invokes the configured <code>PresentationController</code> and
632      * </code>Authorizer</code> for the view to get the exported action flags
633      * and edit modes that can be used in conditional logic
634      *
635      * @param view - view instance that is being built and presentation/authorizer pulled for
636      * @param model - Object that contains the model data
637      */
638     protected void retrieveEditModesAndActionFlags(View view, UifFormBase model) {
639         ViewPresentationController presentationController = view.getPresentationController();
640         ViewAuthorizer authorizer = view.getAuthorizer();
641 
642         Person user = GlobalVariables.getUserSession().getPerson();
643 
644         Set<String> actionFlags = presentationController.getActionFlags(view, model);
645         actionFlags = authorizer.getActionFlags(view, model, user, actionFlags);
646 
647         view.setActionFlags(new BooleanMap(actionFlags));
648 
649         Set<String> editModes = presentationController.getEditModes(view, model);
650         editModes = authorizer.getEditModes(view, model, user, editModes);
651 
652         view.setEditModes(new BooleanMap(editModes));
653     }
654 
655     /**
656      * Sets up the view context which will be available to other components
657      * through their context for conditional logic evaluation
658      *
659      * @param view - view instance to set context for
660      * @param model - object containing the view data
661      */
662     protected void setViewContext(View view, Object model) {
663         view.pushAllToContext(getPreModelContext(view));
664 
665         // evaluate view expressions for further context
666         for (Entry<String, String> variableExpression : view.getExpressionVariables().entrySet()) {
667             String variableName = variableExpression.getKey();
668             Object value = getExpressionEvaluatorService().evaluateExpression(model, view.getContext(),
669                     variableExpression.getValue());
670             view.pushObjectToContext(variableName, value);
671         }
672     }
673 
674     /**
675      * Returns the general context that is available before the apply model
676      * phase (during the initialize phase)
677      *
678      * @param view - view instance for context
679      * @return Map<String, Object> context map
680      */
681     protected Map<String, Object> getPreModelContext(View view) {
682         Map<String, Object> context = new HashMap<String, Object>();
683 
684         context.put(UifConstants.ContextVariableNames.VIEW, view);
685         context.put(UifConstants.ContextVariableNames.VIEW_HELPER, this);
686 
687         Map<String, String> properties = KRADServiceLocator.getKualiConfigurationService().getAllProperties();
688         context.put(UifConstants.ContextVariableNames.CONFIG_PROPERTIES, properties);
689         context.put(UifConstants.ContextVariableNames.CONSTANTS, KRADConstants.class);
690         context.put(UifConstants.ContextVariableNames.UIF_CONSTANTS, UifConstants.class);
691 
692         return context;
693     }
694 
695     /**
696      * Applies the model data to a component of the View instance
697      *
698      * <p>
699      * The component is invoked to to apply the model data. Here the component
700      * can generate any additional fields needed or alter the configured fields.
701      * After the component is invoked a hook for custom helper service
702      * processing is invoked. Finally the method is recursively called for all
703      * the component children
704      * </p>
705      *
706      * @param view view instance the component belongs to
707      * @param component the component instance the model should be applied to
708      * @param model top level object containing the data
709      * @param visitedIds tracks components ids that have been seen for adjusting duplicates
710      */
711     protected void performComponentApplyModel(View view, Component component, Object model,
712             Map<String, Integer> visitedIds) {
713         if (component == null) {
714             return;
715         }
716 
717         // set context on component for evaluating expressions
718         component.pushAllToContext(getCommonContext(view, component));
719 
720         for (PropertyReplacer replacer : component.getPropertyReplacers()) {
721             getExpressionEvaluatorService().evaluateExpressionsOnConfigurable(view, replacer, model,
722                     component.getContext());
723         }
724 
725         for (ComponentModifier modifier : component.getComponentModifiers()) {
726             getExpressionEvaluatorService().evaluateExpressionsOnConfigurable(view, modifier, model,
727                     component.getContext());
728         }
729 
730         getExpressionEvaluatorService().evaluateExpressionsOnConfigurable(view, component, model,
731                 component.getContext());
732 
733         // evaluate expressions on component security
734         ComponentSecurity componentSecurity = component.getComponentSecurity();
735         getExpressionEvaluatorService().evaluateExpressionsOnConfigurable(view, componentSecurity, model,
736                 component.getContext());
737 
738         // evaluate expressions on the binding info object
739         if (component instanceof DataBinding) {
740             BindingInfo bindingInfo = ((DataBinding) component).getBindingInfo();
741             getExpressionEvaluatorService().evaluateExpressionsOnConfigurable(view, bindingInfo, model,
742                     component.getContext());
743         }
744 
745         // set context evaluate expressions on the layout manager
746         if (component instanceof Container) {
747             LayoutManager layoutManager = ((Container) component).getLayoutManager();
748 
749             if (layoutManager != null) {
750                 layoutManager.getContext().putAll(getCommonContext(view, component));
751                 layoutManager.pushObjectToContext(UifConstants.ContextVariableNames.PARENT, component);
752                 layoutManager.pushObjectToContext(UifConstants.ContextVariableNames.MANAGER, layoutManager);
753 
754                 getExpressionEvaluatorService().evaluateExpressionsOnConfigurable(view, layoutManager, model,
755                         layoutManager.getContext());
756 
757                 layoutManager.setId(adjustIdIfNecessary(layoutManager.getId(), visitedIds));
758             }
759         }
760 
761         // sync the component with previous client side state
762         syncClientSideStateForComponent(component, ((ViewModel) model).getClientStateForSyncing());
763 
764         // invoke authorizer and presentation controller to set component state
765         applyAuthorizationAndPresentationLogic(view, component, (ViewModel) model);
766 
767         // adjust ids for duplicates if necessary
768         //component.setId(adjustIdIfNecessary(component.getId(), visitedIds));
769 
770         // invoke component to perform its conditional logic
771         Component parent = (Component) component.getContext().get(UifConstants.ContextVariableNames.PARENT);
772         component.performApplyModel(view, model, parent);
773 
774         // invoke service override hook
775         performCustomApplyModel(view, component, model);
776 
777         // invoke component modifiers configured to run in the apply model phase
778         runComponentModifiers(view, component, model, UifConstants.ViewPhases.APPLY_MODEL);
779 
780         // get children and recursively perform conditional logic
781         for (Component nestedComponent : component.getComponentsForLifecycle()) {
782             if (nestedComponent != null) {
783                 nestedComponent.pushObjectToContext(UifConstants.ContextVariableNames.PARENT, component);
784                 performComponentApplyModel(view, nestedComponent, model, visitedIds);
785             }
786         }
787     }
788 
789     /**
790      * Checks against the visited ids to see if the id is duplicate, if so it is adjusted to make
791      * an unique id by appending an unique sequence number
792      *
793      * @param id id to adjust if necessary
794      * @param visitedIds tracks components ids that have been seen for adjusting duplicates
795      * @return String original or adjusted id
796      */
797     protected String adjustIdIfNecessary(String id, Map<String, Integer> visitedIds) {
798         String adjustedId = id;
799 
800         if (visitedIds.containsKey(id)) {
801             Integer nextAdjustSeq = visitedIds.get(id);
802             adjustedId = id + nextAdjustSeq;
803 
804             // verify the adjustedId does not already exist
805             while (visitedIds.containsKey(adjustedId)) {
806                 nextAdjustSeq = nextAdjustSeq + 1;
807                 adjustedId = id + nextAdjustSeq;
808             }
809 
810             visitedIds.put(adjustedId, new Integer(1));
811 
812             nextAdjustSeq = nextAdjustSeq + 1;
813             visitedIds.put(id, nextAdjustSeq);
814         } else {
815             visitedIds.put(id, new Integer(1));
816         }
817 
818         return adjustedId;
819     }
820 
821     /**
822      * Invokes the view's configured {@link ViewAuthorizer} and {@link ViewPresentationController} to set state of
823      * the component
824      *
825      * <p>
826      * The following authorization is done here:
827      * Fields: edit, view, required, mask, and partial mask
828      * Groups: edit and view
829      * Actions: take action
830      * </p>
831      *
832      * <p>
833      * Note additional checks are also done for fields that are part of a collection group. This authorization is
834      * found in {@link org.kuali.rice.krad.uif.container.CollectionGroupBuilder}
835      * </p>
836      *
837      * @param view - view instance the component belongs to and from which the authorizer and presentation controller
838      * will be pulled
839      * @param component - component instance to authorize
840      * @param model - model object containing the data for the view
841      */
842     protected void applyAuthorizationAndPresentationLogic(View view, Component component, ViewModel model) {
843         ViewPresentationController presentationController = view.getPresentationController();
844         ViewAuthorizer authorizer = view.getAuthorizer();
845 
846         Person user = GlobalVariables.getUserSession().getPerson();
847 
848         // if component not flagged for render no need to check auth and controller logic
849         if (!component.isRender()) {
850             return;
851         }
852 
853         // check top level view edit authorization
854         if (component instanceof View) {
855             if (!view.isReadOnly()) {
856                 boolean canEditView = authorizer.canEditView(view, model, user);
857                 if (canEditView) {
858                     canEditView = presentationController.canEditView(view, model);
859                 }
860                 view.setReadOnly(!canEditView);
861             }
862         }
863 
864         // perform group authorization and presentation logic
865         else if (component instanceof Group) {
866             Group group = (Group) component;
867 
868             // if group is not hidden, do authorization for viewing the group
869             if (!group.isHidden()) {
870                 boolean canViewGroup = authorizer.canViewGroup(view, model, group, group.getId(), user);
871                 if (canViewGroup) {
872                     canViewGroup = presentationController.canViewGroup(view, model, group, group.getId());
873                 }
874                 group.setHidden(!canViewGroup);
875                 group.setRender(canViewGroup);
876             }
877 
878             // if group is editable, do authorization for editing the group
879             if (!group.isReadOnly()) {
880                 boolean canEditGroup = authorizer.canEditGroup(view, model, group, group.getId(), user);
881                 if (canEditGroup) {
882                     canEditGroup = presentationController.canEditGroup(view, model, group, group.getId());
883                 }
884                 group.setReadOnly(!canEditGroup);
885             }
886         }
887 
888         // perform field authorization and presentation logic
889         else if (component instanceof Field && !(component instanceof ActionField)) {
890             Field field = (Field) component;
891 
892             String propertyName = null;
893             if (field instanceof DataBinding) {
894                 propertyName = ((DataBinding) field).getPropertyName();
895             }
896 
897             // if field is not hidden, do authorization for viewing the field
898             if (!field.isHidden()) {
899                 boolean canViewField = authorizer.canViewField(view, model, field, propertyName, user);
900                 if (canViewField) {
901                     canViewField = presentationController.canViewField(view, model, field, propertyName);
902                 }
903                 field.setHidden(!canViewField);
904                 field.setRender(canViewField);
905             }
906 
907             // if field is not readOnly, check edit authorization
908             if (!field.isReadOnly()) {
909                 // check field edit authorization
910                 boolean canEditField = authorizer.canEditField(view, model, field, propertyName, user);
911                 if (canEditField) {
912                     canEditField = presentationController.canEditField(view, model, field, propertyName);
913                 }
914                 field.setReadOnly(!canEditField);
915             }
916 
917             // if field is not already required, invoke presentation logic to determine if it should be
918             if ((field.getRequired() == null) || !field.getRequired().booleanValue()) {
919                 boolean fieldIsRequired = presentationController.fieldIsRequired(view, model, field, propertyName);
920             }
921 
922             if (field instanceof DataField) {
923                 DataField dataField = (DataField) field;
924 
925                 // check mask authorization
926                 boolean canUnmaskValue = authorizer.canUnmaskField(view, model, dataField, dataField.getPropertyName(),
927                         user);
928                 if (!canUnmaskValue) {
929                     dataField.setApplyMask(true);
930                     dataField.setMaskFormatter(dataField.getDataFieldSecurity().getAttributeSecurity().
931                             getMaskFormatter());
932                 } else {
933                     // check partial mask authorization
934                     boolean canPartiallyUnmaskValue = authorizer.canPartialUnmaskField(view, model, dataField,
935                             dataField.getPropertyName(), user);
936                     if (!canPartiallyUnmaskValue) {
937                         dataField.setApplyMask(true);
938                         dataField.setMaskFormatter(
939                                 dataField.getDataFieldSecurity().getAttributeSecurity().getPartialMaskFormatter());
940                     }
941                 }
942             }
943         }
944 
945         // perform action authorization and presentation logic
946         else if (component instanceof ActionField || component instanceof Action) {
947             Action action = null;
948             if (component instanceof ActionField) {
949                 action = ((ActionField) component).getAction();
950             } else {
951                 action = (Action) component;
952             }
953 
954             boolean canTakeAction = authorizer.canPerformAction(view, model, action, action.getActionEvent(),
955                     action.getId(), user);
956             if (canTakeAction) {
957                 canTakeAction = presentationController.canPerformAction(view, model, action, action.getActionEvent(),
958                         action.getId());
959             }
960             action.setRender(canTakeAction);
961         }
962 
963         // perform widget authorization and presentation logic
964         else if (component instanceof Widget) {
965             Widget widget = (Widget) component;
966 
967             // if widget is not hidden, do authorization for viewing the widget
968             if (!widget.isHidden()) {
969                 boolean canViewWidget = authorizer.canViewWidget(view, model, widget, widget.getId(), user);
970                 if (canViewWidget) {
971                     canViewWidget = presentationController.canViewWidget(view, model, widget, widget.getId());
972                 }
973                 widget.setHidden(!canViewWidget);
974                 widget.setRender(canViewWidget);
975             }
976 
977             // if widget is not readOnly, check edit authorization
978             if (!widget.isReadOnly()) {
979                 boolean canEditWidget = authorizer.canEditWidget(view, model, widget, widget.getId(), user);
980                 if (canEditWidget) {
981                     canEditWidget = presentationController.canEditWidget(view, model, widget, widget.getId());
982                 }
983                 widget.setReadOnly(!canEditWidget);
984             }
985         }
986     }
987 
988     /**
989      * Runs any configured <code>ComponentModifiers</code> for the given
990      * component that match the given run phase and who run condition evaluation
991      * succeeds
992      *
993      * <p>
994      * If called during the initialize phase, the performInitialization method will be invoked on
995      * the <code>ComponentModifier</code> before running
996      * </p>
997      *
998      * @param view - view instance for context
999      * @param component - component instance whose modifiers should be run
1000      * @param model - model object for context
1001      * @param runPhase - current phase to match on
1002      */
1003     protected void runComponentModifiers(View view, Component component, Object model, String runPhase) {
1004         for (ComponentModifier modifier : component.getComponentModifiers()) {
1005             // if run phase is initialize, invoke initialize method on modifier first
1006             if (StringUtils.equals(runPhase, UifConstants.ViewPhases.INITIALIZE)) {
1007                 modifier.performInitialization(view, model, component);
1008             }
1009 
1010             // check run phase matches
1011             if (StringUtils.equals(modifier.getRunPhase(), runPhase)) {
1012                 // check condition (if set) evaluates to true
1013                 boolean runModifier = true;
1014                 if (StringUtils.isNotBlank(modifier.getRunCondition())) {
1015                     Map<String, Object> context = new HashMap<String, Object>();
1016                     context.put(UifConstants.ContextVariableNames.COMPONENT, component);
1017                     context.put(UifConstants.ContextVariableNames.VIEW, view);
1018 
1019                     String conditionEvaluation = getExpressionEvaluatorService().evaluateExpressionTemplate(model,
1020                             context, modifier.getRunCondition());
1021                     runModifier = Boolean.parseBoolean(conditionEvaluation);
1022                 }
1023 
1024                 if (runModifier) {
1025                     modifier.performModification(view, model, component);
1026                 }
1027             }
1028         }
1029     }
1030 
1031     /**
1032      * Gets global objects for the context map and pushes them to the context
1033      * for the component
1034      *
1035      * @param view - view instance for component
1036      * @param component - component instance to push context to
1037      */
1038     public Map<String, Object> getCommonContext(View view, Component component) {
1039         Map<String, Object> context = new HashMap<String, Object>();
1040 
1041         context.putAll(view.getContext());
1042         context.put(UifConstants.ContextVariableNames.THEME_IMAGES, view.getTheme().getImageDirectory());
1043         context.put(UifConstants.ContextVariableNames.COMPONENT, component);
1044 
1045         return context;
1046     }
1047 
1048     /**
1049      * @see org.kuali.rice.krad.uif.service.ViewHelperService#performFinalize(org.kuali.rice.krad.uif.view.View,
1050      *      java.lang.Object)
1051      */
1052     @Override
1053     public void performFinalize(View view, Object model) {
1054         // get script for generating growl messages
1055         String growlScript = buildGrowlScript(view);
1056         ((ViewModel) model).setGrowlScript(growlScript);
1057 
1058         Map<String, Object> clientState = new HashMap<String, Object>();
1059         performComponentFinalize(view, view, model, null, clientState);
1060 
1061         String clientStateScript = buildClientSideStateScript(view, clientState, false);
1062         String viewPreLoadScript = view.getPreLoadScript();
1063         if (StringUtils.isNotBlank(viewPreLoadScript)) {
1064             clientStateScript = viewPreLoadScript + clientStateScript;
1065         }
1066         view.setPreLoadScript(clientStateScript);
1067 
1068         // apply default values if they have not been applied yet
1069         if (!((ViewModel) model).isDefaultsApplied()) {
1070             applyDefaultValues(view, view, model);
1071             ((ViewModel) model).setDefaultsApplied(true);
1072         }
1073     }
1074 
1075     /**
1076      * Builds script that will initialize configuration parameters and component state on the client
1077      *
1078      * <p>
1079      * Here client side state is initialized along with configuration variables that need exposed to script
1080      * </p>
1081      *
1082      * @param view - view instance that is being built
1083      * @param clientSideState - map of key/value pairs that should be exposed as client side state
1084      * @param updateOnly - boolean that indicates whether we are just updating a component (true), or the full view
1085      */
1086     protected String buildClientSideStateScript(View view, Map<String, Object> clientSideState, boolean updateOnly) {
1087         // merge any additional client side state added to the view during processing
1088         // state from view will override in all cases except when both values are maps, in which the maps
1089         // be combined for the new value
1090         for (Entry<String, Object> additionalState : view.getClientSideState().entrySet()) {
1091             if (!clientSideState.containsKey(additionalState.getKey())) {
1092                 clientSideState.put(additionalState.getKey(), additionalState.getValue());
1093             } else {
1094                 Object state = clientSideState.get(additionalState.getKey());
1095                 Object mergeState = additionalState.getValue();
1096                 if ((state instanceof Map) && (mergeState instanceof Map)) {
1097                     ((Map) state).putAll((Map) mergeState);
1098                 } else {
1099                     clientSideState.put(additionalState.getKey(), additionalState.getValue());
1100                 }
1101             }
1102         }
1103 
1104         // script for initializing client side state on load
1105         String clientStateScript = "";
1106         if (!clientSideState.isEmpty()) {
1107             if (updateOnly) {
1108                 clientStateScript = "updateViewState({";
1109             } else {
1110                 clientStateScript = "initializeViewState({";
1111             }
1112 
1113             for (Entry<String, Object> stateEntry : clientSideState.entrySet()) {
1114                 clientStateScript += "'" + stateEntry.getKey() + "':";
1115                 clientStateScript += ScriptUtils.translateValue(stateEntry.getValue());
1116                 clientStateScript += ",";
1117             }
1118             clientStateScript = StringUtils.removeEnd(clientStateScript, ",");
1119             clientStateScript += "});";
1120         }
1121 
1122         // add necessary configuration parameters
1123         if (!updateOnly) {
1124             String kradImageLocation = KRADServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
1125                     "krad.externalizable.images.url");
1126             clientStateScript += "setConfigParam('"
1127                     + UifConstants.ClientSideVariables.KRAD_IMAGE_LOCATION
1128                     + "','"
1129                     + kradImageLocation
1130                     + "');";
1131 
1132             String kradURL = KRADServiceLocator.getKualiConfigurationService().getPropertyValueAsString("krad.url");
1133             clientStateScript +=
1134                     "setConfigParam('" + UifConstants.ClientSideVariables.KRAD_URL + "','" + kradURL + "');";
1135         }
1136 
1137         return clientStateScript;
1138     }
1139 
1140     /**
1141      * Builds JS script that will invoke the show growl method to display a growl message when the page is
1142      * rendered
1143      *
1144      * <p>
1145      * A growl call will be created for any explicit growl messages added to the message map.
1146      * </p>
1147      *
1148      * <p>
1149      * Growls are only generated if @{link org.kuali.rice.krad.uif.view.View#isGrowlMessagingEnabled()} is enabled.
1150      * If not, the growl messages are set as info messages for the page
1151      * </p>
1152      *
1153      * @param view - view instance for which growls are being generated
1154      * @return String JS script string for generated growl messages
1155      */
1156     protected String buildGrowlScript(View view) {
1157         String growlScript = "";
1158 
1159         MessageService messageService = KRADServiceLocatorWeb.getMessageService();
1160 
1161         MessageMap messageMap = GlobalVariables.getMessageMap();
1162         for (GrowlMessage growl : messageMap.getGrowlMessages()) {
1163             if (view.isGrowlMessagingEnabled()) {
1164                 String message = messageService.getMessageText(growl.getNamespaceCode(), growl.getComponentCode(),
1165                         growl.getMessageKey());
1166 
1167                 if (StringUtils.isNotBlank(message)) {
1168                     if (growl.getMessageParameters() != null) {
1169                         message = message.replace("'", "''");
1170                         message = MessageFormat.format(message, (Object[]) growl.getMessageParameters());
1171                     }
1172 
1173                     // escape single quotes in message or title since that will cause problem with plugin
1174                     message = message.replace("'", "\\'");
1175 
1176                     String title = growl.getTitle();
1177                     if (StringUtils.isNotBlank(growl.getTitleKey())) {
1178                         title = messageService.getMessageText(growl.getNamespaceCode(), growl.getComponentCode(),
1179                                 growl.getTitleKey());
1180                     }
1181                     title = title.replace("'", "\\'");
1182 
1183                     growlScript =
1184                             growlScript + "showGrowl('" + message + "', '" + title + "', '" + growl.getTheme() + "');";
1185                 }
1186             } else {
1187                 ErrorMessage infoMessage = new ErrorMessage(growl.getMessageKey(), growl.getMessageParameters());
1188                 infoMessage.setNamespaceCode(growl.getNamespaceCode());
1189                 infoMessage.setComponentCode(growl.getComponentCode());
1190 
1191                 messageMap.putInfoForSectionId(KRADConstants.GLOBAL_INFO, infoMessage);
1192             }
1193         }
1194 
1195         return growlScript;
1196     }
1197 
1198     /**
1199      * Update state of the given component and does final preparation for
1200      * rendering
1201      *
1202      * @param view - view instance the component belongs to
1203      * @param component - the component instance that should be updated
1204      * @param model - top level object containing the data
1205      * @param parent - Parent component for the component being finalized
1206      * @param clientSideState - map to add client state to
1207      */
1208     protected void performComponentFinalize(View view, Component component, Object model, Component parent,
1209             Map<String, Object> clientSideState) {
1210         if (component == null) {
1211             return;
1212         }
1213 
1214         // implement readonly request overrides
1215         ViewModel viewModel = (ViewModel) model;
1216         if ((component instanceof DataBinding) && view.isSupportsRequestOverrideOfReadOnlyFields() && !viewModel
1217                 .getReadOnlyFieldsList().isEmpty()) {
1218             String propertyName = ((DataBinding) component).getPropertyName();
1219             if (viewModel.getReadOnlyFieldsList().contains(propertyName)) {
1220                 component.setReadOnly(true);
1221             }
1222         }
1223 
1224         // invoke configured method finalizers
1225         invokeMethodFinalizer(view, component, model);
1226 
1227         // invoke component to update its state
1228         component.performFinalize(view, model, parent);
1229 
1230         // add client side state for annotated component properties
1231         addClientSideStateForComponent(component, clientSideState);
1232 
1233         // invoke service override hook
1234         performCustomFinalize(view, component, model, parent);
1235 
1236         // invoke component modifiers setup to run in the finalize phase
1237         runComponentModifiers(view, component, model, UifConstants.ViewPhases.FINALIZE);
1238         
1239         // add the components template to the views list of components
1240         if (!component.isSelfRendered() && StringUtils.isNotBlank(component.getTemplate()) &&
1241                 !view.getViewTemplates().contains(component.getTemplate())) {
1242             view.getViewTemplates().add(component.getTemplate());
1243         }
1244 
1245         // get components children and recursively update state
1246         for (Component nestedComponent : component.getComponentsForLifecycle()) {
1247             performComponentFinalize(view, nestedComponent, model, component, clientSideState);
1248         }
1249     }
1250 
1251     /**
1252      * Reflects the class for the given component to find any fields that are annotated with
1253      * <code>ClientSideState</code> and adds the corresponding property name/value pair to the client side state
1254      * map
1255      *
1256      * <p>
1257      * Note if the component is the <code>View</code, state is added directly to the client side state map, while
1258      * for other components a nested Map is created to hold the state, which is then placed into the client side
1259      * state map with the component id as the key
1260      * </p>
1261      *
1262      * @param component - component instance to get client state for
1263      * @param clientSideState - map to add client side variable name/values to
1264      */
1265     protected void addClientSideStateForComponent(Component component, Map<String, Object> clientSideState) {
1266         Map<String, Annotation> annotatedFields = CloneUtils.getFieldsWithAnnotation(component.getClass(),
1267                 ClientSideState.class);
1268 
1269         if (!annotatedFields.isEmpty()) {
1270             Map<String, Object> componentClientState = null;
1271             if (component instanceof View) {
1272                 componentClientState = clientSideState;
1273             } else {
1274                 if (clientSideState.containsKey(component.getId())) {
1275                     componentClientState = (Map<String, Object>) clientSideState.get(component.getId());
1276                 } else {
1277                     componentClientState = new HashMap<String, Object>();
1278                     clientSideState.put(component.getId(), componentClientState);
1279                 }
1280             }
1281 
1282             for (Entry<String, Annotation> annotatedField : annotatedFields.entrySet()) {
1283                 ClientSideState clientSideStateAnnot = (ClientSideState) annotatedField.getValue();
1284 
1285                 String variableName = clientSideStateAnnot.variableName();
1286                 if (StringUtils.isBlank(variableName)) {
1287                     variableName = annotatedField.getKey();
1288                 }
1289 
1290                 Object value = ObjectPropertyUtils.getPropertyValue(component, annotatedField.getKey());
1291                 componentClientState.put(variableName, value);
1292             }
1293         }
1294     }
1295 
1296     /**
1297      * Updates the properties of the given component instance with the value found from the corresponding map of
1298      * client state (if found)
1299      *
1300      * @param component - component instance to update
1301      * @param clientSideState - map of state to sync with
1302      */
1303     protected void syncClientSideStateForComponent(Component component, Map<String, Object> clientSideState) {
1304         // find the map of state that was sent for component (if any)
1305         Map<String, Object> componentState = null;
1306         if (component instanceof View) {
1307             componentState = clientSideState;
1308         } else {
1309             if (clientSideState.containsKey(component.getId())) {
1310                 componentState = (Map<String, Object>) clientSideState.get(component.getId());
1311             }
1312         }
1313 
1314         // if state was sent, match with fields on the component that are annotated to have client state
1315         if ((componentState != null) && (!componentState.isEmpty())) {
1316             Map<String, Annotation> annotatedFields = CloneUtils.getFieldsWithAnnotation(component.getClass(),
1317                     ClientSideState.class);
1318 
1319             for (Entry<String, Annotation> annotatedField : annotatedFields.entrySet()) {
1320                 ClientSideState clientSideStateAnnot = (ClientSideState) annotatedField.getValue();
1321 
1322                 String variableName = clientSideStateAnnot.variableName();
1323                 if (StringUtils.isBlank(variableName)) {
1324                     variableName = annotatedField.getKey();
1325                 }
1326 
1327                 if (componentState.containsKey(variableName)) {
1328                     Object value = componentState.get(variableName);
1329                     ObjectPropertyUtils.setPropertyValue(component, annotatedField.getKey(), value);
1330                 }
1331             }
1332         }
1333     }
1334 
1335     /**
1336      * Invokes the finalize method for the component (if configured) and sets
1337      * the render output for the component to the returned method string (if
1338      * method is not a void type)
1339      *
1340      * @param view - view instance that contains the component
1341      * @param component - component to run finalize method for
1342      * @param model - top level object containing the data
1343      */
1344     protected void invokeMethodFinalizer(View view, Component component, Object model) {
1345         String finalizeMethodToCall = component.getFinalizeMethodToCall();
1346         MethodInvoker finalizeMethodInvoker = component.getFinalizeMethodInvoker();
1347 
1348         if (StringUtils.isBlank(finalizeMethodToCall) && (finalizeMethodInvoker == null)) {
1349             return;
1350         }
1351 
1352         if (finalizeMethodInvoker == null) {
1353             finalizeMethodInvoker = new MethodInvoker();
1354         }
1355 
1356         // if method not set on invoker, use finalizeMethodToCall, note staticMethod could be set(don't know since
1357         // there is not a getter), if so it will override the target method in prepare
1358         if (StringUtils.isBlank(finalizeMethodInvoker.getTargetMethod())) {
1359             finalizeMethodInvoker.setTargetMethod(finalizeMethodToCall);
1360         }
1361 
1362         // if target class or object not set, use view helper service
1363         if ((finalizeMethodInvoker.getTargetClass() == null) && (finalizeMethodInvoker.getTargetObject() == null)) {
1364             finalizeMethodInvoker.setTargetObject(view.getViewHelperService());
1365         }
1366 
1367         // setup arguments for method
1368         List<Object> additionalArguments = component.getFinalizeMethodAdditionalArguments();
1369         if (additionalArguments == null) {
1370             additionalArguments = new ArrayList<Object>();
1371         }
1372 
1373         Object[] arguments = new Object[2 + additionalArguments.size()];
1374         arguments[0] = component;
1375         arguments[1] = model;
1376 
1377         int argumentIndex = 1;
1378         for (Object argument : additionalArguments) {
1379             argumentIndex++;
1380             arguments[argumentIndex] = argument;
1381         }
1382         finalizeMethodInvoker.setArguments(arguments);
1383 
1384         // invoke finalize method
1385         try {
1386             LOG.debug("Invoking finalize method: "
1387                     + finalizeMethodInvoker.getTargetMethod()
1388                     + " for component: "
1389                     + component.getId());
1390             finalizeMethodInvoker.prepare();
1391 
1392             Class<?> methodReturnType = finalizeMethodInvoker.getPreparedMethod().getReturnType();
1393             if (StringUtils.equals("void", methodReturnType.getName())) {
1394                 finalizeMethodInvoker.invoke();
1395             } else {
1396                 String renderOutput = (String) finalizeMethodInvoker.invoke();
1397 
1398                 component.setSelfRendered(true);
1399                 component.setRenderedHtmlOutput(renderOutput);
1400             }
1401         } catch (Exception e) {
1402             LOG.error("Error invoking finalize method for component: " + component.getId(), e);
1403             throw new RuntimeException("Error invoking finalize method for component: " + component.getId(), e);
1404         }
1405     }
1406 
1407     /**
1408      * @see org.kuali.rice.krad.uif.service.ViewHelperService#cleanViewAfterRender(org.kuali.rice.krad.uif.view.View)
1409      */
1410     @Override
1411     public void cleanViewAfterRender(View view) {
1412         ViewCleaner.cleanView(view);
1413     }
1414 
1415     /**
1416      * @see org.kuali.rice.krad.uif.service.ViewHelperService#processCollectionAddLine(org.kuali.rice.krad.uif.view.View,
1417      *      java.lang.Object, java.lang.String)
1418      */
1419     @Override
1420     public void processCollectionAddLine(View view, Object model, String collectionPath) {
1421         // get the collection group from the view
1422         CollectionGroup collectionGroup = view.getViewIndex().getCollectionGroupByPath(collectionPath);
1423         if (collectionGroup == null) {
1424             logAndThrowRuntime("Unable to get collection group component for path: " + collectionPath);
1425         }
1426 
1427         // get the collection instance for adding the new line
1428         Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model, collectionPath);
1429         if (collection == null) {
1430             logAndThrowRuntime("Unable to get collection property from model for path: " + collectionPath);
1431         }
1432 
1433         // now get the new line we need to add
1434         String addLinePath = collectionGroup.getAddLineBindingInfo().getBindingPath();
1435         Object addLine = ObjectPropertyUtils.getPropertyValue(model, addLinePath);
1436         if (addLine == null) {
1437             logAndThrowRuntime("Add line instance not found for path: " + addLinePath);
1438         }
1439 
1440         processBeforeAddLine(view, collectionGroup, model, addLine);
1441 
1442         // validate the line to make sure it is ok to add
1443         boolean isValidLine = performAddLineValidation(view, collectionGroup, model, addLine);
1444         if (isValidLine) {
1445             // TODO: should check to see if there is an add line method on the
1446             // collection parent and if so call that instead of just adding to
1447             // the collection (so that sequence can be set)
1448             addLine(collection, addLine, collectionGroup.getAddLinePlacement().equals("TOP"));
1449 
1450             // make a new instance for the add line
1451             collectionGroup.initializeNewCollectionLine(view, model, collectionGroup, true);
1452         }
1453 
1454         ((UifFormBase) model).getAddedCollectionItems().add(addLine);
1455 
1456         processAfterAddLine(view, collectionGroup, model, addLine);
1457     }
1458 
1459     /**
1460      * @see org.kuali.rice.krad.uif.service.ViewHelperService#processCollectionSaveLine(org.kuali.rice.krad.uif.view.View,
1461      *      java.lang.Object, java.lang.String, int)
1462      */
1463     @Override
1464     public void processCollectionSaveLine(View view, Object model, String collectionPath, int selectedLineIndex) {
1465         // get the collection group from the view
1466         CollectionGroup collectionGroup = view.getViewIndex().getCollectionGroupByPath(collectionPath);
1467         if (collectionGroup == null) {
1468             logAndThrowRuntime("Unable to get collection group component for path: " + collectionPath);
1469         }
1470 
1471         // get the collection instance for adding the new line
1472         Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model, collectionPath);
1473         if (collection == null) {
1474             logAndThrowRuntime("Unable to get collection property from model for path: " + collectionPath);
1475         }
1476 
1477         // TODO: look into other ways of identifying a line so we can deal with
1478         // unordered collections
1479         if (collection instanceof List) {
1480             Object saveLine = ((List<Object>) collection).get(selectedLineIndex);
1481 
1482             processBeforeSaveLine(view, collectionGroup, model, saveLine);
1483 
1484             ((UifFormBase) model).getAddedCollectionItems().remove(saveLine);
1485 
1486             processAfterSaveLine(view, collectionGroup, model, saveLine);
1487 
1488         } else {
1489             logAndThrowRuntime("Only List collection implementations are supported for the delete by index method");
1490         }
1491 
1492     }
1493 
1494     /**
1495      * @see org.kuali.rice.krad.uif.service.ViewHelperService#processCollectionAddBlankLine(org.kuali.rice.krad.uif.view.View,
1496      *      java.lang.Object, java.lang.String)
1497      */
1498     @Override
1499     public void processCollectionAddBlankLine(View view, Object model, String collectionPath) {
1500         // get the collection group from the view
1501         CollectionGroup collectionGroup = view.getViewIndex().getCollectionGroupByPath(collectionPath);
1502         if (collectionGroup == null) {
1503             logAndThrowRuntime("Unable to get collection group component for path: " + collectionPath);
1504         }
1505 
1506         // get the collection instance for adding the new line
1507         Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model, collectionPath);
1508         if (collection == null) {
1509             logAndThrowRuntime("Unable to get collection property from model for path: " + collectionPath);
1510         }
1511 
1512         Object newLine = ObjectUtils.newInstance(collectionGroup.getCollectionObjectClass());
1513         applyDefaultValuesForCollectionLine(view, model, collectionGroup, newLine);
1514         addLine(collection, newLine, collectionGroup.getAddLinePlacement().equals("TOP"));
1515 
1516         ((UifFormBase) model).getAddedCollectionItems().add(newLine);
1517     }
1518 
1519     /**
1520      * Add addLine to collection while giving derived classes an opportunity to override
1521      * for things like sorting.
1522      *
1523      * @param collection - the Collection to add the given addLine to
1524      * @param addLine - the line to add to the given collection
1525      * @param insertFirst - indicates if the item should be inserted as the first item
1526      */
1527     protected void addLine(Collection<Object> collection, Object addLine, boolean insertFirst) {
1528         if (insertFirst && (collection instanceof List)) {
1529             ((List) collection).add(0, addLine);
1530         } else {
1531             collection.add(addLine);
1532         }
1533     }
1534 
1535     /**
1536      * Performs validation on the new collection line before it is added to the
1537      * corresponding collection
1538      *
1539      * @param view - view instance that the action was taken on
1540      * @param collectionGroup - collection group component for the collection
1541      * @param addLine - new line instance to validate
1542      * @param model - object instance that contain's the views data
1543      * @return boolean true if the line is valid and it should be added to the
1544      *         collection, false if it was not valid and should not be added to
1545      *         the collection
1546      */
1547     protected boolean performAddLineValidation(View view, CollectionGroup collectionGroup, Object model,
1548             Object addLine) {
1549         boolean isValid = true;
1550 
1551         // TODO: this should invoke rules, subclasses like the document view
1552         // should create the document add line event
1553 
1554         return isValid;
1555     }
1556 
1557     /**
1558      * @see org.kuali.rice.krad.uif.service.ViewHelperService#processCollectionDeleteLine(org.kuali.rice.krad.uif.view.View,
1559      *      java.lang.Object, java.lang.String, int)
1560      */
1561     public void processCollectionDeleteLine(View view, Object model, String collectionPath, int lineIndex) {
1562         // get the collection group from the view
1563         CollectionGroup collectionGroup = view.getViewIndex().getCollectionGroupByPath(collectionPath);
1564         if (collectionGroup == null) {
1565             logAndThrowRuntime("Unable to get collection group component for path: " + collectionPath);
1566         }
1567 
1568         // get the collection instance for adding the new line
1569         Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model, collectionPath);
1570         if (collection == null) {
1571             logAndThrowRuntime("Unable to get collection property from model for path: " + collectionPath);
1572         }
1573 
1574         // TODO: look into other ways of identifying a line so we can deal with
1575         // unordered collections
1576         if (collection instanceof List) {
1577             Object deleteLine = ((List<Object>) collection).get(lineIndex);
1578 
1579             // validate the delete action is allowed for this line
1580             boolean isValid = performDeleteLineValidation(view, collectionGroup, deleteLine);
1581             if (isValid) {
1582                 ((List<Object>) collection).remove(lineIndex);
1583                 processAfterDeleteLine(view, collectionGroup, model, lineIndex);
1584             }
1585         } else {
1586             logAndThrowRuntime("Only List collection implementations are supported for the delete by index method");
1587         }
1588     }
1589 
1590     /**
1591      * Performs validation on the collection line before it is removed from the
1592      * corresponding collection
1593      *
1594      * @param view - view instance that the action was taken on
1595      * @param collectionGroup - collection group component for the collection
1596      * @param deleteLine - line that will be removed
1597      * @return boolean true if the action is allowed and the line should be
1598      *         removed, false if the line should not be removed
1599      */
1600     protected boolean performDeleteLineValidation(View view, CollectionGroup collectionGroup, Object deleteLine) {
1601         boolean isValid = true;
1602 
1603         // TODO: this should invoke rules, sublclasses like the document view
1604         // should create the document delete line event
1605 
1606         return isValid;
1607     }
1608 
1609     /**
1610      * @see org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl#processMultipleValueLookupResults
1611      */
1612     public void processMultipleValueLookupResults(View view, Object model, String collectionPath,
1613             String lookupResultValues) {
1614         // if no line values returned, no population is needed
1615         if (StringUtils.isBlank(lookupResultValues)) {
1616             return;
1617         }
1618 
1619         // retrieve the collection group so we can get the collection class and collection lookup
1620         CollectionGroup collectionGroup = view.getViewIndex().getCollectionGroupByPath(collectionPath);
1621         if (collectionGroup == null) {
1622             throw new RuntimeException("Unable to find collection group for path: " + collectionPath);
1623         }
1624 
1625         Class<?> collectionObjectClass = collectionGroup.getCollectionObjectClass();
1626         Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model,
1627                 collectionGroup.getBindingInfo().getBindingPath());
1628         if (collection == null) {
1629             Class<?> collectionClass = ObjectPropertyUtils.getPropertyType(model,
1630                     collectionGroup.getBindingInfo().getBindingPath());
1631             collection = (Collection<Object>) ObjectUtils.newInstance(collectionClass);
1632             ObjectPropertyUtils.setPropertyValue(model, collectionGroup.getBindingInfo().getBindingPath(), collection);
1633         }
1634 
1635         Map<String, String> fieldConversions = collectionGroup.getCollectionLookup().getFieldConversions();
1636         List<String> toFieldNamesColl = new ArrayList(fieldConversions.values());
1637         Collections.sort(toFieldNamesColl);
1638         String[] toFieldNames = new String[toFieldNamesColl.size()];
1639         toFieldNamesColl.toArray(toFieldNames);
1640 
1641         // first split to get the line value sets
1642         String[] lineValues = StringUtils.split(lookupResultValues, ",");
1643 
1644         // for each returned set create a new instance of collection class and populate with returned line values
1645         for (String lineValue : lineValues) {
1646             Object lineDataObject = null;
1647 
1648             // TODO: need to put this in data object service so logic can be reused
1649             ModuleService moduleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(
1650                     collectionObjectClass);
1651             if (moduleService != null && moduleService.isExternalizable(collectionObjectClass)) {
1652                 lineDataObject = moduleService.createNewObjectFromExternalizableClass(collectionObjectClass.asSubclass(
1653                         ExternalizableBusinessObject.class));
1654             } else {
1655                 lineDataObject = ObjectUtils.newInstance(collectionObjectClass);
1656             }
1657 
1658             // apply default values to new line
1659             applyDefaultValuesForCollectionLine(view, model, collectionGroup, lineDataObject);
1660 
1661             String[] fieldValues = StringUtils.split(lineValue, ":");
1662             if (fieldValues.length != toFieldNames.length) {
1663                 throw new RuntimeException(
1664                         "Value count passed back from multi-value lookup does not match field conversion count");
1665             }
1666 
1667             // set each field value on the line
1668             for (int i = 0; i < fieldValues.length; i++) {
1669                 String fieldName = toFieldNames[i];
1670                 ObjectPropertyUtils.setPropertyValue(lineDataObject, fieldName, fieldValues[i]);
1671             }
1672 
1673             // TODO: duplicate identifier check
1674 
1675             collection.add(lineDataObject);
1676         }
1677     }
1678 
1679     /**
1680      * Finds the <code>Inquirable</code> configured for the given data object
1681      * class and delegates to it for building the inquiry URL
1682      *
1683      * @see org.kuali.rice.krad.uif.service.ViewHelperService#buildInquiryLink(java.lang.Object,
1684      *      java.lang.String, org.kuali.rice.krad.uif.widget.Inquiry)
1685      */
1686     public void buildInquiryLink(Object dataObject, String propertyName, Inquiry inquiry) {
1687         Inquirable inquirable = getViewDictionaryService().getInquirable(dataObject.getClass(), inquiry.getViewName());
1688         if (inquirable != null) {
1689             inquirable.buildInquirableLink(dataObject, propertyName, inquiry);
1690         } else {
1691             // TODO: should we really not render the inquiry just because the top parent doesn't have an inquirable?
1692             // it is possible the path is nested and there does exist an inquiry for the property
1693             // inquirable not found, no inquiry link can be set
1694             inquiry.setRender(false);
1695         }
1696     }
1697 
1698     /**
1699      * @see org.kuali.rice.krad.uif.service.ViewHelperService#applyDefaultValuesForCollectionLine(org.kuali.rice.krad.uif.view.View,
1700      *      java.lang.Object, org.kuali.rice.krad.uif.container.CollectionGroup,
1701      *      java.lang.Object)
1702      */
1703     public void applyDefaultValuesForCollectionLine(View view, Object model, CollectionGroup collectionGroup,
1704             Object line) {
1705         // retrieve all data fields for the collection line
1706         List<DataField> dataFields = ComponentUtils.getComponentsOfTypeDeep(collectionGroup.getAddLineItems(),
1707                 DataField.class);
1708         for (DataField dataField : dataFields) {
1709             String bindingPath = "";
1710             if (StringUtils.isNotBlank(dataField.getBindingInfo().getBindByNamePrefix())) {
1711                 bindingPath = dataField.getBindingInfo().getBindByNamePrefix() + ".";
1712             }
1713             bindingPath += dataField.getBindingInfo().getBindingName();
1714 
1715             populateDefaultValueForField(view, line, dataField, bindingPath);
1716         }
1717     }
1718 
1719     /**
1720      * Iterates through the view components picking up data fields and applying an default value configured
1721      *
1722      * @param view - view instance we are applying default values for
1723      * @param component - component that should be checked for default values
1724      * @param model - model object that values should be set on
1725      */
1726     protected void applyDefaultValues(View view, Component component, Object model) {
1727         if (component == null) {
1728             return;
1729         }
1730 
1731         // if component is a data field apply default value
1732         if (component instanceof DataField) {
1733             DataField dataField = ((DataField) component);
1734 
1735             // need to make sure binding is initialized since this could be on a page we have not initialized yet
1736             dataField.getBindingInfo().setDefaults(view, dataField.getPropertyName());
1737 
1738             populateDefaultValueForField(view, model, dataField, dataField.getBindingInfo().getBindingPath());
1739         }
1740 
1741         List<Component> nestedComponents = component.getComponentsForLifecycle();
1742 
1743         // if view, need to add all pages since only one will be on the lifecycle
1744         if (component instanceof View) {
1745             nestedComponents.addAll(((View) component).getItems());
1746         }
1747 
1748         for (Component nested : nestedComponents) {
1749             applyDefaultValues(view, nested, model);
1750         }
1751     }
1752 
1753     /**
1754      * Applies the default value configured for the given field (if any) to the
1755      * line given object property that is determined by the given binding path
1756      *
1757      * <p>
1758      * Checks for a configured default value or default value class for the
1759      * field. If both are given, the configured static default value will win.
1760      * In addition, if the default value contains an el expression it is
1761      * evaluated against the initial context
1762      * </p>
1763      *
1764      * @param view - view instance the field belongs to
1765      * @param object - object that should be populated
1766      * @param dataField - field to check for configured default value
1767      * @param bindingPath - path to the property on the object that should be populated
1768      */
1769     protected void populateDefaultValueForField(View view, Object object, DataField dataField, String bindingPath) {
1770         // check for configured default value
1771         String defaultValue = dataField.getDefaultValue();
1772         Object[] defaultValues = dataField.getDefaultValues();
1773 
1774         if (StringUtils.isBlank(defaultValue) && defaultValues != null && defaultValues.length > 0)     {
1775             ObjectPropertyUtils.setPropertyValue(object, bindingPath, defaultValues);
1776         }
1777         else {
1778             if (StringUtils.isBlank(defaultValue) && (dataField.getDefaultValueFinderClass() != null)) {
1779                 ValueFinder defaultValueFinder = ObjectUtils.newInstance(dataField.getDefaultValueFinderClass());
1780                 defaultValue = defaultValueFinder.getValue();
1781             }
1782 
1783             // populate default value if given and path is valid
1784             if (StringUtils.isNotBlank(defaultValue) && ObjectPropertyUtils.isWritableProperty(object, bindingPath)) {
1785                 if (getExpressionEvaluatorService().containsElPlaceholder(defaultValue)) {
1786                     Map<String, Object> context = getPreModelContext(view);
1787                     defaultValue = getExpressionEvaluatorService().evaluateExpressionTemplate(null, context, defaultValue);
1788                 }
1789 
1790                 // TODO: this should go through our formatters
1791                 // Skip nullable non-null non-empty objects when setting default
1792                 Object currentValue = ObjectPropertyUtils.getPropertyValue(object, bindingPath);
1793                 Class currentClazz = ObjectPropertyUtils.getPropertyType(object, bindingPath);
1794                 if(currentValue == null || StringUtils.isBlank(currentValue.toString()) ||
1795                         ClassUtils.isPrimitiveOrWrapper(currentClazz)) {
1796                     ObjectPropertyUtils.setPropertyValue(object, bindingPath, defaultValue);
1797                 }
1798             }
1799         }
1800     }
1801 
1802     /**
1803      * Hook for creating new components with code and adding them to a container
1804      *
1805      * <p>
1806      * Subclasses can override this method to check for one or more containers by id and then adding components
1807      * created in code. This is invoked before the initialize method on the container component, so the full
1808      * lifecycle will be run on the components returned.
1809      * </p>
1810      *
1811      * <p>
1812      * New components instances can be retrieved using {@link ComponentFactory}
1813      * </p>
1814      *
1815      * @param view - view instance the container belongs to
1816      * @param model - object containing the view data
1817      * @param container - container instance to add components to
1818      */
1819     protected void addCustomContainerComponents(View view, Object model, Container container) {
1820 
1821     }
1822 
1823     /**
1824      * Hook for service overrides to perform custom initialization on the
1825      * component
1826      *
1827      * @param view - view instance containing the component
1828      * @param component - component instance to initialize
1829      */
1830     protected void performCustomInitialization(View view, Component component) {
1831 
1832     }
1833 
1834     /**
1835      * Hook for service overrides to perform custom apply model logic on the
1836      * component
1837      *
1838      * @param view - view instance containing the component
1839      * @param component - component instance to apply model to
1840      * @param model - Top level object containing the data (could be the form or a
1841      * top level business object, dto)
1842      */
1843     protected void performCustomApplyModel(View view, Component component, Object model) {
1844 
1845     }
1846 
1847     /**
1848      * Hook for service overrides to perform custom component finalization
1849      *
1850      * @param view - view instance containing the component
1851      * @param component - component instance to update
1852      * @param model - Top level object containing the data
1853      * @param parent - Parent component for the component being finalized
1854      */
1855     protected void performCustomFinalize(View view, Component component, Object model, Component parent) {
1856 
1857     }
1858 
1859     /**
1860      * Hook for service overrides to process the new collection line before it
1861      * is added to the collection
1862      *
1863      * @param view - view instance that is being presented (the action was taken
1864      * on)
1865      * @param collectionGroup - collection group component for the collection the line will
1866      * be added to
1867      * @param model - object instance that contain's the views data
1868      * @param addLine - the new line instance to be processed
1869      */
1870     protected void processBeforeAddLine(View view, CollectionGroup collectionGroup, Object model, Object addLine) {
1871 
1872     }
1873 
1874     /**
1875      * Hook for service overrides to process the new collection line after it
1876      * has been added to the collection
1877      *
1878      * @param view - view instance that is being presented (the action was taken
1879      * on)
1880      * @param collectionGroup - collection group component for the collection the line that
1881      * was added
1882      * @param model - object instance that contain's the views data
1883      * @param addLine - the new line that was added
1884      */
1885     protected void processAfterAddLine(View view, CollectionGroup collectionGroup, Object model, Object addLine) {
1886 
1887     }
1888 
1889     /**
1890      * Hook for service overrides to process the save collection line before it
1891      * is validated
1892      *
1893      * @param view - view instance that is being presented (the action was taken
1894      * on)
1895      * @param collectionGroup - collection group component for the collection
1896      * @param model - object instance that contain's the views data
1897      * @param addLine - the new line instance to be processed
1898      */
1899     protected void processBeforeSaveLine(View view, CollectionGroup collectionGroup, Object model, Object addLine) {
1900 
1901     }
1902 
1903     /**
1904      * Hook for service overrides to process the save collection line after it
1905      * has been validated
1906      *
1907      * @param view - view instance that is being presented (the action was taken
1908      * on)
1909      * @param collectionGroup - collection group component for the collection
1910      * @param model - object instance that contains the views data
1911      * @param addLine - the new line that was added
1912      */
1913     protected void processAfterSaveLine(View view, CollectionGroup collectionGroup, Object model, Object addLine) {
1914 
1915     }
1916 
1917     /**
1918      * Hook for service overrides to process the collection line after it has been deleted
1919      *
1920      * @param view - view instance that is being presented (the action was taken on)
1921      * @param collectionGroup - collection group component for the collection the line that
1922      * was added
1923      * @param model - object instance that contains the views data
1924      * @param lineIndex - index of the line that was deleted
1925      */
1926     protected void processAfterDeleteLine(View view, CollectionGroup collectionGroup, Object model, int lineIndex) {
1927 
1928     }
1929 
1930     /**
1931      * Log the error and throw a new runtime exception
1932      *
1933      * @param message - the error message (both to log and throw as a new exception)
1934      */
1935     protected void logAndThrowRuntime(String message) {
1936         LOG.error(message);
1937         throw new RuntimeException(message);
1938     }
1939 
1940     /**
1941      * Gets the data dictionary service
1942      *
1943      * @return DataDictionaryService data dictionary service
1944      */
1945     protected DataDictionaryService getDataDictionaryService() {
1946         if (this.dataDictionaryService == null) {
1947             this.dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
1948         }
1949 
1950         return this.dataDictionaryService;
1951     }
1952 
1953     /**
1954      * Sets the data dictionary service
1955      *
1956      * @param dataDictionaryService
1957      */
1958     public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
1959         this.dataDictionaryService = dataDictionaryService;
1960     }
1961 
1962     /**
1963      * Gets the expression evaluator service
1964      *
1965      * @return ExpressionEvaluatorService expression evaluator service
1966      */
1967     protected ExpressionEvaluatorService getExpressionEvaluatorService() {
1968         if (this.expressionEvaluatorService == null) {
1969             this.expressionEvaluatorService = KRADServiceLocatorWeb.getExpressionEvaluatorService();
1970         }
1971 
1972         return this.expressionEvaluatorService;
1973     }
1974 
1975     /**
1976      * Sets the expression evaluator service
1977      *
1978      * @param expressionEvaluatorService
1979      */
1980     public void setExpressionEvaluatorService(ExpressionEvaluatorService expressionEvaluatorService) {
1981         this.expressionEvaluatorService = expressionEvaluatorService;
1982     }
1983 
1984     /**
1985      * Gets the view dictionary service
1986      *
1987      * @return ViewDictionaryService view dictionary service
1988      */
1989     public ViewDictionaryService getViewDictionaryService() {
1990         if (this.viewDictionaryService == null) {
1991             this.viewDictionaryService = KRADServiceLocatorWeb.getViewDictionaryService();
1992         }
1993         return this.viewDictionaryService;
1994     }
1995 
1996     /**
1997      * Sets the view dictionary service
1998      *
1999      * @param viewDictionaryService
2000      */
2001     public void setViewDictionaryService(ViewDictionaryService viewDictionaryService) {
2002         this.viewDictionaryService = viewDictionaryService;
2003     }
2004 
2005     /**
2006      * Gets the configuration service
2007      *
2008      * @return ConfigurationService configuration service
2009      */
2010     public ConfigurationService getConfigurationService() {
2011         if (this.configurationService == null) {
2012             this.configurationService = KRADServiceLocator.getKualiConfigurationService();
2013         }
2014         return this.configurationService;
2015     }
2016 
2017     /**
2018      * Sets the configuration service
2019      *
2020      * @param configurationService
2021      */
2022     public void setConfigurationService(ConfigurationService configurationService) {
2023         this.configurationService = configurationService;
2024     }
2025 }