View Javadoc

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