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