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