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 org.apache.myfaces.custom.validatebeanbehavior;
20  
21  import java.lang.annotation.Annotation;
22  import java.lang.reflect.InvocationTargetException;
23  import java.lang.reflect.Method;
24  import java.text.SimpleDateFormat;
25  import java.util.ArrayList;
26  import java.util.Calendar;
27  import java.util.Date;
28  import java.util.List;
29  import java.util.Set;
30  
31  import javax.el.ELContext;
32  import javax.el.ValueExpression;
33  import javax.el.ValueReference;
34  import javax.faces.FacesException;
35  import javax.faces.application.ResourceDependency;
36  import javax.faces.component.UICommand;
37  import javax.faces.component.UIComponent;
38  import javax.faces.component.UIForm;
39  import javax.faces.component.UIInput;
40  import javax.faces.component.UIMessages;
41  import javax.faces.component.UIViewRoot;
42  import javax.faces.component.behavior.ClientBehaviorBase;
43  import javax.faces.component.behavior.ClientBehaviorContext;
44  import javax.faces.context.FacesContext;
45  import javax.faces.convert.Converter;
46  import javax.faces.convert.DateTimeConverter;
47  import javax.servlet.ServletContext;
48  import javax.validation.Validation;
49  import javax.validation.ValidatorFactory;
50  import javax.validation.constraints.Future;
51  import javax.validation.constraints.Max;
52  import javax.validation.constraints.Min;
53  import javax.validation.constraints.NotNull;
54  import javax.validation.metadata.BeanDescriptor;
55  import javax.validation.metadata.ConstraintDescriptor;
56  import javax.validation.metadata.PropertyDescriptor;
57  
58  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFClientBehavior;
59  
60  /**
61   * Behavior for Bean Validation validations in JavaScript.
62   * <p/>
63   * This class can be attached to UIComponent instances and validate the entire form.
64   * Any UIMessages instances in the form are looked up and used for positioning error messages.
65   *
66   * @author Jan-Kees van Andel
67   */
68  @JSFClientBehavior(
69          name="s:validateBean",
70          id="org.apache.myfaces.custom.ValidateBeanBehavior")
71  @ResourceDependency(name = "validateBeanBehavior.js")
72  public class ValidateBeanBehavior extends ClientBehaviorBase {
73  
74      /** {@inheritDoc} */
75      @Override
76      public String getScript(final ClientBehaviorContext ctx) {
77          final UIComponent component = ctx.getComponent();
78          if (!(component instanceof UICommand)) {
79              throw new FacesException("Unsupported component: " + component + " Only UICommand components are supported");
80          }
81          final UIViewRoot viewRoot = ctx.getFacesContext().getViewRoot();
82  
83          UIForm form = ComponentUtils.findParentForm(component);
84          List<UIInput> inputsInForm = ComponentUtils.findInputsInForm(form);
85          UIMessages messages = ComponentUtils.findMessagesInTree(viewRoot);
86  
87          return getSubmitHandler(form, inputsInForm, messages);
88      }
89  
90      /**
91       * Fetch all necessary metadata and write a JavaScript submit handler to the client.
92       *
93       * @param form The form which must be validated.
94       * @param inputsInForm The input fields in the form.
95       * @param messages The messages to use for displaying error messages.
96       * @return The JavaScript submit handler call.
97       */
98      private String getSubmitHandler(final UIForm form, final List<UIInput> inputsInForm, final UIMessages messages) {
99          final FacesContext fc = FacesContext.getCurrentInstance();
100 
101         final String clientId = form.getClientId(fc);
102         final String messagesId = (messages != null) ? "'" + messages.getClientId(fc) + "'": "null";
103         final List<ValidationPropertyModel> validations = new ArrayList<ValidationPropertyModel>();
104 
105         for (final UIInput input : inputsInForm) {
106             final PropertyDescriptor propertyDescriptor = getPropertyDescriptor(fc, input);
107             final Set<ConstraintDescriptor<?>> constraints = propertyDescriptor.getConstraintDescriptors();
108             final ValidationPropertyModel model = createValidationModel(input, propertyDescriptor, constraints);
109             validations.add(model);
110         }
111 
112         return writeJavaScript(fc, clientId, messagesId, validations);
113     }
114 
115     /**
116      * Write the JavaScript submit handler to the client.
117      *
118      * @param fc The FacesContext.
119      * @param clientId The client ID of the form.
120      * @param messagesId The client ID of the messages component.
121      * @param validations The list with field validations.
122      * @return The JavaScript event handler string.
123      */
124     private String writeJavaScript(final FacesContext fc, final String clientId,
125                                    final String messagesId, final List<ValidationPropertyModel> validations) {
126         final StringBuilder sb = new StringBuilder();
127         sb.append("return org.jkva.validateBean.validateForm(");
128         sb.append("'").append(clientId).append("', ");
129         sb.append("'").append(messagesId).append("', ");
130         sb.append("[");
131         String sep = "";
132 
133         for (final ValidationPropertyModel validationModel : validations) {
134             sb.append(sep);
135             final String validation = writeJavaScriptForField(validationModel, fc);
136             sb.append(validation);
137             sep = ",";
138         }
139 
140         sb.append("])");
141         return sb.toString();
142     }
143 
144     /**
145      * Write a JSON object with all validation metadata for the given field.
146      *
147      * @param model The validation metadata for a field.
148      * @param fc The FacesContext.
149      * @return The JavaScript event handler string, as a JSON object.
150      */
151     private String writeJavaScriptForField(final ValidationPropertyModel model, final FacesContext fc) {
152         final StringBuilder sb = new StringBuilder();
153         sb.append("{");
154         sb.append("fieldId: '").append(model.getComponent().getClientId(fc)).append("'");
155         if (model.isRequired()) {
156             sb.append(",required: true");
157         }
158         sb.append(",type: '").append(model.getType()).append("'");
159         if (model.getMin() != null) {
160             sb.append(",min: true");
161             sb.append(",minValue: ").append(model.getMin()).append("");
162         }
163         if (model.getMax() != null) {
164             sb.append(",max: true");
165             sb.append(",maxValue: ").append(model.getMax()).append("");
166         }
167         if (model.isFutureDate()) {
168             String nowStr = new SimpleDateFormat(model.getDateFormat()).format(new Date());
169             sb.append(",future: true");
170             sb.append(",nowStr: '").append(nowStr).append("'");
171             sb.append(",dateFormat: '").append(model.getDateFormat()).append("'");
172         }
173         sb.append("}");
174 
175         return sb.toString();
176     }
177 
178     /**
179      * Create the Validation metadata, by inspecting the component, managed bean and {ConstraintDescriptor}s.
180      *
181      * @param component The input component.
182      * @param propertyDescriptor The property descriptor
183      * @param constraints The constraint descriptors.
184      * @return The validation metadata.
185      */
186     private ValidationPropertyModel createValidationModel(final UIComponent component,
187                                                           final PropertyDescriptor propertyDescriptor,
188                                                           final Set<ConstraintDescriptor<?>> constraints) {
189         final ValidationPropertyModel model = new ValidationPropertyModel();
190         model.setComponent(component);
191 
192         if (component instanceof UIInput) {
193             model.setRequired(((UIInput) component).isRequired());
194         }
195 
196         final Class<?> type = propertyDescriptor.getElementClass();
197         if (type.equals(String.class)) {
198             model.setType("text");
199         } else if (Number.class.isAssignableFrom(type)) {
200             model.setType("numeric");
201         } else if (Date.class.isAssignableFrom(type)
202                 || Calendar.class.isAssignableFrom(type)) {
203             model.setType("date");
204         }
205 
206         for (final ConstraintDescriptor<?> constraint : constraints) {
207             final Annotation annotation = constraint.getAnnotation();
208             if (annotation instanceof NotNull) {
209                 model.setRequired(true);
210             } else if (annotation instanceof Min) {
211                 model.setMin(((Min) annotation).value());
212             } else if (annotation instanceof Max) {
213                 model.setMax(((Max) annotation).value());
214             } else if (annotation instanceof Future) {
215                 model.setFutureDate(true);
216             }
217         }
218 
219         final Converter converter = ((UIInput) component).getConverter();
220         if (converter instanceof DateTimeConverter) {
221             model.setDateFormat(((DateTimeConverter) converter).getPattern());
222         }
223 
224         return model;
225     }
226 
227     // Copied from MyFaces Core 2.0.
228     private PropertyDescriptor getPropertyDescriptor(final FacesContext fc, final UIComponent component) {
229         final ValueReferenceWrapper reference = getValueReference(component, fc);
230 
231         if (reference != null) {
232             final Object base = reference.getBase();
233             if (base != null) {
234                 final Class<?> valueBaseClass = base.getClass();
235                 final String valueProperty = (String) reference.getProperty();
236                 if (valueBaseClass != null && valueProperty != null) {
237                     // Initialize Bean Validation.
238                     final ValidatorFactory validatorFactory = createValidatorFactory(fc);
239                     final javax.validation.Validator validator = createValidator(validatorFactory);
240                     final BeanDescriptor beanDescriptor = validator.getConstraintsForClass(valueBaseClass);
241                     if (beanDescriptor.isBeanConstrained()) {
242                         return beanDescriptor.getConstraintsForProperty(valueProperty);
243                     }
244                 }
245             }
246         }
247 
248         return null;
249     }
250 
251     // Copied from MyFaces Core 2.0.
252     private javax.validation.Validator createValidator(final ValidatorFactory validatorFactory) {
253         return validatorFactory //
254                 .usingContext() //
255                 .messageInterpolator(FacesMessageInterpolatorHolder.get(validatorFactory)) //
256                 .getValidator();
257 
258     }
259 
260     // Copied from MyFaces Core 2.0.
261     private ValueReferenceWrapper getValueReference(final UIComponent component, final FacesContext context) {
262         final ValueExpression valueExpression = component.getValueExpression("value");
263         final ELContext elCtx = context.getELContext();
264         if (ExternalSpecifications.isUnifiedELAvailable()) {
265             final ValueReference valueReference = getUELValueReference(valueExpression, elCtx);
266             if (valueReference == null) {
267                 return null;
268             }
269             return new ValueReferenceWrapper(valueReference.getBase(), valueReference.getProperty());
270         } else {
271             return ValueReferenceResolver.resolve(valueExpression, elCtx);
272         }
273     }
274 
275     // Copied from MyFaces Core 2.0.
276     private ValueReference getUELValueReference(final ValueExpression valueExpression, final ELContext elCtx) {
277         final String methodName = "getValueReference";
278         final String methodSignature = valueExpression.getClass().getName() +
279                 "." + methodName +
280                 "(" + ELContext.class + ")";
281         try {
282             final Method method = valueExpression.getClass().getMethod(methodName, ELContext.class);
283             if (!ValueReference.class.equals(method.getReturnType())
284                     && !ValueReference.class.isAssignableFrom(method.getReturnType())) {
285                 throw new NoSuchMethodException(
286                         methodSignature +
287                                 "doesn't return " + ValueReference.class +
288                                 ", but " + method.getReturnType());
289             }
290             return (ValueReference) method.invoke(valueExpression, elCtx);
291         } catch (NoSuchMethodException e) {
292             throw new FacesException(
293                     "MyFaces indicates Unified EL is available, but method: " +
294                             methodSignature +
295                             " is not available", e);
296         } catch (InvocationTargetException e) {
297             throw new FacesException("Exception invoking " + methodSignature, e);
298         } catch (IllegalAccessException e) {
299             throw new FacesException("Exception invoking " + methodSignature, e);
300         }
301     }
302 
303     // Copied from MyFaces Core 2.0.
304     private synchronized ValidatorFactory createValidatorFactory(final FacesContext context) {
305         final Object ctx = context.getExternalContext().getContext();
306         if (ctx instanceof ServletContext) {
307             final ServletContext servletCtx = (ServletContext) ctx;
308             final Object attr = servletCtx.getAttribute(VALIDATOR_FACTORY_KEY);
309             if (attr != null) {
310                 return (ValidatorFactory) attr;
311             } else {
312                 if (ExternalSpecifications.isBeanValidationAvailable()) {
313                     final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
314                     servletCtx.setAttribute(VALIDATOR_FACTORY_KEY, attr);
315                     return factory;
316                 } else {
317                     throw new FacesException(
318                             "Bean Validation (API or implementation) is not present, but required for " +
319                                     this.getClass().getSimpleName());
320                 }
321             }
322         } else {
323             throw new FacesException("Only Servlet environments are supported for " +
324                     this.getClass().getSimpleName());
325         }
326     }
327 
328     // Copied from MyFaces Core 2.0.
329     public static final String VALIDATOR_FACTORY_KEY = "javax.faces.validator.beanValidator.ValidatorFactory";
330 }
331