/* FieldComparator.java
Purpose:
Description:
History:
Jan 8, 2009 5:49:21 PM, Created by henrichen
Copyright (C) 2009 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zul;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.zkoss.lang.Strings;
import org.zkoss.lang.reflect.Fields;
import org.zkoss.util.CollectionsX;
import org.zkoss.zk.ui.UiException;
/**
* <p>Based on the the given field names to compare the field value of the object
* that is passed in {@link #compare} method.</p>
* <p>The field names accept compound "a.b.c" expression. It also
* accept multiple field names that you can give expression in the form
* of e.g. "name, age, salary" and this comparator will compare in that sequence.</p>
*
* @author henrichen
* @since 3.6.0
*/
public class FieldComparator implements Comparator, Serializable {
private static final long serialVersionUID = 20090120111922L;
/** The field names collection. */
private Collection<FieldInfo> _fieldnames;
/** The cached field name string. */
private transient String _orderBy;
/** The original orderBy passed to the constructor. */
private String _rawOrderBy;
/** Whether to treat null as the maximum value. */
private boolean _maxnull;
private boolean _ascending;
/** Compares with the fields per the given "ORDER BY" clause.
* <p>Note: It assumes null as minimum value.
* If not, use {@link #FieldComparator(String, boolean, boolean)}
* instead.</p>
*
* @param orderBy the "ORDER BY" clause to be compared upon for the given object in {@link #compare}.
* @param ascending whether to sort as ascending (or descending).
*/
public FieldComparator(String orderBy, boolean ascending) {
this(orderBy, ascending, false);
}
/** Compares with the fields per the given "ORDER BY" clause.
*
* @param orderBy the "ORDER BY" clause to be compared upon for the given object in {@link #compare}.
* @param ascending whether to sort as ascending (or descending).
* @param nullAsMax whether to consider null as the maximum value.
* If false, null is considered as the minimum value.
*/
public FieldComparator(String orderBy, boolean ascending, boolean nullAsMax) {
if (Strings.isBlank(orderBy)) {
throw new UiException("Empty fieldnames: " + orderBy);
}
_fieldnames = parseFieldNames(orderBy, ascending);
_maxnull = nullAsMax;
_rawOrderBy = orderBy;
_ascending = ascending;
}
public int compare(Object o1, Object o2) {
try {
for (FieldInfo fi : _fieldnames) {
final int res = compare0(o1, o2, fi.fieldname, fi.asc, fi.func);
if (res != 0) {
return res;
}
}
return 0;
} catch (NoSuchMethodException ex) {
throw UiException.Aide.wrap(ex);
}
}
/** Returns the order-by clause.
* Notice that is the parsed result, such as <code>name=category ASC</code>.
* For the original format, please use {@link #getRawOrderBy}.
*/
public String getOrderBy() {
if (_orderBy == null) {
final StringBuffer sb = new StringBuffer(_fieldnames.size() * 16);
final Iterator<FieldInfo> it = _fieldnames.iterator();
if (it.hasNext()) {
appendField(sb, it.next());
}
while (it.hasNext()) {
sb.append(',');
appendField(sb, it.next());
}
_orderBy = sb.toString();
}
return _orderBy;
}
/** Returns the original order-by clause passed to the constructor.
* It is usually the field's name, such as <code>category</code>,
* or a concatenation of field names, such as <code>category.name</code>.
* <p>Notice that, with the field's name, you could retrieve the value
* by use of {@link Fields#getByCompound}.
* @since 5.0.6
*/
public String getRawOrderBy() {
return _rawOrderBy;
}
/** Returns whether the sorting is ascending.
* @since 5.0.6
*/
public boolean isAscending() {
return _ascending;
}
private void appendField(StringBuffer sb, FieldInfo fi) {
if (fi.func != null) {
sb.append(fi.func).append('(').append(fi.fieldname).append(')');
} else {
sb.append(fi.fieldname);
}
sb.append(fi.asc ? " ASC" : " DESC");
}
@SuppressWarnings("unchecked")
private int compare0(Object o1, Object o2, String fieldname, boolean asc, String func)
throws NoSuchMethodException {
// Bug B50-3183438: Access to bean shall be consistent
final Object f1 = o1 instanceof Map ? ((Map) o1).get(fieldname)
: Fields.getByCompound(getCompareObject(o1), fieldname);
final Object f2 = o2 instanceof Map ? ((Map) o2).get(fieldname)
: Fields.getByCompound(getCompareObject(o2), fieldname);
final Object v1 = handleFunction(f1, func);
final Object v2 = handleFunction(f2, func);
if (v1 == null)
return v2 == null ? 0 : (asc == _maxnull) ? 1 : -1;
if (v2 == null)
return (asc == _maxnull) ? -1 : 1;
final int v = ((Comparable) v1).compareTo(v2);
return asc ? v : -v;
}
private Object getCompareObject(Object o) {
if (o instanceof TreeNode)
return ((TreeNode) o).getData();
return o;
}
private Object handleFunction(Object c, String func) {
if ("UPPER".equals(func)) {
if (c instanceof String)
return ((String) c).toUpperCase();
if (c instanceof Character)
return new Character(Character.toUpperCase(((Character) c).charValue()));
} else if ("LOWER".equals(func)) {
if (c instanceof String)
return ((String) c).toLowerCase(java.util.Locale.ENGLISH);
if (c instanceof Character)
return new Character(Character.toLowerCase(((Character) c).charValue()));
}
return c;
}
private Collection<FieldInfo> parseFieldNames(String fieldnames, boolean ascending) {
final Collection<String> fields = CollectionsX.parse(new ArrayList<String>(), fieldnames, ',');
final List<FieldInfo> results = new ArrayList<FieldInfo>(fields.size());
for (final Iterator<String> it = fields.iterator(); it.hasNext();) {
final String field = it.next().trim();
String fieldname;
String ascstr = "asc";
//whether a String
String func = null;
final String ufn = field.toUpperCase();
if (ufn.startsWith("UPPER(") || ufn.startsWith("LOWER(")) { //with function
final int k = field.lastIndexOf(')');
if (k == 0) {
throw new UiException("No closing function ')' mark: " + field);
} else if (k == 6) {
throw new UiException("No data inside function: " + field);
} else {
fieldname = field.substring(6, k);
if ((k + 1) < field.length()) { //with asc
ascstr = field.substring(k + 1);
if (Strings.isBlank(ascstr)) {
ascstr = "asc";
}
}
func = ufn.substring(0, 5);
}
} else {
final int j = field.indexOf(' ');
if (j < 0) {
fieldname = field;
} else {
fieldname = field.substring(0, j);
ascstr = field.substring(j + 1);
}
}
boolean asc;
if ("asc".equalsIgnoreCase(ascstr)) {
asc = ascending;
} else if ("desc".equalsIgnoreCase(ascstr)) {
asc = !ascending;
} else {
throw new UiException("field must be in the form of \"field ASC\" or \"field DESC\":" + ascstr);
}
results.add(new FieldInfo(fieldname, asc, func));
}
return results;
}
private static class FieldInfo implements Serializable {
private String fieldname;
private boolean asc;
private String func;
public FieldInfo(String fieldname, boolean asc, String func) {
this.fieldname = fieldname;
this.asc = asc;
this.func = func;
}
}
}