View Javadoc

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