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
24
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
57
58
59
60
61
62
63 public static ClassProperties forClass(Class<?> pojoClass) throws NoPojomaticPropertiesException {
64 return INSTANCES.get(pojoClass);
65 }
66
67
68
69
70
71
72
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
91
92
93 public Collection<PropertyElement> getEqualsProperties() {
94 return properties.get(PropertyRole.EQUALS);
95 }
96
97
98
99
100
101 public Collection<PropertyElement> getHashCodeProperties() {
102 return properties.get(PropertyRole.HASH_CODE);
103 }
104
105
106
107
108
109 public Collection<PropertyElement> getToStringProperties() {
110 return properties.get(PropertyRole.TO_STRING);
111 }
112
113
114
115
116
117
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
128
129
130
131
132
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
150
151
152
153
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
241
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
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 }