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 */
016package org.kuali.rice.krad.uif.service.impl;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.core.api.config.property.ConfigurationService;
020import org.kuali.rice.core.api.exception.RiceRuntimeException;
021import org.kuali.rice.kim.api.identity.Person;
022import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
023import org.kuali.rice.krad.datadictionary.AttributeDefinition;
024import org.kuali.rice.krad.inquiry.Inquirable;
025import org.kuali.rice.krad.messages.MessageService;
026import org.kuali.rice.krad.service.DataDictionaryService;
027import org.kuali.rice.krad.service.KRADServiceLocator;
028import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
029import org.kuali.rice.krad.service.ModuleService;
030import org.kuali.rice.krad.uif.UifConstants;
031import org.kuali.rice.krad.uif.component.BindingInfo;
032import org.kuali.rice.krad.uif.component.ClientSideState;
033import org.kuali.rice.krad.uif.component.Component;
034import org.kuali.rice.krad.uif.component.ComponentSecurity;
035import org.kuali.rice.krad.uif.component.DataBinding;
036import org.kuali.rice.krad.uif.component.PropertyReplacer;
037import org.kuali.rice.krad.uif.component.RequestParameter;
038import org.kuali.rice.krad.uif.container.CollectionGroup;
039import org.kuali.rice.krad.uif.container.Container;
040import org.kuali.rice.krad.uif.container.Group;
041import org.kuali.rice.krad.uif.control.Control;
042import org.kuali.rice.krad.uif.element.Action;
043import org.kuali.rice.krad.uif.field.ActionField;
044import org.kuali.rice.krad.uif.field.DataField;
045import org.kuali.rice.krad.uif.field.Field;
046import org.kuali.rice.krad.uif.field.FieldGroup;
047import org.kuali.rice.krad.uif.field.InputField;
048import org.kuali.rice.krad.uif.field.RemoteFieldsHolder;
049import org.kuali.rice.krad.uif.layout.LayoutManager;
050import org.kuali.rice.krad.uif.modifier.ComponentModifier;
051import org.kuali.rice.krad.uif.service.ExpressionEvaluatorService;
052import org.kuali.rice.krad.uif.service.ViewDictionaryService;
053import org.kuali.rice.krad.uif.service.ViewHelperService;
054import org.kuali.rice.krad.uif.util.BooleanMap;
055import org.kuali.rice.krad.uif.util.CloneUtils;
056import org.kuali.rice.krad.uif.util.ComponentFactory;
057import org.kuali.rice.krad.uif.util.ComponentUtils;
058import org.kuali.rice.krad.uif.util.ExpressionUtils;
059import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
060import org.kuali.rice.krad.uif.util.ScriptUtils;
061import org.kuali.rice.krad.uif.util.ViewCleaner;
062import org.kuali.rice.krad.uif.util.ViewModelUtils;
063import org.kuali.rice.krad.uif.view.View;
064import org.kuali.rice.krad.uif.view.ViewAuthorizer;
065import org.kuali.rice.krad.uif.view.ViewModel;
066import org.kuali.rice.krad.uif.view.ViewPresentationController;
067import org.kuali.rice.krad.uif.widget.Inquiry;
068import org.kuali.rice.krad.uif.widget.Widget;
069import org.kuali.rice.krad.util.ErrorMessage;
070import org.kuali.rice.krad.util.GlobalVariables;
071import org.kuali.rice.krad.util.GrowlMessage;
072import org.kuali.rice.krad.util.KRADConstants;
073import org.kuali.rice.krad.util.MessageMap;
074import org.kuali.rice.krad.util.ObjectUtils;
075import org.kuali.rice.krad.valuefinder.ValueFinder;
076import org.kuali.rice.krad.web.form.UifFormBase;
077import org.springframework.expression.spel.support.StandardEvaluationContext;
078import org.springframework.util.ClassUtils;
079import org.springframework.util.MethodInvoker;
080
081import java.io.Serializable;
082import java.lang.annotation.Annotation;
083import java.text.MessageFormat;
084import java.util.ArrayList;
085import java.util.Collection;
086import java.util.Collections;
087import java.util.HashMap;
088import java.util.HashSet;
089import java.util.List;
090import java.util.Map;
091import java.util.Map.Entry;
092import java.util.Set;
093
094/**
095 * Default Implementation of <code>ViewHelperService</code>
096 *
097 * @author Kuali Rice Team (rice.collab@kuali.org)
098 */
099public 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}