All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.googlecode.jpattern.org.cojen.util.BeanComparator Maven / Gradle / Ivy

Go to download

This is a copy of the good Cojen project from http://cojen.sourceforge.net/ with package name changed

The newest version!
/*
 *  Copyright 2004-2010 Brian S O'Neill
 *
 *  Licensed 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 com.googlecode.jpattern.org.cojen.util;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.security.AccessController;
import java.security.PrivilegedAction;

import com.googlecode.jpattern.org.cojen.classfile.CodeBuilder;
import com.googlecode.jpattern.org.cojen.classfile.Label;
import com.googlecode.jpattern.org.cojen.classfile.LocalVariable;
import com.googlecode.jpattern.org.cojen.classfile.MethodInfo;
import com.googlecode.jpattern.org.cojen.classfile.Modifiers;
import com.googlecode.jpattern.org.cojen.classfile.Opcode;
import com.googlecode.jpattern.org.cojen.classfile.RuntimeClassFile;
import com.googlecode.jpattern.org.cojen.classfile.TypeDesc;

/**
 * A highly customizable, high-performance Comparator, designed specifically
 * for advanced sorting of JavaBeans. BeanComparators contain dynamically
 * auto-generated code and perform as well as hand written Comparators.
 * 

* BeanComparator instances are immutable; order customization methods * return new BeanComparators with refined rules. Calls to customizers can * be chained together to read like a formula. The following example produces * a Comparator that orders Threads by name, thread group name, and reverse * priority. * *

 * Comparator c = BeanComparator.forClass(Thread.class)
 *     .orderBy("name")
 *     .orderBy("threadGroup.name")
 *     .orderBy("-priority");
 * 
* * The results of sorting Threads using this Comparator and displaying the * results in a table may look like this: * *

* * * * * * * * * * * *
namethreadGroup.namepriority
daemonappGroup9
mainmain5
mainsecureGroup5
sweepermain1
Thread-0main5
Thread-1main5
workerappGroup8
workerappGroup5
workersecureGroup8
workersecureGroup5

* * An equivalent Thread ordering Comparator may be specified as: * *

 * Comparator c = BeanComparator.forClass(Thread.class)
 *     .orderBy("name")
 *     .orderBy("threadGroup")
 *     .using(BeanComparator.forClass(ThreadGroup.class).orderBy("name"))
 *     .orderBy("priority")
 *     .reverse();
 * 
* * The current implementation of BeanComparator has been optimized for fast * construction and execution of BeanComparators. For maximum performance, * however, save and re-use BeanComparators wherever possible. *

* Even though BeanComparator makes use of auto-generated code, instances are * fully Serializable, as long as all passed in Comparators are also * Serializable. * * @author Brian S O'Neill */ public class BeanComparator implements Comparator, Serializable { // Maps Rules to auto-generated Comparators. private static Map cGeneratedComparatorCache; static { cGeneratedComparatorCache = new SoftValuedHashMap(); } /** * Get or create a new BeanComparator for beans of the given type. Without * any {@link #orderBy order-by} properties specified, the returned * BeanComparator can only order against null beans (null is * {@link #nullHigh high} by default), and treats all other comparisons as * equal. */ public static BeanComparator forClass(Class clazz) { return new BeanComparator(clazz); } /** * Compare two objects for equality. */ private static boolean equalTest(Object obj1, Object obj2) { return (obj1 == obj2) ? true : ((obj1 == null || obj2 == null) ? false : obj1.equals(obj2)); } /** * Compare two object classes for equality. */ /* private static boolean equalClassTest(Object obj1, Object obj2) { return (obj1 == obj2) ? true : ((obj1 == null || obj2 == null) ? false : obj1.getClass().equals(obj2.getClass())); } */ private Class mBeanClass; // Maps property names to PropertyDescriptors. private transient Map mProperties; private String mOrderByName; private Comparator mUsingComparator; // bit 0: reverse // bit 1: null low order // bit 2: use String compareTo instead of collator private int mFlags; // Used for comparing strings. private Comparator mCollator; private BeanComparator mParent; // Auto-generated internal Comparator. private transient Comparator mComparator; private transient boolean mHasHashCode; private transient int mHashCode; private BeanComparator(Class clazz) { mBeanClass = clazz; mCollator = String.CASE_INSENSITIVE_ORDER; } private BeanComparator(BeanComparator parent) { mParent = parent; mBeanClass = parent.mBeanClass; mProperties = parent.getProperties(); mCollator = parent.mCollator; } /** * Add an order-by property to produce a more refined Comparator. If the * property does not return a {@link Comparable} object when * {@link #compare compare} is called on the returned comparator, the * property is ignored. Call {@link #using using} on the returned * BeanComparator to specify a Comparator to use for this property instead. *

* The specified propery name may refer to sub-properties using a dot * notation. For example, if the bean being compared contains a property * named "info" of type "Information", and "Information" contains a * property named "text", then ordering by the info text can be specified * by "info.text". Sub-properties of sub-properties may be refered to as * well, a.b.c.d.e etc. *

* If property type is a primitive, ordering is the same as for its * Comparable object peer. Primitive booleans are ordered false low, true * high. Floating point primitves are ordered exactly the same way as * {@link Float#compareTo(Float) Float.compareTo} and * {@link Double#compareTo(Double) Double.compareTo}. *

* As a convenience, property names may have a '-' or '+' character prefix * to specify sort order. A prefix of '-' indicates that the property * is to be sorted in reverse (descending). By default, properties are * sorted in ascending order, and so a prefix of '+' has no effect. *

* Any previously applied {@link #reverse reverse-order}, {@link #nullHigh * null-order} and {@link #caseSensitive case-sensitive} settings are not * carried over, and are reset to the defaults for this order-by property. * * @throws IllegalArgumentException when property doesn't exist or cannot * be read. */ public BeanComparator orderBy(String propertyName) throws IllegalArgumentException { int dot = propertyName.indexOf('.'); String subName; if (dot < 0) { subName = null; } else { subName = propertyName.substring(dot + 1); propertyName = propertyName.substring(0, dot); } boolean reverse = false; if (propertyName.length() > 0) { char prefix = propertyName.charAt(0); switch (prefix) { default: break; case '-': reverse = true; // Fall through case '+': propertyName = propertyName.substring(1); } } BeanProperty prop = getProperties().get(propertyName); if (prop == null) { throw new IllegalArgumentException ("Property '" + propertyName + "' doesn't exist in '" + mBeanClass.getName() + '\''); } if (prop.getReadMethod() == null) { throw new IllegalArgumentException ("Property '" + propertyName + "' cannot be read"); } if (propertyName.equals(mOrderByName)) { // Make String unique so that properties can be specified in // consecutive order-by calls without being eliminated by // reduceRules. A secondary order-by may wish to further refine an // ambiguous comparison using a Comparator. propertyName = new String(propertyName); } BeanComparator bc = new BeanComparator(this); bc.mOrderByName = propertyName; if (subName != null) { BeanComparator subOrder = forClass(prop.getType()); subOrder.mCollator = mCollator; bc = bc.using(subOrder.orderBy(subName)); } return reverse ? bc.reverse() : bc; } /** * Specifiy a Comparator to use on just the last {@link #orderBy order-by} * property. This is good for comparing properties that are not * {@link Comparable} or for applying special ordering rules for a * property. If no order-by properties have been specified, then Comparator * is applied to the compared beans. *

* Any previously applied String {@link #caseSensitive case-sensitive} or * {@link #collate collator} settings are overridden by this Comparator. * If property values being compared are primitive, they are converted to * their object peers before being passed to the Comparator. * * @param c Comparator to use on the last order-by property. Passing null * restores the default comparison for the last order-by property. */ public BeanComparator using(Comparator c) { BeanComparator bc = new BeanComparator(this); bc.mOrderByName = mOrderByName; bc.mUsingComparator = c; bc.mFlags = mFlags; return bc; } /** * Toggle reverse-order option on just the last {@link #orderBy order-by} * property. By default, order is ascending. If no order-by properties have * been specified, then reverse order is applied to the compared beans. */ public BeanComparator reverse() { BeanComparator bc = new BeanComparator(this); bc.mOrderByName = mOrderByName; bc.mUsingComparator = mUsingComparator; bc.mFlags = mFlags ^ 0x01; return bc; } /** * Set the order of comparisons against null as being high (the default) * on just the last {@link #orderBy order-by} property. If no order-by * properties have been specified, then null high order is applied to the * compared beans. Null high order is the default for consistency with the * high ordering of {@link Float#NaN NaN} by * {@link Float#compareTo(Float) Float}. *

* Calling 'nullHigh, reverse' is equivalent to calling 'reverse, nullLow'. */ public BeanComparator nullHigh() { BeanComparator bc = new BeanComparator(this); bc.mOrderByName = mOrderByName; bc.mUsingComparator = mUsingComparator; bc.mFlags = mFlags ^ ((mFlags & 0x01) << 1); return bc; } /** * Set the order of comparisons against null as being low * on just the last {@link #orderBy order-by} property. If no order-by * properties have been specified, then null low order is applied to the * compared beans. *

* Calling 'reverse, nullLow' is equivalent to calling 'nullHigh, reverse'. */ public BeanComparator nullLow() { BeanComparator bc = new BeanComparator(this); bc.mOrderByName = mOrderByName; bc.mUsingComparator = mUsingComparator; bc.mFlags = mFlags ^ ((~mFlags & 0x01) << 1); return bc; } /** * Override the collator and compare just the last order-by property using * {@link String#compareTo(String) String.compareTo}, if it is of type * String. If no order-by properties have been specified then this call is * ineffective. *

* A {@link #using using} Comparator disables this setting. Passing null to * the using method will re-enable a case-sensitive setting. */ public BeanComparator caseSensitive() { if ((mFlags & 0x04) != 0) { // Already case-sensitive. return this; } BeanComparator bc = new BeanComparator(this); bc.mOrderByName = mOrderByName; bc.mUsingComparator = mUsingComparator; bc.mFlags = mFlags | 0x04; return bc; } /** * Set a Comparator for ordering Strings, which is passed on to all * BeanComparators derived from this one. By default, String are compared * using {@link String#CASE_INSENSITIVE_ORDER}. Passing null for a collator * will cause all String comparisons to use * {@link String#compareTo(String) String.compareTo}. *

* A {@link #using using} Comparator disables this setting. Passing null * to the using method will re-enable a collator. * * @param c Comparator to use for ordering all Strings. Passing null * causes all Strings to be ordered by * {@link String#compareTo(String) String.compareTo}. */ public BeanComparator collate(Comparator c) { BeanComparator bc = new BeanComparator(this); bc.mOrderByName = mOrderByName; bc.mUsingComparator = mUsingComparator; bc.mFlags = mFlags & ~0x04; bc.mCollator = c; return bc; } public int compare(T obj1, T obj2) throws ClassCastException { Comparator c = mComparator; if (c == null) { mComparator = c = AccessController.doPrivileged(new PrivilegedAction>() { public Comparator run() { return generateComparator(); } }); } return c.compare(obj1, obj2); } public int hashCode() { if (!mHasHashCode) { setHashCode(new Rules(this)); } return mHashCode; } private void setHashCode(Rules rules) { mHashCode = rules.hashCode(); mHasHashCode = true; } /** * Compares BeanComparators for equality based on their imposed ordering. * Returns true only if the given object is a BeanComparater and it can be * determined without a doubt that the ordering is identical. Because * equality testing is dependent on the behavior of the equals methods of * any 'using' Comparators and/or collators, false may be returned even * though ordering is in fact identical. */ public boolean equals(Object obj) { if (obj instanceof BeanComparator) { BeanComparator bc = (BeanComparator)obj; return mFlags == bc.mFlags && equalTest(mBeanClass, bc.mBeanClass) && equalTest(mOrderByName, bc.mOrderByName) && equalTest(mUsingComparator, bc.mUsingComparator) && equalTest(mCollator, bc.mCollator) && equalTest(mParent, bc.mParent); } else { return false; } } private Map getProperties() { if (mProperties == null) { mProperties = BeanIntrospector.getAllProperties(mBeanClass); } return mProperties; } private Comparator generateComparator() { Rules rules = new Rules(this); if (!mHasHashCode) { setHashCode(rules); } Class clazz; synchronized (cGeneratedComparatorCache) { Object c = cGeneratedComparatorCache.get(rules); if (c == null) { clazz = generateComparatorClass(rules); cGeneratedComparatorCache.put(rules, clazz); } else if (c instanceof Comparator) { return (Comparator)c; } else { clazz = (Class)c; } BeanComparator[] ruleParts = rules.getRuleParts(); Comparator[] collators = new Comparator[ruleParts.length]; Comparator[] usingComparators = new Comparator[ruleParts.length]; boolean singleton = true; for (int i=0; i)c; } } private Class generateComparatorClass(Rules rules) { RuntimeClassFile cf = new RuntimeClassFile (getClass().getName(), null, mBeanClass.getClassLoader()); cf.markSynthetic(); cf.setSourceFile(BeanComparator.class.getName()); try { cf.setTarget(System.getProperty("java.specification.version")); } catch (Exception e) { } cf.addInterface(Comparator.class); cf.addInterface(Serializable.class); // Define fields to hold usage comparator and collator. TypeDesc comparatorType = TypeDesc.forClass(Comparator.class); TypeDesc comparatorArrayType = comparatorType.toArrayType(); cf.addField(Modifiers.PRIVATE, "mCollators", comparatorArrayType).markSynthetic(); cf.addField(Modifiers.PRIVATE, "mUsingComparators", comparatorArrayType).markSynthetic(); // Create constructor to initialize fields. TypeDesc[] paramTypes = { comparatorArrayType, comparatorArrayType }; MethodInfo ctor = cf.addConstructor(Modifiers.PUBLIC, paramTypes); ctor.markSynthetic(); CodeBuilder builder = new CodeBuilder(ctor); builder.loadThis(); builder.invokeSuperConstructor(null); builder.loadThis(); builder.loadLocal(builder.getParameter(0)); builder.storeField("mCollators", comparatorArrayType); builder.loadThis(); builder.loadLocal(builder.getParameter(1)); builder.storeField("mUsingComparators", comparatorArrayType); builder.returnVoid(); // Create the all-important compare method. Method compareMethod, compareToMethod; try { compareMethod = Comparator.class.getMethod ("compare", new Class[] {Object.class, Object.class}); compareToMethod = Comparable.class.getMethod ("compareTo", new Class[] {Object.class}); } catch (NoSuchMethodException e) { throw new InternalError(e.toString()); } MethodInfo mi = cf.addMethod(compareMethod); mi.markSynthetic(); builder = new CodeBuilder(mi); Label endLabel = builder.createLabel(); LocalVariable obj1 = builder.getParameter(0); LocalVariable obj2 = builder.getParameter(1); // The first rule always applies to the beans directly. All others // apply to properties. BeanComparator[] ruleParts = rules.getRuleParts(); BeanComparator bc = ruleParts[0]; if ((bc.mFlags & 0x01) != 0) { // Reverse beans. LocalVariable temp = obj1; obj1 = obj2; obj2 = temp; } // Handle the case when obj1 and obj2 are the same (or both null) builder.loadLocal(obj1); builder.loadLocal(obj2); builder.ifEqualBranch(endLabel, true); // Do null order checks for beans. boolean nullHigh = (bc.mFlags & 0x02) == 0; Label label = builder.createLabel(); builder.loadLocal(obj1); builder.ifNullBranch(label, false); builder.loadConstant(nullHigh ? 1 : -1); builder.returnValue(TypeDesc.INT); label.setLocation(); label = builder.createLabel(); builder.loadLocal(obj2); builder.ifNullBranch(label, false); builder.loadConstant(nullHigh ? -1 : 1); builder.returnValue(TypeDesc.INT); label.setLocation(); // Call 'using' Comparator if one is provided. LocalVariable result = builder.createLocalVariable("result", TypeDesc.INT); if (bc.mUsingComparator != null) { builder.loadThis(); builder.loadField("mUsingComparators", comparatorArrayType); builder.loadConstant(0); builder.loadFromArray(TypeDesc.forClass(Comparator.class)); builder.loadLocal(obj1); builder.loadLocal(obj2); builder.invoke(compareMethod); builder.storeLocal(result); builder.loadLocal(result); label = builder.createLabel(); builder.ifZeroComparisonBranch(label, "=="); builder.loadLocal(result); builder.returnValue(TypeDesc.INT); label.setLocation(); } // Cast bean parameters to correct types so that properties may be // accessed. TypeDesc type = TypeDesc.forClass(bc.mBeanClass); builder.loadLocal(obj1); builder.checkCast(type); builder.storeLocal(obj1); builder.loadLocal(obj2); builder.checkCast(type); builder.storeLocal(obj2); // Generate code to perform comparisons against each property. for (int i=1; i="); builder.loadConstant(-1); builder.branch(done); label.setLocation(); builder.loadLocal(a); builder.loadLocal(b); builder.math(Opcode.FCMPL); label = builder.createLabel(); builder.ifZeroComparisonBranch(label, "<="); builder.loadConstant(1); builder.branch(done); Method floatToIntBits; try { floatToIntBits = Float.class.getMethod ("floatToIntBits", new Class[] {float.class}); } catch (NoSuchMethodException e) { throw new InternalError(e.toString()); } label.setLocation(); builder.loadLocal(a); builder.invoke(floatToIntBits); builder.convert(TypeDesc.INT, TypeDesc.LONG); builder.loadLocal(b); builder.invoke(floatToIntBits); builder.convert(TypeDesc.INT, TypeDesc.LONG); builder.math(Opcode.LCMP); done.setLocation(); } else if (type == double.class) { // Comparison is same as for Double.compareTo(Double). Label done = builder.createLabel(); builder.loadLocal(a); builder.loadLocal(b); done = builder.createLabel(); builder.math(Opcode.DCMPG); Label label = builder.createLabel(); builder.ifZeroComparisonBranch(label, ">="); builder.loadConstant(-1); builder.branch(done); label.setLocation(); builder.loadLocal(a); builder.loadLocal(b); builder.math(Opcode.DCMPL); label = builder.createLabel(); builder.ifZeroComparisonBranch(label, "<="); builder.loadConstant(1); builder.branch(done); Method doubleToLongBits; try { doubleToLongBits = Double.class.getMethod ("doubleToLongBits", new Class[] {double.class}); } catch (NoSuchMethodException e) { throw new InternalError(e.toString()); } label.setLocation(); builder.loadLocal(a); builder.invoke(doubleToLongBits); builder.loadLocal(b); builder.invoke(doubleToLongBits); builder.math(Opcode.LCMP); done.setLocation(); } else if (type == long.class) { builder.loadLocal(a); builder.loadLocal(b); builder.math(Opcode.LCMP); } else if (type == int.class) { builder.loadLocal(a); builder.convert(TypeDesc.INT, TypeDesc.LONG); builder.loadLocal(b); builder.convert(TypeDesc.INT, TypeDesc.LONG); builder.math(Opcode.LCMP); } else { builder.loadLocal(a); builder.loadLocal(b); builder.math(Opcode.ISUB); } } private static void generateComparableCompareTo(CodeBuilder builder, Class type, Method compareToMethod, Label goodLabel, Label nextLabel, LocalVariable a, LocalVariable b) { if (Comparable.class.isAssignableFrom(type)) { builder.loadLocal(a); builder.loadLocal(b); builder.invoke(compareToMethod); if (goodLabel != null) { builder.branch(goodLabel); } } else { // Cast each property to Comparable only if needed. TypeDesc comparableType = TypeDesc.forClass(Comparable.class); boolean locateGoodLabel = false; if (goodLabel == null) { goodLabel = builder.createLabel(); locateGoodLabel = true; } Label tryStart = builder.createLabel().setLocation(); builder.loadLocal(a); builder.checkCast(comparableType); builder.loadLocal(b); builder.checkCast(comparableType); Label tryEnd = builder.createLabel().setLocation(); builder.invoke(compareToMethod); builder.branch(goodLabel); builder.exceptionHandler(tryStart, tryEnd, ClassCastException.class.getName()); // One of the properties is not Comparable, so just go to next. // Discard the exception. builder.pop(); if (nextLabel == null) { builder.loadConstant(0); } else { builder.branch(nextLabel); } if (locateGoodLabel) { goodLabel.setLocation(); } } } // A key that uniquely describes the rules of a BeanComparator. private static class Rules { private BeanComparator[] mRuleParts; private int mHashCode; public Rules(BeanComparator bc) { mRuleParts = reduceRules(bc); // Compute hashCode. int hash = 0; for (int i = mRuleParts.length - 1; i >= 0; i--) { bc = mRuleParts[i]; hash = 31 * hash; hash += bc.mFlags << 4; Object obj = bc.mBeanClass; if (obj != null) { hash += obj.hashCode(); } obj = bc.mOrderByName; if (obj != null) { hash += obj.hashCode(); } obj = bc.mUsingComparator; if (obj != null) { hash += obj.getClass().hashCode(); } obj = bc.mCollator; if (obj != null) { hash += obj.getClass().hashCode(); } } mHashCode = hash; } public BeanComparator[] getRuleParts() { return mRuleParts; } public int hashCode() { return mHashCode; } /** * Equality test determines if rules produce an identical * auto-generated Comparator. */ public boolean equals(Object obj) { if (!(obj instanceof Rules)) { return false; } BeanComparator[] ruleParts1 = getRuleParts(); BeanComparator[] ruleParts2 = ((Rules)obj).getRuleParts(); if (ruleParts1.length != ruleParts2.length) { return false; } for (int i=0; i rules = new ArrayList(); rules.add(bc); String name = bc.mOrderByName; while ((bc = bc.mParent) != null) { // Don't perform string comparison using equals method. if (name != bc.mOrderByName) { rules.add(bc); name = bc.mOrderByName; } } int size = rules.size(); BeanComparator[] bcs = new BeanComparator[size]; // Reverse rules so that they are in forward order. for (int i=0; i