View Javadoc

1   /*
2    * Entreri, an entity-component framework in Java
3    *
4    * Copyright (c) 2013, Michael Ludwig
5    * All rights reserved.
6    *
7    * Redistribution and use in source and binary forms, with or without modification,
8    * are permitted provided that the following conditions are met:
9    *
10   *     Redistributions of source code must retain the above copyright notice,
11   *         this list of conditions and the following disclaimer.
12   *     Redistributions in binary form must reproduce the above copyright notice,
13   *         this list of conditions and the following disclaimer in the
14   *         documentation and/or other materials provided with the distribution.
15   *
16   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17   * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19   * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
20   * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
23   * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26   */
27  package com.lhkbob.entreri.impl;
28  
29  import com.lhkbob.entreri.*;
30  import com.lhkbob.entreri.property.*;
31  
32  import java.lang.annotation.Annotation;
33  import java.lang.reflect.Constructor;
34  import java.lang.reflect.Method;
35  import java.util.*;
36  
37  /**
38   * ReflectionComponentSpecification is an implementation that extracts the component
39   * specification from a {@link Class} object using the reflection APIs defined by Java.
40   * This can only be used after the referenced classes have been compiled and are capable
41   * of being loaded, e.g. the opposite scenario from MirrorComponentSpecification.
42   *
43   * @author Michael Ludwig
44   */
45  class ReflectionComponentSpecification implements ComponentSpecification {
46      private final Class<? extends Component> type;
47      private final List<ReflectionPropertyDeclaration> properties;
48  
49      public ReflectionComponentSpecification(Class<? extends Component> type) {
50          if (!Component.class.isAssignableFrom(type)) {
51              throw fail(type, "Class must extend Component");
52          }
53          if (!type.isInterface()) {
54              throw fail(type, "Component definition must be an interface");
55          }
56  
57          List<ReflectionPropertyDeclaration> properties = new ArrayList<>();
58  
59          // since this is an interface, we're only dealing with public methods
60          // so getMethods() returns everything we're interested in plus the methods
61          // declared in Component, which we'll have to exclude
62          Map<String, Method> getters = new HashMap<>();
63          Map<String, Method> setters = new HashMap<>();
64          Map<String, Integer> setterParameters = new HashMap<>();
65  
66          for (Method method : type.getMethods()) {
67              // exclude methods defined in Component, Owner, Ownable, and Object
68              Class<?> md = method.getDeclaringClass();
69              if (md.equals(Component.class) || md.equals(Owner.class) ||
70                  md.equals(Ownable.class) || md.equals(Object.class)) {
71                  continue;
72              }
73  
74              if (!Component.class.isAssignableFrom(method.getDeclaringClass())) {
75                  throw fail(md, method + ", method is not declared by a component");
76              }
77  
78              if (method.getName().startsWith("is")) {
79                  processGetter(method, "is", getters);
80              } else if (method.getName().startsWith("has")) {
81                  processGetter(method, "has", getters);
82              } else if (method.getName().startsWith("get")) {
83                  processGetter(method, "get", getters);
84              } else if (method.getName().startsWith("set")) {
85                  processSetter(method, setters, setterParameters);
86              } else {
87                  throw fail(md, method + " is an illegal property method");
88              }
89          }
90  
91          for (String property : getters.keySet()) {
92              Method getter = getters.get(property);
93              Method setter = setters.remove(property);
94              Integer param = setterParameters.remove(property);
95  
96              if (setter == null) {
97                  throw fail(type, property + " has no matching setter");
98              } else if (!setter.getParameterTypes()[param]
99                      .equals(getter.getReturnType())) {
100                 throw fail(type, property + " has inconsistent type");
101             }
102 
103             properties.add(new ReflectionPropertyDeclaration(property,
104                                                              createFactory(getter),
105                                                              getter, setter, param));
106         }
107 
108         if (!setters.isEmpty()) {
109             throw fail(type, setters.keySet() + " have no matching getters");
110         }
111 
112         // order the list of properties by their natural ordering
113         Collections.sort(properties);
114         this.type = type;
115         this.properties = Collections.unmodifiableList(properties);
116     }
117 
118     @Override
119     public String getType() {
120         String canonicalName = type.getCanonicalName();
121         String packageName = type.getPackage().getName();
122         if (packageName.isEmpty()) {
123             return canonicalName;
124         } else {
125             // strip off package
126             return canonicalName.substring(getPackage().length() + 1);
127         }
128     }
129 
130     @Override
131     public String getPackage() {
132         return type.getPackage().getName();
133     }
134 
135     @Override
136     public List<? extends PropertyDeclaration> getProperties() {
137         return properties;
138     }
139 
140     private static IllegalComponentDefinitionException fail(Class<?> cls, String msg) {
141         return new IllegalComponentDefinitionException(cls.getCanonicalName(), msg);
142     }
143 
144     /**
145      * Implementation of PropertyDeclaration using the setter and getter methods available
146      * from reflection.
147      */
148     private static class ReflectionPropertyDeclaration implements PropertyDeclaration {
149         private final String name;
150         private final PropertyFactory<?> factory;
151 
152         private final Method setter;
153         private final int setterParameter;
154 
155         private final Method getter;
156         private final boolean isSharedInstance;
157 
158         private final Class<? extends Property> propertyType;
159 
160         @SuppressWarnings("unchecked")
161         private ReflectionPropertyDeclaration(String name, PropertyFactory<?> factory,
162                                               Method getter, Method setter,
163                                               int setterParameter) {
164             this.name = name;
165             this.factory = factory;
166             this.getter = getter;
167             this.setter = setter;
168             this.setterParameter = setterParameter;
169             isSharedInstance = getter.getAnnotation(SharedInstance.class) != null;
170 
171             propertyType = getCreatedType(
172                     (Class<? extends PropertyFactory<?>>) factory.getClass());
173         }
174 
175         @Override
176         public String getName() {
177             return name;
178         }
179 
180         @Override
181         public String getType() {
182             return getter.getReturnType().getCanonicalName();
183         }
184 
185         @Override
186         public String getPropertyImplementation() {
187             return propertyType.getCanonicalName();
188         }
189 
190         @Override
191         public String getSetterMethod() {
192             return setter.getName();
193         }
194 
195         @Override
196         public String getGetterMethod() {
197             return getter.getName();
198         }
199 
200         @Override
201         public int getSetterParameter() {
202             return setterParameter;
203         }
204 
205         @Override
206         public boolean getSetterReturnsComponent() {
207             return !setter.getReturnType().equals(void.class);
208         }
209 
210         @Override
211         public boolean isShared() {
212             return isSharedInstance;
213         }
214 
215         @Override
216         public PropertyFactory<?> getPropertyFactory() {
217             return factory;
218         }
219 
220         @Override
221         public int compareTo(PropertyDeclaration o) {
222             return name.compareTo(o.getName());
223         }
224     }
225 
226     private static void processSetter(Method m, Map<String, Method> setters,
227                                       Map<String, Integer> parameters) {
228         if (!m.getReturnType().equals(m.getDeclaringClass()) &&
229             !m.getReturnType().equals(void.class)) {
230             throw fail(m.getDeclaringClass(), m + " has invalid return type for setter");
231         }
232         if (m.getParameterTypes().length == 0) {
233             throw fail(m.getDeclaringClass(), m + " must have at least one parameter");
234         }
235 
236         if (m.getParameterTypes().length == 1) {
237             String name = getNameFromParameter(m, 0);
238             if (name != null) {
239                 // verify absence of @Named on actual setter
240                 if (m.getAnnotation(Named.class) != null) {
241                     throw fail(m.getDeclaringClass(),
242                                m + ", @Named cannot be on both parameter and method");
243                 }
244             } else {
245                 name = getName(m, "set");
246             }
247 
248             if (setters.containsKey(name)) {
249                 throw fail(m.getDeclaringClass(), name + " already declared on a setter");
250             }
251             setters.put(name, m);
252             parameters.put(name, 0);
253         } else {
254             // verify absence of @Named on actual setter
255             if (m.getAnnotation(Named.class) != null) {
256                 throw fail(m.getDeclaringClass(), m +
257                                                   ", @Named cannot be applied to setter method with multiple parameters");
258             }
259 
260             int numP = m.getParameterTypes().length;
261             for (int i = 0; i < numP; i++) {
262                 String name = getNameFromParameter(m, i);
263                 if (name == null) {
264                     throw fail(m.getDeclaringClass(), m +
265                                                       ", @Named must be applied to each parameter for multi-parameter setter methods");
266                 }
267 
268                 if (setters.containsKey(name)) {
269                     throw fail(m.getDeclaringClass(),
270                                name + " already declared on a setter");
271                 }
272 
273                 setters.put(name, m);
274                 parameters.put(name, i);
275             }
276         }
277     }
278 
279     private static void processGetter(Method m, String prefix,
280                                       Map<String, Method> getters) {
281         String name = getName(m, prefix);
282         if (getters.containsKey(name)) {
283             throw fail(m.getDeclaringClass(), name + " already declared on a getter");
284         }
285         if (m.getParameterTypes().length != 0) {
286             throw fail(m.getDeclaringClass(), m + ", getter must not take arguments");
287         }
288         if (m.getReturnType().equals(void.class)) {
289             throw fail(m.getDeclaringClass(),
290                        m + ", getter must have non-void return type");
291         }
292 
293         getters.put(name, m);
294     }
295 
296     private static String getNameFromParameter(Method m, int p) {
297         for (Annotation annot : m.getParameterAnnotations()[p]) {
298             if (annot instanceof Named) {
299                 return ((Named) annot).value();
300             }
301         }
302         return null;
303     }
304 
305     private static String getName(Method m, String prefix) {
306         Named n = m.getAnnotation(Named.class);
307         if (n != null) {
308             return n.value();
309         } else {
310             return Character.toLowerCase(m.getName().charAt(prefix.length())) +
311                    m.getName().substring(prefix.length() + 1);
312         }
313     }
314 
315     @SuppressWarnings("unchecked")
316     private static Class<? extends Property> getCreatedType(
317             Class<? extends PropertyFactory<?>> factory) {
318         try {
319             return (Class<? extends Property>) factory.getMethod("create")
320                                                       .getReturnType();
321         } catch (NoSuchMethodException e) {
322             throw new RuntimeException("Cannot inspect property factory " + factory, e);
323         }
324     }
325 
326     private static PropertyFactory<?> createFactory(Method getter) {
327         Class<?> baseType = getter.getReturnType();
328 
329         Class<? extends PropertyFactory<?>> factoryType;
330         if (getter.getAnnotation(com.lhkbob.entreri.property.Factory.class) != null) {
331             // prefer getter specification to allow default overriding
332             factoryType = getter.getAnnotation(com.lhkbob.entreri.property.Factory.class)
333                                 .value();
334             validateFactory(getter, factoryType, null);
335         } else {
336             // try to find a default property type
337             Class<? extends Property> mappedType = TypePropertyMapping
338                     .getPropertyForType(baseType);
339             if (mappedType.getAnnotation(com.lhkbob.entreri.property.Factory.class) ==
340                 null) {
341                 throw fail(getter.getDeclaringClass(),
342                            mappedType + " has no @Factory annotation");
343             } else {
344                 factoryType = mappedType
345                         .getAnnotation(com.lhkbob.entreri.property.Factory.class).value();
346                 validateFactory(getter, factoryType, mappedType);
347             }
348         }
349 
350         PropertyFactory<?> factory = invokeConstructor(factoryType, new Attributes(
351                 getter.getAnnotations()));
352         if (factory == null) {
353             factory = invokeConstructor(factoryType);
354         }
355 
356         if (factory == null) {
357             // unable to create a PropertyFactory
358             throw fail(getter.getDeclaringClass(),
359                        "Cannot create PropertyFactory for " + getter);
360         } else {
361             return factory;
362         }
363     }
364 
365     private static void validateFactory(Method getter,
366                                         Class<? extends PropertyFactory<?>> factory,
367                                         Class<? extends Property> propertyType) {
368         boolean isShared = getter.getAnnotation(SharedInstance.class) != null;
369         Class<?> baseType = getter.getReturnType();
370         Class<? extends Property> createdType = getCreatedType(factory);
371 
372         if (propertyType == null) {
373             // rely on factory to determine property type
374             propertyType = createdType;
375         } else {
376             // make sure factory returns an assignable type
377             if (!propertyType.isAssignableFrom(createdType)) {
378                 throw fail(getter.getDeclaringClass(), "Factory creates " + createdType +
379                                                        ", which is incompatible with expected type " +
380                                                        propertyType);
381             }
382         }
383 
384         // verify contract of property
385         if (propertyType.equals(ObjectProperty.class)) {
386             // special case for ObjectProperty to support more permissive assignments
387             // (which to record requires a similar special case in the code generation)
388             if (isShared) {
389                 throw fail(getter.getDeclaringClass(),
390                            propertyType + " can't be used with @SharedInstance");
391             } else if (baseType.isPrimitive()) {
392                 throw fail(getter.getDeclaringClass(),
393                            "ObjectProperty cannot be used with primitive types");
394             }
395             // else we know ObjectProperty is defined correctly because its part of the core library
396         } else {
397             try {
398                 Method g = propertyType.getMethod("get", int.class);
399                 if (!g.getReturnType().equals(baseType)) {
400                     throw fail(getter.getDeclaringClass(),
401                                propertyType + " does not implement " + baseType +
402                                " get()");
403                 }
404                 Method s = propertyType.getMethod("set", int.class, baseType);
405                 if (!s.getReturnType().equals(void.class)) {
406                     throw fail(getter.getDeclaringClass(),
407                                propertyType + " does not implement void set(int, " +
408                                baseType + ")");
409                 }
410             } catch (NoSuchMethodException e) {
411                 throw fail(getter.getDeclaringClass(),
412                            propertyType + " does not implement " + baseType +
413                            " get() or void set(" + baseType + ", int)");
414             }
415 
416             if (isShared) {
417                 if (!ShareableProperty.class.isAssignableFrom(propertyType)) {
418                     throw fail(getter.getDeclaringClass(),
419                                propertyType + " can't be used with @SharedInstance");
420                 }
421 
422                 // verify additional shareable property contract
423                 try {
424                     Method sg = propertyType.getMethod("get", int.class, baseType);
425                     if (!sg.getReturnType().equals(void.class)) {
426                         throw fail(getter.getDeclaringClass(),
427                                    propertyType + " does not implement void get(int, " +
428                                    baseType + ")");
429                     }
430                     Method creator = propertyType.getMethod("createShareableInstance");
431                     if (!creator.getReturnType().equals(baseType)) {
432                         throw fail(getter.getDeclaringClass(),
433                                    propertyType + " does not implement " + baseType +
434                                    " createShareableInstance()");
435                     }
436                 } catch (NoSuchMethodException e) {
437                     throw fail(getter.getDeclaringClass(),
438                                propertyType + " does not implement void get(int, " +
439                                baseType + ") or " + baseType +
440                                " createShareableInstance()");
441                 }
442             }
443         }
444     }
445 
446     private static PropertyFactory<?> invokeConstructor(
447             Class<? extends PropertyFactory<?>> type, Object... args) {
448         Class<?>[] paramTypes = new Class<?>[args.length];
449         for (int i = 0; i < args.length; i++) {
450             paramTypes[i] = args[i].getClass();
451         }
452 
453         try {
454             // must use getDeclaredConstructor in case the class type is private
455             // or the constructor is not public
456             Constructor<?> ctor = type.getDeclaredConstructor(paramTypes);
457             ctor.setAccessible(true);
458             return (PropertyFactory<?>) ctor.newInstance(args);
459         } catch (SecurityException e) {
460             throw new RuntimeException("Unable to inspect factory's constructor", e);
461         } catch (NoSuchMethodException e) {
462             // ignore, fall back to default constructor
463             return null;
464         } catch (Exception e) {
465             // other exceptions should not occur
466             throw new RuntimeException("Unexpected exception during factory creation", e);
467         }
468     }
469 }