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.util;
17  
18  import org.apache.commons.collections.CollectionUtils;
19  import org.kuali.rice.core.api.util.type.KualiDecimal;
20  import org.kuali.rice.krad.comparator.NumericValueComparator;
21  import org.kuali.rice.krad.comparator.TemporalValueComparator;
22  import org.kuali.rice.krad.uif.UifConstants;
23  import org.kuali.rice.krad.uif.container.CollectionGroup;
24  import org.kuali.rice.krad.uif.container.CollectionGroupLineBuilder;
25  import org.kuali.rice.krad.uif.container.collections.LineBuilderContext;
26  import org.kuali.rice.krad.uif.field.DataField;
27  import org.kuali.rice.krad.uif.field.Field;
28  import org.kuali.rice.krad.uif.layout.TableLayoutManager;
29  import org.kuali.rice.krad.uif.layout.collections.TableRow;
30  import org.kuali.rice.krad.uif.layout.collections.TableRowBuilder;
31  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
32  import org.kuali.rice.krad.uif.lifecycle.ViewLifecyclePhase;
33  import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
34  import org.kuali.rice.krad.uif.view.View;
35  import org.kuali.rice.krad.uif.view.ViewModel;
36  import org.kuali.rice.krad.util.KRADUtils;
37  
38  import java.util.Comparator;
39  import java.util.HashMap;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.WeakHashMap;
43  
44  /**
45   * Comparator used for server side sorting of CollectionGroup data.
46   * 
47   * <p>
48   * This may include DataFields, as well as Fields that don't map directly to elements in the model
49   * collection, such as {@link org.kuali.rice.krad.uif.field.LinkField}s that may contain
50   * expressions.
51   * </p>
52   * 
53   * <p>
54   * NOTE: This class is not thread safe, and each instance is intended to be used only once.
55   * </p>
56   * 
57   * @author Kuali Rice Team (rice.collab@kuali.org)
58   */
59  public class MultiColumnComparator implements Comparator<Integer> {
60  
61      private final List<Object> modelCollection;
62      private final CollectionGroup collectionGroup;
63      private final List<ColumnSort> columnSorts;
64      private final View view;
65      private final ViewModel form;
66  
67      // we use the layout manager a lot, so for convenience we'll keep a handy reference to it
68      private final TableLayoutManager tableLayoutManager;
69  
70      // we need the prototype row to be able to get Fields that can be used in extracting & calculating column values
71      private final List<Field> prototypeRow;
72  
73      // if we have to evaluate expressions to sort a column, we want to cache the values so we don't have to
74      // evaluate the same expressions repeatedly.  This cache could get too big, so we'll use a weak reference map
75      private final WeakHashMap<String, String> calculatedValueCache;
76  
77      // Reflection is used to determine the class of certain column values.  Cache those classes
78      private final HashMap<String, Class> propertyClassCache;
79  
80      /**
81       * Constructs a MultiColumnComparator instance
82       * 
83       * @param modelCollection the model collection that the CollectionGroup is associated with
84       * @param collectionGroup the CollectionGroup whose columns are being sorted
85       * @param columnSorts A list from highest to lowest precedence of the column sorts to apply
86       * @param form object containing the view's data
87       * @param view The view
88       */
89      public MultiColumnComparator(List<Object> modelCollection, CollectionGroup collectionGroup,
90              List<ColumnSort> columnSorts, ViewModel form, View view) {
91          this.modelCollection = modelCollection;
92          this.collectionGroup = collectionGroup;
93          this.columnSorts = columnSorts;
94          this.view = view;
95          this.form = form;
96  
97          //
98          // initialize convenience members and calculated members.  Caches first!
99          //
100 
101         calculatedValueCache = new WeakHashMap<String, String>();
102         propertyClassCache = new HashMap<String, Class>();
103 
104         tableLayoutManager = (TableLayoutManager) collectionGroup.getLayoutManager();
105         prototypeRow = buildPrototypeRow();
106     }
107 
108     /**
109      * Compares the modelCollecton element at index1 to the element at index2 based on the provided
110      * {@link org.kuali.rice.krad.uif.util.ColumnSort}s.
111      * 
112      * @param index1 the index of the first modelCollection element used for comparison
113      * @param index2 the index of the second modelCollection element used for comparison
114      * @return 0 if the two elements are considered equal, a positive integer if the element at
115      *         index1 is considered greater, else a negative integer
116      */
117     @Override
118     public int compare(Integer index1, Integer index2) {
119         int sortResult = 0;
120 
121         for (ColumnSort columnSort : columnSorts) {
122             Field protoField = prototypeRow.get(columnSort.getColumnIndex());
123 
124             Object modelElement1 = modelCollection.get(index1);
125             Object modelElement2 = modelCollection.get(index2);
126 
127             if (isOneNull(modelElement1, modelElement2)) { // is one of the modelCollection elements null?
128                 sortResult = compareOneIsNull(modelElement1, modelElement2);
129             } else if (protoField instanceof DataField) {
130                 sortResult = compareDataFieldValues(columnSort, (DataField) protoField, index1, index2);
131             } else {
132                 sortResult = compareFieldStringValues(columnSort, protoField, index1, index2);
133             }
134 
135             if (sortResult != 0) { // stop looking at additional columns, we've made our determination
136                 // Handle sort direction here
137                 if (columnSort.getDirection() == ColumnSort.Direction.DESC) {
138                     sortResult *= -1;
139                 }
140 
141                 break;
142             }
143         }
144 
145         return sortResult;
146     }
147 
148     /**
149      * Compare the DataField values for the two modelCollection element indexes.
150      * 
151      * @param columnSort the comparison metadata (which column number, which direction, what type of
152      *        sort)
153      * @param protoField the prototype DataField for the column being sorted
154      * @param index1 the index of the first modelCollection element for comparison
155      * @param index2 the index of the second modelCollection element for comparison
156      * @return 0 if the two elements are considered equal, a positive integer if the element at
157      *         index1 is considered greater, else a negative integer
158      */
159     private int compareDataFieldValues(ColumnSort columnSort, DataField protoField, Integer index1, Integer index2) {
160         final int sortResult;// for DataFields, try to get the property value and use it directly
161 
162         final Object modelElement1 = modelCollection.get(index1);
163         final Object modelElement2 = modelCollection.get(index2);
164 
165         // get the rest of the property path after the collection
166         final String propertyPath = protoField.getBindingInfo().getBindingName();
167         final Class<?> columnDataClass = getColumnDataClass(propertyPath);
168 
169         // we can do smart comparisons for Comparables
170         if (Comparable.class.isAssignableFrom(columnDataClass)) {
171             Comparable datum1 = (Comparable) ObjectPropertyUtils.getPropertyValue(modelElement1, propertyPath);
172             Comparable datum2 = (Comparable) ObjectPropertyUtils.getPropertyValue(modelElement2, propertyPath);
173 
174             if (isOneNull(datum1, datum2)) {
175                 sortResult = compareOneIsNull(datum1, datum2);
176             } else if (String.class.equals(columnDataClass)) {
177                 sortResult = columnTypeCompare((String) datum1, (String) datum2, columnSort.getSortType());
178             } else {
179                 sortResult = datum1.compareTo(datum2);
180             }
181         } else { // resort to basic column string value comparison if the column data class isn't Comparable
182             sortResult = compareFieldStringValues(columnSort, protoField, index1, index2);
183         }
184 
185         return sortResult;
186     }
187 
188     /**
189      * Attempt to determine the class of the column data value using the given modelCollection.
190      * 
191      * <p>
192      * If the class can not be determined, Object will be returned.
193      * </p>
194      * 
195      * @param propertyPath the path to the datum (which applies to modelCollection elements) whose
196      *        class we are attempting to determine
197      * @return the class of the given property from the modelElements, or Object if the class cannot
198      *         be determined.
199      */
200     private Class<?> getColumnDataClass(String propertyPath) {
201         Class<?> dataClass = propertyClassCache.get(propertyPath);
202 
203         if (dataClass == null) {
204 
205             // for the elements in the modelCollection while dataClass is null
206             for (int i = 0; i < modelCollection.size() && dataClass == null; i++) {
207                 // try getting the class from the modelCollection element
208                 dataClass = ObjectPropertyUtils.getPropertyType(modelCollection.get(i), propertyPath);
209             }
210 
211             if (dataClass == null) {
212                 dataClass = Object.class; // default
213             }
214 
215             propertyClassCache.put(propertyPath, dataClass);
216         }
217 
218         return dataClass;
219     }
220 
221     /**
222      * Compare the field values by computing the two string values and comparing them based on the
223      * sort type.
224      * 
225      * @param columnSort the comparison metadata (which column number, which direction, what type of
226      *        sort)
227      * @param protoField the prototype Field for the column being sorted
228      * @param index1 the index of the first modelCollection element for comparison
229      * @param index2 the index of the second modelCollection element for comparison
230      * @return 0 if the two elements are considered equal, a positive integer if the element at
231      *         index1 is considered greater, else a negative integer
232      */
233     private int compareFieldStringValues(ColumnSort columnSort, Field protoField, Integer index1, Integer index2) {
234         final int sortResult;
235         final String fieldValue1;
236         final String fieldValue2;
237 
238         if (!CollectionUtils.sizeIsEmpty(protoField.getPropertyExpressions())) {
239             // We have to evaluate expressions
240             fieldValue1 = calculateFieldValue(protoField, index1, columnSort.getColumnIndex());
241             fieldValue2 = calculateFieldValue(protoField, index2, columnSort.getColumnIndex());
242         } else {
243             fieldValue1 = KRADUtils.getSimpleFieldValue(modelCollection.get(index1), protoField);
244             fieldValue2 = KRADUtils.getSimpleFieldValue(modelCollection.get(index2), protoField);
245         }
246 
247         sortResult = columnTypeCompare(fieldValue1, fieldValue2, columnSort.getSortType());
248         return sortResult;
249     }
250 
251     /**
252      * Calculates the value for a field that may contain expressions.
253      * 
254      * <p>
255      * Checks for a cached value for this calculated value, and if there isn't one, expressions are
256      * evaluated before getting the value, which is then cached and returned.
257      * </p>
258      * 
259      * @param protoField the Field whose expressions need evaluation
260      * @param collectionIndex the index of the model collection element being used in the
261      *        calculation
262      * @param columnIndex the index of the column whose value is being calculated
263      * @return the calculated value for the field for this collection line
264      */
265     private String calculateFieldValue(Field protoField, Integer collectionIndex, int columnIndex) {
266         final String fieldValue1;
267 
268         // cache key format is "<elementIndex>,<columnIndex>"
269         final String cacheKey = String.format("%d,%d", collectionIndex, columnIndex);
270         String cachedValue = calculatedValueCache.get(cacheKey);
271 
272         if (cachedValue == null) {
273             View view = ViewLifecycle.getView();
274             
275             Object collectionElement = modelCollection.get(collectionIndex);
276             ExpressionEvaluator expressionEvaluator = ViewLifecycle.getExpressionEvaluator();
277 
278             // set up expression context
279             Map<String, Object> viewContext = view.getContext();
280             Map<String, Object> expressionContext = new HashMap<String, Object>();
281             
282             if (viewContext != null) {
283                 expressionContext.putAll(viewContext);
284             }
285 
286             ViewLifecyclePhase phase = ViewLifecycle.getPhase();
287             if (phase.getParent() instanceof CollectionGroup) {
288                 CollectionGroup collectionGroup = (CollectionGroup) phase.getParent();
289                 expressionContext.put(UifConstants.ContextVariableNames.COLLECTION_GROUP, collectionGroup);
290                 expressionContext.put(UifConstants.ContextVariableNames.MANAGER, collectionGroup.getLayoutManager());
291                 expressionContext.put(UifConstants.ContextVariableNames.PARENT, collectionGroup);
292             }
293             
294             expressionContext.put(UifConstants.ContextVariableNames.LINE, collectionElement);
295             expressionContext.put(UifConstants.ContextVariableNames.INDEX, collectionIndex);
296             expressionContext.put(UifConstants.ContextVariableNames.COMPONENT, protoField);
297 
298             expressionEvaluator.evaluateExpressionsOnConfigurable(view, protoField, expressionContext);
299 
300             fieldValue1 = KRADUtils.getSimpleFieldValue(collectionElement, protoField);
301 
302             calculatedValueCache.put(cacheKey, fieldValue1);
303         } else {
304             fieldValue1 = cachedValue;
305         }
306 
307         return fieldValue1;
308     }
309 
310     /**
311      * Compare the string values based on the given sortType, which must match one of the constants
312      * in {@link org.kuali.rice.krad.uif.UifConstants.TableToolsValues}.
313      * 
314      * @param val1 The first string value for comparison
315      * @param val2 The second string value for comparison
316      * @param sortType the sort type
317      * @return 0 if the two elements are considered equal, a positive integer if the element at
318      *         index1 is considered greater, else a negative integer
319      */
320     private int columnTypeCompare(String val1, String val2, String sortType) {
321         final int result;
322 
323         if (isOneNull(val1, val2)) {
324             result = compareOneIsNull(val1, val2);
325         } else if (UifConstants.TableToolsValues.STRING.equals(sortType)) {
326             result = val1.compareTo(val2);
327         } else if (UifConstants.TableToolsValues.NUMERIC.equals(sortType)) {
328             result = NumericValueComparator.getInstance().compare(val1, val2);
329         } else if (UifConstants.TableToolsValues.PERCENT.equals(sortType)) {
330             result = NumericValueComparator.getInstance().compare(val1, val2);
331         } else if (UifConstants.TableToolsValues.DATE.equals(sortType)) {
332             result = TemporalValueComparator.getInstance().compare(val1, val2);
333         } else if (UifConstants.TableToolsValues.CURRENCY.equals(sortType)) {
334             // strip off non-numeric symbols, convert to KualiDecimals, and compare
335             KualiDecimal decimal1 = new KualiDecimal(val1.replaceAll("[^0-9.]", ""));
336             KualiDecimal decimal2 = new KualiDecimal(val2.replaceAll("[^0-9.]", ""));
337 
338             result = decimal1.compareTo(decimal2);
339         } else {
340             throw new RuntimeException("unknown sort type: " + sortType);
341         }
342 
343         return result;
344     }
345 
346     /**
347      * Is one of the given objects null?
348      * 
349      * @param o1 the first object
350      * @param o2 the second object
351      * @return true if one of the given references is null, false otherwise
352      */
353     private boolean isOneNull(Object o1, Object o2) {
354         return (o1 == null || o2 == null);
355     }
356 
357     /**
358      * Compare two referenced objects (assuming at least one of them is null).
359      * 
360      * <p>
361      * The arbitrary determination here is that a non-null reference is greater than a null
362      * reference, and two null references are equal.
363      * </p>
364      * 
365      * @param o1 the first object
366      * @param o2 the second object
367      * @return 0 if both are null, 1 if the first is non-null, and -1 if the second is non-null.
368      */
369     private int compareOneIsNull(Object o1, Object o2) {
370         if (o1 == null) {
371             if (o2 == null) {
372                 return 0;
373             }
374 
375             return -1;
376         }
377 
378         if (o2 != null) {
379             throw new IllegalStateException("at least one parameter must be null");
380         }
381 
382         return 1;
383     }
384 
385     /**
386      * Build a List of prototype Fields representing a row of the table.
387      * 
388      * <p>Any DataFields will have their binding paths shortened to access the model collection
389      * elements directly, instead of via the data object</p>
390      * 
391      * @return a List of prototype Fields representing a row in the table
392      */
393     protected List<Field> buildPrototypeRow() {
394         LineBuilderContext lineBuilderContext = new LineBuilderContext(0, modelCollection.get(0), null, false, form,
395                 collectionGroup, collectionGroup.getLineActions());
396 
397         CollectionGroupLineBuilder collectionGroupLineBuilder =
398                 collectionGroup.getCollectionGroupBuilder().getCollectionGroupLineBuilder(lineBuilderContext);
399         collectionGroupLineBuilder.preprocessLine();
400 
401         TableRowBuilder tableRowBuilder = new TableRowBuilder(collectionGroup, lineBuilderContext);
402         TableRow tableRow = tableRowBuilder.buildRow();
403 
404         return tableRow.getColumns();
405     }
406 }