View Javadoc

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