001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.krad.util;
017
018import org.apache.commons.beanutils.PropertyUtils;
019import org.apache.commons.collections.comparators.ComparableComparator;
020import org.kuali.rice.core.api.exception.KualiException;
021import org.kuali.rice.core.api.util.type.TypeUtils;
022
023import java.beans.PropertyDescriptor;
024import java.io.Serializable;
025import java.lang.reflect.InvocationTargetException;
026import java.util.Collections;
027import java.util.Comparator;
028import java.util.Iterator;
029import java.util.List;
030
031/**
032 * BeanPropertyComparator compares the two beans using multiple property names
033 * 
034 * 
035 */
036public 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}