View Javadoc

1   package uk.org.lidalia.lang;
2   
3   import java.lang.reflect.Field;
4   import java.security.PrivilegedAction;
5   import java.util.Set;
6   import java.util.concurrent.ExecutionException;
7   
8   import com.google.common.base.Function;
9   import com.google.common.base.Joiner;
10  import com.google.common.base.Optional;
11  import com.google.common.base.Predicate;
12  import com.google.common.cache.CacheBuilder;
13  import com.google.common.cache.CacheLoader;
14  import com.google.common.cache.LoadingCache;
15  import com.google.common.collect.FluentIterable;
16  import com.google.common.collect.ImmutableSet;
17  import com.google.common.collect.Sets;
18  
19  import static com.google.common.base.Optional.fromNullable;
20  import static java.security.AccessController.doPrivileged;
21  import static java.util.Arrays.asList;
22  import static uk.org.lidalia.lang.Classes.inSameClassHierarchy;
23  import static uk.org.lidalia.lang.Exceptions.throwUnchecked;
24  
25  /**
26   * A class that provides implementations of {@link #equals(Object)}, {@link #hashCode()} and {@link #toString()} for its subtypes.
27   * <p>
28   * These implementations are based on annotating the fields of the subtypes with the {@link Identity} annotation.
29   */
30  public abstract class RichObject {
31  
32      private static final int PRIME = 37;
33      private static final int INITIAL_HASHCODE_VALUE = 17;
34  
35      private static final LoadingCache<Class<?>, FluentIterable<FieldFacade>> IDENTITY_FIELDS =
36              CacheBuilder.newBuilder().weakKeys().softValues().build(new IdentityFieldLoader());
37      private static final Joiner FIELD_JOINER = Joiner.on(",");
38      private static final Function<Object, Integer> toHashCode = new Function<Object, Integer>() {
39          @Override
40          public Integer apply(final Object fieldValue) {
41              return fieldValue.hashCode();
42          }
43      };
44  
45      /**
46       * Implementation of equals based on fields annotated with {@link Identity}.
47       *
48       * Applies equality rules on the following basis (in addition to the rules in {@link Object#equals(Object)}):
49       * <ul>
50       * <li> other's runtime class must be the same, a super or a sub type of the runtime class of this instance
51       * <li> other's runtime class must have exactly the same set of fields annotated with {@link Identity} as those on the runtime
52       *      class of this instance, where the set of fields in each case comprises those on the class and all of its superclasses
53       * <li> the value of any field annotated with {@link Identity} on this must be equal to the value of the same field on other
54       * </ul>
55       * <p>
56       * The practical result of this is that an instance of subtype B of subtype A of RichObject can only be equal to an instance
57       * of subtype A if B does not annotate any of its fields with {@link Identity}.
58       *
59       * @param other the object to compare against
60       * @return true if the other type is logically equal to this
61       */
62      @Override public final boolean equals(final Object other) {
63          // Usual equals checks
64          if (other == this) {
65              return true;
66          }
67          if (other == null) {
68              return false;
69          }
70  
71          // One of the two must be a subtype of the other
72          if (!(other instanceof RichObject) || !inSameClassHierarchy(getClass(), other.getClass())) {
73              return false;
74          }
75  
76          final RichObject that = (RichObject) other;
77  
78          // They must have precisely the same set of identity members to meet the
79          // symmetric & transitive requirement of equals
80          final FluentIterable<FieldFacade> fieldsOfThis = fields();
81          return fieldsOfThis.toSet().equals(that.fields().toSet())
82                  && fieldsOfThis.allMatch(hasEqualValueIn(that));
83      }
84  
85      private FluentIterable<FieldFacade> fields() {
86          try {
87              return IDENTITY_FIELDS.get(getClass());
88          } catch (ExecutionException e) {
89              return throwUnchecked(e.getCause(), null);
90          }
91      }
92  
93      private Predicate<FieldFacade> hasEqualValueIn(final RichObject other) {
94          return new Predicate<FieldFacade>() {
95              @Override
96              public boolean apply(final FieldFacade field) {
97                  return valueOf(field).equals(other.valueOf(field));
98              }
99          };
100     }
101 
102     /**
103      * Default implementation of hashCode - can be overridden to provide more efficient ones provided the contract specified
104      * in {@link Object#hashCode()} is maintained with respect to {@link #equals(Object)}.
105      *
106      * @return hash code computed from the hashes of all the fields annotated with {@link Identity}
107      */
108     @Override public int hashCode() {
109         int result = INITIAL_HASHCODE_VALUE;
110         for (final FieldFacade field : fields()) {
111             final int toAdd = valueOf(field).transform(toHashCode).or(0);
112             result = PRIME * result + toAdd;
113         }
114         return result;
115     }
116 
117     /**
118      * Default implementation of toString.
119      *
120      * @return a string in the form ClassName[field1=value1,field2=value2] where the fields are those annotated with
121      * {@link Identity}
122      */
123     @Override public String toString() {
124         final Iterable<String> fieldsAsStrings = fields().transform(toStringValueOfField());
125         return getClass().getSimpleName()+"["+FIELD_JOINER.join(fieldsAsStrings)+"]";
126     }
127 
128     private Function<FieldFacade, String> toStringValueOfField() {
129         return new Function<FieldFacade, String>() {
130             @Override
131             public String apply(final FieldFacade field) {
132                 return field.getName() + "=" + valueOf(field).or("absent");
133             }
134         };
135     }
136 
137     private Optional<Object> valueOf(final FieldFacade field) {
138         return field.valueOn(this);
139     }
140 
141     private static class IdentityFieldLoader extends CacheLoader<Class<?>, FluentIterable<FieldFacade>> {
142 
143         @Override
144         public FluentIterable<FieldFacade> load(final Class<?> key) {
145             return FluentIterable.from(doLoad(key));
146         }
147 
148         private static final Predicate<FieldFacade> onlyIdentityFields = new Predicate<FieldFacade>() {
149             @Override
150             public boolean apply(final FieldFacade field) {
151                 return field.isIdentityField();
152             }
153         };
154 
155         private static final Function<Field, FieldFacade> toFieldFacade = new Function<Field, FieldFacade>() {
156             @Override
157             public FieldFacade apply(final Field field) {
158                 return new FieldFacade(field);
159             }
160         };
161 
162         private static final Function<Class<?>, Set<FieldFacade>> toFieldSet = new Function<Class<?>, Set<FieldFacade>>() {
163             @Override
164             public Set<FieldFacade> apply(final Class<?> input) {
165                 return doLoad(input);
166             }
167         };
168 
169         private static Set<FieldFacade> doLoad(final Class<?> key) {
170             final ImmutableSet<FieldFacade> localIdentityFieldSet = FluentIterable.from(asList(key.getDeclaredFields()))
171                     .transform(toFieldFacade)
172                     .filter(onlyIdentityFields)
173                     .toSet();
174             final Optional<? extends Class<?>> superClass = fromNullable(key.getSuperclass());
175             final Set<FieldFacade> superIdentityFieldSet = superClass.transform(toFieldSet).or(ImmutableSet.<FieldFacade>of());
176             return Sets.union(localIdentityFieldSet, superIdentityFieldSet);
177         }
178     }
179 
180     private static class FieldFacade extends WrappedValue {
181         private final Field field;
182 
183         FieldFacade(final Field field) {
184             super(field);
185             this.field = field;
186         }
187 
188         public Optional<Object> valueOn(final Object target) {
189             try {
190                 if (!field.isAccessible()) {
191                     makeAccessible();
192                 }
193                 return fromNullable(field.get(target));
194             } catch (IllegalAccessException e) {
195                 throw new IllegalStateException(field+" was not accessible; all fields should be accessible", e);
196             }
197         }
198 
199         public String getName() {
200             return field.getName();
201         }
202 
203         public boolean isIdentityField() {
204             return field.isAnnotationPresent(Identity.class);
205         }
206 
207         private void makeAccessible() {
208             doPrivileged(new PrivilegedAction<Void>() {
209                 @Override
210                 public Void run() {
211                     field.setAccessible(true);
212                     return null;
213                 }
214             });
215         }
216     }
217 }