001/*
002 *  Copyright 2013 Christopher Pheby
003 *
004 *  Licensed under the Apache 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.apache.org/licenses/LICENSE-2.0
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.jadira.reflection.hashcode;
017
018import java.lang.reflect.Modifier;
019import java.util.IdentityHashMap;
020
021import org.jadira.reflection.access.api.ClassAccess;
022import org.jadira.reflection.access.api.ClassAccessFactory;
023import org.jadira.reflection.access.api.FieldAccess;
024import org.jadira.reflection.access.api.MethodAccess;
025import org.jadira.reflection.access.portable.PortableClassAccessFactory;
026import org.jadira.reflection.access.unsafe.UnsafeClassAccessFactory;
027import org.jadira.reflection.core.platform.FeatureDetection;
028
029/**
030 * This class is based on the capabilities provided by Commons-Lang's
031 * HashCodeBuilder. It is not a complete "drop-in" replacement, but is near enough
032 * in functionality and in terms of its API that it should be quite
033 * straightforward to switch from one class to the other.
034 * 
035 * HashCodeBuilder makes use of Jadira's reflection capabilities. This means, like
036 * Cloning there is a brief warm-up time entailed in preparing introspection
037 * when a class is first processed. If you are already performing cloning on a
038 * type you will not be impacted by this as any necessary warm-up will have
039 * already taken place.
040 * 
041 * Once the initial warmup has been performed, you will, usually, find enhanced
042 * performance compared to standard reflection based approaches. The strategy to
043 * be used can be configured.
044 *
045 * Where objects that are reachable from the object being compared do not
046 * override hashCode(), the default behaviour is to rely on
047 * Object#hashCode(Object) which uses the object identity for those
048 * objects. This is also the behaviour of Commons Lang. You can modify this to
049 * reflect into any reachable object that does not override equals by enabling
050 * the defaultDeepReflect property.
051 * 
052 * Transient fields are not compared by the current implementation of this
053 * class.
054 */
055public class HashCodeBuilder {
056
057        private static final ThreadLocal<HashCodeBuilder> reflectionBuilder = new ThreadLocal<HashCodeBuilder>() {
058                protected HashCodeBuilder initialValue() {
059                        return new HashCodeBuilder(37, 17);
060                };
061        };
062
063        private IdentityHashMap<Object, Object> seenReferences;
064
065        /**
066         * Constant to use in building the hashCode.
067         */
068        private final int constant;
069
070        /**
071         * Running total of the hashCode.
072         */
073        private int computedTotal;
074
075        private final int seed;
076
077        private ClassAccessFactory classAccessFactory;
078
079        private boolean defaultDeepReflect = false;
080
081        private <C> void reflectionAppend(C object) {
082
083                if (seenReferences.containsKey(object)) {
084                        return;
085                }
086                seenReferences.put(object, object);
087
088                @SuppressWarnings("unchecked")
089                ClassAccess<? super C> classAccess = (ClassAccess<C>) classAccessFactory
090                                .getClassAccess(object.getClass());
091
092                while ((classAccess.getType() != Object.class)
093                                && (!classAccess.providesHashCode())) {
094
095                        ClassAccess<? super C> classAccessInHierarchy = classAccess;
096                                                
097                        for (FieldAccess<? super C> fieldAccess : classAccessInHierarchy.getDeclaredFieldAccessors()) {
098
099                                if ((fieldAccess.field().getName().indexOf('$') == -1)
100                                                && (!Modifier.isTransient(fieldAccess.field().getModifiers()))
101                                                && (!Modifier.isStatic(fieldAccess.field().getModifiers()))) {
102                                
103                                        Class<?> type = fieldAccess.fieldClass();
104                                        
105                                        if (type.isPrimitive()) {
106                                                if (java.lang.Boolean.TYPE == type) {
107                                                        append(fieldAccess.getBooleanValue(object));
108                                                } else if (java.lang.Byte.TYPE == type) {
109                                                        append(fieldAccess.getByteValue(object));
110                                                } else if (java.lang.Character.TYPE == type) {
111                                                        append(fieldAccess.getCharValue(object));
112                                                } else if (java.lang.Short.TYPE == type) {
113                                                        append(fieldAccess.getShortValue(object));
114                                                } else if (java.lang.Integer.TYPE == type) {
115                                                        append(fieldAccess.getIntValue(object));
116                                                } else if (java.lang.Long.TYPE == type) {
117                                                        append(fieldAccess.getLongValue(object));
118                                                } else if (java.lang.Float.TYPE == type) {
119                                                        append(fieldAccess.getFloatValue(object));
120                                                } else if (java.lang.Double.TYPE == type) {
121                                                        append(fieldAccess.getDoubleValue(object));
122                                                }
123                                        } else {
124                                                final Object value = fieldAccess.getValue(object);
125                                                append(value);
126                                        }
127                                }
128                        }
129
130                        classAccessInHierarchy = classAccessInHierarchy.getSuperClassAccess();
131                }
132                if (classAccess.getType() != Object.class) {
133
134                        final MethodAccess<Object> methodAccess;
135                        try {
136                                @SuppressWarnings("unchecked")
137                                final MethodAccess<Object> myMethodAccess = (MethodAccess<Object>) classAccess
138                                                .getDeclaredMethodAccess(classAccess.getType().getMethod(
139                                                                "hashCode"));
140                                methodAccess = myMethodAccess;
141                        } catch (NoSuchMethodException e) {
142                                throw new IllegalStateException("Cannot find hashCode() method");
143                        } catch (SecurityException e) {
144                                throw new IllegalStateException("Cannot find hashCode() method");
145                        }
146                        append(((Integer) (methodAccess.invoke(object))).intValue());
147                }
148        }
149
150        public static <T> int reflectionHashCode(T object) {
151                return reflectionHashCode(37, 17, object);
152        }
153
154        public static <T> int reflectionHashCode(int initialNonZeroOddNumber,
155                        int multiplierNonZeroOddNumber, T object) {
156
157                if (object == null) {
158                        throw new IllegalArgumentException(
159                                        "Cannot build hashcode for null object");
160                }
161
162                HashCodeBuilder builder = reflectionBuilder.get();
163                // In case we recurse into this method
164                reflectionBuilder.set(null);
165
166                if (builder.seed == initialNonZeroOddNumber
167                                && builder.constant == multiplierNonZeroOddNumber) {
168                        builder.reset();
169                } else {
170                        builder = new HashCodeBuilder(initialNonZeroOddNumber,
171                                        multiplierNonZeroOddNumber);
172                }
173
174                builder.reflectionAppend(object);
175
176                final int hashCode = builder.toHashCode();
177                reflectionBuilder.set(builder);
178
179                return hashCode;
180        }
181
182        public HashCodeBuilder() {
183
184                seenReferences = new IdentityHashMap<Object, Object>();
185
186                constant = 37;
187                computedTotal = seed = 17;
188
189                if (FeatureDetection.hasUnsafe()) {
190                        this.classAccessFactory = UnsafeClassAccessFactory.get();
191                } else {
192                        this.classAccessFactory = PortableClassAccessFactory.get();
193                }
194        }
195
196        public HashCodeBuilder(int initialNonZeroOddNumber,
197                        int multiplierNonZeroOddNumber) {
198
199                seenReferences = new IdentityHashMap<Object, Object>();
200
201                if ((initialNonZeroOddNumber % 2 == 0)
202                                || (initialNonZeroOddNumber == 0)) {
203                        throw new IllegalArgumentException(
204                                        "Initial Value must be odd and non-zero but was: "
205                                                        + initialNonZeroOddNumber);
206                } else if ((multiplierNonZeroOddNumber % 2 == 0)
207                                || (multiplierNonZeroOddNumber == 0)) {
208                        throw new IllegalArgumentException(
209                                        "Multiplier must be odd and non-zero but was: "
210                                                        + multiplierNonZeroOddNumber);
211                }
212
213                constant = multiplierNonZeroOddNumber;
214                computedTotal = seed = initialNonZeroOddNumber;
215
216                if (FeatureDetection.hasUnsafe()) {
217                        this.classAccessFactory = UnsafeClassAccessFactory.get();
218                } else {
219                        this.classAccessFactory = PortableClassAccessFactory.get();
220                }
221        }
222
223        public HashCodeBuilder withDefaultDeepReflect(boolean newDefaultDeepReflect) {
224                this.defaultDeepReflect = newDefaultDeepReflect;
225                return this;
226        }
227
228        public HashCodeBuilder withClassAccessFactory(
229                        ClassAccessFactory classAccessFactory) {
230                this.classAccessFactory = classAccessFactory;
231                return this;
232        }
233
234        public HashCodeBuilder append(boolean value) {
235                computedTotal = computedTotal * constant + (value ? 0 : 1);
236                return this;
237        }
238
239        public HashCodeBuilder append(boolean[] array) {
240                if (seenReferences.containsKey(array)) {
241                        return this;
242                }
243                seenReferences.put(array, array);
244
245                if (array == null) {
246                        appendNull();
247                } else {
248                        for (boolean element : array) {
249                                append(element);
250                        }
251                }
252                return this;
253        }
254
255        public HashCodeBuilder append(byte value) {
256                computedTotal = (computedTotal * constant) + value;
257                return this;
258        }
259
260        public HashCodeBuilder append(byte[] array) {
261                if (seenReferences.containsKey(array)) {
262                        return this;
263                }
264                seenReferences.put(array, array);
265
266                if (array == null) {
267                        appendNull();
268                } else {
269                        for (byte element : array) {
270                                append(element);
271                        }
272                }
273                return this;
274        }
275
276        public HashCodeBuilder append(char value) {
277                computedTotal = (computedTotal * constant) + value;
278                return this;
279        }
280
281        public HashCodeBuilder append(char[] array) {
282                if (seenReferences.containsKey(array)) {
283                        return this;
284                }
285                seenReferences.put(array, array);
286
287                if (array == null) {
288                        appendNull();
289                } else {
290                        for (char element : array) {
291                                append(element);
292                        }
293                }
294                return this;
295        }
296
297        public HashCodeBuilder append(double value) {
298                return append(Double.doubleToLongBits(value));
299        }
300
301        public HashCodeBuilder append(double[] array) {
302                if (seenReferences.containsKey(array)) {
303                        return this;
304                }
305                seenReferences.put(array, array);
306
307                if (array == null) {
308                        appendNull();
309                } else {
310                        for (double element : array) {
311                                append(element);
312                        }
313                }
314                return this;
315        }
316
317        public HashCodeBuilder append(float value) {
318                computedTotal = (computedTotal * constant)
319                                + Float.floatToIntBits(value);
320                return this;
321        }
322
323        public HashCodeBuilder append(float[] array) {
324                if (seenReferences.containsKey(array)) {
325                        return this;
326                }
327                seenReferences.put(array, array);
328
329                if (array == null) {
330                        appendNull();
331                } else {
332                        for (float element : array) {
333                                append(element);
334                        }
335                }
336                return this;
337        }
338
339        public HashCodeBuilder append(int value) {
340                computedTotal = (computedTotal * constant) + value;
341                return this;
342        }
343
344        public HashCodeBuilder append(int[] array) {
345                if (seenReferences.containsKey(array)) {
346                        return this;
347                }
348                seenReferences.put(array, array);
349
350                if (array == null) {
351                        appendNull();
352                } else {
353                        for (int element : array) {
354                                append(element);
355                        }
356                }
357                return this;
358        }
359
360        public HashCodeBuilder append(long value) {
361                computedTotal = computedTotal * constant
362                                + (int) (value ^ (value >>> 32));
363                return this;
364        }
365
366        public HashCodeBuilder append(long[] array) {
367                if (seenReferences.containsKey(array)) {
368                        return this;
369                }
370                seenReferences.put(array, array);
371
372                if (array == null) {
373                        appendNull();
374                } else {
375                        for (long element : array) {
376                                append(element);
377                        }
378                }
379                return this;
380        }
381
382        public HashCodeBuilder append(Object object) {
383
384                if (object == null) {
385                        appendNull();
386                        return this;
387                } else {
388                        if (object.getClass() == null) {
389                                appendNull();
390                        } else if (object.getClass().isArray()) {
391
392                                if (object instanceof long[]) {
393                                        append((long[]) object);
394                                } else if (object instanceof int[]) {
395                                        append((int[]) object);
396                                } else if (object instanceof short[]) {
397                                        append((short[]) object);
398                                } else if (object instanceof char[]) {
399                                        append((char[]) object);
400                                } else if (object instanceof byte[]) {
401                                        append((byte[]) object);
402                                } else if (object instanceof double[]) {
403                                        append((double[]) object);
404                                } else if (object instanceof float[]) {
405                                        append((float[]) object);
406                                } else if (object instanceof boolean[]) {
407                                        append((boolean[]) object);
408                                } else {
409                                        append((Object[]) object);
410                                }
411                        } else {
412                                computedTotal = (computedTotal * constant);
413                                if (defaultDeepReflect) {
414                                        reflectionAppend(object);
415                                } else {
416                                        computedTotal = computedTotal + object.hashCode();
417                                }
418                        }
419                }
420                return this;
421        }
422
423        public HashCodeBuilder append(Object[] array) {
424                if (seenReferences.containsKey(array)) {
425                        return this;
426                }
427                seenReferences.put(array, array);
428
429                if (array == null) {
430                        appendNull();
431                } else {
432                        for (Object element : array) {
433                                append(element);
434                        }
435                }
436                return this;
437        }
438
439        public HashCodeBuilder append(short value) {
440                computedTotal = (computedTotal * constant) + value;
441                return this;
442        }
443
444        public HashCodeBuilder append(short[] array) {
445                if (seenReferences.containsKey(array)) {
446                        return this;
447                }
448                seenReferences.put(array, array);
449
450                if (array == null) {
451                        appendNull();
452                } else {
453                        for (short element : array) {
454                                append(element);
455                        }
456                }
457                return this;
458        }
459
460        public HashCodeBuilder appendSuper(int superHashCode) {
461                computedTotal = (computedTotal * constant) + superHashCode;
462                return this;
463        }
464
465        protected void appendNull() {
466                computedTotal = computedTotal * constant;
467        }
468
469        public int toHashCode() {
470                return computedTotal;
471        }
472
473        public int hashCode() {
474                return toHashCode();
475        }
476
477        public void reset() {
478                seenReferences = new IdentityHashMap<Object, Object>();
479                computedTotal = seed;
480        }
481}