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

uk.org.lidalia.lang.RichObject Maven / Gradle / Ivy

package uk.org.lidalia.lang;

import java.lang.reflect.Field;
import java.security.PrivilegedAction;
import java.util.Set;
import java.util.concurrent.ExecutionException;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;

import static com.google.common.base.Optional.fromNullable;
import static java.security.AccessController.doPrivileged;
import static java.util.Arrays.asList;
import static uk.org.lidalia.lang.Classes.inSameClassHierarchy;
import static uk.org.lidalia.lang.Exceptions.throwUnchecked;

/**
 * A class that provides implementations of {@link #equals(Object)}, {@link #hashCode()} and {@link #toString()} for its subtypes.
 * 

* These implementations are based on annotating the fields of the subtypes with the {@link Identity} annotation. */ public abstract class RichObject { private static final int PRIME = 37; private static final int INITIAL_HASHCODE_VALUE = 17; private static final LoadingCache, FluentIterable> IDENTITY_FIELDS = CacheBuilder.newBuilder().weakKeys().softValues().build(new IdentityFieldLoader()); private static final Joiner FIELD_JOINER = Joiner.on(","); private static final Function toHashCode = new Function() { @Override public Integer apply(final Object fieldValue) { return fieldValue.hashCode(); } }; /** * Implementation of equals based on fields annotated with {@link Identity}. * * Applies equality rules on the following basis (in addition to the rules in {@link Object#equals(Object)}): *

    *
  • other's runtime class must be the same, a super or a sub type of the runtime class of this instance *
  • other's runtime class must have exactly the same set of fields annotated with {@link Identity} as those on the runtime * class of this instance, where the set of fields in each case comprises those on the class and all of its superclasses *
  • the value of any field annotated with {@link Identity} on this must be equal to the value of the same field on other *
*

* The practical result of this is that an instance of subtype B of subtype A of RichObject can only be equal to an instance * of subtype A if B does not annotate any of its fields with {@link Identity}. * * @param other the object to compare against * @return true if the other type is logically equal to this */ @Override public final boolean equals(final Object other) { // Usual equals checks if (other == this) { return true; } if (other == null) { return false; } // One of the two must be a subtype of the other if (!(other instanceof RichObject) || !inSameClassHierarchy(getClass(), other.getClass())) { return false; } final RichObject that = (RichObject) other; // They must have precisely the same set of identity members to meet the // symmetric & transitive requirement of equals final FluentIterable fieldsOfThis = fields(); return fieldsOfThis.toSet().equals(that.fields().toSet()) && fieldsOfThis.allMatch(hasEqualValueIn(that)); } private FluentIterable fields() { try { return IDENTITY_FIELDS.get(getClass()); } catch (ExecutionException e) { return throwUnchecked(e.getCause(), null); } } private Predicate hasEqualValueIn(final RichObject other) { return new Predicate() { @Override public boolean apply(final FieldFacade field) { return valueOf(field).equals(other.valueOf(field)); } }; } /** * Default implementation of hashCode - can be overridden to provide more efficient ones provided the contract specified * in {@link Object#hashCode()} is maintained with respect to {@link #equals(Object)}. * * @return hash code computed from the hashes of all the fields annotated with {@link Identity} */ @Override public int hashCode() { int result = INITIAL_HASHCODE_VALUE; for (final FieldFacade field : fields()) { final int toAdd = valueOf(field).transform(toHashCode).or(0); result = PRIME * result + toAdd; } return result; } /** * Default implementation of toString. * * @return a string in the form ClassName[field1=value1,field2=value2] where the fields are those annotated with * {@link Identity} */ @Override public String toString() { final Iterable fieldsAsStrings = fields().transform(toStringValueOfField()); return getClass().getSimpleName()+"["+FIELD_JOINER.join(fieldsAsStrings)+"]"; } private Function toStringValueOfField() { return new Function() { @Override public String apply(final FieldFacade field) { return field.getName() + "=" + valueOf(field).or("absent"); } }; } private Optional valueOf(final FieldFacade field) { return field.valueOn(this); } private static class IdentityFieldLoader extends CacheLoader, FluentIterable> { @Override public FluentIterable load(final Class key) { return FluentIterable.from(doLoad(key)); } private static final Predicate onlyIdentityFields = new Predicate() { @Override public boolean apply(final FieldFacade field) { return field.isIdentityField(); } }; private static final Function toFieldFacade = new Function() { @Override public FieldFacade apply(final Field field) { return new FieldFacade(field); } }; private static final Function, Set> toFieldSet = new Function, Set>() { @Override public Set apply(final Class input) { return doLoad(input); } }; private static Set doLoad(final Class key) { final ImmutableSet localIdentityFieldSet = FluentIterable.from(asList(key.getDeclaredFields())) .transform(toFieldFacade) .filter(onlyIdentityFields) .toSet(); final Optional> superClass = fromNullable(key.getSuperclass()); final Set superIdentityFieldSet = superClass.transform(toFieldSet).or(ImmutableSet.of()); return Sets.union(localIdentityFieldSet, superIdentityFieldSet); } } private static class FieldFacade extends WrappedValue { private final Field field; FieldFacade(final Field field) { super(field); this.field = field; } public Optional valueOn(final Object target) { try { if (!field.isAccessible()) { makeAccessible(); } return fromNullable(field.get(target)); } catch (IllegalAccessException e) { throw new IllegalStateException(field+" was not accessible; all fields should be accessible", e); } } public String getName() { return field.getName(); } public boolean isIdentityField() { return field.isAnnotationPresent(Identity.class); } private void makeAccessible() { doPrivileged(new PrivilegedAction() { @Override public Void run() { field.setAccessible(true); return null; } }); } } }