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