001 // Copyright 2006, 2007, 2008, 2010, 2011 The Apache Software Foundation
002 //
003 // Licensed under the Apache License, Version 2.0 (the "License");
004 // you may not use this file except in compliance with the License.
005 // You may obtain a copy of the License at
006 //
007 // http://www.apache.org/licenses/LICENSE-2.0
008 //
009 // Unless required by applicable law or agreed to in writing, software
010 // distributed under the License is distributed on an "AS IS" BASIS,
011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012 // See the License for the specific language governing permissions and
013 // limitations under the License.
014
015 package org.apache.tapestry5.internal.services;
016
017 import org.apache.tapestry5.ComponentResources;
018 import org.apache.tapestry5.Field;
019 import org.apache.tapestry5.FieldValidator;
020 import org.apache.tapestry5.Validator;
021 import org.apache.tapestry5.ioc.MessageFormatter;
022 import org.apache.tapestry5.ioc.Messages;
023 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
024 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
025 import org.apache.tapestry5.ioc.services.TypeCoercer;
026 import org.apache.tapestry5.runtime.Component;
027 import org.apache.tapestry5.services.FieldValidatorSource;
028 import org.apache.tapestry5.services.FormSupport;
029 import org.apache.tapestry5.validator.ValidatorMacro;
030
031 import java.util.List;
032 import java.util.Locale;
033 import java.util.Map;
034
035 import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newList;
036
037 @SuppressWarnings("all")
038 public class FieldValidatorSourceImpl implements FieldValidatorSource
039 {
040 private final Messages globalMessages;
041
042 private final Map<String, Validator> validators;
043
044 private final TypeCoercer typeCoercer;
045
046 private final FormSupport formSupport;
047
048 private final ValidatorMacro validatorMacro;
049
050 public FieldValidatorSourceImpl(Messages globalMessages, TypeCoercer typeCoercer,
051 FormSupport formSupport, Map<String, Validator> validators, ValidatorMacro validatorMacro)
052 {
053 this.globalMessages = globalMessages;
054 this.typeCoercer = typeCoercer;
055 this.formSupport = formSupport;
056 this.validators = validators;
057 this.validatorMacro = validatorMacro;
058 }
059
060 public FieldValidator createValidator(Field field, String validatorType, String constraintValue)
061 {
062 Component component = (Component) field;
063 assert InternalUtils.isNonBlank(validatorType);
064 ComponentResources componentResources = component.getComponentResources();
065 String overrideId = componentResources.getId();
066
067 // So, if you use a TextField on your EditUser page, we want to search the messages
068 // of the EditUser page (the container), not the TextField (which will always be the same).
069
070 Messages overrideMessages = componentResources.getContainerMessages();
071
072 return createValidator(field, validatorType, constraintValue, overrideId, overrideMessages, null);
073 }
074
075 public FieldValidator createValidator(Field field, String validatorType, String constraintValue, String overrideId,
076 Messages overrideMessages, Locale locale)
077 {
078
079 ValidatorSpecification originalSpec = new ValidatorSpecification(validatorType, constraintValue);
080
081 List<ValidatorSpecification> org = CollectionFactory.newList(originalSpec);
082
083 List<ValidatorSpecification> specs = expandMacros(org);
084
085 List<FieldValidator> fieldValidators = CollectionFactory.<FieldValidator>newList();
086
087 for (ValidatorSpecification spec : specs)
088 {
089 fieldValidators.add(createValidator(field, spec, overrideId, overrideMessages));
090 }
091
092 return new CompositeFieldValidator(fieldValidators);
093 }
094
095 private FieldValidator createValidator(Field field, ValidatorSpecification spec, String overrideId,
096 Messages overrideMessages)
097 {
098
099 String validatorType = spec.getValidatorType();
100
101 assert InternalUtils.isNonBlank(validatorType);
102 Validator validator = validators.get(validatorType);
103
104 if (validator == null)
105 throw new IllegalArgumentException(ServicesMessages.unknownValidatorType(validatorType,
106 InternalUtils.sortedKeys(validators)));
107
108 // I just have this thing about always treating parameters as finals, so
109 // we introduce a second variable to treat a mutable.
110
111 String formValidationid = formSupport.getFormValidationId();
112
113 Object coercedConstraintValue = computeConstraintValue(validatorType, validator, spec.getConstraintValue(),
114 formValidationid, overrideId, overrideMessages);
115
116 MessageFormatter formatter = findMessageFormatter(formValidationid, overrideId, overrideMessages, validatorType,
117 validator);
118
119 return new FieldValidatorImpl(field, coercedConstraintValue, formatter, validator, formSupport);
120 }
121
122 private Object computeConstraintValue(String validatorType, Validator validator, String constraintValue,
123 String formId, String overrideId, Messages overrideMessages)
124 {
125 Class constraintType = validator.getConstraintType();
126
127 String constraintText = findConstraintValue(validatorType, constraintType, constraintValue, formId, overrideId,
128 overrideMessages);
129
130 if (constraintText == null)
131 return null;
132
133 return typeCoercer.coerce(constraintText, constraintType);
134 }
135
136 private String findConstraintValue(String validatorType, Class constraintType, String constraintValue,
137 String formValidationId, String overrideId, Messages overrideMessages)
138 {
139 if (constraintValue != null)
140 return constraintValue;
141
142 if (constraintType == null)
143 return null;
144
145 // If no constraint was provided, check to see if it is available via a localized message
146 // key. This is really handy for complex validations such as patterns.
147
148 String perFormKey = formValidationId + "-" + overrideId + "-" + validatorType;
149
150 if (overrideMessages.contains(perFormKey))
151 return overrideMessages.get(perFormKey);
152
153 String generalKey = overrideId + "-" + validatorType;
154
155 if (overrideMessages.contains(generalKey))
156 return overrideMessages.get(generalKey);
157
158 throw new IllegalArgumentException(ServicesMessages.missingValidatorConstraint(validatorType, constraintType,
159 perFormKey, generalKey));
160 }
161
162 private MessageFormatter findMessageFormatter(String formId, String overrideId, Messages overrideMessages,
163 String validatorType, Validator validator)
164 {
165
166 String overrideKey = formId + "-" + overrideId + "-" + validatorType + "-message";
167
168 if (overrideMessages.contains(overrideKey))
169 return overrideMessages.getFormatter(overrideKey);
170
171 overrideKey = overrideId + "-" + validatorType + "-message";
172
173 if (overrideMessages.contains(overrideKey))
174 return overrideMessages.getFormatter(overrideKey);
175
176 String key = validator.getMessageKey();
177
178 return globalMessages.getFormatter(key);
179 }
180
181 public FieldValidator createValidators(Field field, String specification)
182 {
183 List<ValidatorSpecification> specs = toValidatorSpecifications(specification);
184
185 List<FieldValidator> fieldValidators = CollectionFactory.newList();
186
187 for (ValidatorSpecification spec : specs)
188 {
189 fieldValidators.add(createValidator(field, spec.getValidatorType(), spec.getConstraintValue()));
190 }
191
192 if (fieldValidators.size() == 1)
193 return fieldValidators.get(0);
194
195 return new CompositeFieldValidator(fieldValidators);
196 }
197
198 List<ValidatorSpecification> toValidatorSpecifications(String specification)
199 {
200 return expandMacros(parse(specification));
201 }
202
203 private List<ValidatorSpecification> expandMacros(List<ValidatorSpecification> specs)
204 {
205 Map<String, Boolean> expandedMacros = CollectionFactory.newCaseInsensitiveMap();
206 List<ValidatorSpecification> queue = CollectionFactory.newList(specs);
207 List<ValidatorSpecification> result = CollectionFactory.newList();
208
209 while (!queue.isEmpty())
210 {
211 ValidatorSpecification head = queue.remove(0);
212
213 String validatorType = head.getValidatorType();
214
215 String expanded = validatorMacro.valueForMacro(validatorType);
216 if (expanded != null)
217 {
218 if (head.getConstraintValue() != null)
219 throw new RuntimeException(String.format(
220 "'%s' is a validator macro, not a validator, and can not have a constraint value.",
221 validatorType));
222
223 if (expandedMacros.containsKey(validatorType))
224 throw new RuntimeException(String.format("Validator macro '%s' appears more than once.",
225 validatorType));
226
227 expandedMacros.put(validatorType, true);
228
229 List<ValidatorSpecification> parsed = parse(expanded);
230
231 // Add the new validator specifications to the front of the queue, replacing the validator macro
232
233 for (int i = 0; i < parsed.size(); i++)
234 {
235 queue.add(i, parsed.get(i));
236 }
237 } else
238 {
239 result.add(head);
240 }
241 }
242
243 return result;
244 }
245
246 /**
247 * A code defining what the parser is looking for.
248 */
249 enum State
250 {
251
252 /**
253 * The start of a validator type.
254 */
255 TYPE_START,
256 /**
257 * The end of a validator type.
258 */
259 TYPE_END,
260 /**
261 * Equals sign after a validator type, or a comma.
262 */
263 EQUALS_OR_COMMA,
264 /**
265 * The start of a constraint value.
266 */
267 VALUE_START,
268 /**
269 * The end of the constraint value.
270 */
271 VALUE_END,
272 /**
273 * The comma after a constraint value.
274 */
275 COMMA
276 }
277
278 static List<ValidatorSpecification> parse(String specification)
279 {
280 List<ValidatorSpecification> result = newList();
281
282 char[] input = specification.toCharArray();
283
284 int cursor = 0;
285 int start = -1;
286
287 String type = null;
288 boolean skipWhitespace = true;
289 State state = State.TYPE_START;
290
291 while (cursor < input.length)
292 {
293 char ch = input[cursor];
294
295 if (skipWhitespace && Character.isWhitespace(ch))
296 {
297 cursor++;
298 continue;
299 }
300
301 skipWhitespace = false;
302
303 switch (state)
304 {
305
306 case TYPE_START:
307
308 if (Character.isLetter(ch))
309 {
310 start = cursor;
311 state = State.TYPE_END;
312 break;
313 }
314
315 parseError(cursor, specification);
316
317 case TYPE_END:
318
319 if (Character.isLetter(ch))
320 {
321 break;
322 }
323
324 type = specification.substring(start, cursor);
325
326 skipWhitespace = true;
327 state = State.EQUALS_OR_COMMA;
328 continue;
329
330 case EQUALS_OR_COMMA:
331
332 if (ch == '=')
333 {
334 skipWhitespace = true;
335 state = State.VALUE_START;
336 break;
337 }
338
339 if (ch == ',')
340 {
341 result.add(new ValidatorSpecification(type));
342 type = null;
343 state = State.COMMA;
344 continue;
345 }
346
347 parseError(cursor, specification);
348
349 case VALUE_START:
350
351 start = cursor;
352 state = State.VALUE_END;
353 break;
354
355 case VALUE_END:
356
357 // The value ends when we hit whitespace or a comma
358
359 if (Character.isWhitespace(ch) || ch == ',')
360 {
361 String value = specification.substring(start, cursor);
362
363 result.add(new ValidatorSpecification(type, value));
364 type = null;
365
366 skipWhitespace = true;
367 state = State.COMMA;
368 continue;
369 }
370
371 break;
372
373 case COMMA:
374
375 if (ch == ',')
376 {
377 skipWhitespace = true;
378 state = State.TYPE_START;
379 break;
380 }
381
382 parseError(cursor, specification);
383 } // case
384
385 cursor++;
386 } // while
387
388 // cursor is now one character past end of string.
389 // Cleanup whatever state we were in the middle of.
390
391 switch (state)
392 {
393 case TYPE_END:
394
395 type = specification.substring(start);
396
397 case EQUALS_OR_COMMA:
398
399 result.add(new ValidatorSpecification(type));
400 break;
401
402 // Case when the specification ends with an equals sign.
403
404 case VALUE_START:
405 result.add(new ValidatorSpecification(type, ""));
406 break;
407
408 case VALUE_END:
409
410 result.add(new ValidatorSpecification(type, specification.substring(start)));
411 break;
412
413 // For better or worse, ending the string with a comma is valid.
414
415 default:
416 }
417
418 return result;
419 }
420
421 private static void parseError(int cursor, String specification)
422 {
423 throw new RuntimeException(ServicesMessages.validatorSpecificationParseError(cursor, specification));
424 }
425 }