View Javadoc
1   package org.pojomatic.internal;
2   
3   import java.lang.reflect.Field;
4   import java.lang.reflect.Member;
5   import java.lang.reflect.Method;
6   import java.lang.reflect.Modifier;
7   import java.util.ArrayList;
8   import java.util.Collection;
9   import java.util.EnumMap;
10  import java.util.LinkedHashMap;
11  import java.util.LinkedHashSet;
12  import java.util.List;
13  import java.util.Map;
14  import java.util.Set;
15  import java.util.regex.Pattern;
16  
17  import org.pojomatic.Pojomator;
18  import org.pojomatic.PropertyElement;
19  import org.pojomatic.NoPojomaticPropertiesException;
20  import org.pojomatic.annotations.*;
21  
22  /**
23   * The properties of a class used for {@link Pojomator#doHashCode(Object)},
24   * {@link Pojomator#doEquals(Object, Object)}, and {@link Pojomator#doToString(Object)}.
25   */
26  public class ClassProperties {
27    private static final Pattern ACCESSOR_PATTERN = Pattern.compile("(get|is)\\P{Ll}.*");
28  
29    private final Map<PropertyRole, List<PropertyElement>> properties = makeProperties();
30  
31    private final Class<?> equalsParentClass;
32  
33    private final boolean subclassCannotOverrideEquals;
34  
35    private final static SelfPopulatingMap<Class<?>, ClassProperties> INSTANCES =
36      new SelfPopulatingMap<Class<?>, ClassProperties>() {
37        @Override
38        protected ClassProperties create(Class<?> key) {
39          return new ClassProperties(key);
40        }
41      };
42  
43    private final static class ClassContributionTracker {
44      private Class<?> clazz = Object.class;
45  
46      public void noteContribution(Class<?> contributingClass) {
47        clazz = contributingClass;
48      }
49  
50      public Class<?> getMostSpecificContributingClass() {
51        return clazz;
52      }
53    }
54  
55    /**
56     * Get an instance for the given {@code pojoClass}.  Instances are cached, so calling this method
57     * repeatedly is not inefficient.
58     * @param pojoClass the class to inspect for properties
59     * @return The {@code ClassProperties} for {@code pojoClass}.
60     * @throws NoPojomaticPropertiesException if {@code pojoClass} has no properties annotated for use
61     * with Pojomatic.
62     */
63    public static ClassProperties forClass(Class<?> pojoClass) throws NoPojomaticPropertiesException {
64      return INSTANCES.get(pojoClass);
65    }
66  
67    /**
68     * Creates an instance for the given {@code pojoClass}.
69     *
70     * @param pojoClass the class to inspect for properties
71     * @throws NoPojomaticPropertiesException if {@code pojoClass} has no properties annotated for use
72     * with Pojomatic.
73     */
74    private ClassProperties(Class<?> pojoClass) throws NoPojomaticPropertiesException {
75      if (pojoClass.isInterface()) {
76        extractClassProperties(pojoClass, new OverridableMethods(), new ClassContributionTracker());
77        equalsParentClass = pojoClass;
78      }
79      else {
80        ClassContributionTracker classContributionTracker = new ClassContributionTracker();
81        walkHierarchy(pojoClass, new OverridableMethods(), classContributionTracker);
82        equalsParentClass = classContributionTracker.getMostSpecificContributingClass();
83      }
84      verifyPropertiesNotEmpty(pojoClass);
85      subclassCannotOverrideEquals = pojoClass.isAnnotationPresent(SubclassCannotOverrideEquals.class)
86        || pojoClass.isInterface();
87    }
88  
89    /**
90     * Gets the properties to use for {@link Pojomator#doEquals(Object, Object)}.
91     * @return the properties to use for {@link Pojomator#doEquals(Object, Object)}.
92     */
93    public Collection<PropertyElement> getEqualsProperties() {
94      return properties.get(PropertyRole.EQUALS);
95    }
96  
97    /**
98     * Gets the properties to use for {@link Pojomator#doHashCode(Object)}.
99     * @return the properties to use for {@link Pojomator#doHashCode(Object)}.
100    */
101   public Collection<PropertyElement> getHashCodeProperties() {
102     return properties.get(PropertyRole.HASH_CODE);
103   }
104 
105   /**
106    * Gets the properties to use for {@link Pojomator#doToString(Object)}.
107    * @return the properties to use for {@link Pojomator#doToString(Object)}.
108    */
109   public Collection<PropertyElement> getToStringProperties() {
110     return properties.get(PropertyRole.TO_STRING);
111   }
112 
113   /**
114    * Get the union of all properties used for any Pojomator methods. The resulting set will have a predictable iteration
115    * order: first, the ordered list of elements used for equals, followed by an ordered list of any additional elements
116    * used for toString.
117    * @return the union of all properties used for any Pojomator methods.
118    */
119   public Set<PropertyElement> getAllProperties() {
120     LinkedHashSet<PropertyElement> allProperties = new LinkedHashSet<>();
121     allProperties.addAll(properties.get(PropertyRole.EQUALS));
122     allProperties.addAll(properties.get(PropertyRole.TO_STRING));
123     return allProperties;
124   }
125 
126   /**
127    * Whether instances of {@code otherClass} are candidates for being equal to instances of
128    * the class this {@code ClassProperties} instance was created for.
129    * @param otherClass the class to check for compatibility for equals with.
130    * @return {@code true} if instances of {@code otherClass} are candidates for being equal to
131    * instances of the class this {@code ClassProperties} instance was created for, or {@code false}
132    * otherwise.
133    */
134   public boolean isCompatibleForEquals(Class<?> otherClass) {
135     if (!equalsParentClass.isAssignableFrom(otherClass)) {
136       return false;
137     }
138     else {
139       if (subclassCannotOverrideEquals) {
140         return true;
141       }
142       else {
143         return equalsParentClass.equals(forClass(otherClass).equalsParentClass);
144       }
145     }
146   }
147 
148   /**
149    * Walk up to the top of the hierarchy of {@code clazz}, then start extracting properties from it, working back down
150    * the inheritance chain from parent to child.
151    * @param clazz the class to inspect
152    * @param overridableMethods used to track which methods can be overridden
153    * @param classContributionTracker used to track the most specific class which contributes properties
154    */
155   private void walkHierarchy(
156     Class<?> clazz,
157     OverridableMethods overridableMethods,
158     ClassContributionTracker classContributionTracker) {
159     if (clazz != Object.class) {
160       walkHierarchy(clazz.getSuperclass(), overridableMethods, classContributionTracker);
161       extractClassProperties(clazz, overridableMethods, classContributionTracker);
162       if (clazz.isAnnotationPresent(OverridesEquals.class)) {
163         classContributionTracker.noteContribution(clazz);
164       }
165     }
166   }
167 
168   private void extractClassProperties(
169     Class<?> clazz,
170     OverridableMethods overridableMethods,
171     ClassContributionTracker classContributionTracker) {
172     AutoProperty autoProperty = clazz.getAnnotation(AutoProperty.class);
173     final DefaultPojomaticPolicy classPolicy =
174       (autoProperty != null) ? autoProperty.policy() : null;
175     final AutoDetectPolicy autoDetectPolicy =
176       (autoProperty != null) ? autoProperty.autoDetect() : null;
177 
178     Map<PropertyRole, Map<String, PropertyElement>> fieldsMap = extractFields(
179       clazz, classPolicy, autoDetectPolicy, classContributionTracker);
180     Map<PropertyRole, Map<String, PropertyElement>> methodsMap = extractMethods(
181       clazz, classPolicy, autoDetectPolicy, overridableMethods, classContributionTracker);
182     if (containsValues(fieldsMap) || containsValues(methodsMap)) {
183       PropertyClassVisitor propertyClassVisitor = PropertyClassVisitor.visitClass(clazz, fieldsMap, methodsMap);
184       if (propertyClassVisitor != null) {
185         for (PropertyRole role: PropertyRole.values()) {
186           properties.get(role).addAll(propertyClassVisitor.getSortedProperties().get(role));
187         }
188       }
189       else {
190         throw new RuntimeException("no class bytes for " + clazz);
191       }
192     }
193   }
194 
195   private static boolean containsValues(Map<?, ? extends Map<?, ?>> mapOfMaps) {
196     for (Map<?, ?> map: mapOfMaps.values()) {
197       if (! map.isEmpty()) {
198         return true;
199       }
200     }
201     return false;
202   }
203 
204   private Map<PropertyRole, Map<String, PropertyElement>> extractMethods(
205     Class<?> clazz,
206     final DefaultPojomaticPolicy classPolicy,
207     final AutoDetectPolicy autoDetectPolicy,
208     final OverridableMethods overridableMethods,
209     final ClassContributionTracker classContributionTracker) {
210     Map<PropertyRole, Map<String, PropertyElement>> propertiesMap = makePropertiesMap();
211     for (Method method : clazz.getDeclaredMethods()) {
212       if (method.isSynthetic()) {
213         continue;
214       }
215       Property property = method.getAnnotation(Property.class);
216       if (isStatic(method)) {
217         if (property != null) {
218           throw new IllegalArgumentException(
219             "Static method " + clazz.getName() + "." + method.getName()
220             + "() is annotated with @Property");
221         }
222         else {
223           continue;
224         }
225       }
226 
227       PojomaticPolicy propertyPolicy = null;
228       if (property != null) {
229         if (!methodSignatureIsAccessor(method)) {
230           throw new IllegalArgumentException(
231             "Method " + method +
232             " is annotated with @Property but either takes arguments or returns void");
233         }
234         propertyPolicy = property.policy();
235       }
236       else if (!methodIsAccessor(method)) {
237         continue;
238       }
239 
240       /* add all methods that are explicitly annotated or auto-detected, and not overriding already
241        * added methods */
242       if (propertyPolicy != null || AutoDetectPolicy.METHOD == autoDetectPolicy) {
243         PropertyAccessor propertyAccessor = null;
244         for (PropertyRole role : overridableMethods.checkAndMaybeAddRolesToMethod(
245           method, PropertyFilter.getRoles(propertyPolicy, classPolicy))) {
246           if (propertyAccessor == null) {
247             propertyAccessor = new PropertyAccessor(method, getPropertyName(property));
248           }
249           propertiesMap.get(role).put(method.getName(), propertyAccessor);
250           if (PropertyRole.EQUALS == role) {
251             classContributionTracker.noteContribution(clazz);
252           }
253         }
254       }
255     }
256     return propertiesMap;
257   }
258 
259   private Map<PropertyRole, Map<String, PropertyElement>> extractFields(
260     Class<?> clazz,
261     final DefaultPojomaticPolicy classPolicy,
262     final AutoDetectPolicy autoDetectPolicy,
263     final ClassContributionTracker classContributionTracker) {
264     Map<PropertyRole, Map<String, PropertyElement>> propertiesMap = makePropertiesMap();
265     for (Field field : clazz.getDeclaredFields()) {
266       if (field.isSynthetic()) {
267         continue;
268       }
269       Property property = field.getAnnotation(Property.class);
270       if (isStatic(field)) {
271         if (property != null) {
272           throw new IllegalArgumentException(
273             "Static field " + clazz.getName() + "." + field.getName()
274             + " is annotated with @Property");
275         }
276         else {
277           continue;
278         }
279       }
280 
281       final PojomaticPolicy propertyPolicy = (property != null) ? property.policy() : null;
282 
283       /* add all fields that are explicitly annotated or auto-detected */
284       if (propertyPolicy != null || AutoDetectPolicy.FIELD == autoDetectPolicy) {
285         PropertyField propertyField = null;
286         for (PropertyRole role : PropertyFilter.getRoles(propertyPolicy, classPolicy)) {
287           if (propertyField == null) {
288             propertyField = new PropertyField(field, getPropertyName(property));
289           }
290           propertiesMap.get(role).put(field.getName(), propertyField);
291           if (PropertyRole.EQUALS == role) {
292             classContributionTracker.noteContribution(clazz);
293           }
294         }
295       }
296     }
297     return propertiesMap;
298   }
299 
300   private void verifyPropertiesNotEmpty(Class<?> pojoClass) {
301     for (Collection<PropertyElement> propertyElements : properties.values()) {
302       if (!propertyElements.isEmpty()) {
303         return;
304       }
305     }
306     throw new NoPojomaticPropertiesException(pojoClass);
307   }
308 
309   private String getPropertyName(Property property) {
310     return property == null ? "" : property.name();
311   }
312 
313   private static boolean methodIsAccessor(Method method) {
314     return methodSignatureIsAccessor(method)
315       && isAccessorName(method.getName());
316   }
317 
318   private static boolean methodSignatureIsAccessor(Method method) {
319     return ! Void.TYPE.equals(method.getReturnType())
320       && method.getParameterTypes().length == 0;
321   }
322 
323   private static boolean isAccessorName(String name) {
324     return ACCESSOR_PATTERN.matcher(name).matches();
325   }
326 
327   private static boolean isStatic(Member member) {
328     return Modifier.isStatic(member.getModifiers());
329   }
330 
331   private static Map<PropertyRole, List<PropertyElement>> makeProperties() {
332     Map<PropertyRole, List<PropertyElement>> properties =
333       new EnumMap<>(PropertyRole.class);
334     for (PropertyRole role : PropertyRole.values()) {
335       properties.put(role, new ArrayList<PropertyElement>());
336     }
337     return properties;
338   }
339 
340   private static Map<PropertyRole, Map<String, PropertyElement>> makePropertiesMap() {
341     Map<PropertyRole, Map<String, PropertyElement>> properties =
342         new EnumMap<>(PropertyRole.class);
343     for (PropertyRole role : PropertyRole.values()) {
344       properties.put(role, new LinkedHashMap<String, PropertyElement>());
345     }
346     return properties;
347   }
348 }