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.modifier;
17  
18  import com.google.common.collect.Lists;
19  import org.apache.commons.lang.StringUtils;
20  import org.apache.log4j.Logger;
21  import org.kuali.rice.krad.datadictionary.parse.BeanTag;
22  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
23  import org.kuali.rice.krad.datadictionary.parse.BeanTags;
24  import org.kuali.rice.krad.uif.UifConstants;
25  import org.kuali.rice.krad.uif.UifPropertyPaths;
26  import org.kuali.rice.krad.uif.component.Component;
27  import org.kuali.rice.krad.uif.container.Group;
28  import org.kuali.rice.krad.uif.element.Header;
29  import org.kuali.rice.krad.uif.field.DataField;
30  import org.kuali.rice.krad.uif.field.Field;
31  import org.kuali.rice.krad.uif.field.SpaceField;
32  import org.kuali.rice.krad.uif.layout.GridLayoutManager;
33  import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
34  import org.kuali.rice.krad.uif.util.ComponentFactory;
35  import org.kuali.rice.krad.uif.util.ComponentUtils;
36  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
37  import org.kuali.rice.krad.uif.view.View;
38  
39  import java.util.ArrayList;
40  import java.util.HashMap;
41  import java.util.HashSet;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.Set;
45  
46  /**
47   * Generates <code>Field</code> instances to produce a comparison view among
48   * objects of the same type
49   *
50   * <p>
51   * Modifier is initialized with a List of <code>ComparableInfo</code> instances.
52   * For each comparable info, a copy of the configured group field is made and
53   * adjusted to the binding object path for the comparable. The comparison fields
54   * are ordered based on the configured order property of the comparable. In
55   * addition, a <code>HeaderField<code> can be generated to label each group
56   * of comparison fields.
57   * </p>
58   *
59   * @author Kuali Rice Team (rice.collab@kuali.org)
60   */
61  @BeanTags({@BeanTag(name = "compareFieldCreate-modifier-bean", parent = "Uif-CompareFieldCreate-Modifier"),
62          @BeanTag(name = "maintenanceCompare-modifier-bean", parent = "Uif-MaintenanceCompare-Modifier")})
63  public class CompareFieldCreateModifier extends ComponentModifierBase {
64      private static final Logger LOG = Logger.getLogger(CompareFieldCreateModifier.class);
65  
66      private static final long serialVersionUID = -6285531580512330188L;
67  
68      private int defaultOrderSequence;
69      private boolean generateCompareHeaders;
70  
71      private Header headerFieldPrototype;
72      private List<ComparableInfo> comparables;
73  
74      public CompareFieldCreateModifier() {
75          defaultOrderSequence = 1;
76          generateCompareHeaders = true;
77  
78          comparables = new ArrayList<ComparableInfo>();
79      }
80  
81      /**
82       * Calls <code>ViewHelperService</code> to initialize the header field prototype
83       *
84       * @see org.kuali.rice.krad.uif.modifier.ComponentModifier#performInitialization(org.kuali.rice.krad.uif.view.View,
85       *      java.lang.Object, org.kuali.rice.krad.uif.component.Component)
86       */
87      @Override
88      public void performInitialization(View view, Object model, Component component) {
89          super.performInitialization(view, model, component);
90  
91          if (headerFieldPrototype != null) {
92              view.getViewHelperService().performComponentInitialization(view, model, headerFieldPrototype);
93          }
94      }
95  
96      /**
97       * Generates the comparison fields
98       *
99       * <p>
100      * First the configured List of <code>ComparableInfo</code> instances are
101      * sorted based on their order property. Then if generateCompareHeaders is
102      * set to true, a <code>HeaderField</code> is created for each comparable
103      * using the headerFieldPrototype and the headerText given by the
104      * comparable. Finally for each field configured on the <code>Group</code>,
105      * a corresponding comparison field is generated for each comparable and
106      * adjusted to the binding object path given by the comparable in addition
107      * to suffixing the id and setting the readOnly property
108      * </p>
109      *
110      * @see org.kuali.rice.krad.uif.modifier.ComponentModifier#performModification(org.kuali.rice.krad.uif.view.View,
111      *      java.lang.Object, org.kuali.rice.krad.uif.component.Component)
112      */
113     @SuppressWarnings("unchecked")
114     @Override
115     public void performModification(View view, Object model, Component component) {
116         if ((component != null) && !(component instanceof Group)) {
117             throw new IllegalArgumentException(
118                     "Compare field initializer only support Group components, found type: " + component.getClass());
119         }
120 
121         if (component == null) {
122             return;
123         }
124         
125         Group group = (Group) component;
126 
127         // list to hold the generated compare items
128         List<Component> comparisonItems = new ArrayList<Component>();
129 
130         // sort comparables by their order property
131         List<ComparableInfo> groupComparables = (List<ComparableInfo>) ComponentUtils.sort(comparables,
132                 defaultOrderSequence);
133 
134         // evaluate expressions on comparables
135         Map<String, Object> context = new HashMap<String, Object>();
136         
137         Map<String, Object> viewContext = view.getContext();
138         if (viewContext != null) {
139             context.putAll(view.getContext());
140         }
141         
142         context.put(UifConstants.ContextVariableNames.COMPONENT, component);
143 
144         ExpressionEvaluator expressionEvaluator =
145                 view.getViewHelperService().getExpressionEvaluator();
146 
147         for (ComparableInfo comparable : groupComparables) {
148             expressionEvaluator.evaluateExpressionsOnConfigurable(view, comparable, context);
149         }
150 
151         // generate compare header
152         if (isGenerateCompareHeaders()) {
153             // add space field for label column
154             SpaceField spaceField = ComponentFactory.getSpaceField();
155             view.assignComponentIds(spaceField);
156 
157             comparisonItems.add(spaceField);
158 
159             for (ComparableInfo comparable : groupComparables) {
160                 Header compareHeaderField = ComponentUtils.copy(headerFieldPrototype, comparable.getIdSuffix());
161                 compareHeaderField.setHeaderText(comparable.getHeaderText());
162 
163                 comparisonItems.add(compareHeaderField);
164             }
165             
166             // if group is using grid layout, make first row a header
167             if (group.getLayoutManager() instanceof GridLayoutManager) {
168                 ((GridLayoutManager) group.getLayoutManager()).setRenderFirstRowHeader(true);
169             }
170         }
171 
172         // find the comparable to use for comparing value changes (if
173         // configured)
174         boolean performValueChangeComparison = false;
175         String compareValueObjectBindingPath = null;
176         for (ComparableInfo comparable : groupComparables) {
177             if (comparable.isCompareToForValueChange()) {
178                 performValueChangeComparison = true;
179                 compareValueObjectBindingPath = comparable.getBindingObjectPath();
180             }
181         }
182 
183         // generate the compare items from the configured group
184         boolean changeIconShowedOnHeader = false;
185         for (Component item : group.getItems()) {
186             int defaultSuffix = 0;
187             boolean suppressLabel = false;
188 
189             for (ComparableInfo comparable : groupComparables) {
190                 String idSuffix = comparable.getIdSuffix();
191                 if (StringUtils.isBlank(idSuffix)) {
192                     idSuffix = UifConstants.IdSuffixes.COMPARE + defaultSuffix;
193                 }
194 
195                 Component compareItem = ComponentUtils.copy(item, idSuffix);
196 
197                 ComponentUtils.setComponentPropertyDeep(compareItem, UifPropertyPaths.BIND_OBJECT_PATH,
198                         comparable.getBindingObjectPath());
199                 if (comparable.isReadOnly()) {
200                     compareItem.setReadOnly(true);
201                     if (compareItem.getPropertyExpressions().containsKey("readOnly")) {
202                         compareItem.getPropertyExpressions().remove("readOnly");
203                     }
204                 }
205 
206                 // label will be enabled for first comparable only
207                 if (suppressLabel && (compareItem instanceof Field)) {
208                     ((Field) compareItem).getFieldLabel().setRender(false);
209                 }
210 
211                 // do value comparison
212                 if (performValueChangeComparison && comparable.isHighlightValueChange() && !comparable
213                         .isCompareToForValueChange()) {
214                     boolean valueChanged = performValueComparison(group, compareItem, model,
215                             compareValueObjectBindingPath);
216 
217                     // add icon to group header if not done so yet
218                     if (valueChanged && !changeIconShowedOnHeader && isGenerateCompareHeaders()) {
219                         Group groupToSetHeader = null;
220                         if (group.getDisclosure() != null && group.getDisclosure().isRender()) {
221                             groupToSetHeader = group;
222                         } else if (group.getContext().get(UifConstants.ContextVariableNames.PARENT) != null) {
223                             // use the parent group to set the notification if available
224                             groupToSetHeader = (Group) group.getContext().get(UifConstants.ContextVariableNames.PARENT);
225                         }
226 
227                         if (groupToSetHeader.getDisclosure().isRender()) {
228                             groupToSetHeader.getDisclosure().setOnDocumentReadyScript(
229                                     "showChangeIconOnDisclosure('" + groupToSetHeader.getId() + "');");
230                         } else if (groupToSetHeader.getHeader() != null) {
231                             groupToSetHeader.getHeader().setOnDocumentReadyScript(
232                                     "showChangeIconOnHeader('" + groupToSetHeader.getHeader().getId() + "');");
233                         }
234 
235                         changeIconShowedOnHeader = true;
236                     }
237                 }
238 
239                 comparisonItems.add(compareItem);
240                 defaultSuffix++;
241 
242                 suppressLabel = true;
243             }
244         }
245 
246         // update the group's list of components
247         group.setItems(comparisonItems);
248     }
249 
250     /**
251      * For each attribute field in the compare item, retrieves the field value and compares against the value for the
252      * main comparable. If the value is different, adds script to the field on ready event to add the change icon to
253      * the field and the containing group header
254      *
255      * @param group group that contains the item and whose header will be highlighted for changes
256      * @param compareItem the compare item being generated and to pull attribute fields from
257      * @param model object containing the data
258      * @param compareValueObjectBindingPath object path for the comparison item
259      * @return true if the value in the field represented by compareItem is equal to the comparison items value, false
260      *         otherwise
261      */
262     protected boolean performValueComparison(Group group, Component compareItem, Object model,
263             String compareValueObjectBindingPath) {
264         // get any attribute fields for the item so we can compare the values
265         List<DataField> itemFields = ComponentUtils.getComponentsOfTypeDeep(compareItem, DataField.class);
266         boolean valueChanged = false;
267         for (DataField field : itemFields) {
268             String fieldBindingPath = field.getBindingInfo().getBindingPath();
269             Object fieldValue = ObjectPropertyUtils.getPropertyValue(model, fieldBindingPath);
270 
271             String compareBindingPath = StringUtils.replaceOnce(fieldBindingPath,
272                     field.getBindingInfo().getBindingObjectPath(), compareValueObjectBindingPath);
273             Object compareValue = ObjectPropertyUtils.getPropertyValue(model, compareBindingPath);
274 
275             if (!((fieldValue == null) && (compareValue == null))) {
276                 // if one is null then value changed
277                 if ((fieldValue == null) || (compareValue == null)) {
278                     valueChanged = true;
279                 } else {
280                     // both not null, compare values
281                     valueChanged = !fieldValue.equals(compareValue);
282                 }
283             }
284             if (valueChanged) {
285                 // add script to show change icon
286                 String onReadyScript = "showChangeIcon('" + field.getId() + "');";
287                 field.setOnDocumentReadyScript(onReadyScript);
288             }
289             // TODO: add script for value changed?
290         }
291         return valueChanged;
292     }
293 
294     /**
295      * Generates an id suffix for the comparable item
296      *
297      * <p>
298      * If the idSuffix to use if configured on the <code>ComparableInfo</code>
299      * it will be used, else the given integer index will be used with an
300      * underscore
301      * </p>
302      *
303      * @param comparable comparable info to check for id suffix
304      * @param index sequence integer
305      * @return id suffix
306      * @see org.kuali.rice.krad.uif.modifier.ComparableInfo#getIdSuffix()
307      */
308     protected String getIdSuffix(ComparableInfo comparable, int index) {
309         String idSuffix = comparable.getIdSuffix();
310         if (StringUtils.isBlank(idSuffix)) {
311             idSuffix = "_" + index;
312         }
313 
314         return idSuffix;
315     }
316 
317     /**
318      * @see org.kuali.rice.krad.uif.modifier.ComponentModifier#getSupportedComponents()
319      */
320     @Override
321     public Set<Class<? extends Component>> getSupportedComponents() {
322         Set<Class<? extends Component>> components = new HashSet<Class<? extends Component>>();
323         components.add(Group.class);
324 
325         return components;
326     }
327 
328     /**
329      * @see org.kuali.rice.krad.uif.modifier.ComponentModifierBase#getComponentPrototypes()
330      */
331     public List<Component> getComponentPrototypes() {
332         List<Component> components = new ArrayList<Component>();
333 
334         components.add(headerFieldPrototype);
335 
336         return components;
337     }
338 
339     /**
340      * Indicates the starting integer sequence value to use for
341      * <code>ComparableInfo</code> instances that do not have the order property
342      * set
343      *
344      * @return default sequence starting value
345      */
346     @BeanTagAttribute(name = "defaultOrderSequence")
347     public int getDefaultOrderSequence() {
348         return this.defaultOrderSequence;
349     }
350 
351     /**
352      * Setter for the default sequence starting value
353      *
354      * @param defaultOrderSequence
355      */
356     public void setDefaultOrderSequence(int defaultOrderSequence) {
357         this.defaultOrderSequence = defaultOrderSequence;
358     }
359 
360     /**
361      * Indicates whether a <code>HeaderField</code> should be created for each
362      * group of comparison fields
363      *
364      * <p>
365      * If set to true, for each group of comparison fields a header field will
366      * be created using the headerFieldPrototype configured on the modifier with
367      * the headerText property of the comparable
368      * </p>
369      *
370      * @return true if the headers should be created, false if no
371      *         headers should be created
372      */
373     @BeanTagAttribute(name = "generateCompareHeaders")
374     public boolean isGenerateCompareHeaders() {
375         return this.generateCompareHeaders;
376     }
377 
378     /**
379      * Setter for the generate comparison headers indicator
380      *
381      * @param generateCompareHeaders
382      */
383     public void setGenerateCompareHeaders(boolean generateCompareHeaders) {
384         this.generateCompareHeaders = generateCompareHeaders;
385     }
386 
387     /**
388      * Prototype instance to use for creating the <code>HeaderField</code> for
389      * each group of comparison fields (if generateCompareHeaders is true)
390      *
391      * @return header field prototype
392      */
393     @BeanTagAttribute(name = "headerFieldPrototype", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
394     public Header getHeaderFieldPrototype() {
395         return this.headerFieldPrototype;
396     }
397 
398     /**
399      * Setter for the header field prototype
400      *
401      * @param headerFieldPrototype
402      */
403     public void setHeaderFieldPrototype(Header headerFieldPrototype) {
404         this.headerFieldPrototype = headerFieldPrototype;
405     }
406 
407     /**
408      * List of <code>ComparableInfo</code> instances the compare fields should
409      * be generated for
410      *
411      * <p>
412      * For each comparable, a copy of the fields configured for the
413      * <code>Group</code> will be created for the comparison view
414      * </p>
415      *
416      * @return comparables to generate fields for
417      */
418     @BeanTagAttribute(name = "comparables", type = BeanTagAttribute.AttributeType.LISTBEAN)
419     public List<ComparableInfo> getComparables() {
420         return this.comparables;
421     }
422 
423     /**
424      * Setter for the list of comparable info instances
425      *
426      * @param comparables
427      */
428     public void setComparables(List<ComparableInfo> comparables) {
429         this.comparables = comparables;
430     }
431 
432 
433     /**
434      * @see org.kuali.rice.krad.uif.modifier.ComponentModifierBase#copy()
435      */
436     @Override
437     protected <T> void copyProperties(T componentModifier) {
438         super.copyProperties(componentModifier);
439         CompareFieldCreateModifier compareFieldCreateModifierCopy = (CompareFieldCreateModifier) componentModifier;
440         compareFieldCreateModifierCopy.setDefaultOrderSequence(this.defaultOrderSequence);
441         compareFieldCreateModifierCopy.setGenerateCompareHeaders(this.generateCompareHeaders);
442 
443         if(comparables != null) {
444             List<ComparableInfo> comparables = Lists.newArrayListWithExpectedSize(getComparables().size());
445             for (ComparableInfo comparable : this.comparables) {
446                 comparables.add((ComparableInfo)comparable.copy());
447             }
448             compareFieldCreateModifierCopy.setComparables(comparables);
449         }
450 
451         if (this.headerFieldPrototype != null) {
452             compareFieldCreateModifierCopy.setHeaderFieldPrototype((Header)this.headerFieldPrototype.copy());
453         }
454     }
455 }