001    /**
002     * Copyright 2005-2013 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.krad.util;
017    
018    import org.apache.commons.beanutils.PropertyUtils;
019    import org.apache.commons.collections.comparators.ComparableComparator;
020    import org.kuali.rice.core.api.exception.KualiException;
021    import org.kuali.rice.core.api.util.type.TypeUtils;
022    
023    import java.beans.PropertyDescriptor;
024    import java.io.Serializable;
025    import java.lang.reflect.InvocationTargetException;
026    import java.util.Collections;
027    import java.util.Comparator;
028    import java.util.Iterator;
029    import java.util.List;
030    
031    /**
032     * BeanPropertyComparator compares the two beans using multiple property names
033     * 
034     * 
035     */
036    public class BeanPropertyComparator implements Comparator, Serializable {
037        private static final long serialVersionUID = -2675700473766186018L;
038        boolean ignoreCase;
039        private List propertyNames;
040        private Comparator stringComparator;
041        private Comparator booleanComparator;
042        private Comparator genericComparator;
043    
044        /**
045         * Constructs a PropertyComparator for comparing beans using the properties named in the given List
046         *
047         * <p>if the List is null, the beans will be compared directly
048         * by Properties will be compared in the order in which they are listed. Case will be ignored
049         * in String comparisons.</p>
050         * 
051         * @param propertyNames List of property names (as Strings) used to compare beans
052         */
053        public BeanPropertyComparator(List propertyNames) {
054            this(propertyNames, true);
055        }
056    
057        /**
058         * Constructs a PropertyComparator for comparing beans using the properties named in the given List.
059         *
060         * <p>Properties will be compared
061         * in the order in which they are listed. Case will be ignored if ignoreCase is true.</p>
062         * 
063         * @param propertyNames List of property names (as Strings) used to compare beans
064         * @param ignoreCase if true, case will be ignored during String comparisons
065         */
066        public BeanPropertyComparator(List propertyNames, boolean ignoreCase) {
067            if (propertyNames == null) {
068                throw new IllegalArgumentException("invalid (null) propertyNames list");
069            }
070            if (propertyNames.size() == 0) {
071                throw new IllegalArgumentException("invalid (empty) propertyNames list");
072            }
073            this.propertyNames = Collections.unmodifiableList(propertyNames);
074            this.ignoreCase = ignoreCase;
075    
076            if (ignoreCase) {
077                this.stringComparator = String.CASE_INSENSITIVE_ORDER;
078            }
079            else {
080                this.stringComparator = ComparableComparator.getInstance();
081            }
082            this.booleanComparator = new Comparator() {
083                public int compare(Object o1, Object o2) {
084                    int compared = 0;
085    
086                    Boolean b1 = (Boolean) o1;
087                    Boolean b2 = (Boolean) o2;
088    
089                    if (!b1.equals(b2)) {
090                        if (b1.equals(Boolean.FALSE)) {
091                            compared = -1;
092                        }
093                        else {
094                            compared = 1;
095                        }
096                    }
097    
098                    return compared;
099                }
100    
101            };
102            this.genericComparator = ComparableComparator.getInstance();
103        }
104    
105    
106        /**
107         * Compare two JavaBeans by the properties given to the constructor.
108         * 
109         * @param o1 Object The first bean to get data from to compare against
110         * @param o2 Object The second bean to get data from to compare
111         * @return int negative or positive based on order
112         */
113        public int compare(Object o1, Object o2) {
114            int compared = 0;
115    
116            try {
117                for (Iterator i = propertyNames.iterator(); (compared == 0) && i.hasNext();) {
118                    String currentProperty = i.next().toString();
119    
120                    // choose appropriate comparator
121                    Comparator currentComparator = null;
122                    try {
123                        PropertyDescriptor propertyDescriptor = PropertyUtils.getPropertyDescriptor(o1, currentProperty);
124                        Class propertyClass = propertyDescriptor.getPropertyType();
125                        if (propertyClass.equals(String.class)) {
126                            currentComparator = this.stringComparator;
127                        }
128                        else if (TypeUtils.isBooleanClass(propertyClass)) {
129                            currentComparator = this.booleanComparator;
130                        }
131                        else {
132                            currentComparator = this.genericComparator;
133                        }
134                    }
135                    catch (NullPointerException e) {
136                        throw new BeanComparisonException("unable to find property '" + o1.getClass().getName() + "." + currentProperty + "'", e);
137                    }
138    
139                    // compare the values
140                    Object value1 = PropertyUtils.getProperty(o1, currentProperty);
141                    Object value2 = PropertyUtils.getProperty(o2, currentProperty);
142                    /* Fix for KULRICE-5170 : BeanPropertyComparator throws exception when a null value is found in sortable non-string data type column */
143                    if ( value1 == null && value2 == null)
144                        return 0;
145                    else if ( value1 == null)
146                        return -1;
147                    else if ( value2 == null )
148                        return 1;
149                    /* End KULRICE-5170 Fix*/
150                    compared = currentComparator.compare(value1, value2);
151                }
152            }
153            catch (IllegalAccessException e) {
154                throw new BeanComparisonException("unable to compare property values", e);
155            }
156            catch (NoSuchMethodException e) {
157                throw new BeanComparisonException("unable to compare property values", e);
158            }
159            catch (InvocationTargetException e) {
160                throw new BeanComparisonException("unable to compare property values", e);
161            }
162    
163            return compared;
164        }
165        
166        public static class BeanComparisonException extends KualiException {
167            private static final long serialVersionUID = 2622379680100640029L;
168    
169            /**
170             * @param message
171             * @param t
172             */
173            public BeanComparisonException(String message, Throwable t) {
174                super(message, t);
175            }
176        }
177    }