View Javadoc

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