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