View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.trinidad.model;
20  
21  import java.text.Collator;
22  
23  import java.util.ArrayList;
24  import java.util.Collections;
25  import java.util.Comparator;
26  import java.util.HashMap;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  
31  import javax.el.ELContext;
32  import javax.el.ELResolver;
33  import javax.el.FunctionMapper;
34  import javax.el.VariableMapper;
35  
36  import javax.faces.FactoryFinder;
37  import javax.faces.application.ApplicationFactory;
38  import javax.faces.context.FacesContext;
39  import javax.faces.model.DataModel;
40  import javax.faces.model.DataModelListener;
41  
42  import org.apache.myfaces.trinidad.context.RequestContext;
43  import org.apache.myfaces.trinidad.logging.TrinidadLogger;
44  
45  
46  /**
47   * Creates a CollectionModel that is sortable.
48   * All properties that implement java.lang.Comparable are deemed sortable.
49   */
50  public class SortableModel extends CollectionModel
51  {
52    /**
53     * This class provides an enumeration to work with the integer values of the
54     * {@link Collator} strength values.
55     */
56    public enum Strength
57    {
58      /** @see Collator#IDENTICAL */
59      IDENTICAL(Collator.IDENTICAL),
60      /** @see Collator#PRIMARY */
61      PRIMARY(Collator.PRIMARY),
62      /** @see Collator#SECONDARY */
63      SECONDARY(Collator.SECONDARY),
64      /** @see Collator#TERTIARY */
65      TERTIARY(Collator.TERTIARY);
66  
67      private Strength(int strength)
68      {
69        _strength = strength;
70      }
71  
72      public int getIntValue()
73      {
74        return _strength;
75      }
76  
77      private final int _strength;
78    }
79  
80    /**
81     * This class provides an enumeration to work with the integer values of the
82     * {@link Collator} decomposition values.
83     */
84    public enum Decomposition
85    {
86      /** @see Collator#PRIMARY */
87      NO_DECOMPOSITION(Collator.NO_DECOMPOSITION),
88      /** @see Collator#SECONDARY */
89      CANONICAL_DECOMPOSITION(Collator.CANONICAL_DECOMPOSITION),
90      /** @see Collator#TERTIARY */
91      FULL_DECOMPOSITION(Collator.FULL_DECOMPOSITION);
92  
93      private Decomposition(int decomposition)
94      {
95        _decomposition = decomposition;
96      }
97  
98      public int getIntValue()
99      {
100       return _decomposition;
101     }
102 
103     private final int _decomposition;
104   }
105 
106   /**
107    * Create a new SortableModel from the given instance.
108    * @param model This will be converted into a {@link DataModel}
109    * @see #setWrappedData
110    */
111   public SortableModel(Object model)
112   {
113     setWrappedData(model);
114   }
115 
116   /**
117    * No arg constructor for use as a managed-bean.
118    * Must call setWrappedData before using this instance.
119    */
120   public SortableModel()
121   {
122   }
123 
124   @Override
125   public Object getRowData()
126   {
127     return _model.getRowData();
128   }
129 
130   @Override
131   public Object getWrappedData()
132   {
133     return _wrappedData;
134   }
135 
136   @Override
137   public boolean isRowAvailable()
138   {
139     return _model.isRowAvailable();
140   }
141 
142   /**
143    * Sets the underlying data being managed by this instance.
144    * @param data This Object will be converted into a
145    * {@link DataModel}.
146    * @see ModelUtils#toDataModel
147    */
148   @Override
149   public void setWrappedData(Object data)
150   {
151     _baseIndicesList = null;
152     _model = ModelUtils.toDataModel(data);
153     _sortCriterion = null;
154     _sortedIndicesList = null;
155     _wrappedData = data;
156   }
157 
158   @Override
159   public int getRowCount()
160   {
161     return _model.getRowCount();
162   }
163 
164   @Override
165   public void setRowIndex(int rowIndex)
166   {
167     int baseIndex = _toBaseIndex(rowIndex);
168     _model.setRowIndex(baseIndex);
169   }
170 
171   @Override
172   public int getRowIndex()
173   {
174     int baseIndex = _model.getRowIndex();
175     return _toSortedIndex(baseIndex);
176   }
177 
178   /**
179    * Gets the row key of the current row
180    * @inheritDoc
181    */
182   @Override
183   public Object getRowKey()
184   {
185     return isRowAvailable()
186       ? _model.getRowIndex()
187       : null;
188   }
189 
190   /**
191    * Finds the row with the matching key and makes it current
192    * @inheritDoc
193    */
194   @Override
195   public void setRowKey(Object key)
196   {
197     _model.setRowIndex(_toRowIndex(key));
198   }
199 
200   public void addDataModelListener(DataModelListener listener)
201   {
202     _model.addDataModelListener(listener);
203   }
204 
205   public DataModelListener[] getDataModelListeners()
206   {
207     return _model.getDataModelListeners();
208   }
209 
210   public void removeDataModelListener(DataModelListener listener)
211   {
212     _model.removeDataModelListener(listener);
213   }
214 
215   /**
216    * Checks to see if the underlying collection is sortable by the given property.
217    * @param property The name of the property to sort the underlying collection by.
218    * @return true, if the property implements java.lang.Comparable
219    */
220   @Override
221   public boolean isSortable(String property)
222   {
223     final int oldIndex = _model.getRowIndex();
224     try
225     {
226       _model.setRowIndex(0);
227       if (!_model.isRowAvailable())
228         return false; // if there is no data in the table then nothing is sortable
229 
230       Object data = _model.getRowData();
231       try
232       {
233         //TODO clean up that _getELXyz() calls
234         FacesContext context = FacesContext.getCurrentInstance();
235         ELResolver resolver = _getELResolver(context);
236         ELContext elContext = _getELContext(context, resolver);
237         Object propertyValue = evaluateProperty(resolver, elContext, data, property);
238         // when the value is null, we don't know if we can sort it.
239         // by default let's support sorting of null values, and let the user
240         // turn off sorting if necessary:
241         return (propertyValue instanceof Comparable) ||
242           (propertyValue == null);
243       }
244       catch (RuntimeException e)
245       {
246         // don't propagate this exception out. This is because it might break
247         // the VE.
248         _LOG.warning(e);
249         return false;
250       }
251     }
252     finally
253     {
254       _model.setRowIndex(oldIndex);
255     }
256   }
257 
258   private Object evaluateProperty(ELResolver resolver, ELContext context, Object base, String property)
259   {
260     //simple property -> resolve value directly
261     if (!property.contains( "." ))
262       return resolver.getValue(context, base, property );
263 
264     int index = property.indexOf( '.' );
265     Object newBase = resolver.getValue(context, base, property.substring( 0, index ) );
266 
267     return evaluateProperty(resolver, context, newBase, property.substring( index + 1 ) );
268   }
269 
270   @Override
271   public List<SortCriterion> getSortCriteria()
272   {
273     if (_sortCriterion == null)
274     {
275       return Collections.emptyList();
276     }
277     else
278     {
279       return Collections.singletonList(_sortCriterion);
280     }
281   }
282 
283   @Override
284   public void setSortCriteria(List<SortCriterion> criteria)
285   {
286     if ((criteria == null) || (criteria.isEmpty()))
287     {
288       _sortCriterion = null;
289       // restore unsorted order:
290       _baseIndicesList = _sortedIndicesList = null;
291     }
292     else
293     {
294       SortCriterion sc = criteria.get(0);
295       if ((_sortCriterion == null) || (!_sortCriterion.equals(sc)))
296       {
297         // cache the latest sort criterion and do the sorting.
298         _sortCriterion = sc;
299         _sort(_sortCriterion);
300       }
301     }
302   }
303 
304   /**
305    * Get the comparator associated with the given property.
306    *
307    * @param propertyName the property
308    * @return the comparator or null if one has not been set
309    */
310   public Comparator getComparator(
311     String propertyName)
312   {
313     return _propertyComparators == null ?
314       null :
315       _propertyComparators.get(propertyName);
316   }
317 
318   /**
319    * Set a custom comparator to use to sort the given property name.
320    *
321    * @param propertyName the property with which to associate the comparator
322    * @param comparator the comparator to use, or null to remove one
323    */
324   public void setComparator(
325     String     propertyName,
326     Comparator comparator)
327   {
328     assert propertyName != null : "Property name may not be null";
329 
330     if (comparator == null && _propertyComparators != null)
331     {
332       _propertyComparators.remove(propertyName);
333       if (_propertyComparators.isEmpty())
334       {
335         _propertyComparators = null;
336       }
337     }
338     else if (comparator != null)
339     {
340       if (_propertyComparators == null)
341       {
342         _propertyComparators = new HashMap<String, Comparator>();
343       }
344       _propertyComparators.put(propertyName, comparator);
345     }
346 
347     if (_sortCriterion != null && propertyName.equals(_sortCriterion.getProperty()))
348     {
349       _sort(_sortCriterion);
350     }
351   }
352 
353   /**
354    * Convenience method to set a compatator for a property using a {@link Collator} setup with
355    * the given strength and decomposition values.
356    *
357    * @param propertyName the property
358    * @param collatorStrength the stregth to use or null to leave as the default for the
359    * default locale
360    * @param collatorDecomposition the decomposition to use or null to leave as the default for the
361    * default locale
362    * @see #setComparator(String, Comparator)
363    */
364   public void setCollator(
365     String        propertyName,
366     Strength      collatorStrength,
367     Decomposition collatorDecomposition)
368   {
369     Locale locale = null;
370 
371     RequestContext reqCtx = RequestContext.getCurrentInstance();
372     if (reqCtx != null)
373     {
374       FacesContext facesContext = FacesContext.getCurrentInstance();
375       if (facesContext != null)
376       {
377         locale = _getLocale(reqCtx, facesContext);
378       }
379     }
380 
381     Collator collator = locale == null ? Collator.getInstance() : Collator.getInstance(locale);
382     if (collatorDecomposition != null)
383     {
384       collator.setDecomposition(collatorDecomposition.getIntValue());
385     }
386 
387     if (collatorStrength != null)
388     {
389       collator.setStrength(collatorStrength.getIntValue());
390     }
391 
392     setComparator(propertyName, collator);
393   }
394 
395   @Override
396   public String toString()
397   {
398     return "SortableModel[" + _model + "]";
399   }
400 
401   /**
402    * Sorts the underlying collection by the given property, in the
403    * given direction, with the given strength.
404    * @param sortCriterion sort criterion controlling sort behavior.
405    * @todo support -1 for rowCount
406    */
407   private void _sort(SortCriterion sortCriterion)
408   {
409     //TODO: support -1 for rowCount:
410     int sz = getRowCount();
411     if ((_baseIndicesList == null) || (_baseIndicesList.size() != sz))
412     {
413       // we do not want to mutate the original data.
414       // however, instead of copying the data and sorting the copy,
415       // we will create a list of indices into the original data, and
416       // sort the indices. This way, when certain rows are made current
417       // in this Collection, we can make them current in the underlying
418       // DataModel as well.
419 
420       _baseIndicesList = new IntList(sz);
421     }
422 
423     final int rowIndex = _model.getRowIndex();
424     _model.setRowIndex(0);
425     // Make sure the model has that row 0! (It could be empty.)
426     if (_model.isRowAvailable())
427     {
428       FacesContext context = FacesContext.getCurrentInstance();
429       RequestContext rc = RequestContext.getCurrentInstance();
430       ELResolver resolver = _getELResolver(context);
431       ELContext elContext = _getELContext(context, resolver);
432       Locale locale = _getLocale(rc, context);
433       Comparator<Integer> comp =
434         new Comp(resolver, elContext, locale, sortCriterion.getProperty(),
435                  sortCriterion.getSortStrength());
436       if (!sortCriterion.isAscending())
437         comp = new Inverter<Integer>(comp);
438 
439       Collections.sort(_baseIndicesList, comp);
440       _sortedIndicesList = null;
441     }
442 
443     _model.setRowIndex(rowIndex);
444   }
445 
446   private int _toSortedIndex(int baseIndex)
447   {
448     if ((_sortedIndicesList == null) && (_baseIndicesList != null))
449     {
450       _sortedIndicesList = (IntList) _baseIndicesList.clone();
451       for(int i=0; i<_baseIndicesList.size(); i++)
452       {
453         Integer base = _baseIndicesList.get(i);
454         _sortedIndicesList.set(base.intValue(), i);
455       }
456     }
457 
458     return _convertIndex(baseIndex, _sortedIndicesList);
459   }
460 
461   private int _toBaseIndex(int sortedIndex)
462   {
463     return _convertIndex(sortedIndex, _baseIndicesList);
464   }
465 
466   private int _convertIndex(int index, List<Integer> indices)
467   {
468     if (index < 0) // -1 is special
469       return index;
470 
471     if ((indices != null) && (indices.size() > index))
472     {
473       index = indices.get(index).intValue();
474     }
475     return index;
476   }
477 
478   private int _toRowIndex(Object rowKey)
479   {
480     if (rowKey == null)
481       return -1;
482 
483     try
484     {
485       return ((Integer)rowKey).intValue();
486     }
487     catch (ClassCastException e)
488     {
489       _LOG.warning("INVALID_ROWKEY", new Object[]{rowKey , rowKey.getClass()});
490       _LOG.warning(e);
491       return -1;
492     }
493   }
494 
495 
496 
497   private static final class IntList extends ArrayList<Integer>
498   {
499     public IntList(int size)
500     {
501       super(size);
502       _expandToSize(size);
503     }
504 
505     private void _expandToSize(int desiredSize)
506     {
507       for(int i=0; i<desiredSize; i++)
508       {
509         add(i);
510       }
511     }
512 
513     private static final long serialVersionUID = 1L;
514   }
515 
516   private final class Comp implements Comparator<Integer>
517   {
518     public Comp(
519       ELResolver   resolver,
520       ELContext    context,
521       Locale       locale,
522       String       property,
523       SortStrength sortStrength)
524     {
525       _resolver = resolver;
526       _context  = context;
527 
528       // use Collator as comparator whenever locale or strength is available,
529       // so sorting is natural to that locale.
530       if (locale != null || sortStrength != null)
531       {
532         if (locale != null)
533           _collator = Collator.getInstance(locale);
534         else
535           _collator = Collator.getInstance();
536 
537         if (sortStrength != null)
538           _collator.setStrength(sortStrength.getStrength());
539       }
540       else
541       {
542         _collator = null;
543       }
544 
545       _prop = property;
546     }
547 
548     @SuppressWarnings("unchecked")
549     public int compare(
550       Integer o1,
551       Integer o2)
552     {
553       int index1 = o1.intValue();
554       int index2 = o2.intValue();
555 
556       _model.setRowIndex(index1);
557       Object instance1 = _model.getRowData();
558       Object value1 = evaluateProperty(_resolver, _context, instance1, _prop );
559 
560       _model.setRowIndex(index2);
561       Object instance2 = _model.getRowData();
562       Object value2 = evaluateProperty(_resolver, _context, instance2, _prop );
563 
564       if (value1 == null)
565         return (value2 == null) ? 0 : -1;
566 
567       if (value2 == null)
568         return 1;
569 
570       Comparator comparator = getComparator(_prop);
571       if (comparator == null)
572       {
573         // bug 4545164. Sometimes, isSortable returns true
574         // even if the underlying object is not a Comparable.
575         // This happens if the object at rowIndex zero is null.
576         // So test before we cast:
577         if (value1 instanceof Comparable)
578         {
579           if ((value1 instanceof String) && (value2 instanceof String))
580           {
581             return _compare((String) value1, (String) value2);
582           }
583           else
584           {
585             return ((Comparable<Object>) value1).compareTo(value2);
586           }
587         }
588         else
589         {
590           // if the object is not a Comparable, then
591           // the best we can do is string comparison:
592           return _compare(value1.toString(), value2.toString());
593         }
594       }
595       else
596       {
597         return comparator.compare(value1, value2);
598       }
599     }
600 
601     private int _compare(
602       String s1,
603       String s2)
604     {
605       if (_collator != null)
606       {
607         return _collator.compare(s1, s2);
608       }
609       else
610       {
611         return s1.compareTo(s2);
612       }
613     }
614 
615     private final ELResolver _resolver;
616     private final ELContext  _context;
617     private final Collator _collator;
618     private final String _prop;
619   }
620 
621   private static final class Inverter<T> implements Comparator<T>
622   {
623     public Inverter(Comparator<T> comp)
624     {
625       _comp = comp;
626     }
627 
628     public int compare(T o1, T o2)
629     {
630       return _comp.compare(o2, o1);
631     }
632 
633     private final Comparator<T> _comp;
634   }
635 
636   /**
637    * Quickie implementation of ELContext for use
638    * if we're not being called in the JSF lifecycle
639    */
640   private static final class ELContextImpl extends ELContext
641   {
642     public ELContextImpl(ELResolver resolver)
643     {
644       _resolver = resolver;
645     }
646 
647     @Override
648     public ELResolver getELResolver()
649     {
650       return _resolver;
651     }
652 
653     @Override
654     public FunctionMapper getFunctionMapper()
655     {
656       // Because we're only really being used to pass
657       // to an ELResolver, no FunctionMapper is needed
658       return null;
659     }
660 
661     @Override
662     public VariableMapper getVariableMapper()
663     {
664       // Because we're only really being used to pass
665       // to an ELResolver, no VariableMapper is needed
666       return null;
667     }
668 
669     private final ELResolver _resolver;
670   }
671 
672   static Object __resolveProperty(Object object, String propertyName)
673   {
674     FacesContext context = FacesContext.getCurrentInstance();
675     ELResolver resolver = _getELResolver(context);
676     ELContext elContext = _getELContext(context, resolver);
677     return resolver.getValue(elContext, object, propertyName);
678   }
679 
680   static private ELContext _getELContext(
681     FacesContext context, ELResolver resolver)
682   {
683     // Hopefully, we have a FacesContext.  If not, we're
684     // going to have to synthesize one!
685     if (context != null)
686       return context.getELContext();
687 
688     return new ELContextImpl(resolver);
689   }
690 
691   static private ELResolver _getELResolver(FacesContext context)
692   {
693     // First try the FacesContext, which is a faster way to
694     // get the ELResolver (and the 99.9% scenario)
695     if (context != null)
696       return context.getApplication().getELResolver();
697 
698     // If that fails, then we're likely outside of the JSF lifecycle.
699     // Look to the ApplicationFactory.
700     ApplicationFactory factory = (ApplicationFactory)
701       FactoryFinder.getFactory(FactoryFinder.APPLICATION_FACTORY);
702     return factory.getApplication().getELResolver();
703 
704   }
705 
706   static private Locale _getLocale(RequestContext requestContext, FacesContext facesContext)
707   {
708     if (requestContext != null)
709       return requestContext.getFormattingLocale();
710 
711     if (facesContext != null)
712       return facesContext.getViewRoot().getLocale();
713 
714     return null;
715   }
716 
717   private SortCriterion _sortCriterion = null;
718 
719   private DataModel _model = null;
720   private Object _wrappedData = null;
721 
722   private Map<String, Comparator> _propertyComparators;
723 
724   private IntList _sortedIndicesList = null, // from baseIndex to sortedIndex
725     _baseIndicesList = null; // from sortedIndex to baseIndex
726 
727   static private final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(SortableModel.class);
728 }