Implementing a dynamic null-safe comparator in Groovy
Recently i've run into this requirement: in a gsp grails view, i had to present a fixed (not ordenable) list of items ordered first by one column in descendant orden, and in case of equality by another column. The logic for both ordenations was the natural order of Double values that could be null. So the first issue i had to deal with was making a null-safe comparison of Double objects without getting a NullPointerException.
Fortunately we have Jakarta Commons Collections and it's pretty cool gems, one of which is the utility comparators ready to use it provides.
But this was not enougth for me. My list of items was (as usual) represented by an ordered list of beans, but none of them was to be null in my case. The concrete properties of this bean i had to take into account when ordering it is what could be null, and the NullComparator shipped with this API allows me to include null references in my list, and even give them a high or low priority over the rest of not null elements, but doesn't prevent a NullPointerException to occur when the property you're using to compare is null.
After a quick Google search for an "already made" solution for this problem i wasn't able to find the component i was looking for, so i decided to implement it on my own making it reusable for any kind of bean, taking advantage of the dynamic features of the Groovy language. So these are the steps i followed.
Creating a Groovy class that implements the Comparable interface
Any collection in Groovy has a set of sort() methods that returns an ordered list of the collection if it's not a list, or the ordered list itself if the collection is already an instance of the List interface. The several variants of this method allows us to customize the behaviour of the sorting process. The one that interests us now receives a Comparator object as an argument. The first step we are getting into consists in creating a Grovvy class that implements this interface and behaves as we need. As we said earlier, we want a generic comparator that allows us to order any list despite the type of the object we are using to represent its items, so we will deal with beans with properties referenced as instances of the Object class.
Our class int this very first step of construction would look something like this:
/** * Dynamic comparator for any Java Bean, that makes a null safe comparison * of two objects comparing them in a null safe fashion by the natural * ordering of the proprerty whose name is specified in the constructor * User: mnavas * Date: 11/23/11 * Time: 9:51 AM * To change this template use File | Settings | File Templates. */ class DynamicNullSafeComparator implements Comparator { private Log log = LogFactory.getLog(getClass()) List<String> propNames ... int compare(Object o1, Object o2) { ... } }
Just a couple of things to point out in this code snippet:
We have a property in the class, a List of String objects, where we will hold the names of the properties we will have into account when it comes down to order a collection. We will make this comparator able to order by any property of our working bean, and in case of equality it will be able to user a second, third, and so forth property to resolve which element comes first. The desired ordering for all the properties relevant in the comparison will be given in the list itself (the order of its elements will define in which order we want to take the bean properties into account).
As we are implementing the Comparator interface, we need a compare method as defined in it. We will dig into the details of this method implementation later.
Next thing to do: we are implementing a generic comparator, so despite our requirements doesn't force us to deal with null objects, our comparator will support it. On the other hand, we have to order first by the first element on the properties names list, and in case of equality fall down to the next property until we get a non equals comparison result. Let's have a look at a primer version of our compare method trying to achieve this behaviour:
int compare(Object o1, Object o2) { int result = 0; propNames.each {nextName-> if(result == 0) { NullComparator nullComparator = new NullComparator(nullsAreHigh) result = nullComparator.compare(o1, o2) } } //println "Resultado del compare: ${result}" result }
This implementation uses the default internal ComparableComparator of the NullComparator, which uses natural order (given by the implementation of the Comparable interface) of the objects being compared.
First enhacement: selecting the order (asc or desc)
We are already comparing objects in a null safe fashion, and by two or more fields, but, what about ordering? Our class doesn't allow us to select ascendent or descendent order. To do so, let's view directly the code that does the trick:
class DynamicNullSafeComparator implements Comparator { //Log instance private Log log = LogFactory.getLog(getClass()) //List with the properties to order by List<String> propNames //The ordering of this sorting OrderingDirection orderingDirection //A Boolean to indicates wether null values should be pushed first or pulled back in the list Boolean nullsAreHigh //En anumeration that defines the different ordering directions static enum OrderingDirection { ASC, DESC } //The interface's method implementation int compare(Object o1, Object o2) { int result = 0; propNames.each {nextName-> if(result == 0) { NullComparator nullComparator = new NullComparator(new LocalComparator(nextName), nullsAreHigh) result = nullComparator.compare(o1, o2) } } result } //A convenient comparator that does the trick of the ordering direction for us private class LocalComparator implements Comparator { //Bean property to hold the name of the property to compare in the two objects String propName //A public constructor to make it easier to create an instance passing over a property name LocalComparator(String propName) { this.propName=propName } int compare(Object o1, Object o2) { Object first, second if(ordering==OrderingDirection.ASC) { first=o1 second=o2 } else { first=o2 second=o1 } first."${propName}".compare(second."${propName}") } } }
The key to the ordering direction is in the inner class LocalComparator. As we can see, depending on the ordering direction it considers either object as the first or second one. We have added several properties to our class, in order to allow the client code to specificate, among the names of the properties involved in the sorting, whether null values should have hight or low priority and whether we want the sorting to be made in ascendant or descendant order. We also take advantage of the dynamic nature of Groovy to dynamically call the getter for the property being compared in the two beans passed into the compare() method.
Second enhacement; null safety in bean properties
What happens if the objects in the list that is to be sorted are not null but in some or all of them one or several of the properties involved are null? With the current implementation we may get a (guess what ...) NullPointerException !!
... first."${propName}".compare(second."${propName}") ...
This is a problem we can solve again using a NullComparator. The bean property being compared is in fact another object, so we could do something like this in the compare method of the LocalComparator inner class:
int compare(Object o1, Object o2) { Object first, second int ret; if(ordering==OrderingDirection.ASC) { first=o1 second=o2 } else { first=o2 second=o1 } NullComparator localNc = new NullComparator(nullsAreHigh) localNc.compare(first?."${propName}", second?."${propName}") }
Now if either of the properties is null, the null safe Groovy operand (?.) will pass a null value into the compare method of this inner NullComparator without throwing an exception. At the same time, we can control the priority of this null value in the same way we did before.
Third enhacement: putting all together and nicer
For the sake of better encapsulation we will make this comparator class inmutable; that is, we set its state at construction time and we cannot change it; we have to create a new object if we want to use a comparator with different properties. This is a good practice that functional programming brings to us. To do so in Groovy we take this two steps:
Addind a set of constructors that allows client code to create the object with the desired state. We'll make public only one of them and force calling code to stablish all the properties of the comparator in a row.
Implementing setters for the properties of the comparator in a way we don't allow changing them (neither list of property names to be considered in sorting, nor the ascendant or deescendant order or the priority of null values).
This is the final picture of the class, removing comments and other non executable stuff:
package util import org.apache.commons.collections.comparators.NullComparator import org.apache.commons.lang.Validate import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory class DynamicNullSafeComparator implements Comparator { private Log log = LogFactory.getLog(getClass()) List<String> propNames OrderingDirection ordering Boolean nullsAreHigh static enum OrderingDirection { ASC, DESC } private DynamicNullSafeComparator(List<String> propNames) { Validate.notNull(propNames, 'The list of property names to take into sorting cannot be null') this.propNames = propNames } private DynamicNullSafeComparator(List<String> propNames, OrderingDirection ordering) { this(propNames) Validate.notNull(ordering, 'The ordering parameter cannot be null') this.ordering=ordering } public DynamicNullSafeComparator(List<String> propNames, OrderingDirection ordering, Boolean nullsAreHigh) { this(propNames, ordering) Validate.notNull(nullsAreHigh, 'the nullsAreHight parameter cannot be null') this.nullsAreHigh=nullsAreHigh } public void setPropNames(String pn) { throwInmutableException() } public void setOrdering(OrderingDirection ordering) { throwInmutableException() } public void setNullsAreHigh(Boolean b) { throwInmutableException() } private void throwInmutableException() { throw new UnsupportedOperationException('''This is a read-only property, because this object is inmutable. If you need to compare using another property or another comparison conditions, create another instance''') } int compare(Object o1, Object o2) { int result = 0; propNames.each {nextName-> if(result == 0) { NullComparator nullComparator = new NullComparator(new LocalComparator(nextName), nullsAreHigh) result = nullComparator.compare(o1, o2) } } result } private class LocalComparator implements Comparator { String propName public LocalComparator(String pName) { propName = pName } int compare(Object o1, Object o2) { Object first, second int ret; if(ordering==OrderingDirection.ASC) { first=o1 second=o2 } else { first=o2 second=o1 } NullComparator localNc = new NullComparator(nullsAreHigh) localNc.compare(first?."${propName}", second?."${propName}") } } }
If we had decided to do things in the right an orthodox way, we should already have some unit tests to verify that our implementation works well. We include a complete set of tests here, in which we use Groovy Expando dynamic objects to create different sets of fixtures and beans to test several use cases. You can copy and paste this code and verify that everything works well (or not):
import util.DynamicNullSafeComparator.OrderingDirection as OrderingDirection class DynamicNullSafeComparatorTest extends GroovyTestCase{ DynamicNullSafeComparator comparator /* * Tests about state violations */ void testNullAllArgConstructor() { shouldFail(IllegalArgumentException.class, { comparator = new DynamicNullSafeComparator([null], null, null) }) } void testNullOrderingParameter() { shouldFail(IllegalArgumentException.class, { comparator = new DynamicNullSafeComparator(['aProperty'], null, true) }) } private def createNewValidComparator() { new DynamicNullSafeComparator(['aProperty'], OrderingDirection.ASC, true) } void testNullHighOrLowNulls() { shouldFail(IllegalArgumentException.class, { comparator = new DynamicNullSafeComparator(['aProperty'], OrderingDirection.ASC, null) }) } void testInmutablityNameProperty() { comparator = createNewValidComparator() shouldFail(UnsupportedOperationException.class, { comparator.propNames='anotherProperty' }) } void testInmutabilityOrderingProperty() { comparator = createNewValidComparator() shouldFail(UnsupportedOperationException.class, { comparator.ordering=DynamicNullSafeComparator.OrderingDirection.DESC }) } void testInmutabilityNullOrderProperty() { comparator = createNewValidComparator() shouldFail(UnsupportedOperationException.class, { comparator.nullsAreHigh=false }) } /* * Test about sorting a single property */ private def createListHappyPath() { [new Expando(name: 'Mariano'), new Expando(name: 'Alfredo'), new Expando(name: 'Jose')] } private def createListOneNullObject() { [null, new Expando(name: 'Jose'), new Expando(name: 'Alfredo')] } private def createListOneNullPropertyInOneObject() { [new Expando(), new Expando(name: 'Jose'), new Expando(name: 'Alfredo')] } void testHappyPathAscNullsHigh() { def els = createListHappyPath() doGenericTest(els, 'name', [els[1], els[2], els[0]], OrderingDirection.ASC, true) } void testOneNullObjectAscNullsHight() { def els = createListOneNullObject() doGenericTest(els, 'name', [els[2], els[1], els[0]], OrderingDirection.ASC, true) } void testNullPropertyInOneObjectAscNullsLow() { def els = createListOneNullPropertyInOneObject() doGenericTest(els, 'name', [els[0], els[2], els[1]], OrderingDirection.ASC, false) } void testDescOrderNullsHigh() { def els = createListHappyPath() doGenericTest(els, 'name', [els[0], els[2], els[1]], OrderingDirection.DESC, true) } void testDescOrderWithNullObjectNullsLow() { def els = createListOneNullObject() doGenericTest(els, 'name', [els[0], els[1], els[2]], OrderingDirection.DESC, false) } void testDescOrderWithNullObjectNullsHigh() { def els = createListOneNullObject() doGenericTest(els, 'name', [els[1], els[2], els[0]], OrderingDirection.DESC, true) } private void doGenericTest(def beansList, String comparatorPropName, def expectedList, OrderingDirection order, Boolean nullsHigh) { if(order) { comparator = new DynamicNullSafeComparator([comparatorPropName], order, nullsHigh) } else { comparator = new DynamicNullSafeComparator([comparatorPropName], OrderingDirection.ASC, nullsHigh?:false) } beansList.sort(comparator) assert beansList == expectedList } /* * Test about sorting by two properties */ void testHappyPathWithTwoPropertiesAsc() { def input = [new Expando(name: 'Pepe', edad: 18), new Expando(name: 'Jose', edad: 25), new Expando(name: 'Jose', edad: 30)] def expected = [input[1], input[2], input[0]] genericTestTwoProperties(input, expected, OrderingDirection.ASC, true, ['name', 'edad']) } void testTwoPropertiesOneNullObjectAscNullsHigh() { def input = [null, new Expando(name: 'Jose', edad: 25), new Expando(name: 'Jose', edad: 30)] def expected = [input[1], input[2], input[0]] genericTestTwoProperties(input, expected, OrderingDirection.ASC, true, ['name', 'edad']) } void testTwoPropertiesOneNullPropertyAscNullsHigh() { def input = [new Expando(name: null, edad: 15), new Expando(name: 'Jose', edad: 25), new Expando(name: 'Jose', edad: 30)] def expected = [input[1], input[2], input[0]] genericTestTwoProperties(input, expected, OrderingDirection.ASC, true, ['name', 'edad']) } void testTwoPropertiesSecondOneNullAscNullsHigh() { def input = [new Expando(name: 'Pepe', edad: 15), new Expando(name: 'Jose', edad: null), new Expando(name: 'Jose', edad: 30)] def expected = [input[2], input[1], input[0]] genericTestTwoProperties(input, expected, OrderingDirection.ASC, true, ['name', 'edad']) } void testTwoPropertiesSecondOneNullAscNullsLow() { def input = [new Expando(name: 'Pepe', edad: 15), new Expando(name: 'Jose', edad: null), new Expando(name: 'Jose', edad: 30)] def expected = [input[1], input[2], input[0]] genericTestTwoProperties(input, expected, OrderingDirection.ASC, false, ['name', 'edad']) } void testTwoPropertiesSecondOneNullDescNullsHigh() { def input = [new Expando(name: 'Pepe', edad: 15), new Expando(name: 'Jose', edad: null), new Expando(name: 'Jose', edad: 30)] def expected = [input[0], input[1], input[2]] genericTestTwoProperties(input, expected, OrderingDirection.DESC, true, ['name', 'edad']) } void testTwoPropertiesFirstOneNullAscNullsLow() { def input = [new Expando(name: 'Pepe', edad: 15), new Expando(name: null, edad: 45), new Expando(name: 'Jose', edad: 30)] def expected = [input[1], input[2], input[0]] genericTestTwoProperties(input, expected, OrderingDirection.ASC, false, ['name', 'edad']) } void testTwoPropertiesFirstOneNullAscNullsHigh() { def input = [new Expando(name: 'Pepe', edad: 15), new Expando(name: null, edad: 45), new Expando(name: 'Jose', edad: 30)] def expected = [input[2], input[0], input[1]] genericTestTwoProperties(input, expected, OrderingDirection.ASC, true, ['name', 'edad']) } private void genericTestTwoProperties(def input, def expected, OrderingDirection order, Boolean nullsHigh, List<String> propNames) { comparator = new DynamicNullSafeComparator(propNames, order, nullsHigh) def actual = input.sort(comparator) assert expected == actual } /* * Some corner cases tests */ void testSecondPropertyAlwaysNull() { genericTestOnePropertyAlwaysNull('staffValorationsRounded', 'customerValorationsRounded') } void testFirstPropertyAlwaysNull() { genericTestOnePropertyAlwaysNull('customerValorationsRounded', 'staffValorationsRounded') } private void genericTestOnePropertyAlwaysNull(String prop1, String prop2) { Expando b1 = new Expando(name: 'Circo del Sol', customerValorationsRounded: null, staffValorationsRounded: new Double(8.2)) Expando b2 = new Expando(name: 'Teresa Rabal', customerValorationsRounded: null, staffValorationsRounded: new Double(7.7)) Expando b3 = new Expando(name: 'Zingaros del mundo', customerValorationsRounded: null, staffValorationsRounded: new Double(7.3)) Expando b4 = new Expando(name: 'El Gran circo de los niños', customerValorationsRounded: null, staffValorationsRounded: new Double(8.1)) def inputList = [b2,b4,b3,b1] def expected = [b1,b4,b2,b3] comparator = new DynamicNullSafeComparator([prop1, prop2], OrderingDirection.DESC, true) def actual = inputList.sort(comparator) assert expected == actual } }
What's next (another one improvement)
A new improvement we can include in our comparator is the posibility of stablishing a different ordering for each property on the list. Up to now we are able to sort ie this list by the property 'name' of our bean, and in case of equality use the property 'age' to break even. But the order for the two properties have to be the same for both. Making the order configurable for each property is pretty simple, we just replace the order property of our comparator with a List or OrderingDirection elements, this way:
We convert the Ordering direction property into a list of directions (one for each property, in corresponding order).
List<String> propNames List<OrderingDirection> ordering
We modify the contructors and setters to adapt to this change.
private DynamicNullSafeComparator(List<String> propNames, List<OrderingDirection> ordering) { this(propNames) Validate.notNull(ordering, 'The ordering parameter cannot be null') this.ordering=ordering } public DynamicNullSafeComparator(List<String> propNames, List<OrderingDirection> ordering, Boolean nullsAreHigh) { this(propNames, ordering) Validate.notNull(nullsAreHigh, 'the nullsAreHight parameter cannot be null') this.nullsAreHigh=nullsAreHigh } public void setOrdering(List<OrderingDirection> ordering) { throwInmutableException() }
Now we modify the LocalComparator inner class to receive, along the property name to compare, the order for that property.
private class LocalComparator implements Comparator { String propName OrderingDirection directionForThisProperty public LocalComparator(String pName, OrderingDirection direction) { propName = pName directionForThisProperty=direction } int compare(Object o1, Object o2) { Object first, second int ret; if(directionForThisProperty==OrderingDirection.ASC) { first=o1 second=o2 } else { first=o2 second=o1 } NullComparator localNc = new NullComparator(nullsAreHigh) localNc.compare(first?."${propName}", second?."${propName}") } }
To end with, in compare() method we modify the call to the LocalComparator to pass over the corresponding direction.
int compare(Object o1, Object o2) { int result = 0; int cont = 0; propNames.each {nextName-> if(result == 0) { NullComparator nullComparator = new NullComparator(new LocalComparator(nextName, ordering[cont]), nullsAreHigh) result = nullComparator.compare(o1, o2) cont++ } } result }
And finally we write some tests to verify that everything works fine. We include first the new tests and then the complete test suite with the modifications nededed to make the already made tests work properly:
//New tests /* * Ordering by different order in each property */ void testTwoPropertiesBothAsc() { def input = [new Expando(name: 'Maria', edad: 15), new Expando(name: 'Jose', edad: 25), new Expando(name: 'Jose', edad: 30)] def expected = [input[1], input[2], input[0]] genericTestTwoProperties(input, expected, [OrderingDirection.ASC, OrderingDirection.ASC], true, ['name', 'edad']) } void testTwoPropertiesFirstAscSecondDesc() { def input = [new Expando(name: 'Maria', edad: 15), new Expando(name: 'Jose', edad: 25), new Expando(name: 'Jose', edad: 30)] def expected = [input[2], input[1], input[0]] genericTestTwoProperties(input, expected, [OrderingDirection.ASC, OrderingDirection.DESC], true, ['name', 'edad']) } void testTwoPropertiesFirstDescSecondAsc() { def input = [new Expando(name: 'Maria', edad: 15), new Expando(name: 'Jose', edad: 25), new Expando(name: 'Jose', edad: 30)] def expected = [input[0], input[1], input[2]] genericTestTwoProperties(input, expected, [OrderingDirection.DESC, OrderingDirection.ASC], false, ['name', 'edad']) } .... //Complete test suite with this new three tests included package util import util.DynamicNullSafeComparator.OrderingDirection as OrderingDirection class DynamicNullSafeComparatorTest extends GroovyTestCase{ DynamicNullSafeComparator comparator /* * Tests about state violations */ void testNullAllArgConstructor() { shouldFail(IllegalArgumentException.class, { comparator = new DynamicNullSafeComparator([null], null, null) }) } void testNullOrderingParameter() { shouldFail(IllegalArgumentException.class, { comparator = new DynamicNullSafeComparator(['aProperty'], null, true) }) } private def createNewValidComparator() { new DynamicNullSafeComparator(['aProperty'], [OrderingDirection.ASC], true) } void testNullHighOrLowNulls() { shouldFail(IllegalArgumentException.class, { comparator = new DynamicNullSafeComparator(['aProperty'], [OrderingDirection.ASC], null) }) } void testInmutablityNameProperty() { comparator = createNewValidComparator() shouldFail(UnsupportedOperationException.class, { comparator.propNames='anotherProperty' }) } void testInmutabilityOrderingProperty() { comparator = createNewValidComparator() shouldFail(UnsupportedOperationException.class, { comparator.ordering=[DynamicNullSafeComparator.OrderingDirection.DESC] }) } void testInmutabilityNullOrderProperty() { comparator = createNewValidComparator() shouldFail(UnsupportedOperationException.class, { comparator.nullsAreHigh=false }) } /* * Test about sorting a single property */ private def createListHappyPath() { [new Expando(name: 'Mariano'), new Expando(name: 'Alfredo'), new Expando(name: 'Jose')] } private def createListOneNullObject() { [null, new Expando(name: 'Jose'), new Expando(name: 'Alfredo')] } private def createListOneNullPropertyInOneObject() { [new Expando(), new Expando(name: 'Jose'), new Expando(name: 'Alfredo')] } void testHappyPathAscNullsHigh() { def els = createListHappyPath() doGenericTest(els, 'name', [els[1], els[2], els[0]], [OrderingDirection.ASC], true) } void testOneNullObjectAscNullsHight() { def els = createListOneNullObject() doGenericTest(els, 'name', [els[2], els[1], els[0]], [OrderingDirection.ASC], true) } void testNullPropertyInOneObjectAscNullsLow() { def els = createListOneNullPropertyInOneObject() doGenericTest(els, 'name', [els[0], els[2], els[1]], [OrderingDirection.ASC], false) } void testDescOrderNullsHigh() { def els = createListHappyPath() doGenericTest(els, 'name', [els[0], els[2], els[1]], [OrderingDirection.DESC], true) } void testDescOrderWithNullObjectNullsLow() { def els = createListOneNullObject() doGenericTest(els, 'name', [els[0], els[1], els[2]], [OrderingDirection.DESC], false) } void testDescOrderWithNullObjectNullsHigh() { def els = createListOneNullObject() doGenericTest(els, 'name', [els[1], els[2], els[0]], [OrderingDirection.DESC], true) } private void doGenericTest(def beansList, String comparatorPropName, def expectedList, List<OrderingDirection> order, Boolean nullsHigh) { if(order) { comparator = new DynamicNullSafeComparator([comparatorPropName], order, nullsHigh) } else { comparator = new DynamicNullSafeComparator([comparatorPropName], [OrderingDirection.ASC], nullsHigh?:false) } beansList.sort(comparator) assert beansList == expectedList } /* * Test about sorting by two properties */ void testHappyPathWithTwoPropertiesAsc() { def input = [new Expando(name: 'Pepe', edad: 18), new Expando(name: 'Jose', edad: 25), new Expando(name: 'Jose', edad: 30)] //comparator = new DynamicNullSafeComparator(['name', 'edad'], OrderingDirection.ASC, true) def expected = [input[1], input[2], input[0]] genericTestTwoProperties(input, expected, [OrderingDirection.ASC, OrderingDirection.ASC], true, ['name', 'edad']) } void testTwoPropertiesOneNullObjectAscNullsHigh() { def input = [null, new Expando(name: 'Jose', edad: 25), new Expando(name: 'Jose', edad: 30)] def expected = [input[1], input[2], input[0]] genericTestTwoProperties(input, expected, [OrderingDirection.ASC, OrderingDirection.ASC], true, ['name', 'edad']) } void testTwoPropertiesOneNullPropertyAscNullsHigh() { def input = [new Expando(name: null, edad: 15), new Expando(name: 'Jose', edad: 25), new Expando(name: 'Jose', edad: 30)] def expected = [input[1], input[2], input[0]] genericTestTwoProperties(input, expected, [OrderingDirection.ASC, OrderingDirection.ASC], true, ['name', 'edad']) } void testTwoPropertiesSecondOneNullAscNullsHigh() { def input = [new Expando(name: 'Pepe', edad: 15), new Expando(name: 'Jose', edad: null), new Expando(name: 'Jose', edad: 30)] def expected = [input[2], input[1], input[0]] genericTestTwoProperties(input, expected, [OrderingDirection.ASC, OrderingDirection.ASC], true, ['name', 'edad']) } void testTwoPropertiesSecondOneNullAscNullsLow() { def input = [new Expando(name: 'Pepe', edad: 15), new Expando(name: 'Jose', edad: null), new Expando(name: 'Jose', edad: 30)] def expected = [input[1], input[2], input[0]] genericTestTwoProperties(input, expected, [OrderingDirection.ASC, OrderingDirection.ASC], false, ['name', 'edad']) } void testTwoPropertiesSecondOneNullDescNullsHigh() { def input = [new Expando(name: 'Pepe', edad: 15), new Expando(name: 'Jose', edad: null), new Expando(name: 'Jose', edad: 30)] def expected = [input[0], input[1], input[2]] genericTestTwoProperties(input, expected, [OrderingDirection.DESC], true, ['name', 'edad']) } void testTwoPropertiesFirstOneNullAscNullsLow() { def input = [new Expando(name: 'Pepe', edad: 15), new Expando(name: null, edad: 45), new Expando(name: 'Jose', edad: 30)] def expected = [input[1], input[2], input[0]] genericTestTwoProperties(input, expected, [OrderingDirection.ASC], false, ['name', 'edad']) } void testTwoPropertiesFirstOneNullAscNullsHigh() { def input = [new Expando(name: 'Pepe', edad: 15), new Expando(name: null, edad: 45), new Expando(name: 'Jose', edad: 30)] def expected = [input[2], input[0], input[1]] genericTestTwoProperties(input, expected, [OrderingDirection.ASC], true, ['name', 'edad']) } private void genericTestTwoProperties(def input, def expected, List<OrderingDirection> order, Boolean nullsHigh, List<String> propNames) { comparator = new DynamicNullSafeComparator(propNames, order, nullsHigh) def actual = input.sort(comparator) assert expected == actual } /* * Corner cases tests */ void testSecondPropertyAlwaysNull() { genericTestOnePropertyAlwaysNull('valoracionReviewsThisYearRounded', 'valoracionSurveyThisYearRounded') } void testFirstPropertyAlwaysNull() { genericTestOnePropertyAlwaysNull('valoracionSurveyThisYearRounded', 'valoracionReviewsThisYearRounded') } private void genericTestOnePropertyAlwaysNull(String prop1, String prop2) { Expando b1 = new Expando(name: '40 Flats', valoracionSurveyThisYearRounded: null, valoracionReviewsThisYearRounded: new Double(8.2)) Expando b2 = new Expando(name: '4C PUERTA EUROPA', valoracionSurveyThisYearRounded: null, valoracionReviewsThisYearRounded: new Double(7.7)) Expando b3 = new Expando(name: '562 Nogaro', valoracionSurveyThisYearRounded: null, valoracionReviewsThisYearRounded: new Double(7.3)) Expando b4 = new Expando(name: 'Van der Valk Brussels Airport', valoracionSurveyThisYearRounded: null, valoracionReviewsThisYearRounded: new Double(8.1)) def inputList = [b2,b4,b3,b1] def expected = [b1,b4,b2,b3] comparator = new DynamicNullSafeComparator([prop1, prop2], [OrderingDirection.DESC], true) def actual = inputList.sort(comparator) assert expected == actual } /* * Ordering by different order in each property */ void testTwoPropertiesBothAsc() { def input = [new Expando(name: 'Maria', edad: 15), new Expando(name: 'Jose', edad: 25), new Expando(name: 'Jose', edad: 30)] def expected = [input[1], input[2], input[0]] genericTestTwoProperties(input, expected, [OrderingDirection.ASC, OrderingDirection.ASC], true, ['name', 'edad']) } void testTwoPropertiesFirstAscSecondDesc() { def input = [new Expando(name: 'Maria', edad: 15), new Expando(name: 'Jose', edad: 25), new Expando(name: 'Jose', edad: 30)] def expected = [input[2], input[1], input[0]] genericTestTwoProperties(input, expected, [OrderingDirection.ASC, OrderingDirection.DESC], true, ['name', 'edad']) } void testTwoPropertiesFirstDescSecondAsc() { def input = [new Expando(name: 'Maria', edad: 15), new Expando(name: 'Jose', edad: 25), new Expando(name: 'Jose', edad: 30)] def expected = [input[0], input[1], input[2]] genericTestTwoProperties(input, expected, [OrderingDirection.DESC, OrderingDirection.ASC], false, ['name', 'edad']) } }
Some restrictions of this comparator
We've seen so far the simplicity and power of this generic comparator. Unfortunately it has some limitations we have to keep in mind:
It doesn't allow nested properties. We can sort by any direct property of any bean, but we cannot use the dot notation to "navigate" to nested properties and order by them. We can instead take the parent objects of those properties and order them, associating each object to its corresponding parent, and reconstruct the object graph at later time. Homework for you: implement an example of this use case scenario.
We can only sort items that implements the Comparable interface, so the used algorithm relies on the natural order of the objects in use. We can fix this with another simple impovement, which may be the subject of another post in near future.
Take care and see you soon on the Java Developer Diary.