View Javadoc

1   /*
2    * $Id: OValValidationInterceptor.java 777502 2009-05-22 12:58:29Z musachy $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  package org.apache.struts2.oval.interceptor;
22  
23  import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
24  import com.opensymphony.xwork2.interceptor.PrefixMethodInvocationUtil;
25  import com.opensymphony.xwork2.ActionInvocation;
26  import com.opensymphony.xwork2.ActionProxy;
27  import com.opensymphony.xwork2.ModelDriven;
28  import com.opensymphony.xwork2.Validateable;
29  import com.opensymphony.xwork2.inject.Inject;
30  import com.opensymphony.xwork2.validator.ValidatorContext;
31  import com.opensymphony.xwork2.validator.DelegatingValidatorContext;
32  import com.opensymphony.xwork2.util.logging.Logger;
33  import com.opensymphony.xwork2.util.logging.LoggerFactory;
34  import com.opensymphony.xwork2.util.ValueStack;
35  import net.sf.oval.Validator;
36  import net.sf.oval.ConstraintViolation;
37  import net.sf.oval.configuration.Configurer;
38  import net.sf.oval.context.FieldContext;
39  import net.sf.oval.context.OValContext;
40  import net.sf.oval.context.MethodReturnValueContext;
41  
42  import java.util.ArrayList;
43  import java.util.Arrays;
44  import java.util.Collections;
45  import java.util.List;
46  import java.lang.reflect.Method;
47  import java.lang.reflect.Field;
48  
49  import org.apache.struts2.oval.annotation.Profiles;
50  import org.apache.commons.lang.xwork.StringUtils;
51  
52  /*
53   This interceptor provides validation using the OVal validation framework
54   */
55  public class OValValidationInterceptor extends MethodFilterInterceptor {
56      private static final Logger LOG = LoggerFactory.getLogger(OValValidationInterceptor.class);
57  
58      protected final static String VALIDATE_PREFIX = "validate";
59      protected final static String ALT_VALIDATE_PREFIX = "validateDo";
60  
61      protected boolean alwaysInvokeValidate = true;
62      protected boolean programmatic = true;
63      protected OValValidationManager validationManager;
64      private boolean validateJPAAnnotations;
65  
66      @Inject
67      public void setValidationManager(OValValidationManager validationManager) {
68          this.validationManager = validationManager;
69      }
70  
71      /***
72       * Enable OVal support fopr JPA
73       */
74      public void setValidateJPAAnnotations(boolean validateJPAAnnotations) {
75          this.validateJPAAnnotations = validateJPAAnnotations;
76      }
77  
78      /***
79       * Determines if {@link com.opensymphony.xwork2.Validateable}'s <code>validate()</code> should be called,
80       * as well as methods whose name that start with "validate". Defaults to "true".
81       *
82       * @param programmatic <tt>true</tt> then <code>validate()</code> is invoked.
83       */
84      public void setProgrammatic(boolean programmatic) {
85          this.programmatic = programmatic;
86      }
87  
88      /***
89       * Determines if {@link com.opensymphony.xwork2.Validateable}'s <code>validate()</code> should always
90       * be invoked. Default to "true".
91       *
92       * @param alwaysInvokeValidate <tt>true</tt> then <code>validate()</code> is always invoked.
93       */
94      public void setAlwaysInvokeValidate(String alwaysInvokeValidate) {
95          this.alwaysInvokeValidate = Boolean.parseBoolean(alwaysInvokeValidate);
96      }
97  
98      protected String doIntercept(ActionInvocation invocation) throws Exception {
99          Object action = invocation.getAction();
100         ActionProxy proxy = invocation.getProxy();
101         ValueStack valueStack = invocation.getStack();
102         String methodName = proxy.getMethod();
103         String context = proxy.getConfig().getName();
104 
105         if (LOG.isDebugEnabled()) {
106             LOG.debug("Validating [#0/#1] with method [#2]", invocation.getProxy().getNamespace(), invocation.getProxy().getActionName(), methodName);
107         }
108 
109         //OVal vallidatio (no XML yet)
110         performOValValidation(action, valueStack, methodName, context);
111 
112         //Validatable.valiedate() and validateX()
113         performProgrammaticValidation(invocation, action);
114 
115         return invocation.invoke();
116     }
117 
118     private void performProgrammaticValidation(ActionInvocation invocation, Object action) throws Exception {
119         if (action instanceof Validateable && programmatic) {
120             // keep exception that might occured in validateXXX or validateDoXXX
121             Exception exception = null;
122 
123             Validateable validateable = (Validateable) action;
124             if (LOG.isDebugEnabled()) {
125                 LOG.debug("Invoking validate() on action [#0]", validateable.toString());
126             }
127 
128             try {
129                 PrefixMethodInvocationUtil.invokePrefixMethod(
130                         invocation,
131                         new String[]{VALIDATE_PREFIX, ALT_VALIDATE_PREFIX});
132             } catch (Exception e) {
133                 // If any exception occurred while doing reflection, we want
134                 // validate() to be executed
135                 LOG.warn("An exception occured while executing the prefix method", e);
136                 exception = e;
137             }
138 
139             if (alwaysInvokeValidate) {
140                 validateable.validate();
141             }
142 
143             if (exception != null) {
144                 // rethrow if something is wrong while doing validateXXX / validateDoXXX
145                 throw exception;
146             }
147         }
148     }
149 
150     protected void performOValValidation(Object action, ValueStack valueStack, String methodName, String context) throws NoSuchMethodException {
151         Class clazz = action.getClass();
152         //read validation from xmls
153         List<Configurer> configurers = validationManager.getConfigurers(clazz, context, validateJPAAnnotations);
154 
155         Validator validator = configurers.isEmpty() ? new Validator() : new Validator(configurers);
156         //if the method is annotated with a @Profiles annotation, use those profiles
157         Method method = clazz.getMethod(methodName, new Class[0]);
158         if (method != null) {
159             Profiles profiles = method.getAnnotation(Profiles.class);
160             if (profiles != null) {
161                 String[] profileNames = profiles.value();
162                 if (profileNames != null && profileNames.length > 0) {
163                     validator.disableAllProfiles();
164                     if (LOG.isDebugEnabled())
165                         LOG.debug("Enabling profiles [#0]", StringUtils.join(profileNames, ","));
166                     for (String profileName : profileNames)
167                         validator.enableProfile(profileName);
168                 }
169             }
170         }
171 
172         //perform validation
173         List<ConstraintViolation> violations = validator.validate(action);
174         addValidationErrors(violations.toArray(new ConstraintViolation[0]), action, valueStack, null);
175     }
176 
177 	private void addValidationErrors(ConstraintViolation[] violations, Object action, ValueStack valueStack, String parentFieldname) {
178 		if (violations != null) {
179             ValidatorContext validatorContext = new DelegatingValidatorContext(action);
180             for (ConstraintViolation violation : violations) {
181                 //translate message
182                 String key = violation.getMessage();
183 
184                 //push the validator into the stack
185                 valueStack.push(violation.getContext());
186                 String message = key;
187                 try {
188                     message = validatorContext.getText(key);
189                 } finally {
190                     valueStack.pop();
191                 }
192 
193                 if (isActionError(violation)) {
194                 	LOG.debug("Adding action error '#0'", message);
195                     validatorContext.addActionError(message);
196                 } else {
197                     ValidationError validationError = buildValidationError(violation, message);
198 
199                     // build field name
200                     String fieldName = validationError.getFieldName();
201                     if (parentFieldname != null) {
202                     	fieldName = parentFieldname + "." + fieldName;
203                     }
204 
205                     LOG.debug("Adding field error [#0] with message '#1'", fieldName, validationError.getMessage());
206                     validatorContext.addFieldError(fieldName, validationError.getMessage());
207 
208                     // don't add "model." prefix to fields of model in model driven action
209                     if ((action instanceof ModelDriven) && "model".equals(fieldName)) {
210                     	fieldName = null;
211                     }
212 
213                     // add violations of member object fields
214                     addValidationErrors(violation.getCauses(), action, valueStack, fieldName);
215                 }
216             }
217         }
218 	}
219 
220 
221 
222 
223     /***
224      * Get field name and message, used to add the validation error to fieldErrors
225      */
226     protected ValidationError buildValidationError(ConstraintViolation violation, String message) {
227         OValContext context = violation.getContext();
228         if (context instanceof FieldContext) {
229             Field field = ((FieldContext) context).getField();
230             String className = field.getDeclaringClass().getName();
231 
232             //the default OVal message shows the field name as ActionClass.fieldName
233             String finalMessage = StringUtils.removeStart(message, className + ".");
234 
235             return new ValidationError(field.getName(), finalMessage);
236         } else if (context instanceof MethodReturnValueContext) {
237             Method method = ((MethodReturnValueContext) context).getMethod();
238             String className = method.getDeclaringClass().getName();
239             String methodName = method.getName();
240 
241             //the default OVal message shows the field name as ActionClass.fieldName
242             String finalMessage = StringUtils.removeStart(message, className + ".");
243 
244             String fieldName = null;
245             if (methodName.startsWith("get")) {
246                 fieldName = StringUtils.uncapitalize(StringUtils.removeStart(methodName, "get"));
247             } else if (methodName.startsWith("is")) {
248                 fieldName = StringUtils.uncapitalize(StringUtils.removeStart(methodName, "is"));
249             }
250 
251             //the result will have the full method name, like "getName()", replace it by "name" (obnly if it is a field)
252             if (fieldName != null)
253                 finalMessage = finalMessage.replaceAll(methodName + "//(.*?//)", fieldName);
254 
255             return new ValidationError(StringUtils.defaultString(fieldName, methodName), finalMessage);
256         }
257 
258         return new ValidationError(violation.getCheckName(), message);
259     }
260 
261     /***
262      * Decide if a violation should be added to the fieldErrors or actionErrors
263      */
264     protected boolean isActionError(ConstraintViolation violation) {
265         return false;
266     }
267 
268     class ValidationError {
269         private String fieldName;
270         private String message;
271 
272         ValidationError(String fieldName, String message) {
273             this.fieldName = fieldName;
274             this.message = message;
275         }
276 
277         public String getFieldName() {
278             return fieldName;
279         }
280 
281         public String getMessage() {
282             return message;
283         }
284     }
285 }