/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.myfaces.component.html.ext;
import java.sql.ResultSet;
import java.text.CollationKey;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.faces.context.FacesContext;
import javax.faces.model.ArrayDataModel;
import javax.faces.model.DataModel;
import javax.faces.model.ListDataModel;
import javax.faces.model.ResultDataModel;
import javax.faces.model.ResultSetDataModel;
import javax.faces.model.ScalarDataModel;
import javax.servlet.jsp.jstl.sql.Result;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
*
*/
public final class SortableModel extends DataModel
{
private static final Log log = LogFactory.getLog(SortableModel.class);
private SortCriterion _sortCriterion = null;
private DataModel _model = null;
private Object _wrappedData = null;
private IntList _sortedIndicesList = null, // from baseIndex to sortedIndex
_baseIndicesList = null; // from sortedIndex to baseIndex
private static final Class OBJECT_ARRAY_CLASS = (new Object[0]).getClass();
/**
* Create a new SortableModel from the given instance.
* @param model This will be converted into a {@link DataModel}
* @see #setWrappedData
*/
public SortableModel(Object model)
{
setWrappedData(model);
}
/**
* No arg constructor for use as a managed-bean.
* Must call setWrappedData before using this instance.
*/
public SortableModel(){}
public Object getRowData()
{
return _model.getRowData();
}
public Object getWrappedData()
{
return _wrappedData;
}
public boolean isRowAvailable()
{
return _model.isRowAvailable();
}
/**
* Sets the underlying data being managed by this instance.
* @param data This Object will be converted into a
* {@link DataModel}.
*/
public void setWrappedData(Object data)
{
_baseIndicesList = null;
_model = toDataModel(data);
_sortCriterion = null;
_sortedIndicesList = null;
_wrappedData = data;
}
protected DataModel toDataModel(Object data)
{
if (data == null)
{
return EMPTY_DATA_MODEL;
}
else if (data instanceof DataModel)
{
return (DataModel) data;
}
else if (data instanceof List)
{
return new ListDataModel((List) data);
}
// accept a Collection is not supported in the Spec
else if (data instanceof Collection)
{
return new ListDataModel(new ArrayList((Collection) data));
}
else if (OBJECT_ARRAY_CLASS.isAssignableFrom(data.getClass()))
{
return new ArrayDataModel((Object[]) data);
}
else if (data instanceof ResultSet)
{
return new ResultSetDataModel((ResultSet) data);
}
else if (data instanceof Result)
{
return new ResultDataModel((Result) data);
}
else
{
return new ScalarDataModel(data);
}
}
public int getRowCount()
{
return _model.getRowCount();
}
public void setRowIndex(int rowIndex)
{
int baseIndex = _toBaseIndex(rowIndex);
_model.setRowIndex(baseIndex);
}
public int getRowIndex()
{
int baseIndex = _model.getRowIndex();
return _toSortedIndex(baseIndex);
}
/**
* Checks to see if the underlying collection is sortable by the given property.
* @param property The name of the property to sort the underlying collection by.
* @return true, if the property implements java.lang.Comparable
*/
public boolean isSortable(String property)
{
final int oldIndex = _model.getRowIndex();
try
{
_model.setRowIndex(0);
if (!_model.isRowAvailable())
return false; // if there is no data in the table then nothing is sortable
try
{
Object propertyValue = PropertyUtils.getProperty(_model.getRowData(),property);
// when the value is null, we don't know if we can sort it.
// by default let's support sorting of null values, and let the user
// turn off sorting if necessary:
return (propertyValue instanceof Comparable) || (propertyValue == null);
}
catch (RuntimeException e)
{
// don't propagate this exception out. This is because it might break
// the VE.
log.warn(e);
return false;
}
catch (Exception e) {
log.warn(e);
return false;
}
}
finally
{
_model.setRowIndex(oldIndex);
}
}
public List getSortCriteria()
{
return (_sortCriterion == null) ? Collections.EMPTY_LIST : Collections.singletonList(_sortCriterion);
}
public void setSortCriteria(List criteria)
{
if ((criteria == null) || (criteria.isEmpty()))
{
_sortCriterion = null;
// restore unsorted order:
_baseIndicesList = _sortedIndicesList = null;
}
else
{
SortCriterion sc = (SortCriterion) criteria.get(0);
if ((_sortCriterion == null) || (!_sortCriterion.equals(sc)))
{
_sortCriterion = sc;
_sort(_sortCriterion.getProperty(), _sortCriterion.isAscending());
}
}
}
public String toString()
{
return "SortableModel[" + _model + "]";
}
/**
* Sorts the underlying collection by the given property, in the
* given direction.
* @param property The name of the property to sort by. The value of this
* property must implement java.lang.Comparable.
* @param isAscending true if the collection is to be sorted in
* ascending order.
* @todo support -1 for rowCount
*/
private void _sort(String property, boolean isAscending)
{
//TODO: support -1 for rowCount:
int sz = getRowCount();
if ((_baseIndicesList == null) || (_baseIndicesList.size() != sz))
{
// we do not want to mutate the original data.
// however, instead of copying the data and sorting the copy,
// we will create a list of indices into the original data, and
// sort the indices. This way, when certain rows are made current
// in this Collection, we can make them current in the underlying
// DataModel as well.
_baseIndicesList = new IntList(sz);
}
final int rowIndex = _model.getRowIndex();
_model.setRowIndex(0);
// Make sure the model has that row 0! (It could be empty.)
if (_model.isRowAvailable())
{
Comparator comp = new Comp(property);
if (!isAscending)
comp = new Inverter(comp);
Collections.sort(_baseIndicesList, comp);
_sortedIndicesList = null;
}
_model.setRowIndex(rowIndex);
}
private int _toSortedIndex(int baseIndex)
{
if ((_sortedIndicesList == null) && (_baseIndicesList != null))
{
_sortedIndicesList = (IntList) _baseIndicesList.clone();
for(int i=0; i<_baseIndicesList.size(); i++)
{
Integer base = (Integer) _baseIndicesList.get(i);
_sortedIndicesList.set(base.intValue(), new Integer(i));
}
}
return _convertIndex(baseIndex, _sortedIndicesList);
}
private int _toBaseIndex(int sortedIndex)
{
return _convertIndex(sortedIndex, _baseIndicesList);
}
private int _convertIndex(int index, List indices)
{
if (index < 0) // -1 is special
return index;
if ((indices != null) && (indices.size() > index))
{
index = ((Integer) indices.get(index)).intValue();
}
return index;
}
private static final class IntList extends ArrayList implements Cloneable
{
public IntList(int size)
{
super(size);
_expandToSize(size);
}
private void _expandToSize(int desiredSize)
{
for(int i=0; i<desiredSize; i++)
add(new Integer(i));
}
}
private final class Comp implements Comparator
{
private final String _prop;
private Collator _collator;
private Map _collationKeys;
public Comp(String property)
{
_prop = property;
_collator = Collator.getInstance(FacesContext.getCurrentInstance().getViewRoot().getLocale());
_collationKeys = new HashMap();
}
public int compare(Object o1, Object o2)
{
int index1 = ((Integer) o1).intValue();
int index2 = ((Integer) o2).intValue();
Object value1 = null;
Object value2 = null;
try {
_model.setRowIndex(index1);
value1 = PropertyUtils.getProperty(_model.getRowData(),_prop);
_model.setRowIndex(index2);
value2 = PropertyUtils.getProperty(_model.getRowData(),_prop);
}
catch (Exception exc) {
log.error(exc);
}
if (value1 == null)
return (value2 == null) ? 0 : -1;
if (value2 == null)
return 1;
// ?? Sometimes, isSortable returns true
// even if the underlying object is not a Comparable.
// This happens if the object at rowIndex zero is null.
// So test before we cast:
if (value1 instanceof String) {
//if the object is a String we best compare locale-sesitive
CollationKey collationKey1 = getCollationKey((String)value1);
CollationKey collationKey2 = getCollationKey((String)value2);
return collationKey1.compareTo(collationKey2);
}
else if (value1 instanceof Comparable)
{
return ((Comparable) value1).compareTo(value2);
}
else
{
// if the object is not a Comparable, then
// the best we can do is string comparison:
return value1.toString().compareTo(value2.toString());
}
}
private CollationKey getCollationKey(String propertyValue) {
CollationKey key = (CollationKey)_collationKeys.get(propertyValue);
if (key == null) {
key = _collator.getCollationKey(propertyValue);
_collationKeys.put(propertyValue, key);
}
return key;
}
}
/**
*
*/
private static final class Inverter implements Comparator
{
private final Comparator _comp;
public Inverter(Comparator comp)
{
_comp = comp;
}
public int compare(Object o1, Object o2)
{
return _comp.compare(o2, o1);
}
}
/**
*
*/
private static final DataModel EMPTY_DATA_MODEL = new _SerializableDataModel()
{
public boolean isRowAvailable()
{
return false;
}
public int getRowCount()
{
return 0;
}
public Object getRowData()
{
throw new IllegalArgumentException();
}
public int getRowIndex()
{
return -1;
}
public void setRowIndex(int i)
{
if (i < -1)
throw new IndexOutOfBoundsException("Index < 0 : " + i);
}
public Object getWrappedData()
{
return null;
}
public void setWrappedData(Object obj)
{
if (obj == null)
return; //Clearing is allowed
throw new UnsupportedOperationException(this.getClass().getName()
+ " UnsupportedOperationException");
}
};
}