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 javax.annotation.processing.Filer;
33  import javax.lang.model.element.*;
34  import javax.lang.model.type.MirroredTypeException;
35  import javax.lang.model.type.TypeKind;
36  import javax.lang.model.type.TypeMirror;
37  import javax.lang.model.util.ElementFilter;
38  import javax.lang.model.util.Elements;
39  import javax.lang.model.util.Types;
40  import javax.tools.FileObject;
41  import javax.tools.StandardLocation;
42  import java.io.IOException;
43  import java.util.*;
44  
45  /**
46   * MirrorComponentSpecification is an implementation that extracts a component
47   * specification from the mirror API defined in javax.lang.model.  This should only be
48   * used in the context of an annotation processor with a valid processing environment.
49   *
50   * @author Michael Ludwig
51   */
52  class MirrorComponentSpecification implements ComponentSpecification {
53      private final String typeName;
54      private final String packageName;
55      private final List<MirrorPropertyDeclaration> properties;
56  
57      public MirrorComponentSpecification(TypeElement type, Types tu, Elements eu,
58                                          Filer io) {
59          TypeMirror baseComponentType = eu
60                  .getTypeElement(Component.class.getCanonicalName()).asType();
61          TypeMirror ownerType = eu.getTypeElement(Owner.class.getCanonicalName()).asType();
62          TypeMirror ownableType = eu.getTypeElement(Ownable.class.getCanonicalName())
63                                     .asType();
64          TypeMirror objectType = eu.getTypeElement(Object.class.getCanonicalName())
65                                    .asType();
66  
67          if (!tu.isAssignable(type.asType(), baseComponentType)) {
68              throw fail(type.asType(), "Class must extend Component");
69          }
70          if (!type.getKind().equals(ElementKind.INTERFACE)) {
71              throw fail(type.asType(), "Component definition must be an interface");
72          }
73  
74          List<MirrorPropertyDeclaration> properties = new ArrayList<>();
75  
76          // since this is an interface, we're only dealing with public methods
77          // so getMethods() returns everything we're interested in plus the methods
78          // declared in Component, which we'll have to exclude
79          List<? extends ExecutableElement> methods = ElementFilter
80                  .methodsIn(eu.getAllMembers(type));
81          Map<String, ExecutableElement> getters = new HashMap<>();
82          Map<String, ExecutableElement> setters = new HashMap<>();
83          Map<String, Integer> setterParameters = new HashMap<>();
84  
85          for (ExecutableElement m : methods) {
86              // exclude methods defined in Component, Owner, and Ownable
87              String name = m.getSimpleName().toString();
88              TypeMirror declare = m.getEnclosingElement().asType();
89  
90              if (tu.isSameType(declare, baseComponentType) ||
91                  tu.isSameType(declare, ownableType) ||
92                  tu.isSameType(declare, ownerType) ||
93                  tu.isSameType(declare, objectType)) {
94                  continue;
95              }
96  
97              if (!tu.isAssignable(declare, baseComponentType)) {
98                  throw fail(declare, name + ", method is not declared in Component");
99              }
100 
101             if (name.startsWith("is")) {
102                 processGetter(m, "is", getters);
103             } else if (name.startsWith("has")) {
104                 processGetter(m, "has", getters);
105             } else if (name.startsWith("get")) {
106                 processGetter(m, "get", getters);
107             } else if (name.startsWith("set")) {
108                 processSetter(m, setters, setterParameters, tu);
109             } else {
110                 throw fail(declare, name + " is an illegal property method");
111             }
112         }
113 
114         for (String property : getters.keySet()) {
115             ExecutableElement getter = getters.get(property);
116             ExecutableElement setter = setters.remove(property);
117             Integer param = setterParameters.remove(property);
118 
119             if (setter == null) {
120                 throw fail(type.asType(), property + " has no matching setter");
121             } else if (!tu.isSameType(getter.getReturnType(),
122                                       setter.getParameters().get(param).asType())) {
123                 throw fail(type.asType(), property + " has inconsistent type");
124             }
125 
126             TypeMirror propertyType = getPropertyType(getter, tu, eu, io);
127             properties.add(new MirrorPropertyDeclaration(property, getter, setter, param,
128                                                          propertyType));
129         }
130 
131         if (!setters.isEmpty()) {
132             throw fail(type.asType(), setters.keySet() + " have no matching getters");
133         }
134 
135         // order the list of properties by their natural ordering
136         Collections.sort(properties);
137 
138         String qualifiedName = type.getQualifiedName().toString();
139         String packageName = eu.getPackageOf(type).getQualifiedName().toString();
140         if (packageName.isEmpty()) {
141             typeName = qualifiedName;
142             this.packageName = "";
143         } else {
144             typeName = qualifiedName.substring(packageName.length() + 1);
145             this.packageName = packageName;
146         }
147 
148         this.properties = Collections.unmodifiableList(properties);
149     }
150 
151     @Override
152     public String getType() {
153         return typeName;
154     }
155 
156     @Override
157     public String getPackage() {
158         return packageName;
159     }
160 
161     @Override
162     public List<? extends PropertyDeclaration> getProperties() {
163         return properties;
164     }
165 
166     private static IllegalComponentDefinitionException fail(TypeMirror type, String msg) {
167         return new IllegalComponentDefinitionException(type.toString(), msg);
168     }
169 
170     private static class MirrorPropertyDeclaration implements PropertyDeclaration {
171         private final String name;
172 
173         private final String setter;
174         private final int setterParameter;
175         private final boolean setterReturnsComponent;
176 
177         private final String getter;
178         private final boolean isSharedInstance;
179 
180         private final String type;
181         private final String propertyType;
182 
183         public MirrorPropertyDeclaration(String name, ExecutableElement getter,
184                                          ExecutableElement setter, int parameter,
185                                          TypeMirror propertyType) {
186             this.name = name;
187             this.getter = getter.getSimpleName().toString();
188             this.setter = setter.getSimpleName().toString();
189             this.propertyType = propertyType.toString();
190             setterParameter = parameter;
191 
192             type = getter.getReturnType().toString();
193             setterReturnsComponent = !setter.getReturnType().getKind()
194                                             .equals(TypeKind.VOID);
195 
196             isSharedInstance = getter.getAnnotation(SharedInstance.class) != null;
197         }
198 
199         @Override
200         public String getName() {
201             return name;
202         }
203 
204         @Override
205         public String getType() {
206             return type;
207         }
208 
209         @Override
210         public String getPropertyImplementation() {
211             return propertyType;
212         }
213 
214         @Override
215         public String getSetterMethod() {
216             return setter;
217         }
218 
219         @Override
220         public String getGetterMethod() {
221             return getter;
222         }
223 
224         @Override
225         public int getSetterParameter() {
226             return setterParameter;
227         }
228 
229         @Override
230         public boolean getSetterReturnsComponent() {
231             return setterReturnsComponent;
232         }
233 
234         @Override
235         public boolean isShared() {
236             return isSharedInstance;
237         }
238 
239         @Override
240         public PropertyFactory<?> getPropertyFactory() {
241             throw new UnsupportedOperationException(
242                     "Cannot create PropertyFactory with mirror API");
243         }
244 
245         @Override
246         public int compareTo(PropertyDeclaration o) {
247             return name.compareTo(o.getName());
248         }
249     }
250 
251     private static void processSetter(ExecutableElement m,
252                                       Map<String, ExecutableElement> setters,
253                                       Map<String, Integer> parameters, Types tu) {
254         TypeMirror declaringClass = m.getEnclosingElement().asType();
255         if (!tu.isSameType(m.getReturnType(), m.getEnclosingElement().asType()) &&
256             !m.getReturnType().getKind().equals(TypeKind.VOID)) {
257             throw fail(declaringClass, m + " has invalid return type for setter");
258         }
259 
260         List<? extends VariableElement> params = m.getParameters();
261         if (params.isEmpty()) {
262             throw fail(declaringClass, m + " must have at least one parameter");
263         }
264 
265         if (params.size() == 1) {
266             String name = getNameFromParameter(params.get(0));
267             if (name != null) {
268                 // verify absence of @Named on actual setter
269                 if (m.getAnnotation(Named.class) != null) {
270                     throw fail(declaringClass,
271                                m + ", @Named cannot be on both parameter and method");
272                 }
273             } else {
274                 name = getName(m, "set");
275             }
276 
277             if (setters.containsKey(name)) {
278                 throw fail(declaringClass, name + " already declared on a setter");
279             }
280             setters.put(name, m);
281             parameters.put(name, 0);
282         } else {
283             // verify absence of @Named on actual setter
284             if (m.getAnnotation(Named.class) != null) {
285                 throw fail(declaringClass, m +
286                                            ", @Named cannot be applied to setter method with multiple parameters");
287             }
288 
289             int i = 0;
290             for (VariableElement p : params) {
291                 String name = getNameFromParameter(p);
292                 if (name == null) {
293                     throw fail(declaringClass, m +
294                                                ", @Named must be applied to each parameter for multi-parameter setter methods");
295                 }
296 
297                 if (setters.containsKey(name)) {
298                     throw fail(declaringClass, name + " already declared on a setter");
299                 }
300 
301                 setters.put(name, m);
302                 parameters.put(name, i++);
303             }
304         }
305     }
306 
307     private static void processGetter(ExecutableElement m, String prefix,
308                                       Map<String, ExecutableElement> getters) {
309         TypeMirror declaringClass = m.getEnclosingElement().asType();
310 
311         String name = getName(m, prefix);
312         if (getters.containsKey(name)) {
313             throw fail(declaringClass, name + " already declared on a getter");
314         }
315         if (!m.getParameters().isEmpty()) {
316             throw fail(declaringClass, m + ", getter must not take arguments");
317         }
318         if (m.getReturnType().getKind().equals(TypeKind.VOID)) {
319             throw fail(declaringClass, m + ", getter must have non-void return type");
320         }
321 
322         getters.put(name, m);
323     }
324 
325     private static String getNameFromParameter(VariableElement parameter) {
326         Named n = parameter.getAnnotation(Named.class);
327         if (n != null) {
328             return n.value();
329         } else {
330             return null;
331         }
332     }
333 
334     private static String getName(ExecutableElement m, String prefix) {
335         Named n = m.getAnnotation(Named.class);
336         if (n != null) {
337             return n.value();
338         } else {
339             String name = m.getSimpleName().toString();
340             return Character.toLowerCase(name.charAt(prefix.length())) +
341                    name.substring(prefix.length() + 1);
342         }
343     }
344 
345     private static TypeMirror getPropertyType(ExecutableElement getter, Types tu,
346                                               Elements eu, Filer io) {
347         TypeMirror baseType = getter.getReturnType();
348 
349         TypeMirror factory = getFactory(getter);
350         if (factory != null) {
351             // prefer getter specification to allow default overriding
352             return validateFactory(getter, factory, null, tu, eu);
353         } else {
354             // try to find a default property type
355             TypeElement mappedType;
356             switch (baseType.getKind()) {
357             case BOOLEAN:
358                 mappedType = eu.getTypeElement(BooleanProperty.class.getCanonicalName());
359                 break;
360             case BYTE:
361                 mappedType = eu.getTypeElement(ByteProperty.class.getCanonicalName());
362                 break;
363             case CHAR:
364                 mappedType = eu.getTypeElement(CharProperty.class.getCanonicalName());
365                 break;
366             case DOUBLE:
367                 mappedType = eu.getTypeElement(DoubleProperty.class.getCanonicalName());
368                 break;
369             case FLOAT:
370                 mappedType = eu.getTypeElement(FloatProperty.class.getCanonicalName());
371                 break;
372             case INT:
373                 mappedType = eu.getTypeElement(IntProperty.class.getCanonicalName());
374                 break;
375             case LONG:
376                 mappedType = eu.getTypeElement(LongProperty.class.getCanonicalName());
377                 break;
378             case SHORT:
379                 mappedType = eu.getTypeElement(ShortProperty.class.getCanonicalName());
380                 break;
381             default:
382                 FileObject mapping;
383                 try {
384                     mapping = io.getResource(StandardLocation.CLASS_PATH, "",
385                                              TypePropertyMapping.MAPPING_DIR +
386                                              baseType.toString());
387                 } catch (IOException e) {
388                     // if an IO is thrown here, it means it couldn't find the file
389                     mapping = null;
390                 }
391 
392                 if (mapping != null) {
393                     try {
394                         String content = mapping.getCharContent(true).toString().trim();
395                         mappedType = eu.getTypeElement(content);
396                     } catch (IOException e) {
397                         // if an IO is thrown here, however, it means errors accessing
398                         // the file, which we can't recover from
399                         throw new RuntimeException(e);
400                     }
401                 } else {
402                     mappedType = eu
403                             .getTypeElement(ObjectProperty.class.getCanonicalName());
404                 }
405             }
406 
407             factory = getFactory(mappedType);
408             if (factory == null) {
409                 throw fail(getter.getEnclosingElement().asType(),
410                            mappedType + " has no @Factory annotation");
411             } else {
412                 return validateFactory(getter, factory, mappedType, tu, eu);
413             }
414         }
415     }
416 
417     private static TypeMirror getFactory(Element e) {
418         try {
419             com.lhkbob.entreri.property.Factory factory = e
420                     .getAnnotation(com.lhkbob.entreri.property.Factory.class);
421             if (factory != null) {
422                 factory.value(); // will throw an exception
423             }
424             return null;
425         } catch (MirroredTypeException te) {
426             return te.getTypeMirror();
427         }
428     }
429 
430     private static final EnumSet<TypeKind> PRIMITIVES = EnumSet
431             .of(TypeKind.BOOLEAN, TypeKind.BYTE, TypeKind.CHAR, TypeKind.DOUBLE,
432                 TypeKind.FLOAT, TypeKind.INT, TypeKind.LONG, TypeKind.SHORT);
433 
434     private static TypeMirror validateFactory(ExecutableElement getter,
435                                               TypeMirror factory,
436                                               TypeElement propertyType, Types tu,
437                                               Elements eu) {
438         TypeMirror declaringClass = getter.getEnclosingElement().asType();
439         boolean isShared = getter.getAnnotation(SharedInstance.class) != null;
440         TypeMirror baseType = getter.getReturnType();
441 
442         TypeMirror createdType = null;
443         List<? extends ExecutableElement> factoryMethods = ElementFilter
444                 .methodsIn(eu.getAllMembers((TypeElement) tu.asElement(factory)));
445         for (ExecutableElement m : factoryMethods) {
446             if (m.getSimpleName().contentEquals("create")) {
447                 createdType = m.getReturnType();
448                 break;
449             }
450         }
451         if (createdType == null) {
452             throw fail(declaringClass, factory + " is missing create() method");
453         }
454 
455         if (propertyType == null) {
456             // rely on factory to determine property type
457             propertyType = (TypeElement) tu.asElement(createdType);
458         } else {
459             // make sure factory returns an assignable type
460             if (!tu.isAssignable(createdType, propertyType.asType())) {
461                 throw fail(declaringClass, "Factory creates " + createdType +
462                                            ", which is incompatible with expected type " +
463                                            propertyType);
464             }
465         }
466 
467         // verify contract of property
468         TypeMirror asType = propertyType.asType();
469         if (tu.isSameType(asType,
470                           eu.getTypeElement(ObjectProperty.class.getCanonicalName())
471                             .asType())) {
472             // special case for ObjectProperty to support more permissive assignments
473             // (which to record requires a similar special case in the code generation)
474             if (isShared) {
475                 throw fail(declaringClass,
476                            propertyType + " can't be used with @SharedInstance");
477             } else if (PRIMITIVES.contains(asType.getKind())) {
478                 throw fail(declaringClass,
479                            "ObjectProperty cannot be used with primitive types");
480             }
481             // else we know ObjectProperty is defined correctly because its part of the core library
482         } else {
483             TypeMirror intType = tu.getPrimitiveType(TypeKind.INT);
484             TypeMirror voidType = tu.getNoType(TypeKind.VOID);
485             List<? extends ExecutableElement> methods = ElementFilter
486                     .methodsIn(eu.getAllMembers(propertyType));
487 
488             if (!findMethod(methods, tu, "get", baseType, intType)) {
489                 throw fail(declaringClass,
490                            propertyType + " does not implement " + baseType + " get()");
491             }
492             if (!findMethod(methods, tu, "set", voidType, intType, baseType)) {
493                 throw fail(declaringClass,
494                            propertyType + " does not implement void set(int, " +
495                            baseType + ")");
496             }
497 
498             if (isShared) {
499                 // we could instantiate the declared type, but that crashes if the parameter
500                 // type must be a primitive, so the erased type gives us a good enough check
501                 TypeMirror share = tu.erasure(
502                         eu.getTypeElement(ShareableProperty.class.getCanonicalName())
503                           .asType());
504                 if (!tu.isAssignable(asType, share)) {
505                     throw fail(declaringClass,
506                                propertyType + " can't be used with @SharedInstance");
507                 }
508 
509                 // verify additional shareable property contract
510                 if (!findMethod(methods, tu, "get", voidType, intType, baseType)) {
511                     throw fail(declaringClass,
512                                propertyType + " does not implement void get(int, " +
513                                baseType + ")");
514                 }
515                 if (!findMethod(methods, tu, "createShareableInstance", baseType)) {
516                     throw fail(declaringClass,
517                                propertyType + " does not implement " + baseType +
518                                " createShareableInstance()");
519                 }
520             }
521         }
522 
523         return tu.erasure(asType);
524     }
525 
526     private static boolean findMethod(List<? extends ExecutableElement> methods, Types tu,
527                                       String name, TypeMirror returnType,
528                                       TypeMirror... params) {
529         for (ExecutableElement m : methods) {
530             if (m.getSimpleName().contentEquals(name) &&
531                 tu.isSameType(returnType, m.getReturnType())) {
532                 // now check parameters
533                 List<? extends VariableElement> realParams = m.getParameters();
534                 if (params.length == realParams.size()) {
535                     boolean found = true;
536                     for (int i = 0; i < params.length; i++) {
537                         if (!tu.isSameType(params[i], realParams.get(i).asType())) {
538                             found = false;
539                             break;
540                         }
541                     }
542 
543                     if (found) {
544                         return true;
545                     }
546                 }
547             }
548         }
549 
550         return false;
551     }
552 }