All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jadira.reflection.equals.EqualsBuilder Maven / Gradle / Ivy

There is a newer version: 7.0.0.CR1
Show newest version
/*
 *  Copyright 2013 Christopher Pheby
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.jadira.reflection.equals;

import java.lang.reflect.Modifier;
import java.util.IdentityHashMap;

import org.jadira.reflection.access.api.ClassAccess;
import org.jadira.reflection.access.api.ClassAccessFactory;
import org.jadira.reflection.access.api.FieldAccess;
import org.jadira.reflection.access.api.MethodAccess;
import org.jadira.reflection.access.portable.PortableClassAccessFactory;
import org.jadira.reflection.access.unsafe.UnsafeClassAccessFactory;
import org.jadira.reflection.core.identity.Tuple;
import org.jadira.reflection.core.platform.FeatureDetection;

/**
 * This class is based on the capabilities provided by Commons-Lang's
 * EqualsBuilder. It is not a complete "drop-in" replacement, but is near enough
 * in functionality and in terms of its API that it should be quite
 * straightforward to switch from one class to the other.
 * 
 * EqualsBuilder makes use of Jadira's reflection capabilities. This means, like
 * Cloning there is a brief warm-up time entailed in preparing introspection
 * when a class is first processed. If you are already performing cloning on a
 * type you will not be impacted by this as any necessary warm-up will have already taken place.
 * 
 * Once the initial warmup has been performed, you will, usually, find enhanced performance
 * compared to standard reflection based approaches. The strategy to be used can be configured.
 *
 * Where objects that are reachable from the object being compared do not override equals(),
 * the default behaviour is to rely on {@link Object#equals(Object)} which compares object
 * identity for those objects. This is also the behaviour of Commons Lang. You
 * can modify this to reflect into any reachable object that does not override equals by
 * enabling the defaultDeepReflect property.
 * 
 * Transient fields are not compared by the current implementation of this
 * class.
 */
public class EqualsBuilder {

	private static final ThreadLocal reflectionBuilder = new ThreadLocal() {
		protected EqualsBuilder initialValue() {
			return new EqualsBuilder();
		};
	};

	private IdentityHashMap, Tuple> seenReferences;

	/**
	 * If the fields tested are equal. The default value is true.
	 */
	private boolean isEquals = true;

	private boolean defaultDeepReflect = false;

	private ClassAccessFactory classAccessFactory;

	public EqualsBuilder() {
		seenReferences = new IdentityHashMap, Tuple>();

		if (FeatureDetection.hasUnsafe()) {
			this.classAccessFactory = UnsafeClassAccessFactory.get();
		} else {
			this.classAccessFactory = PortableClassAccessFactory.get();
		}
	}

	public EqualsBuilder withDefaultDeepReflect(boolean newDefaultDeepReflect) {
		this.defaultDeepReflect = newDefaultDeepReflect;
		return this;
	}

	public EqualsBuilder withClassAccessFactory(
			ClassAccessFactory classAccessFactory) {
		this.classAccessFactory = classAccessFactory;
		return this;
	}

	public static boolean reflectionEquals(Object lhs, Object rhs) {

		if (lhs == rhs) {
			return true;
		}
		if (lhs == null || rhs == null) {
			return false;
		}

		Class lhsClass = lhs.getClass();
		Class rhsClass = rhs.getClass();

		Class testClass;
		if (lhsClass.isInstance(rhs)) {
			if (rhsClass.isInstance(lhs)) {
				testClass = lhsClass;
			} else {
				testClass = rhsClass;
			}
		} else if (rhsClass.isInstance(lhs)) {
			if (lhsClass.isInstance(rhs)) {
				testClass = rhsClass;
			} else {
				testClass = lhsClass;
			}
		} else {
			return false;
		}

		EqualsBuilder equalsBuilder = reflectionBuilder.get();
		// In case we recurse into this method
		reflectionBuilder.set(null);

		equalsBuilder.reset();

		ClassAccess classAccess = equalsBuilder.classAccessFactory
				.getClassAccess(testClass);

		try {
			while ((classAccess.getType() != Object.class)
					&& (!classAccess.providesEquals())) {
				equalsBuilder.reflectionAppend(lhs, rhs, classAccess);
				classAccess = classAccess.getSuperClassAccess();
			}
		} catch (IllegalArgumentException e) {
			return false;
		}

		if (classAccess.getType() != Object.class) {

			final MethodAccess methodAccess;
			try {
				@SuppressWarnings("unchecked")
				final MethodAccess myMethodAccess = (MethodAccess) classAccess.getDeclaredMethodAccess(classAccess.getType().getMethod(
								"equals", Object.class));
				methodAccess = myMethodAccess;
			} catch (NoSuchMethodException e) {
				throw new IllegalStateException("Cannot find equals() method");
			} catch (SecurityException e) {
				throw new IllegalStateException("Cannot find equals() method");
			}
			equalsBuilder.setEquals(((Boolean) methodAccess.invoke(lhs, rhs))
					.booleanValue());
		}

		final boolean isEqual = equalsBuilder.isEquals();
		reflectionBuilder.set(equalsBuilder);

		return isEqual;
	}

	private void reflectionAppend(Object lhs, Object rhs,
			ClassAccess classAccess) {

		Tuple t = Tuple.of(lhs, rhs);
		if (seenReferences.containsKey(t)) {
			return;
		}
		seenReferences.put(t, t);

		@SuppressWarnings("unchecked")
		ClassAccess classAccessInHierarchy = (ClassAccess) classAccess;
		
		while (classAccessInHierarchy != null) {
		
			FieldAccess[] fields = (FieldAccess[]) classAccessInHierarchy.getDeclaredFieldAccessors();
	
			for (int i = 0; (i < fields.length) && (isEquals); i++) {
				FieldAccess f = fields[i];
				if ((f.field().getName().indexOf('$') == -1)
						&& (!Modifier.isTransient(f.field().getModifiers()))
						&& (!Modifier.isStatic(f.field().getModifiers()))) {
	
					Class type = f.fieldClass();
					if (type.isPrimitive()) {
						if (java.lang.Boolean.TYPE == type) {
							append(f.getBooleanValue(lhs), f.getBooleanValue(rhs));
						} else if (java.lang.Byte.TYPE == type) {
							append(f.getByteValue(lhs), f.getByteValue(rhs));
						} else if (java.lang.Character.TYPE == type) {
							append(f.getCharValue(lhs), f.getCharValue(rhs));
						} else if (java.lang.Short.TYPE == type) {
							append(f.getShortValue(lhs), f.getShortValue(rhs));
						} else if (java.lang.Integer.TYPE == type) {
							append(f.getIntValue(lhs), f.getIntValue(rhs));
						} else if (java.lang.Long.TYPE == type) {
							append(f.getLongValue(lhs), f.getLongValue(rhs));
						} else if (java.lang.Float.TYPE == type) {
							append(f.getFloatValue(lhs), f.getFloatValue(rhs));
						} else if (java.lang.Double.TYPE == type) {
							append(f.getDoubleValue(lhs), f.getDoubleValue(rhs));
						}
					} else {
						append(f.getValue(lhs), f.getValue(rhs));
					}
				}
			}
			
			classAccessInHierarchy = classAccessInHierarchy.getSuperClassAccess();
		}
	}

	public EqualsBuilder append(Object lhs, Object rhs) {

		preCheckObject(lhs, rhs);

		Class lhsClass = lhs.getClass();
		if (!lhsClass.isArray()) {
			if (defaultDeepReflect) {
				reflectionEquals(lhs, rhs);
			} else {
				this.setEquals(lhs.equals(rhs));
			}
		} else if (lhs.getClass() != rhs.getClass()) {
			this.setEquals(false);
		} else if (lhs instanceof long[]) {
			append((long[]) lhs, (long[]) rhs);
		} else if (lhs instanceof int[]) {
			append((int[]) lhs, (int[]) rhs);
		} else if (lhs instanceof short[]) {
			append((short[]) lhs, (short[]) rhs);
		} else if (lhs instanceof char[]) {
			append((char[]) lhs, (char[]) rhs);
		} else if (lhs instanceof byte[]) {
			append((byte[]) lhs, (byte[]) rhs);
		} else if (lhs instanceof double[]) {
			append((double[]) lhs, (double[]) rhs);
		} else if (lhs instanceof float[]) {
			append((float[]) lhs, (float[]) rhs);
		} else if (lhs instanceof boolean[]) {
			append((boolean[]) lhs, (boolean[]) rhs);
		} else {
			append((Object[]) lhs, (Object[]) rhs);
		}

		return this;
	}

	public EqualsBuilder append(long lhs, long rhs) {
		if (isEquals == false) {
			return this;
		}
		isEquals = (lhs == rhs);
		return this;
	}

	public EqualsBuilder append(int lhs, int rhs) {
		if (isEquals == false) {
			return this;
		}
		isEquals = (lhs == rhs);
		return this;
	}

	public EqualsBuilder append(short lhs, short rhs) {
		if (isEquals == false) {
			return this;
		}
		isEquals = (lhs == rhs);
		return this;
	}

	public EqualsBuilder append(char lhs, char rhs) {
		if (isEquals == false) {
			return this;
		}
		isEquals = (lhs == rhs);
		return this;
	}

	public EqualsBuilder append(byte lhs, byte rhs) {
		if (isEquals == false) {
			return this;
		}
		isEquals = (lhs == rhs);
		return this;
	}

	public EqualsBuilder append(double lhs, double rhs) {
		if (isEquals == false) {
			return this;
		}
		return append(Double.doubleToLongBits(lhs),
				Double.doubleToLongBits(rhs));
	}

	public EqualsBuilder append(float lhs, float rhs) {

		if (isEquals == false) {
			return this;
		}
		return append(Float.floatToIntBits(lhs), Float.floatToIntBits(rhs));
	}

	public EqualsBuilder append(boolean lhs, boolean rhs) {
		if (isEquals == false) {
			return this;
		}
		isEquals = (lhs == rhs);
		return this;
	}

	public EqualsBuilder append(Object[] lhs, Object[] rhs) {

		preCheckObject(lhs, rhs);
		if (!isEquals) {
			return this;
		}

		if (lhs.length != rhs.length) {
			this.setEquals(false);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals; i++) {
			append(lhs[i], rhs[i]);
		}
		return this;
	}

	public EqualsBuilder append(long[] lhs, long[] rhs) {

		preCheckObject(lhs, rhs);
		if (!isEquals) {
			return this;
		}

		if (lhs.length != rhs.length) {
			this.setEquals(false);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals; i++) {
			append(lhs[i], rhs[i]);
		}
		return this;
	}

	public EqualsBuilder append(int[] lhs, int[] rhs) {

		preCheckObject(lhs, rhs);
		if (!isEquals) {
			return this;
		}

		if (lhs.length != rhs.length) {
			this.setEquals(false);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals; i++) {
			append(lhs[i], rhs[i]);
		}
		return this;
	}

	public EqualsBuilder append(short[] lhs, short[] rhs) {

		preCheckObject(lhs, rhs);
		if (!isEquals) {
			return this;
		}

		if (lhs.length != rhs.length) {
			this.setEquals(false);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals; i++) {
			append(lhs[i], rhs[i]);
		}
		return this;
	}

	public EqualsBuilder append(char[] lhs, char[] rhs) {

		preCheckObject(lhs, rhs);
		if (!isEquals) {
			return this;
		}

		if (lhs.length != rhs.length) {
			this.setEquals(false);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals; i++) {
			append(lhs[i], rhs[i]);
		}
		return this;
	}

	public EqualsBuilder append(byte[] lhs, byte[] rhs) {

		preCheckObject(lhs, rhs);
		if (!isEquals) {
			return this;
		}

		if (lhs.length != rhs.length) {
			this.setEquals(false);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals; i++) {
			append(lhs[i], rhs[i]);
		}
		return this;
	}

	public EqualsBuilder append(double[] lhs, double[] rhs) {

		preCheckObject(lhs, rhs);
		if (!isEquals) {
			return this;
		}

		if (lhs.length != rhs.length) {
			this.setEquals(false);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals; i++) {
			append(lhs[i], rhs[i]);
		}
		return this;
	}

	public EqualsBuilder append(float[] lhs, float[] rhs) {

		preCheckObject(lhs, rhs);
		if (!isEquals) {
			return this;
		}

		if (lhs.length != rhs.length) {
			this.setEquals(false);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals; i++) {
			append(lhs[i], rhs[i]);
		}
		return this;
	}

	public EqualsBuilder append(boolean[] lhs, boolean[] rhs) {

		preCheckObject(lhs, rhs);
		if (!isEquals) {
			return this;
		}

		if (lhs.length != rhs.length) {
			this.setEquals(false);
			return this;
		}

		for (int i = 0; i < lhs.length && isEquals; i++) {
			append(lhs[i], rhs[i]);
		}
		return this;
	}

	private void preCheckObject(Object lhs, Object rhs) {
		if (!isEquals) {
			return;
		}
		if (lhs == rhs) {
			return;
		}
		if (lhs == null || rhs == null) {
			this.setEquals(false);
			return;
		}
	}

	private void setEquals(boolean equals) {
		this.isEquals = equals;
	}

	public boolean isEquals() {
		return this.isEquals;
	}

	public void reset() {
		seenReferences = new IdentityHashMap, Tuple>();
		this.isEquals = true;
	}
}