RichObject.java
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.
* <p>
* 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<Class<?>, FluentIterable<FieldFacade>> IDENTITY_FIELDS =
CacheBuilder.newBuilder().weakKeys().softValues().build(new IdentityFieldLoader());
private static final Joiner FIELD_JOINER = Joiner.on(",");
private static final Function<Object, Integer> toHashCode = new Function<Object, Integer>() {
@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)}):
* <ul>
* <li> other's runtime class must be the same, a super or a sub type of the runtime class of this instance
* <li> 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
* <li> the value of any field annotated with {@link Identity} on this must be equal to the value of the same field on other
* </ul>
* <p>
* 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<FieldFacade> fieldsOfThis = fields();
return fieldsOfThis.toSet().equals(that.fields().toSet())
&& fieldsOfThis.allMatch(hasEqualValueIn(that));
}
private FluentIterable<FieldFacade> fields() {
try {
return IDENTITY_FIELDS.get(getClass());
} catch (ExecutionException e) {
return throwUnchecked(e.getCause(), null);
}
}
private Predicate<FieldFacade> hasEqualValueIn(final RichObject other) {
return new Predicate<FieldFacade>() {
@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<String> fieldsAsStrings = fields().transform(toStringValueOfField());
return getClass().getSimpleName()+"["+FIELD_JOINER.join(fieldsAsStrings)+"]";
}
private Function<FieldFacade, String> toStringValueOfField() {
return new Function<FieldFacade, String>() {
@Override
public String apply(final FieldFacade field) {
return field.getName() + "=" + valueOf(field).or("absent");
}
};
}
private Optional<Object> valueOf(final FieldFacade field) {
return field.valueOn(this);
}
private static class IdentityFieldLoader extends CacheLoader<Class<?>, FluentIterable<FieldFacade>> {
@Override
public FluentIterable<FieldFacade> load(final Class<?> key) {
return FluentIterable.from(doLoad(key));
}
private static final Predicate<FieldFacade> onlyIdentityFields = new Predicate<FieldFacade>() {
@Override
public boolean apply(final FieldFacade field) {
return field.isIdentityField();
}
};
private static final Function<Field, FieldFacade> toFieldFacade = new Function<Field, FieldFacade>() {
@Override
public FieldFacade apply(final Field field) {
return new FieldFacade(field);
}
};
private static final Function<Class<?>, Set<FieldFacade>> toFieldSet = new Function<Class<?>, Set<FieldFacade>>() {
@Override
public Set<FieldFacade> apply(final Class<?> input) {
return doLoad(input);
}
};
private static Set<FieldFacade> doLoad(final Class<?> key) {
final ImmutableSet<FieldFacade> localIdentityFieldSet = FluentIterable.from(asList(key.getDeclaredFields()))
.transform(toFieldFacade)
.filter(onlyIdentityFields)
.toSet();
final Optional<? extends Class<?>> superClass = fromNullable(key.getSuperclass());
final Set<FieldFacade> superIdentityFieldSet = superClass.transform(toFieldSet).or(ImmutableSet.<FieldFacade>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<Object> 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<Void>() {
@Override
public Void run() {
field.setAccessible(true);
return null;
}
});
}
}
}