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 }