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 015package org.apache.tapestry5.internal.services; 016 017import org.apache.tapestry5.ComponentResources; 018import org.apache.tapestry5.Field; 019import org.apache.tapestry5.FieldValidator; 020import org.apache.tapestry5.Validator; 021import org.apache.tapestry5.commons.MessageFormatter; 022import org.apache.tapestry5.commons.Messages; 023import org.apache.tapestry5.commons.services.TypeCoercer; 024import org.apache.tapestry5.commons.util.CollectionFactory; 025import org.apache.tapestry5.ioc.internal.util.InternalUtils; 026import org.apache.tapestry5.runtime.Component; 027import org.apache.tapestry5.services.FieldValidatorSource; 028import org.apache.tapestry5.services.FormSupport; 029import org.apache.tapestry5.validator.ValidatorMacro; 030 031import static org.apache.tapestry5.commons.util.CollectionFactory.newList; 032 033import java.util.List; 034import java.util.Locale; 035import java.util.Map; 036 037@SuppressWarnings("all") 038public 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(String.format("Unknown validator type '%s'. Configured validators are %s.", validatorType, InternalUtils.join(InternalUtils.sortedKeys(validators)))); 106 107 // I just have this thing about always treating parameters as finals, so 108 // we introduce a second variable to treat a mutable. 109 110 String formValidationid = formSupport.getFormValidationId(); 111 112 Object coercedConstraintValue = computeConstraintValue(validatorType, validator, spec.getConstraintValue(), 113 formValidationid, overrideId, overrideMessages); 114 115 MessageFormatter formatter = findMessageFormatter(formValidationid, overrideId, overrideMessages, validatorType, 116 validator); 117 118 return new FieldValidatorImpl(field, coercedConstraintValue, formatter, validator, formSupport); 119 } 120 121 private Object computeConstraintValue(String validatorType, Validator validator, String constraintValue, 122 String formId, String overrideId, Messages overrideMessages) 123 { 124 Class constraintType = validator.getConstraintType(); 125 126 String constraintText = findConstraintValue(validatorType, constraintType, constraintValue, formId, overrideId, 127 overrideMessages); 128 129 if (constraintText == null) 130 return null; 131 132 return typeCoercer.coerce(constraintText, constraintType); 133 } 134 135 private String findConstraintValue(String validatorType, Class constraintType, String constraintValue, 136 String formValidationId, String overrideId, Messages overrideMessages) 137 { 138 if (constraintValue != null) 139 return constraintValue; 140 141 if (constraintType == null) 142 return null; 143 144 // If no constraint was provided, check to see if it is available via a localized message 145 // key. This is really handy for complex validations such as patterns. 146 147 String perFormKey = formValidationId + "-" + overrideId + "-" + validatorType; 148 149 if (overrideMessages.contains(perFormKey)) 150 return overrideMessages.get(perFormKey); 151 152 String generalKey = overrideId + "-" + validatorType; 153 154 if (overrideMessages.contains(generalKey)) 155 return overrideMessages.get(generalKey); 156 157 throw new IllegalArgumentException(String.format("Validator '%s' requires a validation constraint (of type %s) but none was provided. The constraint may be provided inside the @Validator annotation on the property, or in the associated component message catalog as key '%s' or key '%s'.", validatorType, constraintType.getName(), perFormKey, 158 generalKey)); 159 } 160 161 private MessageFormatter findMessageFormatter(String formId, String overrideId, Messages overrideMessages, 162 String validatorType, Validator validator) 163 { 164 165 String overrideKey = formId + "-" + overrideId + "-" + validatorType + "-message"; 166 167 if (overrideMessages.contains(overrideKey)) 168 return overrideMessages.getFormatter(overrideKey); 169 170 overrideKey = overrideId + "-" + validatorType + "-message"; 171 172 if (overrideMessages.contains(overrideKey)) 173 return overrideMessages.getFormatter(overrideKey); 174 175 String key = validator.getMessageKey(); 176 177 return globalMessages.getFormatter(key); 178 } 179 180 public FieldValidator createValidators(Field field, String specification) 181 { 182 List<ValidatorSpecification> specs = toValidatorSpecifications(specification); 183 184 List<FieldValidator> fieldValidators = CollectionFactory.newList(); 185 186 for (ValidatorSpecification spec : specs) 187 { 188 fieldValidators.add(createValidator(field, spec.getValidatorType(), spec.getConstraintValue())); 189 } 190 191 if (fieldValidators.size() == 1) 192 return fieldValidators.get(0); 193 194 return new CompositeFieldValidator(fieldValidators); 195 } 196 197 List<ValidatorSpecification> toValidatorSpecifications(String specification) 198 { 199 return expandMacros(parse(specification)); 200 } 201 202 private List<ValidatorSpecification> expandMacros(List<ValidatorSpecification> specs) 203 { 204 Map<String, Boolean> expandedMacros = CollectionFactory.newCaseInsensitiveMap(); 205 List<ValidatorSpecification> queue = CollectionFactory.newList(specs); 206 List<ValidatorSpecification> result = CollectionFactory.newList(); 207 208 while (!queue.isEmpty()) 209 { 210 ValidatorSpecification head = queue.remove(0); 211 212 String validatorType = head.getValidatorType(); 213 214 String expanded = validatorMacro.valueForMacro(validatorType); 215 if (expanded != null) 216 { 217 if (head.getConstraintValue() != null) 218 throw new RuntimeException(String.format( 219 "'%s' is a validator macro, not a validator, and can not have a constraint value.", 220 validatorType)); 221 222 if (expandedMacros.containsKey(validatorType)) 223 throw new RuntimeException(String.format("Validator macro '%s' appears more than once.", 224 validatorType)); 225 226 expandedMacros.put(validatorType, true); 227 228 List<ValidatorSpecification> parsed = parse(expanded); 229 230 // Add the new validator specifications to the front of the queue, replacing the validator macro 231 232 for (int i = 0; i < parsed.size(); i++) 233 { 234 queue.add(i, parsed.get(i)); 235 } 236 } else 237 { 238 result.add(head); 239 } 240 } 241 242 return result; 243 } 244 245 /** 246 * A code defining what the parser is looking for. 247 */ 248 enum State 249 { 250 251 /** 252 * The start of a validator type. 253 */ 254 TYPE_START, 255 /** 256 * The end of a validator type. 257 */ 258 TYPE_END, 259 /** 260 * Equals sign after a validator type, or a comma. 261 */ 262 EQUALS_OR_COMMA, 263 /** 264 * The start of a constraint value. 265 */ 266 VALUE_START, 267 /** 268 * The end of the constraint value. 269 */ 270 VALUE_END, 271 /** 272 * The comma after a constraint value. 273 */ 274 COMMA 275 } 276 277 static List<ValidatorSpecification> parse(String specification) 278 { 279 List<ValidatorSpecification> result = newList(); 280 281 char[] input = specification.toCharArray(); 282 283 int cursor = 0; 284 int start = -1; 285 286 String type = null; 287 boolean skipWhitespace = true; 288 State state = State.TYPE_START; 289 290 while (cursor < input.length) 291 { 292 char ch = input[cursor]; 293 294 if (skipWhitespace && Character.isWhitespace(ch)) 295 { 296 cursor++; 297 continue; 298 } 299 300 skipWhitespace = false; 301 302 switch (state) 303 { 304 305 case TYPE_START: 306 307 if (Character.isLetter(ch)) 308 { 309 start = cursor; 310 state = State.TYPE_END; 311 break; 312 } 313 314 parseError(cursor, specification); 315 316 case TYPE_END: 317 318 if (Character.isLetter(ch)) 319 { 320 break; 321 } 322 323 type = specification.substring(start, cursor); 324 325 skipWhitespace = true; 326 state = State.EQUALS_OR_COMMA; 327 continue; 328 329 case EQUALS_OR_COMMA: 330 331 if (ch == '=') 332 { 333 skipWhitespace = true; 334 state = State.VALUE_START; 335 break; 336 } 337 338 if (ch == ',') 339 { 340 result.add(new ValidatorSpecification(type)); 341 type = null; 342 state = State.COMMA; 343 continue; 344 } 345 346 parseError(cursor, specification); 347 348 case VALUE_START: 349 350 start = cursor; 351 state = State.VALUE_END; 352 break; 353 354 case VALUE_END: 355 356 // The value ends when we hit whitespace or a comma 357 358 if (Character.isWhitespace(ch) || ch == ',') 359 { 360 String value = specification.substring(start, cursor); 361 362 result.add(new ValidatorSpecification(type, value)); 363 type = null; 364 365 skipWhitespace = true; 366 state = State.COMMA; 367 continue; 368 } 369 370 break; 371 372 case COMMA: 373 374 if (ch == ',') 375 { 376 skipWhitespace = true; 377 state = State.TYPE_START; 378 break; 379 } 380 381 parseError(cursor, specification); 382 } // case 383 384 cursor++; 385 } // while 386 387 // cursor is now one character past end of string. 388 // Cleanup whatever state we were in the middle of. 389 390 switch (state) 391 { 392 case TYPE_END: 393 394 type = specification.substring(start); 395 396 case EQUALS_OR_COMMA: 397 398 result.add(new ValidatorSpecification(type)); 399 break; 400 401 // Case when the specification ends with an equals sign. 402 403 case VALUE_START: 404 result.add(new ValidatorSpecification(type, "")); 405 break; 406 407 case VALUE_END: 408 409 result.add(new ValidatorSpecification(type, specification.substring(start))); 410 break; 411 412 // For better or worse, ending the string with a comma is valid. 413 414 default: 415 } 416 417 return result; 418 } 419 420 private static void parseError(int cursor, String specification) 421 { 422 throw new RuntimeException(String.format("Unexpected character '%s' at position %d of input string: %s", specification.charAt(cursor), cursor + 1, 423 specification)); 424 } 425}