1 /*
2 * Copyright (c) 2002-2006 by OpenSymphony
3 * All rights reserved.
4 */
5 package com.opensymphony.xwork2.validator;
6
7
8 import com.opensymphony.xwork2.ActionContext;
9 import com.opensymphony.xwork2.ActionInvocation;
10 import com.opensymphony.xwork2.ActionProxy;
11 import com.opensymphony.xwork2.inject.Inject;
12 import com.opensymphony.xwork2.util.FileManager;
13 import com.opensymphony.xwork2.util.ValueStack;
14 import com.opensymphony.xwork2.util.logging.Logger;
15 import com.opensymphony.xwork2.util.logging.LoggerFactory;
16 import com.opensymphony.xwork2.validator.validators.VisitorFieldValidator;
17
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.util;
21
22 /**
23 * AnnotationActionValidatorManager is the entry point into XWork's annotations-based validator framework.
24 * Validation rules are specified as annotations within the source files.
25 *
26 * @author Rainer Hermanns
27 * @author jepjep
28 */
29 public class AnnotationActionValidatorManager implements ActionValidatorManager {
30
31 /**
32 * The file suffix for any validation file.
33 */
34 protected static final String VALIDATION_CONFIG_SUFFIX = "-validation.xml";
35
36 private final Map<String, List<ValidatorConfig>> validatorCache = Collections.synchronizedMap(new HashMap<String, List<ValidatorConfig>>());
37 private final Map<String, List<ValidatorConfig>> validatorFileCache = Collections.synchronizedMap(new HashMap<String, List<ValidatorConfig>>());
38 private static final Logger LOG = LoggerFactory.getLogger(AnnotationActionValidatorManager.class);
39
40 private ValidatorFactory validatorFactory;
41 private ValidatorFileParser validatorFileParser;
42
43 @Inject
44 public void setValidatorFactory(ValidatorFactory fac) {
45 this.validatorFactory = fac;
46 }
47
48 @Inject
49 public void setValidatorFileParser(ValidatorFileParser parser) {
50 this.validatorFileParser = parser;
51 }
52
53 public synchronized List<Validator> getValidators(Class clazz, String context) {
54 return getValidators(clazz, context, null);
55 }
56
57 public synchronized List<Validator> getValidators(Class clazz, String context, String method) {
58 final String validatorKey = buildValidatorKey(clazz);
59
60 if (validatorCache.containsKey(validatorKey)) {
61 if (FileManager.isReloadingConfigs()) {
62 validatorCache.put(validatorKey, buildValidatorConfigs(clazz, context, true, null));
63 }
64 } else {
65 validatorCache.put(validatorKey, buildValidatorConfigs(clazz, context, false, null));
66 }
67
68 // get the set of validator configs
69 List<ValidatorConfig> cfgs = validatorCache.get(validatorKey);
70
71 ValueStack stack = ActionContext.getContext().getValueStack();
72
73 // create clean instances of the validators for the caller's use
74 ArrayList<Validator> validators = new ArrayList<Validator>(cfgs.size());
75 for (ValidatorConfig cfg : cfgs) {
76 if (method == null || method.equals(cfg.getParams().get("methodName"))) {
77 Validator validator = validatorFactory.getValidator(
78 new ValidatorConfig.Builder(cfg)
79 .removeParam("methodName")
80 .build());
81 validator.setValidatorType(cfg.getType());
82 validator.setValueStack(stack);
83 validators.add(validator);
84 }
85 }
86
87 return validators;
88 }
89
90 public void validate(Object object, String context) throws ValidationException {
91 validate(object, context, (String) null);
92 }
93
94 public void validate(Object object, String context, String method) throws ValidationException {
95 ValidatorContext validatorContext = new DelegatingValidatorContext(object);
96 validate(object, context, validatorContext, method);
97 }
98
99 public void validate(Object object, String context, ValidatorContext validatorContext) throws ValidationException {
100 validate(object, context, validatorContext, null);
101 }
102
103 public void validate(Object object, String context, ValidatorContext validatorContext, String method) throws ValidationException {
104 List<Validator> validators = getValidators(object.getClass(), context, method);
105 Set<String> shortcircuitedFields = null;
106
107 for (final Validator validator: validators) {
108 try {
109 validator.setValidatorContext(validatorContext);
110
111 if (LOG.isDebugEnabled()) {
112 LOG.debug("Running validator: " + validator + " for object " + object + " and method " + method);
113 }
114
115 FieldValidator fValidator = null;
116 String fullFieldName = null;
117
118 if (validator instanceof FieldValidator) {
119 fValidator = (FieldValidator) validator;
120 fullFieldName = new InternalValidatorContextWrapper(fValidator.getValidatorContext()).getFullFieldName(fValidator.getFieldName());
121
122 if ((shortcircuitedFields != null) && shortcircuitedFields.contains(fullFieldName)) {
123 if (LOG.isDebugEnabled()) {
124 LOG.debug("Short-circuited, skipping");
125 }
126
127 continue;
128 }
129 }
130
131 if (validator instanceof ShortCircuitableValidator && ((ShortCircuitableValidator) validator).isShortCircuit())
132 {
133 // get number of existing errors
134 List<String> errs = null;
135
136 if (fValidator != null) {
137 if (validatorContext.hasFieldErrors()) {
138 Collection<String> fieldErrors = validatorContext.getFieldErrors().get(fullFieldName);
139
140 if (fieldErrors != null) {
141 errs = new ArrayList<String>(fieldErrors);
142 }
143 }
144 } else if (validatorContext.hasActionErrors()) {
145 Collection<String> actionErrors = validatorContext.getActionErrors();
146
147 if (actionErrors != null) {
148 errs = new ArrayList<String>(actionErrors);
149 }
150 }
151
152 validator.validate(object);
153
154 if (fValidator != null) {
155 if (validatorContext.hasFieldErrors()) {
156 Collection<String> errCol = validatorContext.getFieldErrors().get(fullFieldName);
157
158 if ((errCol != null) && !errCol.equals(errs)) {
159 if (LOG.isDebugEnabled()) {
160 LOG.debug("Short-circuiting on field validation");
161 }
162
163 if (shortcircuitedFields == null) {
164 shortcircuitedFields = new TreeSet<String>();
165 }
166
167 shortcircuitedFields.add(fullFieldName);
168 }
169 }
170 } else if (validatorContext.hasActionErrors()) {
171 Collection<String> errCol = validatorContext.getActionErrors();
172
173 if ((errCol != null) && !errCol.equals(errs)) {
174 if (LOG.isDebugEnabled()) {
175 LOG.debug("Short-circuiting");
176 }
177
178 break;
179 }
180 }
181
182 continue;
183 }
184
185 validator.validate(object);
186 } finally {
187 validator.setValidatorContext( null );
188 }
189
190 }
191 }
192
193 /**
194 * Builds a key for validators - used when caching validators.
195 *
196 * @param clazz the action.
197 * @return a validator key which is the class name plus context.
198 */
199 protected static String buildValidatorKey(Class clazz) {
200 ActionInvocation invocation = ActionContext.getContext().getActionInvocation();
201 ActionProxy proxy = invocation.getProxy();
202
203 //the key needs to use the name of the action from the config file,
204 //instead of the url, so wild card actions will have the same validator
205 //see WW-2996
206 StringBuilder sb = new StringBuilder(clazz.getName());
207 sb.append("/");
208 sb.append(proxy.getConfig().getName());
209 sb.append("|");
210 sb.append(proxy.getMethod());
211 return sb.toString();
212 }
213
214 private List<ValidatorConfig> buildAliasValidatorConfigs(Class aClass, String context, boolean checkFile) {
215 String fileName = aClass.getName().replace('.', '/') + "-" + context.replace('/', '-') + VALIDATION_CONFIG_SUFFIX;
216
217 return loadFile(fileName, aClass, checkFile);
218 }
219
220
221 protected List<ValidatorConfig> buildClassValidatorConfigs(Class aClass, boolean checkFile) {
222
223 String fileName = aClass.getName().replace('.', '/') + VALIDATION_CONFIG_SUFFIX;
224
225 List<ValidatorConfig> result = new ArrayList<ValidatorConfig>(loadFile(fileName, aClass, checkFile));
226
227 AnnotationValidationConfigurationBuilder builder = new AnnotationValidationConfigurationBuilder(validatorFactory);
228
229 List<ValidatorConfig> annotationResult = new ArrayList<ValidatorConfig>(builder.buildAnnotationClassValidatorConfigs(aClass));
230
231 result.addAll(annotationResult);
232
233 return result;
234
235 }
236
237 /**
238 * <p>This method 'collects' all the validator configurations for a given
239 * action invocation.</p>
240 * <p/>
241 * <p>It will traverse up the class hierarchy looking for validators for every super class
242 * and directly implemented interface of the current action, as well as adding validators for
243 * any alias of this invocation. Nifty!</p>
244 * <p/>
245 * <p>Given the following class structure:
246 * <pre>
247 * interface Thing;
248 * interface Animal extends Thing;
249 * interface Quadraped extends Animal;
250 * class AnimalImpl implements Animal;
251 * class QuadrapedImpl extends AnimalImpl implements Quadraped;
252 * class Dog extends QuadrapedImpl;
253 * </pre></p>
254 * <p/>
255 * <p>This method will look for the following config files for Dog:
256 * <pre>
257 * Animal
258 * Animal-context
259 * AnimalImpl
260 * AnimalImpl-context
261 * Quadraped
262 * Quadraped-context
263 * QuadrapedImpl
264 * QuadrapedImpl-context
265 * Dog
266 * Dog-context
267 * </pre></p>
268 * <p/>
269 * <p>Note that the validation rules for Thing is never looked for because no class in the
270 * hierarchy directly implements Thing.</p>
271 *
272 * @param clazz the Class to look up validators for.
273 * @param context the context to use when looking up validators.
274 * @param checkFile true if the validation config file should be checked to see if it has been
275 * updated.
276 * @param checked the set of previously checked class-contexts, null if none have been checked
277 * @return a list of validator configs for the given class and context.
278 */
279 private List<ValidatorConfig> buildValidatorConfigs(Class clazz, String context, boolean checkFile, Set<String> checked) {
280 List<ValidatorConfig> validatorConfigs = new ArrayList<ValidatorConfig>();
281
282 if (checked == null) {
283 checked = new TreeSet<String>();
284 } else if (checked.contains(clazz.getName())) {
285 return validatorConfigs;
286 }
287
288 if (clazz.isInterface()) {
289 Class[] interfaces = clazz.getInterfaces();
290
291 for (Class anInterface : interfaces) {
292 validatorConfigs.addAll(buildValidatorConfigs(anInterface, context, checkFile, checked));
293 }
294 } else {
295 if (!clazz.equals(Object.class)) {
296 validatorConfigs.addAll(buildValidatorConfigs(clazz.getSuperclass(), context, checkFile, checked));
297 }
298 }
299
300 // look for validators for implemented interfaces
301 Class[] interfaces = clazz.getInterfaces();
302
303 for (Class anInterface1 : interfaces) {
304 if (checked.contains(anInterface1.getName())) {
305 continue;
306 }
307
308 validatorConfigs.addAll(buildClassValidatorConfigs(anInterface1, checkFile));
309
310 if (context != null) {
311 validatorConfigs.addAll(buildAliasValidatorConfigs(anInterface1, context, checkFile));
312 }
313
314 checked.add(anInterface1.getName());
315 }
316
317 validatorConfigs.addAll(buildClassValidatorConfigs(clazz, checkFile));
318
319 if (context != null) {
320 validatorConfigs.addAll(buildAliasValidatorConfigs(clazz, context, checkFile));
321 }
322
323 checked.add(clazz.getName());
324
325 return validatorConfigs;
326 }
327
328 private List<ValidatorConfig> loadFile(String fileName, Class clazz, boolean checkFile) {
329 List<ValidatorConfig> retList = Collections.emptyList();
330
331 if ((checkFile && FileManager.fileNeedsReloading(fileName, clazz)) || !validatorFileCache.containsKey(fileName)) {
332 InputStream is = null;
333
334 try {
335 is = FileManager.loadFile(fileName, clazz);
336
337 if (is != null) {
338 retList = new ArrayList<ValidatorConfig>(validatorFileParser.parseActionValidatorConfigs(validatorFactory, is, fileName));
339 }
340 } catch (Exception e) {
341 LOG.error("Caught exception while loading file " + fileName, e);
342 } finally {
343 if (is != null) {
344 try {
345 is.close();
346 } catch (IOException e) {
347 LOG.error("Unable to close input stream for " + fileName, e);
348 }
349 }
350 }
351
352 validatorFileCache.put(fileName, retList);
353 } else {
354 retList = validatorFileCache.get(fileName);
355 }
356
357 return retList;
358 }
359
360
361
362 /**
363 * An {@link com.opensymphony.xwork2.validator.ValidatorContext} wrapper that
364 * returns the full field name
365 * {@link com.opensymphony.xwork2.validator.AbstractActionValidatorManager.InternalValidatorContextWrapper#getFullFieldName(String)}
366 * by consulting it's parent if its an {@link com.opensymphony.xwork2.validator.validators.VisitorFieldValidator.AppendingValidatorContext}.
367 * <p/>
368 * Eg. if we have nested Visitor
369 * AddressVisitor nested inside PersonVisitor, when using the normal #getFullFieldName, we will get
370 * "address.somefield", we lost the parent, with this wrapper, we will get "person.address.somefield".
371 * This is so that the key is used to register errors, so that we don't screw up short-curcuit feature
372 * when using nested visitor. See XW-571 (nested visitor validators break short-circuit functionality)
373 * at http://jira.opensymphony.com/browse/XW-571
374 */
375 protected class InternalValidatorContextWrapper {
376 private ValidatorContext validatorContext = null;
377
378 InternalValidatorContextWrapper(ValidatorContext validatorContext) {
379 this.validatorContext = validatorContext;
380 }
381
382 /**
383 * Get the full field name by consulting the parent, so that when we are using nested visitors (
384 * visitor nested inside visitor etc.) we still get the full field name including its parents.
385 * See XW-571 for more details.
386 * @param field
387 * @return String
388 */
389 public String getFullFieldName(String field) {
390 if (validatorContext instanceof VisitorFieldValidator.AppendingValidatorContext) {
391 VisitorFieldValidator.AppendingValidatorContext appendingValidatorContext =
392 (VisitorFieldValidator.AppendingValidatorContext) validatorContext;
393 return appendingValidatorContext.getFullFieldNameFromParent(field);
394 }
395 return validatorContext.getFullFieldName(field);
396 }
397
398 }
399 }