View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.service.impl;
17  
18  import com.google.common.collect.Sets;
19  import org.apache.commons.lang.ObjectUtils;
20  import org.apache.commons.lang.StringUtils;
21  import org.apache.log4j.Logger;
22  import org.kuali.rice.core.api.CoreApiServiceLocator;
23  import org.kuali.rice.core.api.config.property.ConfigurationService;
24  import org.kuali.rice.core.api.util.RiceKeyConstants;
25  import org.kuali.rice.kim.api.identity.Person;
26  import org.kuali.rice.krad.bo.PersistableBusinessObject;
27  import org.kuali.rice.krad.data.DataObjectService;
28  import org.kuali.rice.krad.data.DataObjectWrapper;
29  import org.kuali.rice.krad.data.KradDataServiceLocator;
30  import org.kuali.rice.krad.inquiry.Inquirable;
31  import org.kuali.rice.krad.messages.MessageService;
32  import org.kuali.rice.krad.service.DataDictionaryService;
33  import org.kuali.rice.krad.service.KRADServiceLocator;
34  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
35  import org.kuali.rice.krad.service.LegacyDataAdapter;
36  import org.kuali.rice.krad.service.ModuleService;
37  import org.kuali.rice.krad.uif.UifConstants;
38  import org.kuali.rice.krad.uif.component.BindingInfo;
39  import org.kuali.rice.krad.uif.component.Component;
40  import org.kuali.rice.krad.uif.component.PropertyReplacer;
41  import org.kuali.rice.krad.uif.component.RequestParameter;
42  import org.kuali.rice.krad.uif.container.CollectionGroup;
43  import org.kuali.rice.krad.uif.container.Container;
44  import org.kuali.rice.krad.uif.field.DataField;
45  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
46  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleUtils;
47  import org.kuali.rice.krad.uif.service.ViewDictionaryService;
48  import org.kuali.rice.krad.uif.service.ViewHelperService;
49  import org.kuali.rice.krad.uif.util.BooleanMap;
50  import org.kuali.rice.krad.uif.util.CloneUtils;
51  import org.kuali.rice.krad.uif.util.LifecycleElement;
52  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
53  import org.kuali.rice.krad.uif.util.RecycleUtils;
54  import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
55  import org.kuali.rice.krad.uif.view.ExpressionEvaluatorFactory;
56  import org.kuali.rice.krad.uif.view.View;
57  import org.kuali.rice.krad.uif.view.ViewAuthorizer;
58  import org.kuali.rice.krad.uif.view.ViewModel;
59  import org.kuali.rice.krad.uif.view.ViewPresentationController;
60  import org.kuali.rice.krad.uif.widget.Inquiry;
61  import org.kuali.rice.krad.util.ErrorMessage;
62  import org.kuali.rice.krad.util.GlobalVariables;
63  import org.kuali.rice.krad.util.GrowlMessage;
64  import org.kuali.rice.krad.util.KRADConstants;
65  import org.kuali.rice.krad.util.KRADUtils;
66  import org.kuali.rice.krad.util.MessageMap;
67  import org.kuali.rice.krad.valuefinder.ValueFinder;
68  import org.kuali.rice.krad.web.form.UifFormBase;
69  import org.springframework.beans.PropertyAccessorUtils;
70  
71  import java.io.Serializable;
72  import java.lang.annotation.Annotation;
73  import java.text.MessageFormat;
74  import java.util.ArrayList;
75  import java.util.Collection;
76  import java.util.Collections;
77  import java.util.HashMap;
78  import java.util.HashSet;
79  import java.util.LinkedList;
80  import java.util.List;
81  import java.util.Map;
82  import java.util.Map.Entry;
83  import java.util.Queue;
84  import java.util.Set;
85  
86  /**
87   * Default Implementation of {@code ViewHelperService}
88   *
89   * @author Kuali Rice Team (rice.collab@kuali.org)
90   */
91  public class ViewHelperServiceImpl implements ViewHelperService, Serializable {
92      private static final long serialVersionUID = 1772618197133239852L;
93      private static final Logger LOG = Logger.getLogger(ViewHelperServiceImpl.class);
94  
95      private transient ConfigurationService configurationService;
96      private transient DataDictionaryService dataDictionaryService;
97      private transient LegacyDataAdapter legacyDataAdapter;
98      private transient DataObjectService dataObjectService;
99      private transient ViewDictionaryService viewDictionaryService;
100     private transient ExpressionEvaluatorFactory expressionEvaluatorFactory;
101 
102     /**
103      * {@inheritDoc}
104      */
105     @Override
106     public void addCustomContainerComponents(ViewModel model, Container container) {
107 
108     }
109 
110     /**
111      * Finds the <code>Inquirable</code> configured for the given data object class and delegates to
112      * it for building the inquiry URL
113      *
114      * {@inheritDoc}
115      */
116     public void buildInquiryLink(Object dataObject, String propertyName, Inquiry inquiry) {
117         Inquirable inquirable = getViewDictionaryService().getInquirable(dataObject.getClass(), inquiry.getViewName());
118         if (inquirable != null) {
119             inquirable.buildInquirableLink(dataObject, propertyName, inquiry);
120         } else {
121             // TODO: should we really not render the inquiry just because the top parent doesn't have an inquirable?
122             // it is possible the path is nested and there does exist an inquiry for the property
123             // inquirable not found, no inquiry link can be set
124             inquiry.setRender(false);
125         }
126     }
127 
128     /**
129      * {@inheritDoc}
130      */
131     @Override
132     public void performCustomApplyModel(LifecycleElement element, Object model) {
133 
134     }
135 
136     /**
137      * {@inheritDoc}
138      */
139     @Override
140     public void performCustomFinalize(LifecycleElement element, Object model, LifecycleElement parent) {
141 
142     }
143 
144     /**
145      * {@inheritDoc}
146      */
147     @Override
148     public void performCustomInitialization(LifecycleElement element) {
149 
150     }
151 
152     /**
153      * {@inheritDoc}
154      */
155     @Override
156     public void performCustomViewFinalize(Object model) {
157 
158     }
159 
160     /**
161      * {@inheritDoc}
162      */
163     @Override
164     public void performCustomViewInitialization(Object model) {
165 
166     }
167 
168     /**
169      * {@inheritDoc}
170      */
171     @Override
172     public void processAfterAddLine(ViewModel model, Object lineObject, String collectionId, String collectionPath,
173             boolean isValidLine) {
174 
175     }
176 
177     /**
178      * {@inheritDoc}
179      */
180     @Override
181     public void processAfterDeleteLine(ViewModel model, String collectionId, String collectionPath, int lineIndex) {
182 
183     }
184 
185     /**
186      * {@inheritDoc}
187      */
188     @Override
189     public void processAfterSaveLine(ViewModel model, Object lineObject, String collectionId, String collectionPath) {
190 
191     }
192 
193     /**
194      * {@inheritDoc}
195      */
196     @Override
197     public void processBeforeAddLine(ViewModel model, Object addLine, String collectionId, String collectionPath) {
198 
199     }
200 
201     /**
202      * {@inheritDoc}
203      */
204     @Override
205     public void processBeforeSaveLine(ViewModel model, Object lineObject, String collectionId, String collectionPath) {
206 
207     }
208 
209     /**
210      * {@inheritDoc}
211      */
212     @SuppressWarnings("unchecked")
213     @Override
214     public void processCollectionAddBlankLine(ViewModel model, String collectionId, String collectionPath) {
215         if (!(model instanceof ViewModel)) {
216             return;
217         }
218 
219         ViewModel viewModel = (ViewModel) model;
220 
221         if (collectionId == null) {
222             logAndThrowRuntime(
223                     "Unable to get collection group component for Id: " + collectionPath + " path: " + collectionPath);
224         }
225 
226         // get the collection instance for adding the new line
227         Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model, collectionPath);
228         if (collection == null) {
229             logAndThrowRuntime("Unable to get collection property from model for path: " + collectionPath);
230         }
231 
232         Class<?> collectionObjectClass = (Class<?>) viewModel.getViewPostMetadata().getComponentPostData(collectionId,
233                 UifConstants.PostMetadata.COLL_OBJECT_CLASS);
234         Object newLine = KRADUtils.createNewObjectFromClass(collectionObjectClass);
235 
236         List<Object> lineDataObjects = new ArrayList<Object>();
237         lineDataObjects.add(newLine);
238         viewModel.getViewPostMetadata().getAddedCollectionObjects().put(collectionId, lineDataObjects);
239         processAndAddLineObject(viewModel, newLine, collectionId, collectionPath);
240     }
241 
242     /**
243      * {@inheritDoc}
244      */
245     @SuppressWarnings("unchecked")
246     @Override
247     public void processCollectionAddLine(ViewModel model, String collectionId, String collectionPath) {
248         if (!(model instanceof ViewModel)) {
249             return;
250         }
251 
252         ViewModel viewModel = (ViewModel) model;
253 
254         if (collectionId == null) {
255             logAndThrowRuntime(
256                     "Unable to get collection group component for Id: " + collectionPath + " path: " + collectionPath);
257         }
258 
259         // now get the new line we need to add
260         BindingInfo addLineBindingInfo = (BindingInfo) viewModel.getViewPostMetadata().getComponentPostData(
261                 collectionId, UifConstants.PostMetadata.ADD_LINE_BINDING_INFO);
262         Object addLine = ObjectPropertyUtils.getPropertyValue(model, addLineBindingInfo.getBindingPath());
263         if (addLine == null) {
264             logAndThrowRuntime("Add line instance not found for path: " + addLineBindingInfo.getBindingPath());
265         }
266 
267         // Adding an empty list because this item does not need to be further processed, but needs to init
268         // a new add line
269         List<Object> lineDataObjects = new ArrayList<Object>();
270         viewModel.getViewPostMetadata().getAddedCollectionObjects().put(collectionId, lineDataObjects);
271 
272         processAndAddLineObject(viewModel, addLine, collectionId, collectionPath);
273 
274     }
275 
276     /**
277      * Do all processing related to adding a line: calls processBeforeAddLine, performAddLineValidation, addLine,
278      * processAfterAddLine
279      *
280      * @param viewModel object instance that contain's the view's data
281      * @param newLine the new line instance to be processed
282      * @param collectionId the id of the collection being added to
283      * @param collectionPath the path to the collection being modified
284      */
285     protected void processAndAddLineObject(ViewModel viewModel, Object newLine, String collectionId,
286             String collectionPath) {
287         String addLinePlacement = (String) viewModel.getViewPostMetadata().getComponentPostData(collectionId,
288                 UifConstants.PostMetadata.ADD_LINE_PLACEMENT);
289 
290         // get the collection instance for adding the new line
291         Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(viewModel, collectionPath);
292         if (collection == null) {
293             logAndThrowRuntime("Unable to get collection property from model for path: " + collectionPath);
294         }
295 
296         processBeforeAddLine(viewModel, newLine, collectionId, collectionPath);
297 
298         boolean isValidLine = performAddLineValidation(viewModel, newLine, collectionId, collectionPath);
299         if (isValidLine) {
300             int addedIndex = addLine(collection, newLine, addLinePlacement.equals("TOP"));
301             // now link the added line, this is important in situations where perhaps the collection element is
302             // bi-directional and needs to point back to it's parent
303             linkAddedLine(viewModel, collectionPath, addedIndex);
304             
305             if (viewModel instanceof UifFormBase) {
306                 ((UifFormBase) viewModel).getAddedCollectionItems().add(newLine);
307             }
308             processAfterAddLine(viewModel, newLine, collectionId, collectionPath, isValidLine);
309         }
310     }
311 
312     /**
313      * {@inheritDoc}
314      */
315     @Override
316     public void processCollectionDeleteLine(ViewModel model, String collectionId, String collectionPath,
317             int lineIndex) {
318         // get the collection instance for adding the new line
319         Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model, collectionPath);
320         if (collection == null) {
321             logAndThrowRuntime("Unable to get collection property from model for path: " + collectionPath);
322         }
323 
324         // TODO: look into other ways of identifying a line so we can deal with
325         // unordered collections
326         if (collection instanceof List) {
327             Object deleteLine = ((List<Object>) collection).get(lineIndex);
328 
329             // validate the delete action is allowed for this line
330             boolean isValid = performDeleteLineValidation(model, collectionId, collectionPath, deleteLine);
331             if (isValid) {
332                 ((List<Object>) collection).remove(lineIndex);
333                 processAfterDeleteLine(model, collectionId, collectionPath, lineIndex);
334             }
335         } else {
336             logAndThrowRuntime("Only List collection implementations are supported for the delete by index method");
337         }
338     }
339 
340     /**
341      * {@inheritDoc}
342      */
343     @Override
344     public void processCollectionSaveLine(ViewModel model, String collectionId, String collectionPath,
345             int selectedLineIndex) {
346         // get the collection instance for adding the new line
347         Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model, collectionPath);
348         if (collection == null) {
349             logAndThrowRuntime("Unable to get collection property from model for path: " + collectionPath);
350         }
351 
352         // TODO: look into other ways of identifying a line so we can deal with
353         // unordered collections
354         if (collection instanceof List) {
355             Object saveLine = ((List<Object>) collection).get(selectedLineIndex);
356 
357             processBeforeSaveLine(model, saveLine, collectionId, collectionPath);
358 
359             ((UifFormBase) model).getAddedCollectionItems().remove(saveLine);
360 
361             processAfterSaveLine(model, saveLine, collectionId, collectionPath);
362 
363         } else {
364             logAndThrowRuntime("Only List collection implementations are supported for the delete by index method");
365         }
366 
367     }
368 
369     /**
370      * {@inheritDoc}
371      */
372     @SuppressWarnings("unchecked")
373     public void processMultipleValueLookupResults(ViewModel model, String collectionId, String collectionPath,
374             String multiValueReturnFields, String lookupResultValues) {
375         // if no line values returned, no population is needed
376         if (StringUtils.isBlank(lookupResultValues) || !(model instanceof ViewModel)) {
377             return;
378         }
379 
380         ViewModel viewModel = (ViewModel) model;
381 
382         if (StringUtils.isBlank(collectionId)) {
383             throw new RuntimeException(
384                     "Id is not set for this collection lookup: " + collectionId + ", " + "path: " + collectionPath);
385         }
386 
387         // retrieve the collection group so we can get the collection class and collection lookup
388         Class<?> collectionObjectClass = (Class<?>) viewModel.getViewPostMetadata().getComponentPostData(collectionId,
389                 UifConstants.PostMetadata.COLL_OBJECT_CLASS);
390         Collection<Object> collection = ObjectPropertyUtils.getPropertyValue(model, collectionPath);
391         if (collection == null) {
392             Class<?> collectionClass = ObjectPropertyUtils.getPropertyType(model, collectionPath);
393             collection = (Collection<Object>) KRADUtils.createNewObjectFromClass(collectionClass);
394             ObjectPropertyUtils.setPropertyValue(model, collectionPath, collection);
395         }
396 
397         // get the field conversions
398         Map<String, String> fieldConversions =
399                 (Map<String, String>) viewModel.getViewPostMetadata().getComponentPostData(collectionId,
400                         UifConstants.PostMetadata.COLL_LOOKUP_FIELD_CONVERSIONS);
401 
402         // filter the field conversions by what was returned from the multi value lookup return fields
403         Map <String, String> returnedFieldConversions = filterByReturnedFieldConversions(multiValueReturnFields,
404                 fieldConversions);
405 
406         List<String> toFieldNamesColl = new ArrayList<String>(returnedFieldConversions.values());
407         Collections.sort(toFieldNamesColl);
408         String[] toFieldNames = new String[toFieldNamesColl.size()];
409         toFieldNamesColl.toArray(toFieldNames);
410 
411         // first split to get the line value sets
412         String[] lineValues = StringUtils.split(lookupResultValues, ",");
413 
414         List<Object> lineDataObjects = new ArrayList<Object>();
415         // for each returned set create a new instance of collection class and populate with returned line values
416         for (String lineValue : lineValues) {
417             Object lineDataObject = null;
418 
419             // TODO: need to put this in data object service so logic can be reused
420             ModuleService moduleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(
421                     collectionObjectClass);
422             if (moduleService != null && moduleService.isExternalizable(collectionObjectClass)) {
423                 lineDataObject = moduleService.createNewObjectFromExternalizableClass(collectionObjectClass.asSubclass(
424                         org.kuali.rice.krad.bo.ExternalizableBusinessObject.class));
425             } else {
426                 lineDataObject = KRADUtils.createNewObjectFromClass(collectionObjectClass);
427             }
428 
429             String[] fieldValues = StringUtils.splitByWholeSeparatorPreserveAllTokens(lineValue, ":");
430             if (fieldValues.length != toFieldNames.length) {
431                 throw new RuntimeException(
432                         "Value count passed back from multi-value lookup does not match field conversion count");
433             }
434 
435             // set each field value on the line
436             for (int i = 0; i < fieldValues.length; i++) {
437                 String fieldName = toFieldNames[i];
438                 ObjectPropertyUtils.setPropertyValue(lineDataObject, fieldName, fieldValues[i]);
439             }
440 
441             lineDataObjects.add(lineDataObject);
442             processAndAddLineObject(viewModel, lineDataObject, collectionId, collectionPath);
443         }
444 
445         viewModel.getViewPostMetadata().getAddedCollectionObjects().put(collectionId, lineDataObjects);
446     }
447 
448     /**
449      * Add addLine to collection while giving derived classes an opportunity to override for things
450      * like sorting.
451      *
452      * @param collection the Collection to add the given addLine to
453      * @param addLine the line to add to the given collection
454      * @param insertFirst indicates if the item should be inserted as the first item
455      *
456      * @return the index at which the item was added to the collection, or -1 if it was not added
457      */
458     protected int addLine(Collection<Object> collection, Object addLine, boolean insertFirst) {
459         int index = -1;
460         if (insertFirst && (collection instanceof List)) {
461             ((List<Object>) collection).add(0, addLine);
462             index = 0;
463         } else {
464             boolean added = collection.add(addLine);
465             if (added) {
466                 index = collection.size() - 1;
467             }
468         }
469         return index;
470     }
471 
472     protected void linkAddedLine(Object model, String collectionPath, int addedIndex) {
473         int lastSepIndex = PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(collectionPath);
474         if (lastSepIndex != -1) {
475             String collectionParentPath = collectionPath.substring(0, lastSepIndex);
476             Object parent = ObjectPropertyUtils.getPropertyValue(model, collectionParentPath);
477             if (parent != null && getDataObjectService().supports(parent.getClass())) {
478                 DataObjectWrapper<?> wrappedParent = getDataObjectService().wrap(parent);
479                 String collectionName = collectionPath.substring(lastSepIndex + 1);
480                 wrappedParent.linkChanges(Sets.newHashSet(collectionName + "[" + addedIndex + "]"));
481             }
482         }
483     }
484 
485     /**
486      * Performs validation on the new collection line before it is added to the corresponding collection.
487      *
488      * @param viewModel object instance that contain's the view's data
489      * @param newLine the new line instance to be processed
490      * @param collectionId the id of the collection being added to
491      * @param collectionPath the path to the collection being modified
492      */
493     protected boolean performAddLineValidation(ViewModel viewModel, Object newLine, String collectionId,
494             String collectionPath) {
495         boolean isValid = true;
496 
497         Collection<Object> collectionItems = ObjectPropertyUtils.getPropertyValue(viewModel, collectionPath);
498 
499         if (viewModel.getViewPostMetadata().getComponentPostData(collectionId,
500                 UifConstants.PostMetadata.DUPLICATE_LINE_PROPERTY_NAMES) == null) {
501             return isValid;
502         }
503 
504         List<String> duplicateLinePropertyNames = (List<String>) viewModel.getViewPostMetadata().getComponentPostData(
505                 collectionId, UifConstants.PostMetadata.DUPLICATE_LINE_PROPERTY_NAMES);
506 
507         String collectionLabel = null;
508         if (viewModel.getViewPostMetadata().getComponentPostData(collectionId, UifConstants.PostMetadata.COLL_LABEL)
509                 != null) {
510             collectionLabel = (String) viewModel.getViewPostMetadata().getComponentPostData(collectionId,
511                     UifConstants.PostMetadata.COLL_LABEL);
512         }
513 
514         String duplicateLineLabelString = null;
515         if (viewModel.getViewPostMetadata().getComponentPostData(collectionId,
516                 UifConstants.PostMetadata.DUPLICATE_LINE_LABEL_STRING) != null) {
517             duplicateLineLabelString = (String) viewModel.getViewPostMetadata().getComponentPostData(collectionId,
518                     UifConstants.PostMetadata.DUPLICATE_LINE_LABEL_STRING);
519         }
520 
521         if (containsDuplicateLine(newLine, collectionItems, duplicateLinePropertyNames)) {
522             isValid = false;
523             GlobalVariables.getMessageMap().putErrorForSectionId(collectionId, RiceKeyConstants.ERROR_DUPLICATE_ELEMENT,
524                     collectionLabel, duplicateLineLabelString);
525         }
526 
527         return isValid;
528     }
529 
530     /**
531      * Filters the field conversions by the multi value return fields
532      * @param multiValueReturnFields the return fields to filter by, as a comma separated string
533      * @param fieldConversions the map of field conversions to filter
534      * @return a {@link java.util.Map} containing the filtered field conversions
535      */
536     private Map<String, String> filterByReturnedFieldConversions(String multiValueReturnFields,
537             Map<String, String> fieldConversions) {
538 
539         Map <String, String> returnedFieldConversions = new HashMap<String, String>();
540         returnedFieldConversions.putAll(fieldConversions);
541 
542         // parse the multi value return fields string
543         String[] returnedFieldsStrArr = StringUtils.split(multiValueReturnFields, ",");
544         // iterate over the returned fields and get the conversion values.
545         if (returnedFieldsStrArr != null && returnedFieldsStrArr.length > 0) {
546             returnedFieldConversions.clear();
547             for (String fieldConversion : returnedFieldsStrArr) {
548                 if (fieldConversions.containsKey(fieldConversion)) {
549                     returnedFieldConversions.put(fieldConversion, fieldConversions.get(fieldConversion));
550                 }
551             }
552         }
553 
554         return returnedFieldConversions;
555     }
556 
557     /**
558      * Determines whether the new line matches one of the entries in the existing collection, based on the
559      * {@code duplicateLinePropertyNames}.
560      *
561      * @param addLine new line instance to validate
562      * @param collectionItems items in the collection
563      * @param duplicateLinePropertyNames property names to check for duplicates
564      * @return true if there is a duplicate line, false otherwise
565      */
566     private boolean containsDuplicateLine(Object addLine, Collection<Object> collectionItems,
567             List<String> duplicateLinePropertyNames) {
568         if (collectionItems.isEmpty() || duplicateLinePropertyNames.isEmpty()) {
569             return false;
570         }
571 
572         for (Object collectionItem : collectionItems) {
573             if (isDuplicateLine(addLine, collectionItem, duplicateLinePropertyNames)) {
574                 return true;
575             }
576         }
577 
578         return false;
579     }
580 
581     /**
582      * Determines whether the new {@code addLine} is a duplicate of {@code collectionItem}, based on the
583      * {@code duplicateLinePropertyNames}.
584      *
585      * @param addLine new line instance to validate
586      * @param collectionItem existing instance to validate
587      * @param duplicateLinePropertyNames the property names to check for duplicates
588      * @return true if {@code addLine} is a duplicate of {@code collectionItem}, false otherwise
589      */
590     private boolean isDuplicateLine(Object addLine, Object collectionItem, List<String> duplicateLinePropertyNames) {
591         if (duplicateLinePropertyNames.isEmpty()) {
592             return false;
593         }
594 
595         for (String duplicateLinePropertyName : duplicateLinePropertyNames) {
596             Object addLinePropertyValue = ObjectPropertyUtils.getPropertyValue(addLine, duplicateLinePropertyName);
597             Object duplicateLinePropertyValue = ObjectPropertyUtils.getPropertyValue(collectionItem,
598                     duplicateLinePropertyName);
599 
600             if (!ObjectUtils.equals(addLinePropertyValue, duplicateLinePropertyValue)) {
601                 return false;
602             }
603         }
604 
605         return true;
606     }
607 
608     /**
609      * Performs validation on the collection line before it is removed from the corresponding collection.
610      *
611      * @param model object instance that contain's the view's data
612      * @param collectionId the id of the collection being added to
613      * @param collectionPath the path to the collection being modified
614      * @param deleteLine line that will be removed
615      * @return true if the action is allowed and the line should be removed, false if the line should not be removed
616      */
617     protected boolean performDeleteLineValidation(ViewModel model, String collectionId, String collectionPath,
618             Object deleteLine) {
619         return true;
620     }
621 
622     /**
623      * {@inheritDoc}
624      */
625     @Override
626     public void applyDefaultValuesForCollectionLine(CollectionGroup collectionGroup, Object line) {
627         // retrieve all data fields for the collection line
628         List<DataField> dataFields = ViewLifecycleUtils.getElementsOfTypeDeep(collectionGroup.getAddLineItems(),
629                 DataField.class);
630         for (DataField dataField : dataFields) {
631             String bindingPath = "";
632             if (StringUtils.isNotBlank(dataField.getBindingInfo().getBindByNamePrefix())) {
633                 bindingPath = dataField.getBindingInfo().getBindByNamePrefix() + ".";
634             }
635             bindingPath += dataField.getBindingInfo().getBindingName();
636 
637             populateDefaultValueForField(line, dataField, bindingPath);
638         }
639     }
640 
641     /**
642      * {@inheritDoc}
643      */
644     @Override
645     public void applyDefaultValues(Component component) {
646         if (component == null) {
647             return;
648         }
649 
650         View view = ViewLifecycle.getView();
651         Object model = ViewLifecycle.getModel();
652 
653         @SuppressWarnings("unchecked") Queue<LifecycleElement> elementQueue = RecycleUtils.getInstance(
654                 LinkedList.class);
655         elementQueue.offer(component);
656         try {
657             while (!elementQueue.isEmpty()) {
658                 LifecycleElement currentElement = elementQueue.poll();
659 
660                 // if component is a data field apply default value
661                 if (currentElement instanceof DataField) {
662                     DataField dataField = ((DataField) currentElement);
663 
664                     // need to make sure binding is initialized since this could be on a page we have not initialized yet
665                     dataField.getBindingInfo().setDefaults(view, dataField.getPropertyName());
666 
667                     populateDefaultValueForField(model, dataField, dataField.getBindingInfo().getBindingPath());
668                 }
669 
670                 elementQueue.addAll(ViewLifecycleUtils.getElementsForLifecycle(currentElement).values());
671             }
672         } finally {
673             elementQueue.clear();
674             RecycleUtils.recycle(elementQueue);
675         }
676     }
677 
678     /**
679      * {@inheritDoc}
680      */
681     @Override
682     public void populateViewFromRequestParameters(Map<String, String> parameters) {
683         View view = ViewLifecycle.getView();
684 
685         // build Map of property replacers by property name so that we can remove them
686         // if the property was set by a request parameter
687         Map<String, Set<PropertyReplacer>> viewPropertyReplacers = new HashMap<String, Set<PropertyReplacer>>();
688         List<PropertyReplacer> propertyReplacerSource = view.getPropertyReplacers();
689         if (propertyReplacerSource != null) {
690             for (PropertyReplacer replacer : propertyReplacerSource) {
691                 String replacerPropertyName = replacer.getPropertyName();
692                 Set<PropertyReplacer> propertyReplacers = viewPropertyReplacers.get(replacerPropertyName);
693 
694                 if (propertyReplacers == null) {
695                     viewPropertyReplacers.put(replacerPropertyName,
696                             propertyReplacers = new HashSet<PropertyReplacer>());
697                 }
698 
699                 propertyReplacers.add(replacer);
700             }
701         }
702 
703         Map<String, Annotation> annotatedFields = CloneUtils.getFieldsWithAnnotation(view.getClass(),
704                 RequestParameter.class);
705 
706         // for each request parameter allowed on the view, if the request contains a value use
707         // to set on View, and clear and conditional expressions or property replacers for that field
708         Map<String, String> viewRequestParameters = new HashMap<String, String>();
709         for (String fieldToPopulate : annotatedFields.keySet()) {
710             RequestParameter requestParameter = (RequestParameter) annotatedFields.get(fieldToPopulate);
711 
712             // use specified parameter name if given, else use field name to retrieve parameter value
713             String requestParameterName = requestParameter.parameterName();
714             if (StringUtils.isBlank(requestParameterName)) {
715                 requestParameterName = fieldToPopulate;
716             }
717 
718             if (!parameters.containsKey(requestParameterName)) {
719                 continue;
720             }
721 
722             String fieldValue = parameters.get(requestParameterName);
723             if (StringUtils.isNotBlank(fieldValue)) {
724                 viewRequestParameters.put(requestParameterName, fieldValue);
725                 ObjectPropertyUtils.setPropertyValue(view, fieldToPopulate, fieldValue);
726 
727                 // remove any conditional configuration so value is not
728                 // overridden later during the apply model phase
729                 if (view.getPropertyExpressions().containsKey(fieldToPopulate)) {
730                     view.getPropertyExpressions().remove(fieldToPopulate);
731                 }
732 
733                 if (viewPropertyReplacers.containsKey(fieldToPopulate)) {
734                     Set<PropertyReplacer> propertyReplacers = viewPropertyReplacers.get(fieldToPopulate);
735                     for (PropertyReplacer replacer : propertyReplacers) {
736                         view.getPropertyReplacers().remove(replacer);
737                     }
738                 }
739             }
740         }
741 
742         view.setViewRequestParameters(viewRequestParameters);
743     }
744 
745     /**
746      * {@inheritDoc}
747      */
748     @Override
749     public String buildGrowlScript() {
750         View view = ViewLifecycle.getView();
751         String growlScript = "";
752 
753         MessageService messageService = KRADServiceLocatorWeb.getMessageService();
754 
755         MessageMap messageMap = GlobalVariables.getMessageMap();
756         for (GrowlMessage growl : messageMap.getGrowlMessages()) {
757             if (view.isGrowlMessagingEnabled()) {
758                 String message = messageService.getMessageText(growl.getNamespaceCode(), growl.getComponentCode(),
759                         growl.getMessageKey());
760 
761                 if (StringUtils.isNotBlank(message)) {
762                     if (growl.getMessageParameters() != null) {
763                         message = message.replace("'", "''");
764                         message = MessageFormat.format(message, (Object[]) growl.getMessageParameters());
765                     }
766 
767                     // escape single quotes in message or title since that will cause problem with plugin
768                     message = message.replace("'", "\\'");
769 
770                     String title = growl.getTitle();
771                     if (StringUtils.isNotBlank(growl.getTitleKey())) {
772                         title = messageService.getMessageText(growl.getNamespaceCode(), growl.getComponentCode(),
773                                 growl.getTitleKey());
774                     }
775                     title = title.replace("'", "\\'");
776 
777                     growlScript =
778                             growlScript + "showGrowl('" + message + "', '" + title + "', '" + growl.getTheme() + "');";
779                 }
780             } else {
781                 ErrorMessage infoMessage = new ErrorMessage(growl.getMessageKey(), growl.getMessageParameters());
782                 infoMessage.setNamespaceCode(growl.getNamespaceCode());
783                 infoMessage.setComponentCode(growl.getComponentCode());
784 
785                 messageMap.putInfoForSectionId(KRADConstants.GLOBAL_INFO, infoMessage);
786             }
787         }
788 
789         return growlScript;
790     }
791 
792     /**
793      * {@inheritDoc}
794      */
795     @Override
796     public void populateDefaultValueForField(Object object, DataField dataField, String bindingPath) {
797 
798         if (!ObjectPropertyUtils.isReadableProperty(object, bindingPath) || !ObjectPropertyUtils.isWritableProperty(
799                 object, bindingPath)) {
800             return;
801         }
802         Object currentValue = ObjectPropertyUtils.getPropertyValue(object, bindingPath);
803         Object defaultValue = getDefaultValueForField(object, dataField);
804 
805         if (defaultValue != null) {
806             ObjectPropertyUtils.setPropertyValue(object, bindingPath, defaultValue);
807         }
808     }
809 
810     /**
811      * {@inheritDoc}
812      */
813     @Override
814     public Object getDefaultValueForField(Object object, DataField dataField) {
815         View view = ViewLifecycle.getView();
816         Object defaultValue = null;
817 
818         // if dataField.defaultValue is not null and not empty empty string use it
819         if (dataField.getDefaultValue() != null && !(dataField.getDefaultValue() instanceof String && StringUtils
820                 .isBlank((String) dataField.getDefaultValue()))) {
821             defaultValue = dataField.getDefaultValue();
822         } else if ((dataField.getExpressionGraph() != null) && dataField.getExpressionGraph().containsKey(
823                 UifConstants.ComponentProperties.DEFAULT_VALUE)) {
824             defaultValue = dataField.getExpressionGraph().get(UifConstants.ComponentProperties.DEFAULT_VALUE);
825         } else if (dataField.getDefaultValueFinderClass() != null) {
826             ValueFinder defaultValueFinder = KRADUtils.createNewObjectFromClass(dataField.getDefaultValueFinderClass());
827 
828             defaultValue = defaultValueFinder.getValue();
829         } else if ((dataField.getExpressionGraph() != null) && dataField.getExpressionGraph().containsKey(
830                 UifConstants.ComponentProperties.DEFAULT_VALUES)) {
831             defaultValue = dataField.getExpressionGraph().get(UifConstants.ComponentProperties.DEFAULT_VALUES);
832         } else if (dataField.getDefaultValues() != null) {
833             defaultValue = dataField.getDefaultValues();
834         }
835 
836         ExpressionEvaluator expressionEvaluator = ViewLifecycle.getExpressionEvaluator();
837 
838         if ((defaultValue != null) && (defaultValue instanceof String) && expressionEvaluator.containsElPlaceholder(
839                 (String) defaultValue)) {
840             Map<String, Object> context = new HashMap<String, Object>(view.getPreModelContext());
841             context.putAll(dataField.getContext());
842 
843             defaultValue = expressionEvaluator.replaceBindingPrefixes(view, object, (String) defaultValue);
844             defaultValue = expressionEvaluator.evaluateExpressionTemplate(context, (String) defaultValue);
845         }
846 
847         return defaultValue;
848     }
849 
850     /**
851      * {@inheritDoc}
852      */
853     @Override
854     public void refreshReference(Object parentObject, String referenceObjectName) {
855         if (!(parentObject instanceof PersistableBusinessObject)) {
856             LOG.warn("Could not refresh reference " + referenceObjectName + " off class " + parentObject.getClass()
857                     .getName() + ". Class not of type PersistableBusinessObject");
858             return;
859         }
860 
861         LegacyDataAdapter legacyDataAdapter = KRADServiceLocatorWeb.getLegacyDataAdapter();
862         DataDictionaryService dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
863 
864         if (legacyDataAdapter.hasReference(parentObject.getClass(), referenceObjectName) || legacyDataAdapter
865                 .hasCollection(parentObject.getClass(), referenceObjectName)) {
866             // refresh via database mapping
867             legacyDataAdapter.retrieveReferenceObject(parentObject, referenceObjectName);
868         } else if (dataDictionaryService.hasRelationship(parentObject.getClass().getName(), referenceObjectName)) {
869             // refresh via data dictionary mapping
870             Object referenceObject = KradDataServiceLocator.getDataObjectService().wrap(parentObject).getPropertyValue(
871                     referenceObjectName);
872             if (!(referenceObject instanceof PersistableBusinessObject)) {
873                 LOG.warn("Could not refresh reference " + referenceObjectName + " off class " + parentObject.getClass()
874                         .getName() + ". Class not of type PersistableBusinessObject");
875                 return;
876             }
877 
878             referenceObject = legacyDataAdapter.retrieve((PersistableBusinessObject) referenceObject);
879             if (referenceObject == null) {
880                 LOG.warn("Could not refresh reference " + referenceObjectName + " off class " + parentObject.getClass()
881                         .getName() + ".");
882                 return;
883             }
884 
885             try {
886                 KRADUtils.setObjectProperty(parentObject, referenceObjectName, referenceObject);
887             } catch (Exception e) {
888                 LOG.error("Unable to refresh persistable business object: " + referenceObjectName + "\n" + e
889                         .getMessage());
890                 throw new RuntimeException(
891                         "Unable to refresh persistable business object: " + referenceObjectName + "\n" + e
892                                 .getMessage());
893             }
894         } else {
895             LOG.warn("Could not refresh reference " + referenceObjectName + " off class " + parentObject.getClass()
896                     .getName() + ".");
897         }
898     }
899 
900     /**
901      * {@inheritDoc}
902      */
903     @Override
904     public void refreshReferences(String referencesToRefresh) {
905         Object model = ViewLifecycle.getModel();
906         for (String reference : StringUtils.split(referencesToRefresh, KRADConstants.REFERENCES_TO_REFRESH_SEPARATOR)) {
907             if (StringUtils.isBlank(reference)) {
908                 continue;
909             }
910 
911             //ToDo: handle add line
912 
913             if (PropertyAccessorUtils.isNestedOrIndexedProperty(reference)) {
914                 String parentPath = KRADUtils.getNestedAttributePrefix(reference);
915                 Object parentObject = ObjectPropertyUtils.getPropertyValue(model, parentPath);
916                 String referenceObjectName = KRADUtils.getNestedAttributePrimitive(reference);
917 
918                 if (parentObject == null) {
919                     LOG.warn("Unable to refresh references for " + referencesToRefresh +
920                             ". Object not found in model. Nothing refreshed.");
921                     continue;
922                 }
923 
924                 refreshReference(parentObject, referenceObjectName);
925             } else {
926                 refreshReference(model, reference);
927             }
928         }
929     }
930 
931     /**
932      * {@inheritDoc}
933      */
934     @Override
935     public void retrieveEditModesAndActionFlags() {
936         View view = ViewLifecycle.getView();
937         UifFormBase model = (UifFormBase) ViewLifecycle.getModel();
938         ViewPresentationController presentationController = view.getPresentationController();
939         ViewAuthorizer authorizer = view.getAuthorizer();
940 
941         Set<String> actionFlags = presentationController.getActionFlags(view, model);
942         Set<String> editModes = presentationController.getEditModes(view, model);
943 
944         // if user session is not established cannot invoke authorizer
945         if (GlobalVariables.getUserSession() != null) {
946             Person user = GlobalVariables.getUserSession().getPerson();
947 
948             actionFlags = authorizer.getActionFlags(view, model, user, actionFlags);
949             editModes = authorizer.getEditModes(view, model, user, editModes);
950         }
951 
952         view.setActionFlags(new BooleanMap(actionFlags));
953         view.setEditModes(new BooleanMap(editModes));
954     }
955 
956     /**
957      * {@inheritDoc}
958      */
959     @Override
960     public void setViewContext() {
961         View view = ViewLifecycle.getView();
962         view.pushAllToContext(view.getPreModelContext());
963 
964         // evaluate view expressions for further context
965         for (Entry<String, String> variableExpression : view.getExpressionVariables().entrySet()) {
966             String variableName = variableExpression.getKey();
967             Object value = ViewLifecycle.getExpressionEvaluator().evaluateExpression(view.getContext(),
968                     variableExpression.getValue());
969             view.pushObjectToContext(variableName, value);
970         }
971     }
972 
973     /**
974      * Gets the configuration service
975      *
976      * @return configuration service
977      */
978     protected ConfigurationService getConfigurationService() {
979         if (this.configurationService == null) {
980             this.configurationService = CoreApiServiceLocator.getKualiConfigurationService();
981         }
982         return this.configurationService;
983     }
984 
985     /**
986      * Sets the configuration service
987      *
988      * @param configurationService The configuration service.
989      */
990     public void setConfigurationService(ConfigurationService configurationService) {
991         this.configurationService = configurationService;
992     }
993 
994     /**
995      * Gets the data dictionary service
996      *
997      * @return data dictionary service
998      */
999     protected DataDictionaryService getDataDictionaryService() {
1000         if (this.dataDictionaryService == null) {
1001             this.dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
1002         }
1003 
1004         return this.dataDictionaryService;
1005     }
1006 
1007     /**
1008      * Sets the data dictionary service
1009      *
1010      * @param dataDictionaryService The dictionary service.
1011      */
1012     public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
1013         this.dataDictionaryService = dataDictionaryService;
1014     }
1015 
1016     /**
1017      * Gets the view dictionary service
1018      *
1019      * @return view dictionary service
1020      */
1021     protected ViewDictionaryService getViewDictionaryService() {
1022         if (this.viewDictionaryService == null) {
1023             this.viewDictionaryService = KRADServiceLocatorWeb.getViewDictionaryService();
1024         }
1025         return this.viewDictionaryService;
1026     }
1027 
1028     /**
1029      * Sets the view dictionary service
1030      *
1031      * @param viewDictionaryService The view dictionary service.
1032      */
1033     public void setViewDictionaryService(ViewDictionaryService viewDictionaryService) {
1034         this.viewDictionaryService = viewDictionaryService;
1035     }
1036 
1037     /**
1038      * {@inheritDoc}
1039      */
1040     @Override
1041     public ExpressionEvaluatorFactory getExpressionEvaluatorFactory() {
1042         if (expressionEvaluatorFactory == null) {
1043             expressionEvaluatorFactory = KRADServiceLocatorWeb.getExpressionEvaluatorFactory();
1044         }
1045 
1046         return expressionEvaluatorFactory;
1047     }
1048 
1049     /**
1050      * Setter for {@link #getExpressionEvaluatorFactory()}.
1051      *
1052      * @param expressionEvaluatorFactory expression evaluator factory
1053      */
1054     public void setExpressionEvaluatorFactory(ExpressionEvaluatorFactory expressionEvaluatorFactory) {
1055         this.expressionEvaluatorFactory = expressionEvaluatorFactory;
1056     }
1057 
1058     /**
1059      * Get the legacy data adapter.
1060      *
1061      * @return The legacy data adapter.
1062      */
1063     protected LegacyDataAdapter getLegacyDataAdapter() {
1064         if (legacyDataAdapter == null) {
1065             legacyDataAdapter = KRADServiceLocatorWeb.getLegacyDataAdapter();
1066         }
1067         return legacyDataAdapter;
1068     }
1069 
1070     protected DataObjectService getDataObjectService() {
1071         if (dataObjectService == null) {
1072             dataObjectService = KRADServiceLocator.getDataObjectService();
1073         }
1074         return dataObjectService;
1075     }
1076 
1077     protected void setDataObjectService(DataObjectService dataObjectService) {
1078         this.dataObjectService = dataObjectService;
1079     }
1080 
1081     /**
1082      * Set the legacy data adapter.
1083      *
1084      * @param legacyDataAdapter The legacy data adapter.
1085      */
1086     public void setLegacyDataAdapter(LegacyDataAdapter legacyDataAdapter) {
1087         this.legacyDataAdapter = legacyDataAdapter;
1088     }
1089 
1090     /**
1091      * Log an error message using log4j, then throw a runtime exception with the provided message.
1092      *
1093      * @param message The error message.
1094      */
1095     protected void logAndThrowRuntime(String message) {
1096         LOG.error(message);
1097         throw new RuntimeException(message);
1098     }
1099 
1100 }