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  
20  package org.apache.myfaces.component.validate;
21  
22  import java.io.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.IOException;
25  import java.io.ObjectOutputStream;
26  import java.io.Serializable;
27  import java.lang.reflect.Constructor;
28  import java.lang.reflect.Method;
29  import java.security.AccessController;
30  import java.security.PrivilegedActionException;
31  import java.security.PrivilegedExceptionAction;
32  import java.util.ArrayList;
33  import java.util.LinkedHashSet;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Map;
37  import java.util.Set;
38  import java.util.logging.Level;
39  import java.util.logging.Logger;
40  import javax.el.ELContext;
41  import javax.el.ValueExpression;
42  import javax.el.ValueReference;
43  import javax.faces.FacesException;
44  import javax.faces.application.FacesMessage;
45  import javax.faces.component.UIComponent;
46  import javax.faces.component.visit.VisitCallback;
47  import javax.faces.component.visit.VisitContext;
48  import javax.faces.component.visit.VisitResult;
49  import javax.faces.context.FacesContext;
50  import javax.faces.validator.BeanValidator;
51  import static javax.faces.validator.BeanValidator.EMPTY_VALIDATION_GROUPS_PATTERN;
52  import static javax.faces.validator.BeanValidator.MESSAGE_ID;
53  import static javax.faces.validator.BeanValidator.VALIDATION_GROUPS_DELIMITER;
54  import static javax.faces.validator.BeanValidator.VALIDATOR_FACTORY_KEY;
55  import javax.faces.validator.Validator;
56  import javax.faces.validator.ValidatorException;
57  import javax.validation.ConstraintViolation;
58  import javax.validation.MessageInterpolator;
59  import javax.validation.Validation;
60  import javax.validation.ValidatorFactory;
61  import javax.validation.groups.Default;
62  import javax.validation.metadata.BeanDescriptor;
63  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFProperty;
64  import org.apache.myfaces.shared.util.MessageUtils;
65  import org.apache.myfaces.shared.util.MyFacesObjectInputStream;
66  import org.apache.myfaces.util.ExternalSpecifications;
67  
68  /**
69   *
70   */
71  public class WholeBeanValidator implements Validator
72  {
73      private static final Logger log = Logger.getLogger(WholeBeanValidator.class.getName());
74      
75      private static final Class<?>[] DEFAULT_VALIDATION_GROUPS_ARRAY = new Class<?>[] { Default.class };
76  
77      private static final String DEFAULT_VALIDATION_GROUP_NAME = "javax.validation.groups.Default";
78      
79      private static final String CANDIDATE_COMPONENT_VALUES_MAP = "oam.WBV.candidatesMap";
80      
81      private static final String BEAN_VALIDATION_FAILED = "oam.WBV.validationFailed";
82  
83      private Class<?>[] validationGroupsArray;
84  
85      @Override
86      public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException
87      {
88          if (context == null)
89          {
90              throw new NullPointerException("context");
91          }
92          if (component == null)
93          {
94              throw new NullPointerException("component");
95          }
96  
97          ValueExpression valueExpression = component.getValueExpression("value");
98          if (valueExpression == null)
99          {
100             log.warning("cannot validate component with empty value: " 
101                     + component.getClientId(context));
102             return;
103         }
104 
105         Object base = valueExpression.getValue(context.getELContext());
106                 
107         Class<?> valueBaseClass = base.getClass();
108         if (valueBaseClass == null)
109         {
110             return;
111         }
112 
113         // Initialize Bean Validation.
114         ValidatorFactory validatorFactory = createValidatorFactory(context);
115         javax.validation.Validator validator = createValidator(validatorFactory, context, 
116                 (ValidateWholeBeanComponent)component);
117         BeanDescriptor beanDescriptor = validator.getConstraintsForClass(valueBaseClass);
118         if (!beanDescriptor.isBeanConstrained())
119         {
120             return;
121         }
122         
123         // Note that validationGroupsArray was initialized when createValidator was called
124         Class[] validationGroupsArray = this.validationGroupsArray;
125 
126         // Delegate to Bean Validation.
127         
128         // TODO: Use validator.validate(...) over the copy instance.
129         
130         Boolean beanValidationFailed = (Boolean) context.getViewRoot().getTransientStateHelper()
131                 .getTransient(BEAN_VALIDATION_FAILED);
132         
133         if (Boolean.TRUE.equals(beanValidationFailed))
134         {
135             // JSF 2.3 Skip class level validation
136             return;
137         }
138         
139         Map<String, Object> candidatesMap = (Map<String, Object>) context.getViewRoot()
140                 .getTransientStateHelper().getTransient(CANDIDATE_COMPONENT_VALUES_MAP);
141         if (candidatesMap != null)
142         {
143             Object copy = createBeanCopy(base);
144             
145             UpdateBeanCopyCallback callback = new UpdateBeanCopyCallback(this, base, copy, candidatesMap);
146             context.getViewRoot().visitTree(
147                     VisitContext.createVisitContext(context, candidatesMap.keySet(), null), 
148                     callback);
149             
150             Set constraintViolations = validator.validate(copy, validationGroupsArray);
151             if (!constraintViolations.isEmpty())
152             {
153                 Set<FacesMessage> messages = new LinkedHashSet<FacesMessage>(constraintViolations.size());
154                 for (Object violation: constraintViolations)
155                 {
156                     ConstraintViolation constraintViolation = (ConstraintViolation) violation;
157                     String message = constraintViolation.getMessage();
158                     Object[] args = new Object[]{ message, MessageUtils.getLabel(context, component) };
159                     FacesMessage msg = MessageUtils.getMessage(FacesMessage.SEVERITY_ERROR, MESSAGE_ID, args, context);
160                     messages.add(msg);
161                 }
162                 throw new ValidatorException(messages);
163             }
164         }
165     }
166     
167     private Object createBeanCopy(Object base)
168     {
169         Object copy = null;
170         try
171         {
172             copy = base.getClass().newInstance();
173         }
174         catch (Exception ex)
175         {
176             Logger.getLogger(WholeBeanValidator.class.getName()).log(Level.FINEST, null, ex);
177         }
178         
179         if (base instanceof Serializable)
180         {
181             copy = copySerializableObject(base);
182         }
183         else if(base instanceof Cloneable)
184         { 
185             Method cloneMethod;
186             try
187             {
188                 cloneMethod = base.getClass().getMethod("clone");
189                 copy = cloneMethod.invoke(base);
190             }
191             catch (Exception ex) 
192             {
193                 Logger.getLogger(WholeBeanValidator.class.getName()).log(Level.FINEST, null, ex);
194             }
195         }
196         else
197         {
198             Class<?> clazz = base.getClass();
199             try
200             {
201                 Constructor<?> copyConstructor = clazz.getConstructor(clazz);
202                 if (copyConstructor != null)
203                 {
204                     copy = copyConstructor.newInstance(base);
205                 }
206             }
207             catch (Exception ex)
208             {
209                 Logger.getLogger(WholeBeanValidator.class.getName()).log(Level.FINEST, null, ex);
210             }
211         }
212         
213         if (copy == null)
214         {
215             throw new FacesException("Cannot create copy for wholeBeanValidator: "+base.getClass().getName());
216         }
217         
218         return copy;
219     }
220     
221     private Object copySerializableObject(Object base)
222     {
223         Object copy = null;
224         try 
225         {
226             ByteArrayOutputStream baos = new ByteArrayOutputStream();
227             ObjectOutputStream oos = new ObjectOutputStream(baos);
228             oos.writeObject(base);
229             oos.flush();
230             oos.close();
231             baos.close();
232             byte[] byteData = baos.toByteArray();
233             ByteArrayInputStream bais = new ByteArrayInputStream(byteData);
234             try 
235             {
236                 copy = new MyFacesObjectInputStream(bais).readObject();
237             }
238             catch (ClassNotFoundException e)
239             {
240                 //e.printStackTrace();
241             }
242         }
243         catch (IOException e) 
244         {
245             //e.printStackTrace();
246         }
247         return copy;
248     }    
249     
250     private javax.validation.Validator createValidator(final ValidatorFactory validatorFactory, 
251             FacesContext context, ValidateWholeBeanComponent component)
252     {
253         // Set default validation group when setValidationGroups has not been called.
254         // The null check is there to prevent it from happening twice.
255         if (validationGroupsArray == null)
256         {
257             postSetValidationGroups(component);
258         }
259 
260         return validatorFactory //
261                 .usingContext() //
262                 .messageInterpolator(new FacesMessageInterpolator(
263                         validatorFactory.getMessageInterpolator(), context)) //
264                 .getValidator();
265 
266     }
267 
268 
269     /**
270      * Get the ValueReference from the ValueExpression.
271      *
272      * @param valueExpression The ValueExpression for value.
273      * @param context The FacesContext.
274      * @return A ValueReferenceWrapper with the necessary information about the ValueReference.
275      */
276     private ValueReference getValueReference(
277             final ValueExpression valueExpression, final FacesContext context)
278     {
279         ELContext elCtx = context.getELContext();
280         
281         return _ValueReferenceResolver.resolve(valueExpression, elCtx);
282     }
283 
284     /**
285      * This method creates ValidatorFactory instances or retrieves them from the container.
286      *
287      * Once created, ValidatorFactory instances are stored in the container under the key
288      * VALIDATOR_FACTORY_KEY for performance.
289      *
290      * @param context The FacesContext.
291      * @return The ValidatorFactory instance.
292      * @throws FacesException if no ValidatorFactory can be obtained because: a) the
293      * container is not a Servlet container or b) because Bean Validation is not available.
294      */
295     private ValidatorFactory createValidatorFactory(FacesContext context)
296     {
297         Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap();
298         Object attr = applicationMap.get(VALIDATOR_FACTORY_KEY);
299         if (attr instanceof ValidatorFactory)
300         {
301             return (ValidatorFactory) attr;
302         }
303         else
304         {
305             synchronized (this)
306             {
307                 if (ExternalSpecifications.isBeanValidationAvailable())
308                 {
309                     ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
310                     applicationMap.put(VALIDATOR_FACTORY_KEY, factory);
311                     return factory;
312                 }
313                 else
314                 {
315                     throw new FacesException("Bean Validation is not present");
316                 }
317             }
318         }
319     }
320 
321     /**
322      * Fully initialize the validation groups if needed.
323      * If no validation groups are specified, the Default validation group is used.
324      */
325     private void postSetValidationGroups(ValidateWholeBeanComponent component)
326     {
327         String validationGroups = getValidationGroups(component);
328         if (validationGroups == null || validationGroups.matches(EMPTY_VALIDATION_GROUPS_PATTERN))
329         {
330             this.validationGroupsArray = DEFAULT_VALIDATION_GROUPS_ARRAY;
331         }
332         else
333         {
334             String[] classes = validationGroups.split(VALIDATION_GROUPS_DELIMITER);
335             List<Class<?>> validationGroupsList = new ArrayList<Class<?>>(classes.length);
336 
337             for (String clazz : classes)
338             {
339                 clazz = clazz.trim();
340                 if (!clazz.isEmpty())
341                 {
342                     Class<?> theClass = null;
343                     ClassLoader cl = null;
344                     if (System.getSecurityManager() != null) 
345                     {
346                         try 
347                         {
348                             cl = AccessController.doPrivileged(new PrivilegedExceptionAction<ClassLoader>()
349                                     {
350                                         public ClassLoader run() throws PrivilegedActionException
351                                         {
352                                             return Thread.currentThread().getContextClassLoader();
353                                         }
354                                     });
355                         }
356                         catch (PrivilegedActionException pae)
357                         {
358                             throw new FacesException(pae);
359                         }
360                     }
361                     else
362                     {
363                         cl = Thread.currentThread().getContextClassLoader();
364                     }
365                     
366                     try
367                     {                        
368                         // Try WebApp ClassLoader first
369                         theClass = Class.forName(clazz,false,cl);
370                     }
371                     catch (ClassNotFoundException ignore)
372                     {
373                         try
374                         {
375                             // fallback: Try ClassLoader for BeanValidator (i.e. the myfaces.jar lib)
376                             theClass = Class.forName(clazz,false, BeanValidator.class.getClassLoader());
377                         }
378                         catch (ClassNotFoundException e)
379                         {
380                             throw new RuntimeException("Could not load validation group", e);
381                         }                        
382                     }
383                     // the class was found
384                     validationGroupsList.add(theClass);
385                 }
386             }
387                     
388             this.validationGroupsArray = validationGroupsList.toArray(new Class[validationGroupsList.size()]);
389         }
390     }
391 
392     /**
393      * Get the Bean Validation validation groups.
394      * @return The validation groups String.
395      */
396     @JSFProperty
397     public String getValidationGroups(ValidateWholeBeanComponent component)
398     {
399         return component.getValidationGroups();
400     }
401 
402     /**
403      * Set the Bean Validation validation groups.
404      * @param validationGroups The validation groups String, separated by
405      *                         {@link BeanValidator#VALIDATION_GROUPS_DELIMITER}.
406      */
407     public void setValidationGroups(ValidateWholeBeanComponent component, final String validationGroups)
408     {
409         component.setValidationGroups(validationGroups);
410     }
411 
412     /**
413      * Note: Before 2.1.5/2.0.11 there was another strategy for this point to minimize
414      * the instances used, but after checking this with a profiler, it is more expensive to
415      * call FacesContext.getCurrentInstance() than create this object for bean validation.
416      * 
417      * Standard MessageInterpolator, as described in the JSR-314 spec.
418      */
419     private static class FacesMessageInterpolator implements MessageInterpolator
420     {
421         private final FacesContext facesContext;
422         private final MessageInterpolator interpolator;
423 
424         public FacesMessageInterpolator(final MessageInterpolator interpolator, final FacesContext facesContext)
425         {
426             this.interpolator = interpolator;
427             this.facesContext = facesContext;
428         }
429 
430         public String interpolate(final String s, final MessageInterpolator.Context context)
431         {
432             Locale locale = facesContext.getViewRoot().getLocale();
433             return interpolator.interpolate(s, context, locale);
434         }
435 
436         public String interpolate(final String s, final MessageInterpolator.Context context, final Locale locale)
437         {
438             return interpolator.interpolate(s, context, locale);
439         }
440     }
441 
442     private static class UpdateBeanCopyCallback implements VisitCallback
443     {
444         private WholeBeanValidator validator;
445         private Object wholeBeanBase;
446         private Object wholeBeanBaseCopy;
447         private Map<String, Object> candidateValuesMap;
448 
449         public UpdateBeanCopyCallback(WholeBeanValidator validator, Object wholeBeanBase, Object wholeBeanBaseCopy,
450                 Map<String, Object> candidateValuesMap)
451         {
452             this.validator = validator;
453             this.wholeBeanBase = wholeBeanBase;
454             this.wholeBeanBaseCopy = wholeBeanBaseCopy;
455             this.candidateValuesMap = candidateValuesMap;
456         }
457 
458         @Override
459         public VisitResult visit(VisitContext context, UIComponent target)
460         {
461             // The idea is follow almost the same algorithm used by Bean Validation. This 
462             // algorithm calculates the base of the ValueExpression used by the component.
463             // Then a simple equals() check will do the trick to decide when to call
464             // setValue and affect the model. If the base is the same than the value returned by
465             // f:validateWholeBean, you are affecting to same instance.
466             
467             ValueExpression valueExpression = target.getValueExpression("value");
468             if (valueExpression == null)
469             {
470                 log.warning("cannot validate component with empty value: " 
471                         + target.getClientId(context.getFacesContext()));
472                 return VisitResult.ACCEPT;
473             }
474 
475             // Obtain a reference to the to-be-validated object and the property name.
476             ValueReference reference = validator.getValueReference(
477                     valueExpression, context.getFacesContext());
478             if (reference == null)
479             {
480                 return VisitResult.ACCEPT;
481             }
482             
483             Object base = reference.getBase();
484             if (base == null)
485             {
486                 return VisitResult.ACCEPT;
487             }
488 
489             Object referenceProperty = reference.getProperty();
490             if (!(referenceProperty instanceof String))
491             {
492                 // if the property is not a String, the ValueReference does not
493                 // point to a bean method, but e.g. to a value in a Map, thus we 
494                 // can exit bean validation here
495                 return VisitResult.ACCEPT;
496             }
497                         
498             // If the base of the EL expression is the same to the base of the one in f:validateWholeBean
499             if (base == this.wholeBeanBase || base.equals(this.wholeBeanBase))
500             {
501                 // Do the trick over ELResolver and apply it to the copy.
502                 ELContext elCtxDecorator = new _ELContextDecorator(context.getFacesContext().getELContext(),
503                         new CopyBeanInterceptorELResolver(context.getFacesContext().getApplication().getELResolver(),
504                             this.wholeBeanBase, this.wholeBeanBaseCopy));
505                 
506                 valueExpression.setValue(elCtxDecorator, candidateValuesMap.get(
507                         target.getClientId(context.getFacesContext())));
508             }
509             
510             return VisitResult.ACCEPT;
511         }
512     }
513 }