View Javadoc

1   /*
2    * Copyright 2012-2013 smartics, Kronseder & Reiner GmbH
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package de.smartics.properties.spi.core.metadata;
17  
18  import static de.smartics.util.lang.StaticAnalysis.RAWTYPES;
19  import static de.smartics.util.lang.StaticAnalysis.UNCHECKED;
20  
21  import java.lang.annotation.Annotation;
22  import java.lang.reflect.Method;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.List;
26  
27  import javax.validation.constraints.Size;
28  
29  import org.apache.commons.lang.ArrayUtils;
30  
31  import de.smartics.properties.api.core.annotations.AccessType;
32  import de.smartics.properties.api.core.annotations.PropertyDefinitionTime;
33  import de.smartics.properties.api.core.annotations.PropertyElementType;
34  import de.smartics.properties.api.core.annotations.PropertyListElementConstraints;
35  import de.smartics.properties.api.core.annotations.PropertySet;
36  import de.smartics.properties.api.core.domain.PropertiesContext;
37  import de.smartics.properties.api.core.domain.PropertyConstraint;
38  import de.smartics.properties.api.core.domain.PropertyDescriptor;
39  import de.smartics.properties.api.core.domain.PropertyExpression;
40  import de.smartics.properties.api.core.domain.PropertyKey;
41  import de.smartics.properties.api.core.domain.PropertyType;
42  import de.smartics.properties.api.core.domain.PropertyValueRange;
43  import de.smartics.properties.spi.core.constraint.jsr303.GenericPropertyConstraint;
44  import de.smartics.properties.spi.core.constraints.ListValueConstraint;
45  import de.smartics.properties.spi.core.constraints.PropertyRangeConstraint;
46  import de.smartics.properties.spi.core.context.PropertyContextProxy;
47  import de.smartics.properties.spi.core.metadata.PropertyMetaData.Builder;
48  import de.smartics.properties.spi.core.util.PropertyUtils;
49  import de.smartics.properties.spi.core.value.CollectionPropertyValueRange;
50  import de.smartics.properties.spi.core.value.EnumeratedPropertyValueRange;
51  import de.smartics.util.lang.Arguments;
52  
53  /**
54   * Parses runtime annotations to create property metadata.
55   */
56  public final class PropertyMetaDataParser
57  { // NOPMD
58    // ********************************* Fields *********************************
59  
60    // --- constants ------------------------------------------------------------
61  
62    // --- members --------------------------------------------------------------
63  
64    /**
65     * The context to access property metadata.
66     */
67    private final PropertiesContext context;
68  
69    // ****************************** Initializer *******************************
70  
71    // ****************************** Constructors ******************************
72  
73    /**
74     * Default constructor to generate property descriptors with comments.
75     *
76     * @param context the context to access property metadata. May be
77     *          <code>null</code> in which case no comments are generated.
78     */
79    private PropertyMetaDataParser(final PropertiesContext context)
80    {
81      this.context = context;
82    }
83  
84    // ****************************** Inner Classes *****************************
85  
86    // ********************************* Methods ********************************
87  
88    // --- init -----------------------------------------------------------------
89  
90    // --- factory --------------------------------------------------------------
91  
92    /**
93     * Convenience constructor to generate property descriptors without comments.
94     *
95     * @return the created parser instance.
96     */
97    public static PropertyMetaDataParser createWithoutContextAccess()
98    {
99      return new PropertyMetaDataParser(null);
100   }
101 
102   /**
103    * Convenience constructor to generate property descriptors with access to
104    * property comments.
105    *
106    * @param context the context to access comment information for properties.
107    * @return the created parser instance.
108    * @throws NullPointerException if {@code context} is <code>null</code>.
109    */
110   public static PropertyMetaDataParser create(final PropertiesContext context)
111     throws NullPointerException
112   {
113     Arguments.checkNotNull("context", context);
114     return new PropertyMetaDataParser(context);
115   }
116 
117   // --- get&set --------------------------------------------------------------
118 
119   // --- business -------------------------------------------------------------
120 
121   /**
122    * Reads the property descriptor information from the type.
123    *
124    * @param propertySetType the type to read the property descriptor.
125    * @return the property descriptor provided by the methods of the type.
126    * @throws NullPointerException if {@code propertySetType} is
127    *           <code>null</code>.
128    * @throws IllegalArgumentException if {@code propertySetType} is not
129    *           annotated with {@link PropertySet}.
130    */
131   public List<PropertyDescriptor> readDescriptors(final Class<?> propertySetType)
132     throws NullPointerException, IllegalArgumentException
133   {
134     Arguments.checkNotNull("propertySetType", propertySetType);
135     PropertyUtils.checkPropertySetType(propertySetType);
136 
137     final Method[] methods = propertySetType.getMethods();
138     final List<PropertyDescriptor> descriptors =
139         new ArrayList<PropertyDescriptor>(methods.length);
140     for (final Method method : methods)
141     {
142       if (PropertyUtils.isPropertyMethod(method))
143       {
144         final PropertyDescriptor descriptor = readDescriptor(method);
145         descriptors.add(descriptor);
146       }
147     }
148 
149     return descriptors;
150   }
151 
152   /**
153    * Reads the property descriptor information from the method.
154    *
155    * @param method the method to read the property descriptor.
156    * @return the property descriptor provided by this method.
157    * @throws NullPointerException if {@code method} is <code>null</code>.
158    */
159   public PropertyDescriptor readDescriptor(final Method method)
160     throws NullPointerException
161   {
162     Arguments.checkNotNull("method", method);
163 
164     final PropertyMetaData.Builder builder = new PropertyMetaData.Builder();
165 
166     final Class<?> declaringType = method.getDeclaringClass();
167     final PropertyKey key = readKey(method);
168     final PropertyType type = readType(method);
169     builder.with(new PropertyContextProxy(context))
170         .withDeclaringType(declaringType).with(key).with(type);
171 
172     addValueRange(builder, method);
173     addExpression(builder, method);
174     addLifecycle(builder, method);
175     addConstraints(builder, method);
176     addDocumentProxy(builder, method);
177     addComments(builder);
178 
179     return builder.build();
180   }
181 
182   /**
183    * Reads the property key information from the method.
184    *
185    * @param method the method to read the property key.
186    * @return the property key provided by this method.
187    */
188   public PropertyKey readKey(final Method method)
189   {
190     final String set = readPropertySetName(method);
191     final String name = readPropertyKeyName(method);
192     return PropertyKey.create(set, name);
193   }
194 
195   /**
196    * Reads the property set name of the type.
197    *
198    * @param type the type to read the property set name.
199    * @return the property name as specified of defaulted.
200    */
201   public String readPropertySetName(final Class<?> type)
202   {
203     String propertySetName = readPropertySetNameInternal(type);
204 
205     if (" ".equals(propertySetName))
206     {
207       propertySetName = type.getName();
208     }
209     else if ("".equals(propertySetName))
210     {
211       propertySetName = null;
212     }
213 
214     return propertySetName;
215   }
216 
217   private static String readPropertyKeyName(final Method method)
218   {
219     final String keyName;
220 
221     final de.smartics.properties.api.core.annotations.PropertyKeyName propertyKey =
222         method
223             .getAnnotation(de.smartics.properties.api.core.annotations.PropertyKeyName.class);
224     if (propertyKey != null)
225     {
226       keyName = propertyKey.value();
227     }
228     else
229     {
230       final String methodName = method.getName();
231       if (isGetterMethod(methodName))
232       {
233         keyName = calcPropertyName(methodName);
234       }
235       else
236       {
237         keyName = methodName;
238       }
239     }
240 
241     return keyName;
242   }
243 
244   private static String calcPropertyName(final String methodName)
245   {
246     return Character.toLowerCase(methodName.charAt(3))
247            + methodName.substring(4);
248   }
249 
250   private static boolean isGetterMethod(final String methodName)
251   {
252     return methodName.length() > 3 && methodName.startsWith("get")
253            && Character.isUpperCase(methodName.charAt(3));
254   }
255 
256   private static String readPropertySetName(final Method method)
257   {
258     final String propertySetName;
259 
260     final PropertySet primaryPropertySet =
261         method.getAnnotation(PropertySet.class);
262     if (primaryPropertySet != null)
263     {
264       propertySetName = primaryPropertySet.value();
265     }
266     else
267     {
268       final Class<?> type = method.getDeclaringClass();
269       propertySetName = readPropertySetNameInternal(type);
270     }
271 
272     if (" ".equals(propertySetName))
273     {
274       final Class<?> type = method.getDeclaringClass();
275       return type.getName();
276     }
277     else if ("".equals(propertySetName))
278     {
279       return null;
280     }
281 
282     return propertySetName;
283   }
284 
285   private static String readPropertySetNameInternal(final Class<?> type)
286   {
287     final String propertySetName;
288     final PropertySet propertySet = type.getAnnotation(PropertySet.class);
289     if (propertySet != null)
290     {
291       propertySetName = propertySet.value();
292     }
293     else
294     {
295       propertySetName = type.getName();
296     }
297     return propertySetName;
298   }
299 
300   private static PropertyType readType(final Method method)
301   {
302     final Class<?> type = method.getReturnType();
303     final Class<?> subType = determineSubType(method);
304 
305     return new PropertyType(type, subType);
306   }
307 
308   private static Class<?> determineSubType(final Method method)
309   {
310     final PropertyElementType elementType =
311         method.getAnnotation(PropertyElementType.class);
312     final Class<?> subType;
313     if (elementType != null)
314     {
315       subType = elementType.value();
316     }
317     else
318     {
319       subType = null;
320     }
321     return subType;
322   }
323 
324   @SuppressWarnings({ RAWTYPES, UNCHECKED })
325   private static void addValueRange(final Builder builder, final Method method)
326   {
327     final PropertyValueRange<?> valueRange = calculateValueRange(method);
328     if (valueRange != null)
329     {
330       builder.with(valueRange);
331       final PropertyRangeConstraint<?> constraint =
332           new PropertyRangeConstraint(valueRange);
333       builder.add(constraint);
334     }
335   }
336 
337   @SuppressWarnings({ RAWTYPES, UNCHECKED })
338   private static PropertyValueRange<?> calculateValueRange(final Method method)
339   {
340     final Class<?> returnType = method.getReturnType();
341     if (returnType.isEnum())
342     {
343       final PropertyValueRange<?> valueRange =
344           new EnumeratedPropertyValueRange(returnType);
345       return valueRange;
346     }
347     else
348     {
349       final de.smartics.properties.api.core.annotations.PropertyIntValueRange intAnnotation =
350           method
351               .getAnnotation(de.smartics.properties.api.core.annotations.PropertyIntValueRange.class);
352       if (intAnnotation != null)
353       {
354         final Integer[] values =
355             ArrayUtils.toObject(intAnnotation.value());
356         final CollectionPropertyValueRange<Integer> valueRange =
357             new CollectionPropertyValueRange<Integer>(values);
358         return valueRange;
359       }
360 
361       final de.smartics.properties.api.core.annotations.PropertyStringValueRange annotation =
362           method
363               .getAnnotation(de.smartics.properties.api.core.annotations.PropertyStringValueRange.class);
364       if (annotation != null)
365       {
366         final String[] values = annotation.value();
367         final CollectionPropertyValueRange<String> valueRange =
368             new CollectionPropertyValueRange<String>(values);
369         return valueRange;
370       }
371     }
372 
373     return null;
374   }
375 
376   private static void addExpression(final Builder builder, final Method method)
377   {
378     final de.smartics.properties.api.core.annotations.PropertyExpression annotation =
379         method
380             .getAnnotation(de.smartics.properties.api.core.annotations.PropertyExpression.class);
381     if (annotation != null)
382     {
383       final String expressionString = annotation.value();
384       final PropertyExpression expression =
385           PropertyExpression.create(expressionString);
386       builder.with(expression);
387     }
388   }
389 
390   private void addLifecycle(final Builder builder, final Method method)
391   {
392     final de.smartics.properties.api.core.annotations.PropertyLifecycle annotation =
393         method
394             .getAnnotation(de.smartics.properties.api.core.annotations.PropertyLifecycle.class);
395     if (annotation != null)
396     {
397       final PropertyDefinitionTime configurationTime =
398           annotation.definitionTime();
399       if (configurationTime != null)
400       {
401         builder.with(configurationTime);
402       }
403 
404       final AccessType accessType = annotation.access();
405       if (accessType != null)
406       {
407         builder.with(accessType);
408       }
409 
410       final long updateInterval = annotation.updateInterval();
411       builder.withUpdateIntervalInMs(updateInterval);
412     }
413   }
414 
415   @SuppressWarnings({ RAWTYPES, UNCHECKED })
416   private void addConstraints(final Builder builder, final Method method)
417   {
418     final Annotation[] annotations = method.getAnnotations();
419     boolean collectionConstraintAdded = false;
420 
421     for (final Annotation annotation : annotations)
422     {
423       final javax.validation.Constraint annotationAnnotation =
424           annotation.annotationType().getAnnotation(
425               javax.validation.Constraint.class);
426       if (annotationAnnotation != null)
427       {
428         if (annotation.annotationType() != Size.class)
429         {
430           final GenericPropertyConstraint propertyConstraint =
431               new GenericPropertyConstraint(annotation);
432           builder.add(propertyConstraint);
433         }
434         else
435         {
436           checkCollection(builder, method, (Size) annotation);
437           collectionConstraintAdded = true;
438         }
439       }
440     }
441 
442     if (!collectionConstraintAdded)
443     {
444       checkCollection(builder, method, null);
445     }
446   }
447 
448   private void checkCollection(final PropertyMetaData.Builder builder,
449       final Method method, final Size size)
450   {
451     final Class<?> type = builder.getType().getType();
452 
453     if (Collection.class.isAssignableFrom(type))
454     {
455       final PropertyElementType elementTypeAnnotation =
456           method.getAnnotation(PropertyElementType.class);
457 
458       final Class<?> elementType = calcElementType(elementTypeAnnotation);
459       final List<? extends PropertyConstraint<?>> elementConstraints =
460           calcElementConstraints(method);
461       Integer min = null;
462       Integer max = null;
463       if (size != null)
464       {
465         min = size.min();
466         max = size.max();
467       }
468 
469       final PropertyConstraint<?> propertyConstraint =
470           new ListValueConstraint(elementType, min, max, elementConstraints);
471       builder.add(propertyConstraint);
472     }
473   }
474 
475   private Class<?> calcElementType(
476       final PropertyElementType elementTypeAnnotation)
477   {
478     final Class<?> elementType;
479     if (elementTypeAnnotation != null)
480     {
481       elementType = elementTypeAnnotation.value();
482     }
483     else
484     {
485       elementType = String.class;
486     }
487     return elementType;
488   }
489 
490   @SuppressWarnings({ RAWTYPES, UNCHECKED })
491   private List<? extends PropertyConstraint<?>> calcElementConstraints(
492       final Method method)
493   {
494     final Annotation[] annotations = calcElementsConstraintsType(method);
495     if (annotations != null)
496     {
497       final List constraints = new ArrayList(annotations.length);
498 
499       for (final Annotation annotation : annotations)
500       {
501         final javax.validation.Constraint annotationAnnotation =
502             annotation.annotationType().getAnnotation(
503                 javax.validation.Constraint.class);
504         if (annotationAnnotation != null)
505         {
506           final GenericPropertyConstraint propertyConstraint =
507               new GenericPropertyConstraint(annotation);
508           constraints.add(propertyConstraint);
509         }
510       }
511 
512       return constraints;
513     }
514 
515     return new ArrayList(0);
516   }
517 
518   private static Annotation[] calcElementsConstraintsType(final Method method) // NOPMD
519   {
520     final Class<?> enclosingType = method.getDeclaringClass();
521     final Class<?>[] nestedClasses = enclosingType.getDeclaredClasses();
522     if (nestedClasses != null)
523     {
524       for (final Class<?> nestedClass : nestedClasses)
525       {
526         final PropertyListElementConstraints constraintsType =
527             nestedClass.getAnnotation(PropertyListElementConstraints.class);
528         if (constraintsType != null)
529         {
530           final String methodName = method.getName();
531           return calcElementConstraintMethod(nestedClass, methodName);
532         }
533       }
534     }
535 
536     return null;
537   }
538 
539   private static Annotation[] calcElementConstraintMethod(
540       final Class<?> nestedClass, final String methodName)
541   {
542     try
543     {
544       final Method elementConstraintMethod =
545           nestedClass.getMethod(methodName, new Class<?>[0]);
546       return elementConstraintMethod.getAnnotations();
547     }
548     catch (final Exception e)
549     {
550       return null;
551     }
552   }
553 
554   private void addDocumentProxy(final Builder builder, final Method method)
555   {
556     final DocumentMetaDataProxy proxy =
557         new MetaInfDocumentMetaDataProxy(context, method);
558     builder.with(proxy);
559   }
560 
561   private void addComments(final Builder builder)
562   {
563     final PropertyCommentProxy commentProxy = new PropertyCommentProxy(context);
564     builder.with(commentProxy);
565   }
566 
567   // --- object basics --------------------------------------------------------
568 
569 }