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