Class GeneralComparator

  • All Implemented Interfaces:
    Serializable, Comparator

    public class GeneralComparator
    extends Object
    implements Comparator, Serializable
    A general purpose comparator designed to compare any pair of heterogeneous objects.

    Skip to bottom of description.

    Description Contents:

    Basic Operation

    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.

    Ascending/Descending Comparison

    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.

    Comparing values

    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.

    Multi field sorting

    You can specify as many fields as you want to compare on, when the comparison is made (via the {@ #compare(Object,Object)} 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.

    Thread safety and reuse

    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);
    
     }
     

    Efficiency

    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.

    Warning on Getters

    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.

    JDOM Support

    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.

    Format

    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;/>
     

    Examples

    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);
     

    Back to top of description.

    See Also:
    Serialized Form
    • Field Detail

      • count

        public int count
      • ASC

        public static final String ASC
        Use to indicate that a field should be sorted in ascending order.
        See Also:
        Constant Field Values
      • DESC

        public static final String DESC
        Use to indicate that a field should be sorted in descending order.
        See Also:
        Constant Field Values
    • Constructor Detail

      • GeneralComparator

        public GeneralComparator​(Class c)
        Create a new GeneralComparator using the data held in the JDOM element.
        Parameters:
        root - The root JDOM element.
        Throws:
        org.jdom.JDOMException - If the format is incorrect.
        ChainException - If we can't load the class that we need.
        IllegalArgumentException - If the field is invalid.
    • Method Detail

      • getCompareClass

        public Class getCompareClass()
      • addFieldAtIndex

        public void addFieldAtIndex​(String field,
                                    String type,
                                    int index)
                             throws IllegalArgumentException
        Add a new field in at the specified index. Remember that indices start at 0 and proceed in asceding order. If the index specified is <0 then we add the field in at 0, moving everything else down by 1. If the index specified is >(fields.length - 1) then we just add to the end of the fields.
        Parameters:
        field - The field to add.
        type - The type, either GeneralComparator.ASC or GeneralComparator.DESC.
        index - The index to add at.
        Throws:
        IllegalArgumentException - If we can't find the field in the class/class chain passed into the constructor.
      • addFieldBefore

        public void addFieldBefore​(String field,
                                   String type,
                                   String ref)
                            throws IllegalArgumentException
        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.
        Parameters:
        field - The field to add.
        type - Sort either ascending or descending, should be either GeneralComparator.ASC or GeneralComparator.DESC.
        ref - The reference field.
        Throws:
        IllegalArgumentException - If we can't find the field in the class/class chain passed into the constructor.
      • addFieldAfter

        public void addFieldAfter​(String field,
                                  String type,
                                  String ref)
                           throws IllegalArgumentException
        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.
        Parameters:
        field - The field to add.
        type - Sort either ascending or descending, should be either GeneralComparator.ASC or GeneralComparator.DESC.
        ref - The reference field.
        Throws:
        IllegalArgumentException - If we can't find the field in the class/class chain passed into the constructor.
      • removeField

        public void removeField​(String field)
        Remove a field that we sort on. If we don't have the field then we do nothing.
        Parameters:
        field - The field to remove.
      • addField

        public void addField​(String field,
                             String type)
                      throws IllegalArgumentException
        Add a field that we sort on, if you readd the same field then the type is just updated. The order in which you add the fields provides the order in which the objects are sorted.
        Parameters:
        field - The field to sort on.
        type - The type either GeneralComparator.ASC or GeneralComparator.DESC.
        Throws:
        IllegalArgumentException - If we can't find the field in the class/class chain passed into the constructor.
      • compare

        public int compare​(Object obj1,
                           Object obj2)
        Implement the {@link 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.
        Specified by:
        compare in interface Comparator
        Parameters:
        obj1 - The first object.
        obj2 - The second object.
        Returns:
        A value according to the rules laid out in {@link 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.
      • equals

        public boolean equals​(Object obj)
        Implement the {@link Comparator.equals(Object)} method. We just look through our fields and then compare the fields.
        Specified by:
        equals in interface Comparator
        Overrides:
        equals in class Object
        Parameters:
        obj - Another GeneralComparator.
        Returns:
        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.
      • getFields

        protected List getFields()
        Return a List of GeneralComparator.SortField objects, this is used in the equals(Object) method.
        Returns:
        A List of GeneralComparator.SortField objects.
      • getCount

        public int getCount()