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.annotations.PropertyUse.UseType;
37  import de.smartics.properties.api.core.annotations.PropertyValueSecured;
38  import de.smartics.properties.api.core.domain.PropertiesContext;
39  import de.smartics.properties.api.core.domain.PropertyCategories;
40  import de.smartics.properties.api.core.domain.PropertyConstraint;
41  import de.smartics.properties.api.core.domain.PropertyDescriptor;
42  import de.smartics.properties.api.core.domain.PropertyExpression;
43  import de.smartics.properties.api.core.domain.PropertyKey;
44  import de.smartics.properties.api.core.domain.PropertyType;
45  import de.smartics.properties.api.core.domain.PropertyValueRange;
46  import de.smartics.properties.spi.core.constraint.jsr303.GenericPropertyConstraint;
47  import de.smartics.properties.spi.core.constraints.ListValueConstraint;
48  import de.smartics.properties.spi.core.constraints.PropertyRangeConstraint;
49  import de.smartics.properties.spi.core.context.PropertyContextProxy;
50  import de.smartics.properties.spi.core.metadata.PropertyMetaData.Builder;
51  import de.smartics.properties.spi.core.util.PropertyUtils;
52  import de.smartics.properties.spi.core.value.CollectionPropertyValueRange;
53  import de.smartics.properties.spi.core.value.EnumeratedPropertyValueRange;
54  import de.smartics.util.lang.Arg;
55  
56  /**
57   * Parses runtime annotations to create property metadata.
58   */
59  public final class PropertyMetaDataParser
60  { // NOPMD
61    // ********************************* Fields *********************************
62  
63    // --- constants ------------------------------------------------------------
64  
65    // --- members --------------------------------------------------------------
66  
67    /**
68     * The context to access property metadata.
69     */
70    private final PropertiesContext context;
71  
72    // ****************************** Initializer *******************************
73  
74    // ****************************** Constructors ******************************
75  
76    /**
77     * Default constructor to generate property descriptors with comments.
78     *
79     * @param context the context to access property metadata. May be
80     *          <code>null</code> in which case no comments are generated.
81     */
82    private PropertyMetaDataParser(final PropertiesContext context)
83    {
84      this.context = context;
85    }
86  
87    // ****************************** Inner Classes *****************************
88  
89    // ********************************* Methods ********************************
90  
91    // --- init -----------------------------------------------------------------
92  
93    // --- factory --------------------------------------------------------------
94  
95    /**
96     * Convenience constructor to generate property descriptors without comments.
97     *
98     * @return the created parser instance.
99     */
100   public static PropertyMetaDataParser createWithoutContextAccess()
101   {
102     return new PropertyMetaDataParser(null);
103   }
104 
105   /**
106    * Convenience constructor to generate property descriptors with access to
107    * property comments.
108    *
109    * @param context the context to access comment information for properties.
110    * @return the created parser instance.
111    * @throws NullPointerException if {@code context} is <code>null</code>.
112    */
113   public static PropertyMetaDataParser create(final PropertiesContext context)
114     throws NullPointerException
115   {
116     Arg.checkNotNull("context", context);
117     return new PropertyMetaDataParser(context);
118   }
119 
120   // --- get&set --------------------------------------------------------------
121 
122   // --- business -------------------------------------------------------------
123 
124   /**
125    * Reads the property descriptor information from the type.
126    *
127    * @param propertySetType the type to read the property descriptor.
128    * @return the property descriptor provided by the methods of the type.
129    * @throws NullPointerException if {@code propertySetType} is
130    *           <code>null</code>.
131    * @throws IllegalArgumentException if {@code propertySetType} is not
132    *           annotated with {@link PropertySet}.
133    */
134   public List<PropertyDescriptor> readDescriptors(final Class<?> propertySetType)
135     throws NullPointerException, IllegalArgumentException
136   {
137     Arg.checkNotNull("propertySetType", propertySetType);
138     PropertyUtils.checkPropertySetType(propertySetType);
139 
140     final Method[] methods = propertySetType.getMethods();
141     final List<PropertyDescriptor> descriptors =
142         new ArrayList<PropertyDescriptor>(methods.length);
143     for (final Method method : methods)
144     {
145       if (PropertyUtils.isPropertyMethod(method))
146       {
147         final PropertyDescriptor descriptor = readDescriptor(method);
148         descriptors.add(descriptor);
149       }
150     }
151 
152     return descriptors;
153   }
154 
155   /**
156    * Reads the property descriptor information from the method.
157    *
158    * @param method the method to read the property descriptor.
159    * @return the property descriptor provided by this method.
160    * @throws NullPointerException if {@code method} is <code>null</code>.
161    */
162   public PropertyDescriptor readDescriptor(final Method method)
163     throws NullPointerException
164   {
165     Arg.checkNotNull("method", method);
166 
167     final PropertyMetaData.Builder builder = new PropertyMetaData.Builder();
168 
169     final Class<?> declaringType = method.getDeclaringClass();
170     final PropertyKey key = readKey(method);
171     final PropertyType type = readType(method);
172     builder.with(new PropertyContextProxy(context))
173         .withDeclaringType(declaringType).with(key).with(type);
174 
175     addValueRange(builder, method);
176     addExpression(builder, method);
177     addLifecycle(builder, method);
178     addConstraints(builder, method);
179     addDocumentProxy(builder, method, key);
180     addComments(builder);
181     addSecured(builder, method);
182     addCategories(builder, method);
183     addUseType(builder, method);
184 
185     return builder.build();
186   }
187 
188   private static void addSecured(final Builder builder, final Method method)
189   {
190     final boolean secured = readSecured(method);
191     builder.setSecured(secured);
192   }
193 
194   private static boolean readSecured(final Method method)
195   {
196     final PropertyValueSecured secured =
197         method.getAnnotation(PropertyValueSecured.class);
198     final boolean value;
199     if (secured != null)
200     {
201       value = secured.decrypt();
202     }
203     else
204     {
205       value = false;
206     }
207     return value;
208   }
209 
210   /**
211    * Reads the property key information from the method.
212    *
213    * @param method the method to read the property key.
214    * @return the property key provided by this method.
215    */
216   public PropertyKey readKey(final Method method)
217   {
218     final String set = readPropertySetName(method);
219     final String name = readPropertyKeyName(method);
220     return PropertyKey.create(set, name);
221   }
222 
223   /**
224    * Reads the property set name of the type.
225    *
226    * @param type the type to read the property set name.
227    * @return the property name as specified of defaulted.
228    */
229   public String readPropertySetName(final Class<?> type)
230   {
231     String propertySetName = readPropertySetNameInternal(type);
232 
233     if (" ".equals(propertySetName))
234     {
235       propertySetName = type.getName();
236     }
237     else if ("".equals(propertySetName))
238     {
239       propertySetName = null;
240     }
241 
242     return propertySetName;
243   }
244 
245   private static String readPropertyKeyName(final Method method)
246   {
247     final String keyName;
248 
249     final de.smartics.properties.api.core.annotations.PropertyKeyName propertyKey =
250         method
251             .getAnnotation(de.smartics.properties.api.core.annotations.PropertyKeyName.class);
252     if (propertyKey != null)
253     {
254       keyName = propertyKey.value();
255     }
256     else
257     {
258       final String methodName = method.getName();
259       if (isGetterMethod(methodName))
260       {
261         keyName = calcPropertyName(methodName);
262       }
263       else
264       {
265         keyName = methodName;
266       }
267     }
268 
269     return keyName;
270   }
271 
272   private static String calcPropertyName(final String methodName)
273   {
274     return Character.toLowerCase(methodName.charAt(3))
275            + methodName.substring(4);
276   }
277 
278   private static boolean isGetterMethod(final String methodName)
279   {
280     return methodName.length() > 3 && methodName.startsWith("get")
281            && Character.isUpperCase(methodName.charAt(3));
282   }
283 
284   private static String readPropertySetName(final Method method)
285   {
286     final String propertySetName;
287 
288     final PropertySet primaryPropertySet =
289         method.getAnnotation(PropertySet.class);
290     if (primaryPropertySet != null)
291     {
292       propertySetName = primaryPropertySet.value();
293     }
294     else
295     {
296       final Class<?> type = method.getDeclaringClass();
297       propertySetName = readPropertySetNameInternal(type);
298     }
299 
300     if (" ".equals(propertySetName))
301     {
302       final Class<?> type = method.getDeclaringClass();
303       return type.getName();
304     }
305     else if ("".equals(propertySetName))
306     {
307       return null;
308     }
309 
310     return propertySetName;
311   }
312 
313   private static String readPropertySetNameInternal(final Class<?> type)
314   {
315     final String propertySetName;
316     final PropertySet propertySet = type.getAnnotation(PropertySet.class);
317     if (propertySet != null)
318     {
319       propertySetName = propertySet.value();
320     }
321     else
322     {
323       propertySetName = type.getName();
324     }
325     return propertySetName;
326   }
327 
328   private static PropertyType readType(final Method method)
329   {
330     final Class<?> type = method.getReturnType();
331     final Class<?> subType = determineSubType(method);
332 
333     return new PropertyType(type, subType);
334   }
335 
336   private static Class<?> determineSubType(final Method method)
337   {
338     final PropertyElementType elementType =
339         method.getAnnotation(PropertyElementType.class);
340     final Class<?> subType;
341     if (elementType != null)
342     {
343       subType = elementType.value();
344     }
345     else
346     {
347       subType = null;
348     }
349     return subType;
350   }
351 
352   @SuppressWarnings({ RAWTYPES, UNCHECKED })
353   private static void addValueRange(final Builder builder, final Method method)
354   {
355     final PropertyValueRange<?> valueRange = calculateValueRange(method);
356     if (valueRange != null)
357     {
358       builder.with(valueRange);
359       final PropertyRangeConstraint<?> constraint =
360           new PropertyRangeConstraint(valueRange);
361       builder.add(constraint);
362     }
363   }
364 
365   @SuppressWarnings({ RAWTYPES, UNCHECKED })
366   private static PropertyValueRange<?> calculateValueRange(final Method method)
367   {
368     final Class<?> returnType = method.getReturnType();
369     if (returnType.isEnum())
370     {
371       final PropertyValueRange<?> valueRange =
372           new EnumeratedPropertyValueRange(returnType);
373       return valueRange;
374     }
375     else
376     {
377       final de.smartics.properties.api.core.annotations.PropertyIntValueRange intAnnotation =
378           method
379               .getAnnotation(de.smartics.properties.api.core.annotations.PropertyIntValueRange.class);
380       if (intAnnotation != null)
381       {
382         final Integer[] values = ArrayUtils.toObject(intAnnotation.value());
383         final CollectionPropertyValueRange<Integer> valueRange =
384             new CollectionPropertyValueRange<Integer>(values);
385         return valueRange;
386       }
387 
388       final de.smartics.properties.api.core.annotations.PropertyStringValueRange annotation =
389           method
390               .getAnnotation(de.smartics.properties.api.core.annotations.PropertyStringValueRange.class);
391       if (annotation != null)
392       {
393         final String[] values = annotation.value();
394         final CollectionPropertyValueRange<String> valueRange =
395             new CollectionPropertyValueRange<String>(values);
396         return valueRange;
397       }
398     }
399 
400     return null;
401   }
402 
403   private static void addExpression(final Builder builder, final Method method)
404   {
405     final de.smartics.properties.api.core.annotations.PropertyExpression annotation =
406         method
407             .getAnnotation(de.smartics.properties.api.core.annotations.PropertyExpression.class);
408     if (annotation != null)
409     {
410       final String expressionString = annotation.value();
411       final PropertyExpression expression =
412           PropertyExpression.create(expressionString);
413       builder.with(expression);
414     }
415   }
416 
417   private void addLifecycle(final Builder builder, final Method method)
418   {
419     final de.smartics.properties.api.core.annotations.PropertyLifecycle annotation =
420         method
421             .getAnnotation(de.smartics.properties.api.core.annotations.PropertyLifecycle.class);
422     if (annotation != null)
423     {
424       final PropertyDefinitionTime configurationTime =
425           annotation.definitionTime();
426       if (configurationTime != null)
427       {
428         builder.with(configurationTime);
429       }
430 
431       final AccessType accessType = annotation.access();
432       if (accessType != null)
433       {
434         builder.with(accessType);
435       }
436 
437       final long updateInterval = annotation.updateInterval();
438       builder.withUpdateIntervalInMs(updateInterval);
439     }
440   }
441 
442   @SuppressWarnings({ RAWTYPES, UNCHECKED })
443   private void addConstraints(final Builder builder, final Method method)
444   {
445     final Annotation[] annotations = method.getAnnotations();
446     boolean collectionConstraintAdded = false;
447 
448     for (final Annotation annotation : annotations)
449     {
450       final javax.validation.Constraint annotationAnnotation =
451           annotation.annotationType().getAnnotation(
452               javax.validation.Constraint.class);
453       if (annotationAnnotation != null)
454       {
455         if (annotation.annotationType() != Size.class)
456         {
457           final GenericPropertyConstraint propertyConstraint =
458               new GenericPropertyConstraint(annotation, method.getReturnType());
459           builder.add(propertyConstraint);
460         }
461         else
462         {
463           checkCollection(builder, method, (Size) annotation);
464           collectionConstraintAdded = true;
465         }
466       }
467     }
468 
469     if (!collectionConstraintAdded)
470     {
471       checkCollection(builder, method, null);
472     }
473   }
474 
475   private void checkCollection(final PropertyMetaData.Builder builder,
476       final Method method, final Size size)
477   {
478     final Class<?> type = builder.getType().getType();
479 
480     if (Collection.class.isAssignableFrom(type))
481     {
482       final PropertyElementType elementTypeAnnotation =
483           method.getAnnotation(PropertyElementType.class);
484 
485       final Class<?> elementType = calcElementType(elementTypeAnnotation);
486       final List<? extends PropertyConstraint<?>> elementConstraints =
487           calcElementConstraints(method);
488       Integer min = null;
489       Integer max = null;
490       if (size != null)
491       {
492         min = size.min();
493         max = size.max();
494       }
495 
496       final PropertyConstraint<?> propertyConstraint =
497           new ListValueConstraint(elementType, min, max, elementConstraints);
498       builder.add(propertyConstraint);
499     }
500   }
501 
502   private Class<?> calcElementType(
503       final PropertyElementType elementTypeAnnotation)
504   {
505     final Class<?> elementType;
506     if (elementTypeAnnotation != null)
507     {
508       elementType = elementTypeAnnotation.value();
509     }
510     else
511     {
512       elementType = String.class;
513     }
514     return elementType;
515   }
516 
517   @SuppressWarnings({ RAWTYPES, UNCHECKED })
518   private List<? extends PropertyConstraint<?>> calcElementConstraints(
519       final Method method)
520   {
521     final Annotation[] annotations = calcElementsConstraintsType(method);
522     if (annotations != null)
523     {
524       final List constraints = new ArrayList(annotations.length);
525 
526       for (final Annotation annotation : annotations)
527       {
528         final javax.validation.Constraint annotationAnnotation =
529             annotation.annotationType().getAnnotation(
530                 javax.validation.Constraint.class);
531         if (annotationAnnotation != null)
532         {
533           final GenericPropertyConstraint propertyConstraint =
534               new GenericPropertyConstraint(annotation, method.getReturnType()); // FIXME:
535                                                                                  // ===????
536           constraints.add(propertyConstraint);
537         }
538       }
539 
540       return constraints;
541     }
542 
543     return new ArrayList(0);
544   }
545 
546   private static Annotation[] calcElementsConstraintsType(final Method method) // NOPMD
547   {
548     final Class<?> enclosingType = method.getDeclaringClass();
549     final Class<?>[] nestedClasses = enclosingType.getDeclaredClasses();
550     if (nestedClasses != null)
551     {
552       for (final Class<?> nestedClass : nestedClasses)
553       {
554         final PropertyListElementConstraints constraintsType =
555             nestedClass.getAnnotation(PropertyListElementConstraints.class);
556         if (constraintsType != null)
557         {
558           final String methodName = method.getName();
559           return calcElementConstraintMethod(nestedClass, methodName);
560         }
561       }
562     }
563 
564     return null;
565   }
566 
567   private static Annotation[] calcElementConstraintMethod(
568       final Class<?> nestedClass, final String methodName)
569   {
570     try
571     {
572       final Method elementConstraintMethod =
573           nestedClass.getMethod(methodName, new Class<?>[0]);
574       return elementConstraintMethod.getAnnotations();
575     }
576     catch (final Exception e)
577     {
578       return null;
579     }
580   }
581 
582   private void addDocumentProxy(final Builder builder, final Method method,
583       final PropertyKey key)
584   {
585     final DocumentMetaDataProxy proxy =
586         new MetaInfDocumentMetaDataProxy(key, context, method);
587     builder.with(proxy);
588   }
589 
590   private void addComments(final Builder builder)
591   {
592     final PropertyCommentProxy commentProxy = new PropertyCommentProxy(context);
593     builder.with(commentProxy);
594   }
595 
596   private void addCategories(final Builder builder, final Method method)
597   {
598     final List<Class<?>> categories = readCategories(method);
599     if (!categories.isEmpty())
600     {
601       final PropertyCategories.Builder catBuilder =
602           new PropertyCategories.Builder();
603       for (final Class<?> category : categories)
604       {
605         catBuilder.with(category);
606       }
607       final PropertyCategories propertyCategories = catBuilder.build();
608       builder.with(propertyCategories);
609     }
610   }
611 
612   private static List<Class<?>> readCategories(final Method method)
613   {
614     final List<Class<?>> categories = new ArrayList<Class<?>>();
615 
616     final de.smartics.properties.api.core.annotations.PropertyCategories methodAnnotation =
617         method
618             .getAnnotation(de.smartics.properties.api.core.annotations.PropertyCategories.class);
619     addCategoriesFromAnnotation(categories, methodAnnotation);
620 
621     final Class<?> type = method.getDeclaringClass();
622     final de.smartics.properties.api.core.annotations.PropertyCategories typeAnnotation =
623         type.getAnnotation(de.smartics.properties.api.core.annotations.PropertyCategories.class);
624     addCategoriesFromAnnotation(categories, typeAnnotation);
625 
626     return categories;
627   }
628 
629   private static void addCategoriesFromAnnotation(
630       final List<Class<?>> categories,
631       final de.smartics.properties.api.core.annotations.PropertyCategories typeAnnotation)
632   {
633     if (typeAnnotation != null)
634     {
635       final Class<?>[] categoryTypes = typeAnnotation.value();
636       for (final Class<?> categoryType : categoryTypes)
637       {
638         categories.add(categoryType);
639       }
640     }
641   }
642 
643   private void addUseType(final Builder builder, final Method method)
644   {
645     final Class<?> type = method.getDeclaringClass();
646     final de.smartics.properties.api.core.annotations.PropertyUse typeAnnotation =
647         type.getAnnotation(de.smartics.properties.api.core.annotations.PropertyUse.class);
648     if (typeAnnotation != null)
649     {
650       final UseType useType = typeAnnotation.value();
651       if (useType != null)
652       {
653         builder.with(useType);
654       }
655     }
656   }
657 
658   // --- object basics --------------------------------------------------------
659 
660 }