View Javadoc
1   /*
2    * Copyright (C) 2016 uwe
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU General Public License
15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16   */
17  package org.sw4j.tool.annotation.jpa.processor;
18  
19  import java.beans.Introspector;
20  import java.util.LinkedHashMap;
21  import java.util.List;
22  import java.util.Map;
23  import javax.annotation.Nonnull;
24  import javax.annotation.processing.ProcessingEnvironment;
25  import javax.lang.model.element.Element;
26  import javax.lang.model.element.ElementKind;
27  import javax.lang.model.element.ExecutableElement;
28  import javax.lang.model.element.TypeElement;
29  import javax.lang.model.type.TypeKind;
30  import javax.lang.model.type.TypeMirror;
31  import javax.persistence.AccessType;
32  import javax.persistence.Id;
33  import javax.tools.Diagnostic;
34  import org.sw4j.tool.annotation.jpa.generator.model.Attribute;
35  import org.sw4j.tool.annotation.jpa.generator.model.Entity;
36  
37  /**
38   * This is a processor to handle attributes of classes with an @Entity annotation.
39   *
40   * @author Uwe Plonus
41   */
42  public class AttributeProcessor {
43  
44      /** The prefix of a generic property. */
45      private static final String PROPERTY_PREFIX = "get";
46  
47      /** The length of the prefix of a generic property. */
48      private static final int PROPERTY_PREFIX_LENGTH = "get".length();
49  
50      /** The prefix of a boolean property. */
51      private static final String BOOLEAN_PROPERTY_PREFIX = "is";
52  
53      /** The length of the prefix of a boolean property. */
54      private static final int BOOLEAN_PROPERTY_PREFIX_LENGTH = "is".length();
55  
56      /** The processing environment used to access the tool facilities. */
57      private ProcessingEnvironment processingEnv;
58  
59      /**
60       * Default constructor for the attribute processor.
61       *
62       */
63      public AttributeProcessor() {
64      }
65  
66      /**
67       * Initializes the processor with the processing environment.
68       *
69       * @param processingEnv environment to access facilities the tool framework provides to the processor.
70       */
71      @SuppressWarnings("checkstyle:HiddenField")
72      public void init(@Nonnull final ProcessingEnvironment processingEnv) {
73          this.processingEnv = processingEnv;
74      }
75  
76      /**
77       * Process all possible attributes of an {@code Entity} class.
78       *
79       * @param entity the entity this attributes belongs to.
80       * @param possibleAttributes all enclosed elements of the element that denotes the {@code Entity}.
81       */
82      public void process(@Nonnull final Entity entity, @Nonnull final List<? extends Element> possibleAttributes) {
83          AccessType accessType = null;
84          Map<String, Element> possibleFields = new LinkedHashMap<>();
85          Map<String, Element> possibleProperties = new LinkedHashMap<>();
86          Map<String, Element> possibleIds = new LinkedHashMap<>();
87          for (Element possibleAttribute: possibleAttributes) {
88              String attributeName = null;
89              if (isField(possibleAttribute)) {
90                  attributeName = possibleAttribute.getSimpleName().toString();
91                  possibleFields.put(attributeName, possibleAttribute);
92              } else if (isProperty(possibleAttribute)) {
93                  attributeName = getAttributeNameFromProperty(possibleAttribute);
94                  possibleProperties.put(attributeName, possibleAttribute);
95              }
96              if (attributeName != null && isPossibleIdAttribute(possibleAttribute)) {
97                  if (possibleIds.put(attributeName, possibleAttribute) != null) {
98                      this.processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
99                              new StringBuilder("The entity \"")
100                                     .append(entity.getName())
101                                     .append("\" (with class name \"")
102                                     .append(entity.getClassName())
103                                     .append("\") has the same attribute annotated with @Id twice."));
104                 }
105             }
106         }
107 
108         if (possibleIds.size() == 1) {
109             Map.Entry<String, Element> id = possibleIds.entrySet().iterator().next();
110             if (isField(id.getValue())) {
111                 accessType = AccessType.FIELD;
112             } else {
113                 accessType = AccessType.PROPERTY;
114             }
115             if (accessType == AccessType.FIELD) {
116                 for (Map.Entry<String, Element> possibleField: possibleFields.entrySet()) {
117                     processField(entity, possibleField.getValue());
118                 }
119             } else {
120                 for (Map.Entry<String, Element> possibleProperty: possibleProperties.entrySet()) {
121                     processProperty(entity, possibleProperty.getValue());
122                 }
123             }
124         } else if (possibleIds.isEmpty()) {
125             this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, new StringBuilder(
126                     "This annotation processor does not support entities without @Id annotations. The entity \"")
127                     .append(entity.getName())
128                     .append("\" (with class name \"")
129                     .append(entity.getClassName())
130                     .append("\") has no @Id attributes."));
131         } else {
132             this.processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, new StringBuilder(
133                     "This annotation processor does not support entities with multiple @Id annotations. The entity \"")
134                     .append(entity.getName())
135                     .append("\" (with class name \"")
136                     .append(entity.getClassName())
137                     .append("\") has more than 1 @Id attribute."));
138         }
139     }
140 
141     /**
142      * Process a single field attribute.
143      *
144      * @param entity the entity this attribute belongs to.
145      * @param fieldElement the field of the attribute.
146      */
147     private void processField(@Nonnull final Entity entity, @Nonnull final Element fieldElement) {
148         Attribute attribute = new Attribute(fieldElement.getSimpleName().toString(),
149                 isPossibleIdAttribute(fieldElement), getDataTypeFromType(fieldElement.asType()));
150         entity.addAttribute(attribute);
151     }
152 
153     /**
154      * Process a single property attribute.
155      *
156      * @param entity the entity this attribute belongs to.
157      * @param propertyElement the property of the attribute.
158      */
159     private void processProperty(@Nonnull final Entity entity, @Nonnull final Element propertyElement) {
160         TypeMirror returnType = ((ExecutableElement)propertyElement).getReturnType();
161         Attribute attribute = new Attribute(getAttributeNameFromProperty(propertyElement),
162                 isPossibleIdAttribute(propertyElement), getDataTypeFromType(returnType));
163         entity.addAttribute(attribute);
164     }
165 
166     /**
167      * Return the datatype as String from the given TypeMirror.
168      *
169      * @param type the type to convert.
170      * @return the datatype of the type.
171      */
172     private String getDataTypeFromType(TypeMirror type) {
173         String dataType = "";
174         if (type.getKind().isPrimitive()) {
175             // This is a primitive type (e.g. int or float)
176             dataType = type.toString();
177         } else {
178             // The type is either a class or an interface
179             Element dataTypeElement = this.processingEnv.getTypeUtils().asElement(type);
180             if (dataTypeElement != null) {
181                 ElementKind dataTypeKind = dataTypeElement.getKind();
182                 if (dataTypeKind.isClass() || dataTypeKind.isInterface()) {
183                     dataType = ((TypeElement)dataTypeElement).getQualifiedName().toString();
184                 }
185             }
186         }
187         return dataType;
188     }
189 
190     /**
191      * Test if the given field or property is a possible {@code @Id}.
192      *
193      * @param element the element to check.
194      * @return {@code true} if either the fieldElement or the propertyElement denote an {@code @Id}.
195      */
196     private boolean isPossibleIdAttribute(@Nonnull final Element element) {
197         Id idAnnotation = element.getAnnotation(Id.class);
198         return idAnnotation != null;
199     }
200 
201     /**
202      * Checks if the given element is a field.
203      *
204      * @param element the element to check.
205      * @return {@code true} if the element is a field.
206      */
207     private boolean isField(@Nonnull final Element element) {
208         return ElementKind.FIELD.equals(element.getKind());
209     }
210 
211     /**
212      * Checks if the given element is a property. This method only checks for the getter methods.
213      *
214      * @param element the element to check.
215      * @return {@code true} if the element is the getter of a property.
216      */
217     private boolean isProperty(@Nonnull final Element element) {
218         boolean isProperty = false;
219         if (ElementKind.METHOD.equals(element.getKind())) {
220             isProperty = !"".equals(getAttributeNameFromProperty(element));
221         }
222         return isProperty;
223     }
224 
225     /**
226      * Returns the attribute name from the method name of the element. This method assumes that the given element is a
227      * method. If the method is not a valid property (by either starting with "get" or by starting with "is" and being
228      * a boolean property) then an empty string is returned.
229      *
230      * @param element the element to check.
231      * @return either the property name or an empty string.
232      */
233     private String getAttributeNameFromProperty(@Nonnull final Element element) {
234         StringBuilder result = new StringBuilder();
235         String elementName = element.getSimpleName().toString();
236         if (elementName.startsWith(PROPERTY_PREFIX)) {
237             result.append(Introspector.decapitalize(elementName.substring(PROPERTY_PREFIX_LENGTH)));
238         } else if (elementName.startsWith(BOOLEAN_PROPERTY_PREFIX)) {
239             TypeMirror returnType = ((ExecutableElement)element).getReturnType();
240             if (TypeKind.BOOLEAN.equals(returnType.getKind())) {
241                 result.append(Introspector.decapitalize(elementName.substring(BOOLEAN_PROPERTY_PREFIX_LENGTH)));
242             } else {
243                 Element returnElement = this.processingEnv.getTypeUtils().asElement(returnType);
244                 if (returnElement != null && ElementKind.CLASS.equals(returnElement.getKind())) {
245                     if (Boolean.class.getName().equals(((TypeElement)returnElement).getQualifiedName().toString())) {
246                         result.append(Introspector.decapitalize(elementName.substring(2)));
247                     }
248                 }
249             }
250         }
251         return result.toString();
252     }
253 
254 }