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