public class GeneralComparator
extends java.lang.Object
implements java.util.Comparator, java.io.Serializable
Skip to bottom of description.
This class works by you specify a number of fields that are then
used to access into each of the objects. The term field is a bit mis-leading
since you can specify either a field name within a class or a zero argument
method name.
When specifying a field you can use dot notation to acess into return types/
field types.
For instance if you have the following class structure:
public class A { public B = new B (); } public class B { public C = new C (); } public class C { String d = ""; }
Then to perform comparison between between 2 objects of type A on
the d field in Class C you would specify a field of: B.C.d.
Notice that you don't specify the current class, all fields are taken relative
to the current type.
Similarly, if class C actually looked like:
public class C { private String d = ""; public String getD () { return d; } }
Then you would could use the same field since if a public field with the
specified name cannot be found then it is converted to a method name instead,
following JavaBeans conventions. So the d is converted to getD
and a public method is looked for (with no arguments) with that name. Finally if a
get* method cannot be found then a method with just the name, in this case d,
is searched for that takes no arguments. (Note: this has been added because there are a
number of cases where the standard java.*.* classes DO NOT follow the JavaBeans conventions
but you would want to use a GeneralComparator).
You can override the method name conversion by using the actual method name
(in case your method doesn't follow the JavaBeans convention), so if C
looked like:
public class C { private d = ""; public String getMyDField () { return d; } }
You would then use a field of: B.C.getMyDField in which case the method getMyDField would be looked for instead of looking for field.
When you add a field you specify whether you want the comparison to occur in ascending or descending order. Setting to have a descending search just means that the result gained from comparing values is reversed.
If a field value equates to a public Java field in the object then
we call: java.lang.reflect.Field.get(Object)
on each
of the objects passed in and then if the type of the field implements
Comparable
then we call: Comparable.compareTo(Object,Object)
to get the appropriate value. If the type of the field does NOT
implement Comparable
then we call toString on the object
returned and then call String.compareTo(Object,Object)
for
the value to return instead.
Similarly we do the same when the field value equates to a public Java method
of the object. We call java.lang.reflect.Method.invoke(Object,Object[])
with a zero-length argument list and then if the return type of the method
implements Comparable
we call Comparable.compareTo(Object,Object)
with the result of the method calls to get our value. Otherwise we call
toString on the returned values and then call String.compareTo(Object,Object)
to get the value.
The upshot of all this is that you can sort a list of objects on values
returned from method calls or by fields that may be X levels deep within
an Object WITHOUT having to implement a complex version of the Comparable
interface yourself.
You can specify as many fields as you want to compare on, when the comparison is made (via the method) we only compare on as many fields as we need to, so if the result of the first field comparison indicates that the values are different we just return since there is no point doing further comparing, other fields will have no effect. Only if the values are equal do we move onto "lower" fields.
This class is NOT Thread safe and never will be (or should be...) since you would not want other threads modifying the fields you compare on. Once configured however the comparator is perfectly reusable since the only data it holds relates to the fields/methods that are accessed. If you do plan to use across multiple Threads then you need some kind of external synchronization, for example:
public synchronized void sortObjects (List objs) { Collections.sort (objs, myGeneralComparator); }
To try and access into the Object structure we use a Getter
which is basically a List of Field and Methods that should be
accessed/invoked when trying to find the value we require. Since we
are reliant on reflection for this the key factor is the cost of
traversing down the accessor chain (so the size of it will be important).
The accessor chain is gained when you call one of the add*** methods
so the finding of the method/field is a one-off.
The Getter class supports the [ ] notation for accessing Maps and Lists however in terms of comparisons and sorting this means little and should NOT be used!!! This is not checked because there may be situations where you would want to do it, however you would need to ensure that your Lists/Maps contain heterogeneous Objects.
This class is capable of initing itself from a JDOM Element
and also to save it's current state into a JDOM element. This is
primarily to support the ConfigList
and ConfigMap
objects, however it can be used in other places when you may want
to keep the state of the comparator.
When #getAsJDOMElement()
is called it will return a JDOM Element
in the form given below (conversely, the constructor #GeneralComparator(Element)
expects the passed in JDOM element to have the same format):
lt;comparator class="[[NAME OF CLASS THAT IS BEING COMPARED]]"> lt;field id="[[ACCESSOR VALUE INTO THE OBJECT]]" type="either ASC or DESC" /> lt;!-- There can be X number of fields, the minimum is 1. --> lt;/>
Say we wanted to sort a number of Property
objects.
// Create a new GeneralComparator. GeneralComparator g = new GeneralComparator (Property
.class); // Now we want to sort them on type first, then id then description, then // value. g.addField
("type", GeneralComparator.ASC); g.addField
("getID", GeneralComparator.ASC); g.addField
("description", GeneralComparator.ASC); g.addField
("value", GeneralComparator.ASC); // Then (by magic...) get our collection of Properties that can // be sorted. List properties = Helper.getProperties (); // Now sort them using our comparator. Collections.sort (properties, g);
We could always set any of the fields to be a descending sort. Notice
here that we use "getID" since that method name is not using the
correct JavaBeans convention. Also, Property
implements the
Comparable
interface but using the comparator will
override it.
Or if we wanted to sort a slice of messages from a Logger
:
GeneralComparator g = new GeneralComparator (Logger.Message.class); // Sort on the time, but this time sort on the day of the Date // this is a deprecated method but it's not a biggie. We want // them in reverse order. g.addField ("time.day", GeneralComparator.DESC); // Get our slice... long time = System.currentTimeMillis (); Date now = new Date (time); Date oneDayAgo = new Date (time - (24*60*60*1000)); int types = Logger.Message.INFORMATION || Logger.Message.ERROR; List messages = myLogger.getMessages (now, oneDayAgo, types); Collections.sort (messages, g); // We could perform another sort but this time, sorting first // on the type, this should be in ascending order, i.e. ERROR first... g.addFieldBefore ("type", GeneralComparator.ASC, "time.day"); Collections.sort (messages, g);
A quite common need is to sort the entries in a Map rather than the keys but still maintain the key/value relationship. To do so use the code below:
// Get all the entries in the Map as a List. List l = new ArrayList (); l.addAll (myMap.entrySet ()); // Create a GeneralComparator. It is also possible to use the specific // implementation of the Map.Entry interface for the specific Map but // this way keeps it generic. GeneralCompartor gc = new GeneralComparator (Map.Entry.class); // Specify the "value" (i.e. getValue method). gc.addField ("value", GeneralComparator.DESC); // Sort. Collections.sort (l, gc);
Modifier and Type | Class and Description |
---|---|
class |
GeneralComparator.XMLConstants |
Modifier and Type | Field and Description |
---|---|
static java.lang.String |
ASC
Use to indicate that a field should be sorted in ascending order.
|
int |
count |
static java.lang.String |
DESC
Use to indicate that a field should be sorted in descending order.
|
Constructor and Description |
---|
GeneralComparator(java.lang.Class c)
Create a new GeneralComparator using the data held in the JDOM
element.
|
Modifier and Type | Method and Description |
---|---|
void |
addField(Getter field,
java.lang.String type) |
void |
addField(java.lang.String field,
java.lang.String type)
Add a field that we sort on, if you readd the same field then
the type is just updated.
|
void |
addFieldAfter(java.lang.String field,
java.lang.String type,
java.lang.String ref)
Add a new field in AFTER the named field, if we don't have the
named field then we just call
addField(String,String) which
will add the field in after all the others. |
void |
addFieldAtIndex(java.lang.String field,
java.lang.String type,
int index)
Add a new field in at the specified index.
|
void |
addFieldBefore(java.lang.String field,
java.lang.String type,
java.lang.String ref)
Add a new field in BEFORE the named field, if we don't have the
named field then we just call
addField(String,String) which
will add the field in after all the others. |
int |
compare(java.lang.Object obj1,
java.lang.Object obj2)
Implement the
Comparator.compare(Object,Object) method. |
boolean |
equals(java.lang.Object obj)
Implement the
Comparator.equals(Object) method. |
java.lang.Class |
getCompareClass() |
int |
getCount() |
protected java.util.List |
getFields()
Return a List of GeneralComparator.SortField objects, this is used in
the
equals(Object) method. |
void |
removeField(java.lang.String field)
Remove a field that we sort on.
|
public int count
public static final java.lang.String ASC
public static final java.lang.String DESC
public GeneralComparator(java.lang.Class c)
root
- The root JDOM element.org.jdom.JDOMException
- If the format is incorrect.ChainException
- If we can't load the class that we need.java.lang.IllegalArgumentException
- If the field is invalid.public java.lang.Class getCompareClass()
public void addField(Getter field, java.lang.String type) throws java.lang.IllegalArgumentException
java.lang.IllegalArgumentException
public void addFieldAtIndex(java.lang.String field, java.lang.String type, int index) throws java.lang.IllegalArgumentException
field
- The field to add.type
- The type, either GeneralComparator.ASC or GeneralComparator.DESC.index
- The index to add at.java.lang.IllegalArgumentException
- If we can't find the field in the
class/class chain passed into the constructor.public void addFieldBefore(java.lang.String field, java.lang.String type, java.lang.String ref) throws java.lang.IllegalArgumentException
addField(String,String)
which
will add the field in after all the others.field
- The field to add.type
- Sort either ascending or descending, should be either
GeneralComparator.ASC or GeneralComparator.DESC.ref
- The reference field.java.lang.IllegalArgumentException
- If we can't find the field in the
class/class chain passed into the constructor.public void addFieldAfter(java.lang.String field, java.lang.String type, java.lang.String ref) throws java.lang.IllegalArgumentException
addField(String,String)
which
will add the field in after all the others.field
- The field to add.type
- Sort either ascending or descending, should be either
GeneralComparator.ASC or GeneralComparator.DESC.ref
- The reference field.java.lang.IllegalArgumentException
- If we can't find the field in the
class/class chain passed into the constructor.public void removeField(java.lang.String field)
field
- The field to remove.public void addField(java.lang.String field, java.lang.String type) throws java.lang.IllegalArgumentException
field
- The field to sort on.type
- The type either GeneralComparator.ASC or GeneralComparator.DESC.java.lang.IllegalArgumentException
- If we can't find the field in the
class/class chain passed into the constructor.public int compare(java.lang.Object obj1, java.lang.Object obj2)
Comparator.compare(Object,Object)
method.
Here we check each field in turn, we only check subsequent fields if
the "higher" up fields are equal. So if fields 0 and 1 are both equal
then we check field 2 and so on... Note: it is possible that we have
an exception thrown here, however the compare method doesn't allow
for exceptions to be thrown, so we just consume them and return 0, we
only catch IllegalAccessException and InvocationTargetException.compare
in interface java.util.Comparator
obj1
- The first object.obj2
- The second object.Comparator.compare(Object,Object)
,
if either object is null
then we return 0 or we return 0 if
either object returned from the accessor chain "get" call is null.public boolean equals(java.lang.Object obj)
Comparator.equals(Object)
method.
We just look through our fields and then compare the fields.equals
in interface java.util.Comparator
equals
in class java.lang.Object
obj
- Another GeneralComparator.true
if all our fields match those in the passed in
GeneralComparator AND that they are in the same order AND that they
have the same type, false
otherwise.protected java.util.List getFields()
equals(Object)
method.public int getCount()