View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package javax.faces.validator;
20  
21  import java.security.AccessController;
22  import java.security.PrivilegedActionException;
23  import java.security.PrivilegedExceptionAction;
24  import java.util.ArrayList;
25  import java.util.LinkedHashMap;
26  import java.util.LinkedHashSet;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.logging.Logger;
32  
33  import javax.el.ELContext;
34  import javax.el.ELResolver;
35  import javax.el.FunctionMapper;
36  import javax.el.ValueExpression;
37  import javax.el.ValueReference;
38  import javax.el.VariableMapper;
39  import javax.faces.FacesException;
40  import javax.faces.application.FacesMessage;
41  import javax.faces.component.PartialStateHolder;
42  import javax.faces.component.UIComponent;
43  import javax.faces.context.FacesContext;
44  import javax.faces.el.CompositeComponentExpressionHolder;
45  import javax.validation.ConstraintViolation;
46  import javax.validation.MessageInterpolator;
47  import javax.validation.Validation;
48  import javax.validation.ValidatorFactory;
49  import javax.validation.groups.Default;
50  import javax.validation.metadata.BeanDescriptor;
51  
52  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFJspProperty;
53  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFProperty;
54  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFValidator;
55  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFWebConfigParam;
56  
57  /**
58   * <p>
59   * <strong>BeanValidator</strong> is a {@link javax.faces.validator.Validator}
60   * that doesn't do any validation itself, but delegates validation logic to
61   * Bean Validation.
62   * </p>
63   *
64   * @since 2.0
65   */
66  @JSFValidator(
67          name="f:validateBean",
68          bodyContent="empty")
69  @JSFJspProperty(
70          name = "binding",
71          returnType = "javax.faces.validator.BeanValidator",
72          longDesc = "A ValueExpression that evaluates to a BeanValidator.")
73  public class BeanValidator implements Validator, PartialStateHolder
74  {
75  
76      private static final Logger log = Logger.getLogger(BeanValidator.class.getName());
77  
78      /**
79       * Converter ID, as defined by the JSF 2.0 specification.
80       */
81      public static final String VALIDATOR_ID = "javax.faces.Bean";
82  
83      /**
84       * The message ID for this Validator in the message bundles.
85       */
86      public static final String MESSAGE_ID = "javax.faces.validator.BeanValidator.MESSAGE";
87  
88      /**
89       * If this init parameter is present, no Bean Validators should be added to an UIInput by default.
90       * Explicitly adding a BeanValidator to an UIInput is possible though.
91       */
92      @JSFWebConfigParam(defaultValue="true", expectedValues="true, false", since="2.0", group="validation")
93      public static final String DISABLE_DEFAULT_BEAN_VALIDATOR_PARAM_NAME
94              = "javax.faces.validator.DISABLE_DEFAULT_BEAN_VALIDATOR";
95  
96      /**
97       * The key in the ServletContext where the Bean Validation Factory can be found.
98       * In a managed Java EE 6 environment, the container initializes the ValidatorFactory
99       * and stores it in the ServletContext under this key.
100      * If not present, the manually instantiated ValidatorFactory is stored in the ServletContext
101      * under this key for caching purposes.
102      */
103     public static final String VALIDATOR_FACTORY_KEY = "javax.faces.validator.beanValidator.ValidatorFactory";
104 
105     /**
106      * This is used as a separator so multiple validation groups can be specified in one String.
107      */
108     public static final String VALIDATION_GROUPS_DELIMITER = ",";
109 
110     /**
111      * This regular expression is used to match for empty validation groups.
112      * Currently, a string containing only whitespace is classified as empty.
113      */
114     public static final String EMPTY_VALIDATION_GROUPS_PATTERN = "^[\\W,]*$";
115     
116     /**
117      * Enable f:validateWholeBean use.
118      */
119     @JSFWebConfigParam(since="2.3", defaultValue = "false", expectedValues = "true, false", group="validation")
120     public static final String ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME = 
121             "javax.faces.validator.ENABLE_VALIDATE_WHOLE_BEAN";
122     
123     private static final Class<?>[] DEFAULT_VALIDATION_GROUPS_ARRAY = new Class<?>[] { Default.class };
124 
125     private static final String DEFAULT_VALIDATION_GROUP_NAME = "javax.validation.groups.Default";
126     
127     private static final String CANDIDATE_COMPONENT_VALUES_MAP = "oam.WBV.candidatesMap";
128     
129     private static final String BEAN_VALIDATION_FAILED = "oam.WBV.validationFailed";
130 
131     private String validationGroups;
132 
133     private Class<?>[] validationGroupsArray;
134 
135     private boolean isTransient = false;
136 
137     private boolean _initialStateMarked = false;
138 
139     /**
140      * {@inheritDoc}
141      */
142     @Override
143     public void validate(final FacesContext context, final UIComponent component, final Object value)
144     {
145         if (context == null)
146         {
147             throw new NullPointerException("context");
148         }
149         if (component == null)
150         {
151             throw new NullPointerException("component");
152         }
153 
154         ValueExpression valueExpression = component.getValueExpression("value");
155         if (valueExpression == null)
156         {
157             log.warning("cannot validate component with empty value: " 
158                     + component.getClientId(context));
159             return;
160         }
161 
162         // Obtain a reference to the to-be-validated object and the property name.
163         ValueReference reference = getValueReference(valueExpression, context);
164         if (reference == null)
165         {
166             return;
167         }
168         Object base = reference.getBase();
169         if (base == null)
170         {
171             return;
172         }
173         
174         Class<?> valueBaseClass = base.getClass();
175         if (valueBaseClass == null)
176         {
177             return;
178         }
179         
180         Object referenceProperty = reference.getProperty();
181         if (!(referenceProperty instanceof String))
182         {
183             // if the property is not a String, the ValueReference does not
184             // point to a bean method, but e.g. to a value in a Map, thus we 
185             // can exit bean validation here
186             return;
187         }
188         String valueProperty = (String) referenceProperty;
189 
190         // Initialize Bean Validation.
191         ValidatorFactory validatorFactory = createValidatorFactory(context);
192         javax.validation.Validator validator = createValidator(validatorFactory, context);
193         BeanDescriptor beanDescriptor = validator.getConstraintsForClass(valueBaseClass);
194         if (!beanDescriptor.isBeanConstrained())
195         {
196             return;
197         }
198         
199         // Note that validationGroupsArray was initialized when createValidator was called
200         Class[] validationGroupsArray = this.validationGroupsArray;
201 
202         // JSF 2.3: If the ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME application parameter is enabled and this Validator 
203         // instance has validation groups other than or in addition to the Default group
204         boolean containsOtherValidationGroup = false;
205         if (validationGroupsArray != null && validationGroupsArray.length > 0)
206         {
207             for (Class<?> clazz : validationGroupsArray)
208             {
209                 if (!Default.class.equals(clazz))
210                 {
211                     containsOtherValidationGroup = true;
212                     break;
213                 }
214             }
215         }
216         
217         // Delegate to Bean Validation.
218         Set constraintViolations = validator.validateValue(valueBaseClass, valueProperty, value, validationGroupsArray);
219         if (!constraintViolations.isEmpty())
220         {
221             Set<FacesMessage> messages = new LinkedHashSet<FacesMessage>(constraintViolations.size());
222             for (Object violation: constraintViolations)
223             {
224                 ConstraintViolation constraintViolation = (ConstraintViolation) violation;
225                 String message = constraintViolation.getMessage();
226                 Object[] args = new Object[]{ message, _MessageUtils.getLabel(context, component) };
227                 FacesMessage msg = _MessageUtils.getErrorMessage(context, MESSAGE_ID, args);
228                 messages.add(msg);
229             }
230             
231             if (isValidateWholeBeanEnabled(context) && containsOtherValidationGroup)
232             {
233                 // JSF 2.3: record the fact that this field failed validation so that any <f:validateWholeBean /> 
234                 // component later in the tree is able to skip class-level validation for the bean for which this 
235                 // particular field is a property. Regardless of whether or not 
236                 // ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME is set, throw the new exception.            
237                 context.getViewRoot().getTransientStateHelper().putTransient(BEAN_VALIDATION_FAILED, Boolean.TRUE);
238             }
239             
240             throw new ValidatorException(messages);
241         }
242         else
243         {
244             
245             // JSF 2.3: If the returned Set is empty, the ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME application parameter
246             // is enabled and this Validator instance has validation groups other than or in addition to the 
247             // Default group
248             if (isValidateWholeBeanEnabled(context) && containsOtherValidationGroup)
249             {
250                 // record the fact that this field passed validation so that any <f:validateWholeBean /> component 
251                 // later in the tree is able to allow class-level validation for the bean for which this particular 
252                 // field is a property.
253                 
254                 Map<String, Object> candidatesMap = (Map<String, Object>) context.getViewRoot()
255                         .getTransientStateHelper().getTransient(CANDIDATE_COMPONENT_VALUES_MAP);
256                 if (candidatesMap == null)
257                 {
258                     candidatesMap = new LinkedHashMap<String, Object>();
259                     context.getViewRoot().getTransientStateHelper().putTransient(
260                             CANDIDATE_COMPONENT_VALUES_MAP, candidatesMap);
261                 }
262                 candidatesMap.put(component.getClientId(context), value);
263             }
264         }
265     }
266     
267     private boolean isValidateWholeBeanEnabled(FacesContext facesContext)
268     {
269         Boolean value = (Boolean) facesContext.getAttributes().get(ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME);
270         if (value == null)
271         {
272             String enabled = facesContext.getExternalContext().getInitParameter(ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME);
273             if (enabled == null)
274             {
275                 value = Boolean.FALSE;
276             }
277             else
278             {
279                 value = Boolean.valueOf(enabled);
280             }
281         }
282         return Boolean.TRUE.equals(value);
283     }
284 
285     private javax.validation.Validator createValidator(final ValidatorFactory validatorFactory, FacesContext context)
286     {
287         // Set default validation group when setValidationGroups has not been called.
288         // The null check is there to prevent it from happening twice.
289         if (validationGroupsArray == null)
290         {
291             postSetValidationGroups();
292         }
293 
294         return validatorFactory //
295                 .usingContext() //
296                 .messageInterpolator(new FacesMessageInterpolator(
297                         validatorFactory.getMessageInterpolator(), context)) //
298                 .getValidator();
299 
300     }
301 
302     /**
303      * Get the ValueReference from the ValueExpression.
304      *
305      * @param valueExpression The ValueExpression for value.
306      * @param context The FacesContext.
307      * @return A ValueReferenceWrapper with the necessary information about the ValueReference.
308      */
309     private ValueReference getValueReference(
310             final ValueExpression valueExpression, final FacesContext context)
311     {
312         ELContext elCtx = context.getELContext();
313         return _ValueReferenceResolver.resolve(valueExpression, elCtx);
314     }
315 
316     /**
317      * This method creates ValidatorFactory instances or retrieves them from the container.
318      *
319      * Once created, ValidatorFactory instances are stored in the container under the key
320      * VALIDATOR_FACTORY_KEY for performance.
321      *
322      * @param context The FacesContext.
323      * @return The ValidatorFactory instance.
324      * @throws FacesException if no ValidatorFactory can be obtained because: a) the
325      * container is not a Servlet container or b) because Bean Validation is not available.
326      */
327     private ValidatorFactory createValidatorFactory(FacesContext context)
328     {
329         Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap();
330         Object attr = applicationMap.get(VALIDATOR_FACTORY_KEY);
331         if (attr instanceof ValidatorFactory)
332         {
333             return (ValidatorFactory) attr;
334         }
335         else
336         {
337             synchronized (this)
338             {
339                 if (_ExternalSpecifications.isBeanValidationAvailable())
340                 {
341                     ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
342                     applicationMap.put(VALIDATOR_FACTORY_KEY, factory);
343                     return factory;
344                 }
345                 else
346                 {
347                     throw new FacesException("Bean Validation is not present");
348                 }
349             }
350         }
351     }
352 
353     /**
354      * Fully initialize the validation groups if needed.
355      * If no validation groups are specified, the Default validation group is used.
356      */
357     private void postSetValidationGroups()
358     {
359         if (this.validationGroups == null || this.validationGroups.matches(EMPTY_VALIDATION_GROUPS_PATTERN))
360         {
361             this.validationGroupsArray = DEFAULT_VALIDATION_GROUPS_ARRAY;
362         }
363         else
364         {
365             String[] classes = this.validationGroups.split(VALIDATION_GROUPS_DELIMITER);
366             List<Class<?>> validationGroupsList = new ArrayList<Class<?>>(classes.length);
367 
368             for (String clazz : classes)
369             {
370                 clazz = clazz.trim();
371                 if (!clazz.isEmpty())
372                 {
373                     Class<?> theClass = null;
374                     ClassLoader cl = null;
375                     if (System.getSecurityManager() != null) 
376                     {
377                         try 
378                         {
379                             cl = AccessController.doPrivileged(new PrivilegedExceptionAction<ClassLoader>()
380                                     {
381                                         public ClassLoader run() throws PrivilegedActionException
382                                         {
383                                             return Thread.currentThread().getContextClassLoader();
384                                         }
385                                     });
386                         }
387                         catch (PrivilegedActionException pae)
388                         {
389                             throw new FacesException(pae);
390                         }
391                     }
392                     else
393                     {
394                         cl = Thread.currentThread().getContextClassLoader();
395                     }
396                     
397                     try
398                     {                        
399                         // Try WebApp ClassLoader first
400                         theClass = Class.forName(clazz,false,cl);
401                     }
402                     catch (ClassNotFoundException ignore)
403                     {
404                         try
405                         {
406                             // fallback: Try ClassLoader for BeanValidator (i.e. the myfaces.jar lib)
407                             theClass = Class.forName(clazz,false, BeanValidator.class.getClassLoader());
408                         }
409                         catch (ClassNotFoundException e)
410                         {
411                             throw new RuntimeException("Could not load validation group", e);
412                         }                        
413                     }
414                     // the class was found
415                     validationGroupsList.add(theClass);
416                 }
417             }
418                     
419             this.validationGroupsArray = validationGroupsList.toArray(new Class[validationGroupsList.size()]);
420         }
421     }
422 
423     /** {@inheritDoc} */
424     @Override
425     public Object saveState(final FacesContext context)
426     {
427         if (context == null)
428         {
429             throw new NullPointerException("context");
430         }
431 
432         if (!initialStateMarked())
433         {
434            //Full state saving.
435            return this.validationGroups;
436         }
437         else if (DEFAULT_VALIDATION_GROUP_NAME.equals(this.validationGroups))
438         {
439             // default validation groups can be saved as null.
440             return null;
441         }
442         else
443         {
444             // Save it fully. Remember that by MYFACES-2528
445             // validationGroups needs to be stored into the state
446             // because this value is often susceptible to use in "combo"
447             return this.validationGroups;
448         }
449     }
450 
451     /** {@inheritDoc} */
452     @Override
453     public void restoreState(final FacesContext context, final Object state)
454     {
455         if (context == null)
456         {
457             throw new NullPointerException("context");
458         }
459 
460         if (state != null)
461         {
462             this.validationGroups = (String) state;
463         }
464         else
465         {
466             // When the value is being validated, postSetValidationGroups() sets
467             // validationGroups to javax.validation.groups.Default. 
468             this.validationGroups = null;
469         }
470         // Only the String is saved, recalculate the Class[] on state restoration.
471         //postSetValidationGroups();
472     }
473 
474     /**
475      * Get the Bean Validation validation groups.
476      * @return The validation groups String.
477      */
478     @JSFProperty
479     public String getValidationGroups()
480     {
481         return validationGroups;
482     }
483 
484     /**
485      * Set the Bean Validation validation groups.
486      * @param validationGroups The validation groups String, separated by
487      *                         {@link BeanValidator#VALIDATION_GROUPS_DELIMITER}.
488      */
489     public void setValidationGroups(final String validationGroups)
490     {
491         this.validationGroups = validationGroups;
492         this.clearInitialState();
493         //postSetValidationGroups();
494     }
495 
496     @JSFProperty
497     @SuppressWarnings("unused")
498     private Boolean isDisabled()
499     {
500         return null;
501     }
502     
503     @JSFProperty
504     @SuppressWarnings("unused")
505     private String getFor()
506     {
507         return null;
508     }
509 
510     /** {@inheritDoc} */
511     @Override
512     public boolean isTransient()
513     {
514         return isTransient;
515     }
516 
517     /** {@inheritDoc} */
518     @Override
519     public void setTransient(final boolean isTransient)
520     {
521         this.isTransient = isTransient;
522     }
523 
524     /** {@inheritDoc} */
525     @Override
526     public void clearInitialState()
527     {
528         _initialStateMarked = false;
529     }
530 
531     /** {@inheritDoc} */
532     @Override
533     public boolean initialStateMarked()
534     {
535         return _initialStateMarked;
536     }
537 
538     /** {@inheritDoc} */
539     @Override
540     public void markInitialState()
541     {
542         _initialStateMarked = true;
543     }
544     
545     /**
546      * Note: Before 2.1.5/2.0.11 there was another strategy for this point to minimize
547      * the instances used, but after checking this with a profiler, it is more expensive to
548      * call FacesContext.getCurrentInstance() than create this object for bean validation.
549      * 
550      * Standard MessageInterpolator, as described in the JSR-314 spec.
551      */
552     private static class FacesMessageInterpolator implements MessageInterpolator
553     {
554         private final FacesContext facesContext;
555         private final MessageInterpolator interpolator;
556 
557         public FacesMessageInterpolator(final MessageInterpolator interpolator, final FacesContext facesContext)
558         {
559             this.interpolator = interpolator;
560             this.facesContext = facesContext;
561         }
562 
563         @Override
564         public String interpolate(final String s, final Context context)
565         {
566             Locale locale = facesContext.getViewRoot().getLocale();
567             return interpolator.interpolate(s, context, locale);
568         }
569 
570         @Override
571         public String interpolate(final String s, final Context context, final Locale locale)
572         {
573             return interpolator.interpolate(s, context, locale);
574         }
575     }
576 }
577 
578 final class _ValueReferenceResolver
579 {
580     /**
581      * This method can be used to extract the ValueReference from the given ValueExpression.
582      *
583      * @param valueExpression The ValueExpression to resolve.
584      * @param elCtx The ELContext, needed to parse and execute the expression.
585      * @return The ValueReferenceWrapper.
586      */
587     public static ValueReference resolve(ValueExpression valueExpression, final ELContext elCtx)
588     {
589         ValueReference valueReference = valueExpression.getValueReference(elCtx);
590         
591         while (valueReference != null && valueReference.getBase() instanceof CompositeComponentExpressionHolder)
592         {
593             valueExpression = ((CompositeComponentExpressionHolder) valueReference.getBase())
594                                   .getExpression((String) valueReference.getProperty());
595             if(valueExpression == null)
596             {
597                 break;
598             }
599             valueReference = valueExpression.getValueReference(elCtx);
600         }
601         
602         return valueReference;
603     }
604 }
605 
606 /**
607  * This ELContext is used to hook into the EL handling, by decorating the
608  * ELResolver chain with a custom ELResolver.
609  */
610 final class _ELContextDecorator extends ELContext
611 {
612     private final ELContext ctx;
613     private final ELResolver interceptingResolver;
614 
615     /**
616      * Only used by ValueExpressionResolver.
617      *
618      * @param elContext The standard ELContext. All method calls, except getELResolver, are delegated to it.
619      * @param interceptingResolver The ELResolver to be returned by getELResolver.
620      */
621     _ELContextDecorator(final ELContext elContext, final ELResolver interceptingResolver)
622     {
623         this.ctx = elContext;
624         this.interceptingResolver = interceptingResolver;
625     }
626 
627     /**
628      * This is the important one, it returns the passed ELResolver.
629      * @return The ELResolver passed into the constructor.
630      */
631     @Override
632     public ELResolver getELResolver()
633     {
634         return interceptingResolver;
635     }
636 
637     // ############################ Standard delegating implementations ############################
638     @Override
639     public FunctionMapper getFunctionMapper()
640     {
641         return ctx.getFunctionMapper();
642     }
643 
644     @Override
645     public VariableMapper getVariableMapper()
646     {
647         return ctx.getVariableMapper();
648     }
649 
650     @Override
651     public void setPropertyResolved(final boolean resolved)
652     {
653         ctx.setPropertyResolved(resolved);
654     }
655 
656     @Override
657     public boolean isPropertyResolved()
658     {
659         return ctx.isPropertyResolved();
660     }
661 
662     @Override
663     public void putContext(final Class key, Object contextObject)
664     {
665         ctx.putContext(key, contextObject);
666     }
667 
668     @Override
669     public Object getContext(final Class key)
670     {
671         return ctx.getContext(key);
672     }
673 
674     @Override
675     public Locale getLocale()
676     {
677         return ctx.getLocale();
678     }
679 
680     @Override
681     public void setLocale(final Locale locale)
682     {
683         ctx.setLocale(locale);
684     }
685 }