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.constraint.jsr303;
17  
18  import static de.smartics.util.lang.StaticAnalysis.UNCHECKED;
19  
20  import java.io.IOException;
21  import java.io.ObjectInputStream;
22  import java.lang.annotation.Annotation;
23  import java.lang.reflect.Method;
24  import java.util.List;
25  import java.util.Locale;
26  
27  import javax.validation.ConstraintValidator;
28  import javax.validation.ConstraintValidatorContext;
29  import javax.validation.MessageInterpolator;
30  import javax.validation.MessageInterpolator.Context;
31  import javax.validation.Validation;
32  import javax.validation.ValidatorFactory;
33  import javax.validation.constraints.NotNull;
34  import javax.validation.groups.Default;
35  import javax.validation.metadata.ConstraintDescriptor;
36  
37  import org.hibernate.validator.constraints.NotBlank;
38  import org.hibernate.validator.internal.engine.MessageInterpolatorContext;
39  import org.hibernate.validator.internal.metadata.core.ConstraintHelper;
40  
41  import de.smartics.properties.spi.core.constraint.AbstractPropertyConstraint;
42  import de.smartics.util.lang.Arg;
43  import de.smartics.util.lang.NullArgumentException;
44  
45  /**
46   * Generic wrapper for JSR-303 constraints.
47   *
48   * @param <A> the constraint annotation.
49   * @param <T> the type of the property that is validated.
50   */
51  public class GenericPropertyConstraint<A extends Annotation, T> extends
52      AbstractPropertyConstraint<T>
53  {
54    // ********************************* Fields *********************************
55  
56    // --- constants ------------------------------------------------------------
57  
58    /**
59     * The class version identifier.
60     * <p>
61     * The value of this constant is {@value}.
62     */
63    private static final long serialVersionUID = 1L;
64  
65    /**
66     * Helper to fetch validators for default constraints.
67     */
68    private static final ConstraintHelper CONSTRAINTS = new ConstraintHelper();
69  
70    // --- members --------------------------------------------------------------
71  
72    /**
73     * The validator factory to access for message interpolation.
74     */
75    private static final ValidatorFactory FACTORY = Validation
76        .buildDefaultValidatorFactory();
77  
78    /**
79     * The validator to validate values.
80     */
81    private transient ConstraintValidator<A, T> validator;
82  
83    // FIXME: Support list of validators.
84  
85    /**
86     * The JSR-303 constraint annotation.
87     *
88     * @serial
89     */
90    private final A constraintAnnotation;
91  
92    /**
93     * Stores the validator class for serialization.
94     *
95     * @serial
96     */
97    private final Class<? extends ConstraintValidator<A, T>> validatorClass;
98  
99    // ****************************** Initializer *******************************
100 
101   // ****************************** Constructors ******************************
102 
103   /**
104    * Convenience constructor if the validator can be derived from the
105    * {@code constraintAnnotation}.
106    *
107    * @param constraintAnnotation the JSR-303 constraint annotation.
108    * @param propertyType the type of the property selects the validator if more
109    *          than on validator is referenced by the constraint annotation.
110    * @throws NullArgumentException if {@code constraintAnnotation} is
111    *           <code>null</code>.
112    */
113   @SuppressWarnings(UNCHECKED)
114   public GenericPropertyConstraint(final A constraintAnnotation,
115       final Class<?> propertyType) throws NullArgumentException
116   {
117     this((ConstraintValidator<A, T>) createValidator(constraintAnnotation,
118         propertyType), constraintAnnotation);
119   }
120 
121   /**
122    * Default constructor.
123    *
124    * @param validator the validator to validate values.
125    * @param constraintAnnotation the JSR-303 constraint annotation.
126    * @throws NullArgumentException if either {@code validator} or
127    *           {@code constraintAnnotation} is <code>null</code>.
128    */
129   @SuppressWarnings(UNCHECKED)
130   public GenericPropertyConstraint(final ConstraintValidator<A, T> validator,
131       final A constraintAnnotation) throws NullArgumentException
132   {
133     this.validator = Arg.checkNotNull("validator", validator);
134     this.constraintAnnotation =
135         Arg.checkNotNull("constraintAnnotation", constraintAnnotation);
136     this.validatorClass =
137         (Class<? extends ConstraintValidator<A, T>>) validator.getClass();
138 
139     validator.initialize(constraintAnnotation);
140   }
141 
142   // ****************************** Inner Classes *****************************
143 
144   // ********************************* Methods ********************************
145 
146   // --- init -----------------------------------------------------------------
147 
148   private static <A extends Annotation, T> ConstraintValidator<A, T> createValidator(
149       final A constraintAnnotation, final Class<?> propertyType)
150     throws IllegalArgumentException
151   {
152     final javax.validation.Constraint annotation =
153         constraintAnnotation.annotationType().getAnnotation(
154             javax.validation.Constraint.class);
155     if (annotation != null)
156     {
157       final Class<? extends ConstraintValidator<?, ?>>[] validatorClass =
158           annotation.validatedBy();
159       if (validatorClass.length == 1)
160       {
161         return selectValidator(validatorClass);
162       }
163       if (validatorClass.length > 0)
164       {
165         return selectValidator(validatorClass, propertyType);
166       }
167       else
168       {
169         final Class<? extends Annotation> annotationType =
170             constraintAnnotation.annotationType();
171         final List<Class<? extends ConstraintValidator<? extends Annotation, ?>>> validators =
172             CONSTRAINTS.getBuiltInConstraints(annotationType);
173         if (validators.size() == 1)
174         {
175           return selectValidator(validators);
176         }
177         if (!validators.isEmpty())
178         {
179           return selectValidator(validators, propertyType);
180         }
181       }
182     }
183 
184     throw new IllegalArgumentException(
185         "Cannot derive constraint validator from annotation: "
186             + constraintAnnotation);
187   }
188 
189   @SuppressWarnings({ UNCHECKED })
190   private static <A extends Annotation, T> ConstraintValidator<A, T> selectValidator(
191       final Class<? extends ConstraintValidator<?, ?>>[] validatorClass)
192   {
193     final Class<? extends ConstraintValidator<A, T>> type =
194         (Class<? extends ConstraintValidator<A, T>>) validatorClass[0];
195     final ConstraintValidator<A, T> validator = createValidator(type);
196     return validator;
197   }
198 
199   @SuppressWarnings({ UNCHECKED, "rawtypes" })
200   private static <T, A extends Annotation> ConstraintValidator<A, T> selectValidator(
201       final List<Class<? extends ConstraintValidator<? extends Annotation, ?>>> validators)
202   {
203     final Class<? extends ConstraintValidator<A, T>> type =
204         (Class<? extends ConstraintValidator<A, T>>) validators.get(0);
205     final ConstraintValidator validator = createValidator(type);
206     return validator;
207   }
208 
209   @SuppressWarnings(UNCHECKED)
210   private static <A extends Annotation, T> ConstraintValidator<A, T> selectValidator(
211       final List<Class<? extends ConstraintValidator<? extends Annotation, ?>>> validators,
212       final Class<?> propertyType)
213   {
214     final Class<? extends ConstraintValidator<?, ?>>[] x =
215         validators.toArray(new Class[validators.size()]);
216     return selectValidator(x, propertyType);
217   }
218 
219   @SuppressWarnings(UNCHECKED)
220   private static <A extends Annotation, T> ConstraintValidator<A, T> selectValidator(
221       final Class<? extends ConstraintValidator<?, ?>>[] validatorClass,
222       final Class<?> propertyType)
223   {
224     Class<? extends ConstraintValidator<A, T>> bestMatch =
225         (Class<? extends ConstraintValidator<A, T>>) validatorClass[0];
226 
227     for (final Class<? extends ConstraintValidator<A, T>> type : (Class<ConstraintValidator<A, T>>[]) validatorClass)
228     {
229       for (final Method method : type.getMethods())
230       {
231         if ("isValid".equals(method.getName()))
232         {
233           final Class<?>[] types = method.getParameterTypes();
234           if (types.length > 0)
235           {
236             final Class<?> paramType = types[0];
237             if (isArrayOrPrimitive(propertyType, paramType)
238                 || isObjectButNotAnArray(propertyType, paramType))
239             {
240               final ConstraintValidator<A, T> validator = createValidator(type);
241               return validator;
242             }
243             else
244             {
245               if (Object.class.equals(paramType.getComponentType()))
246               {
247                 bestMatch = type;
248               }
249             }
250           }
251         }
252       }
253     }
254 
255     final ConstraintValidator<A, T> validator = createValidator(bestMatch);
256     return validator;
257   }
258 
259   private static boolean isObjectButNotAnArray(final Class<?> propertyType,
260       final Class<?> paramType)
261   {
262     return !propertyType.isArray() && paramType.isAssignableFrom(propertyType);
263   }
264 
265   private static boolean isArrayOrPrimitive(final Class<?> propertyType,
266       final Class<?> paramType)
267   {
268     return (propertyType.isArray() || propertyType.isPrimitive())
269            && paramType.equals(propertyType);
270   }
271 
272   private static <A extends Annotation, T> ConstraintValidator<A, T> createValidator(
273       final Class<? extends ConstraintValidator<A, T>> validatorClass)
274     throws IllegalArgumentException
275   {
276     Throwable cause = null;
277     try
278     {
279       final ConstraintValidator<A, T> validator = validatorClass.newInstance();
280       return validator;
281     }
282     catch (final InstantiationException e)
283     {
284       cause = e;
285       // Fail at the end
286     }
287     catch (final IllegalAccessException e)
288     {
289       cause = e;
290       // Fail at the end
291     }
292 
293     throw new IllegalArgumentException(
294         "Cannot instantiate constraint validator: " + validatorClass.getName(),
295         cause);
296   }
297 
298   // --- get&set --------------------------------------------------------------
299 
300   // --- business -------------------------------------------------------------
301 
302   /**
303    * {@inheritDoc}
304    * <p>
305    * Override to provide more specific information the the string representation
306    * of the constraint annotation.
307    * </p>
308    *
309    * @see de.smartics.properties.api.core.domain.PropertyConstraint#getDescription(java.util.Locale)
310    */
311   // CHECKSTYLE:OFF
312   @Override
313   public String getDescription(final Locale locale) // CHECKSTYLE:ON
314   {
315     return new ConstraintPrettifier(constraintAnnotation).toString();
316   }
317 
318   @Override
319   public String getViolationMessage(final Locale locale,
320       final Object actualValue)
321   {
322     final MessageInterpolator mi = FACTORY.getMessageInterpolator();
323     final ConstraintDescriptor<A> desc =
324         new PropertyConstraintDescriptor<A>(constraintAnnotation);
325     final Context messageContext =
326         new MessageInterpolatorContext(desc, actualValue);
327     final String messageTemplate = extractMessageTemplate();
328     final String message =
329         mi.interpolate(messageTemplate, messageContext, locale);
330     return message;
331   }
332 
333   @Override
334   public final boolean isValid(final T value, final Class<?> group)
335   {
336     if (isConstraintActivated(group))
337     {
338       final ConstraintValidatorContext context =
339           new PropertyConstraintValidatorContext();
340       final boolean valid = validator.isValid(value, context);
341       return valid;
342     }
343     return true;
344   }
345 
346   private boolean isConstraintActivated(final Class<?> group)
347   {
348     try
349     {
350       final Class<?>[] groups =
351           (Class<?>[]) constraintAnnotation.annotationType()
352               .getMethod("groups").invoke(constraintAnnotation);
353       return ((group == null || group == Default.class) && hasNoGroups(groups))
354              || (group != null && group != Default.class && isInGroup(groups,
355                  group));
356     }
357     catch (final Exception e)
358     {
359       return true;
360     }
361   }
362 
363   private boolean hasNoGroups(final Class<?>[] groups)
364   {
365     try
366     {
367       return groups == null || groups.length == 0
368              || (groups.length == 1 && groups[0] == Default.class);
369     }
370     catch (final Exception e)
371     {
372       return true;
373     }
374   }
375 
376   private boolean isInGroup(final Class<?>[] groups, final Class<?> group)
377   {
378     assert (group != null);
379     try
380     {
381       for (final Class<?> current : groups)
382       {
383         if (current.isAssignableFrom(group))
384         {
385           return true;
386         }
387       }
388     }
389     catch (final Exception e)
390     {
391       // Defaults to returning false.
392     }
393     return false;
394   }
395 
396   private String extractMessageTemplate()
397   {
398     try
399     {
400       return String.valueOf(constraintAnnotation.annotationType()
401           .getMethod("message").invoke(constraintAnnotation));
402     }
403     catch (final Exception e)
404     {
405       return "";
406     }
407   }
408 
409   @Override
410   public final boolean isMandatoryConstraint()
411   {
412     final Class<? extends Annotation> type =
413         constraintAnnotation.annotationType();
414     return NotNull.class == type || NotBlank.class == type;
415   }
416 
417   // --- object basics --------------------------------------------------------
418 
419   /**
420    * Reads the object from the given stream.
421    *
422    * @param in the stream to read from.
423    * @throws IOException on read problems.
424    * @throws ClassNotFoundException if a class cannot be found.
425    */
426   private void readObject(final ObjectInputStream in) throws IOException,
427     ClassNotFoundException
428   {
429     in.defaultReadObject();
430 
431     try
432     {
433       this.validator = validatorClass.newInstance();
434     }
435     catch (final Exception e)
436     {
437       throw new IOException("Cannot deserialize validator.", e);
438     }
439   }
440 }