View Javadoc

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