View Javadoc

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