001 // Copyright 2004, 2005 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.tapestry.valid; 016 017 import org.apache.hivemind.ApplicationRuntimeException; 018 import org.apache.hivemind.lib.util.StrategyRegistry; 019 import org.apache.hivemind.lib.util.StrategyRegistryImpl; 020 import org.apache.hivemind.util.PropertyUtils; 021 import org.apache.tapestry.IMarkupWriter; 022 import org.apache.tapestry.IRequestCycle; 023 import org.apache.tapestry.Tapestry; 024 import org.apache.tapestry.form.IFormComponent; 025 026 import java.math.BigDecimal; 027 import java.math.BigInteger; 028 import java.util.HashMap; 029 import java.util.Map; 030 031 /** 032 * Simple validation for standard number classes. This is probably insufficient for anything tricky 033 * and application specific, such as parsing currency. 034 * 035 * @author Howard Lewis Ship 036 * @since 1.0.8 037 */ 038 039 public class NumberValidator extends AbstractNumericValidator 040 { 041 public static final int NUMBER_TYPE_INTEGER = 0; 042 043 public static final int NUMBER_TYPE_REAL = 1; 044 045 private static final Map TYPES = new HashMap(); 046 047 private static StrategyRegistry _numberAdaptors = new StrategyRegistryImpl(); 048 049 static 050 { 051 TYPES.put("boolean", boolean.class); 052 TYPES.put("Boolean", Boolean.class); 053 TYPES.put("java.lang.Boolean", Boolean.class); 054 TYPES.put("char", char.class); 055 TYPES.put("Character", Character.class); 056 TYPES.put("java.lang.Character", Character.class); 057 TYPES.put("short", short.class); 058 TYPES.put("Short", Short.class); 059 TYPES.put("java.lang.Short", Short.class); 060 TYPES.put("int", int.class); 061 TYPES.put("Integer", Integer.class); 062 TYPES.put("java.lang.Integer", Integer.class); 063 TYPES.put("long", long.class); 064 TYPES.put("Long", Long.class); 065 TYPES.put("java.lang.Long", Long.class); 066 TYPES.put("float", float.class); 067 TYPES.put("Float", Float.class); 068 TYPES.put("java.lang.Float", Float.class); 069 TYPES.put("byte", byte.class); 070 TYPES.put("Byte", Byte.class); 071 TYPES.put("java.lang.Byte", Byte.class); 072 TYPES.put("double", double.class); 073 TYPES.put("Double", Double.class); 074 TYPES.put("java.lang.Double", Double.class); 075 TYPES.put("java.math.BigInteger", BigInteger.class); 076 TYPES.put("java.math.BigDecimal", BigDecimal.class); 077 } 078 079 private Class _valueTypeClass = int.class; 080 081 private Number _minimum; 082 083 private Number _maximum; 084 085 /** 086 * This class is not meant for use outside of NumberValidator; it is public only to fascilitate 087 * some unit testing. 088 */ 089 public abstract static class NumberStrategy 090 { 091 /** 092 * Parses a non-empty {@link String}into the correct subclass of {@link Number}. 093 * 094 * @throws NumberFormatException 095 * if the String can not be parsed. 096 */ 097 098 public abstract Number parse(String value); 099 100 /** 101 * Indicates the type of the number represented -- integer or real. The information is used 102 * to build the client-side validator. This method could return a boolean, but returns an 103 * int to allow future extensions of the validator. 104 * 105 * @return one of the predefined number types 106 */ 107 public abstract int getNumberType(); 108 109 public int compare(Number left, Number right) 110 { 111 Number comparisonRight = right; 112 if (!left.getClass().equals(comparisonRight.getClass())) 113 comparisonRight = coerce(comparisonRight); 114 115 Comparable lc = (Comparable) left; 116 117 return lc.compareTo(comparisonRight); 118 } 119 120 /** 121 * Invoked when comparing two Numbers of different types. The number is cooerced from its 122 * ordinary type to the correct type for comparison. 123 * 124 * @since 3.0 125 */ 126 protected abstract Number coerce(Number number); 127 } 128 129 /** 130 * Integer adaptor. 131 */ 132 private abstract static class IntegerNumberAdaptor extends NumberStrategy 133 { 134 public int getNumberType() 135 { 136 return NUMBER_TYPE_INTEGER; 137 } 138 } 139 140 /** 141 * Integer adaptor. 142 */ 143 private abstract static class RealNumberAdaptor extends NumberStrategy 144 { 145 public int getNumberType() 146 { 147 return NUMBER_TYPE_REAL; 148 } 149 } 150 151 /** 152 * Integer adaptor. 153 */ 154 private static class ByteAdaptor extends IntegerNumberAdaptor 155 { 156 public Number parse(String value) 157 { 158 return new Byte(value); 159 } 160 161 protected Number coerce(Number number) 162 { 163 return new Byte(number.byteValue()); 164 } 165 } 166 167 /** 168 * Integer adaptor. 169 */ 170 private static class ShortAdaptor extends IntegerNumberAdaptor 171 { 172 public Number parse(String value) 173 { 174 return new Short(value); 175 } 176 177 protected Number coerce(Number number) 178 { 179 return new Short(number.shortValue()); 180 } 181 } 182 183 /** 184 * Integer adaptor. 185 */ 186 private static class IntAdaptor extends IntegerNumberAdaptor 187 { 188 public Number parse(String value) 189 { 190 return new Integer(value); 191 } 192 193 protected Number coerce(Number number) 194 { 195 return new Integer(number.intValue()); 196 } 197 } 198 199 /** 200 * Integer adaptor. 201 */ 202 private static class LongAdaptor extends IntegerNumberAdaptor 203 { 204 public Number parse(String value) 205 { 206 return new Long(value); 207 } 208 209 protected Number coerce(Number number) 210 { 211 return new Long(number.longValue()); 212 } 213 } 214 215 /** 216 * Integer adaptor. 217 */ 218 private static class FloatAdaptor extends RealNumberAdaptor 219 { 220 public Number parse(String value) 221 { 222 return new Float(value); 223 } 224 225 protected Number coerce(Number number) 226 { 227 return new Float(number.floatValue()); 228 } 229 } 230 231 /** 232 * Integer adaptor. 233 */ 234 private static class DoubleAdaptor extends RealNumberAdaptor 235 { 236 public Number parse(String value) 237 { 238 return new Double(value); 239 } 240 241 protected Number coerce(Number number) 242 { 243 return new Double(number.doubleValue()); 244 } 245 } 246 247 /** 248 * Integer adaptor. 249 */ 250 private static class BigDecimalAdaptor extends RealNumberAdaptor 251 { 252 public Number parse(String value) 253 { 254 return new BigDecimal(value); 255 } 256 257 protected Number coerce(Number number) 258 { 259 return new BigDecimal(number.doubleValue()); 260 } 261 } 262 263 /** 264 * Integer adaptor. 265 */ 266 private static class BigIntegerAdaptor extends IntegerNumberAdaptor 267 { 268 public Number parse(String value) 269 { 270 return new BigInteger(value); 271 } 272 273 protected Number coerce(Number number) 274 { 275 return new BigInteger(number.toString()); 276 } 277 } 278 279 static 280 { 281 NumberStrategy byteAdaptor = new ByteAdaptor(); 282 NumberStrategy shortAdaptor = new ShortAdaptor(); 283 NumberStrategy intAdaptor = new IntAdaptor(); 284 NumberStrategy longAdaptor = new LongAdaptor(); 285 NumberStrategy floatAdaptor = new FloatAdaptor(); 286 NumberStrategy doubleAdaptor = new DoubleAdaptor(); 287 288 _numberAdaptors.register(Byte.class, byteAdaptor); 289 _numberAdaptors.register(byte.class, byteAdaptor); 290 _numberAdaptors.register(Short.class, shortAdaptor); 291 _numberAdaptors.register(short.class, shortAdaptor); 292 _numberAdaptors.register(Integer.class, intAdaptor); 293 _numberAdaptors.register(int.class, intAdaptor); 294 _numberAdaptors.register(Long.class, longAdaptor); 295 _numberAdaptors.register(long.class, longAdaptor); 296 _numberAdaptors.register(Float.class, floatAdaptor); 297 _numberAdaptors.register(float.class, floatAdaptor); 298 _numberAdaptors.register(Double.class, doubleAdaptor); 299 _numberAdaptors.register(double.class, doubleAdaptor); 300 301 _numberAdaptors.register(BigDecimal.class, new BigDecimalAdaptor()); 302 _numberAdaptors.register(BigInteger.class, new BigIntegerAdaptor()); 303 } 304 305 public NumberValidator() 306 { 307 308 } 309 310 /** 311 * Initializes the NumberValidator with properties defined by the initializer. 312 * 313 * @since 4.0 314 */ 315 316 public NumberValidator(String initializer) 317 { 318 PropertyUtils.configureProperties(this, initializer); 319 } 320 321 public String toString(IFormComponent field, Object value) 322 { 323 if (value == null) 324 return null; 325 326 if (getZeroIsNull()) 327 { 328 Number number = (Number) value; 329 330 if (number.doubleValue() == 0.0) 331 return null; 332 } 333 334 return value.toString(); 335 } 336 337 private NumberStrategy getStrategy(IFormComponent field) 338 { 339 NumberStrategy result = getStrategy(_valueTypeClass); 340 341 if (result == null) 342 throw new ApplicationRuntimeException(Tapestry.format("NumberValidator.no-adaptor-for-field", 343 field, 344 _valueTypeClass.getName())); 345 346 return result; 347 } 348 349 /** 350 * Returns an strategy for the given type. 351 * <p> 352 * Note: this method exists only for testing purposes. It is not meant to be invoked by user 353 * code and is subject to change at any time. 354 * 355 * @param type 356 * the type (a Number subclass) for which to return an adaptor 357 * @return the adaptor, or null if no such adaptor may be found 358 * @since 3.0 359 */ 360 public static NumberStrategy getStrategy(Class type) 361 { 362 return (NumberStrategy) _numberAdaptors.getStrategy(type); 363 } 364 365 public Object toObject(IFormComponent field, String value) throws ValidatorException 366 { 367 if (checkRequired(field, value)) 368 return null; 369 370 NumberStrategy adaptor = getStrategy(field); 371 Number result = null; 372 373 try 374 { 375 result = adaptor.parse(value); 376 } 377 catch (NumberFormatException ex) 378 { 379 throw new ValidatorException(buildInvalidNumericFormatMessage(field), 380 ValidationConstraint.NUMBER_FORMAT); 381 } 382 383 if (_minimum != null && adaptor.compare(result, _minimum) < 0) 384 throw new ValidatorException(buildNumberTooSmallMessage(field, _minimum), 385 ValidationConstraint.TOO_SMALL); 386 387 if (_maximum != null && adaptor.compare(result, _maximum) > 0) 388 throw new ValidatorException(buildNumberTooLargeMessage(field, _maximum), 389 ValidationConstraint.TOO_LARGE); 390 391 return result; 392 } 393 394 public Number getMaximum() 395 { 396 return _maximum; 397 } 398 399 public boolean getHasMaximum() 400 { 401 return _maximum != null; 402 } 403 404 public void setMaximum(Number maximum) 405 { 406 _maximum = maximum; 407 } 408 409 public Number getMinimum() 410 { 411 return _minimum; 412 } 413 414 public boolean getHasMinimum() 415 { 416 return _minimum != null; 417 } 418 419 public void setMinimum(Number minimum) 420 { 421 _minimum = minimum; 422 } 423 424 /** 425 * @since 2.2 426 */ 427 428 public void renderValidatorContribution(IFormComponent field, IMarkupWriter writer, 429 IRequestCycle cycle) 430 { 431 if (!isClientScriptingEnabled()) 432 return; 433 434 if (!(isRequired() || _minimum != null || _maximum != null)) 435 return; 436 437 Map symbols = new HashMap(); 438 439 if (isRequired()) 440 symbols.put("requiredMessage", buildRequiredMessage(field)); 441 442 if (isIntegerNumber()) 443 symbols.put("formatMessage", buildInvalidIntegerFormatMessage(field)); 444 else 445 symbols.put("formatMessage", buildInvalidNumericFormatMessage(field)); 446 447 if (_minimum != null || _maximum != null) 448 symbols.put("rangeMessage", buildRangeMessage(field, _minimum, _maximum)); 449 450 processValidatorScript(getScriptPath(), cycle, field, symbols); 451 } 452 453 /** 454 * Sets the value type from a string type name. The name may be a scalar numeric type, a fully 455 * qualified class name, or the name of a numeric wrapper type from java.lang (with the package 456 * name omitted). 457 * 458 * @since 3.0 459 */ 460 461 public void setValueType(String typeName) 462 { 463 Class typeClass = (Class) TYPES.get(typeName); 464 465 if (typeClass == null) 466 throw new ApplicationRuntimeException(Tapestry.format("NumberValidator.unknown-type", typeName)); 467 468 _valueTypeClass = typeClass; 469 } 470 471 /** @since 3.0 * */ 472 473 public void setValueTypeClass(Class valueTypeClass) 474 { 475 _valueTypeClass = valueTypeClass; 476 } 477 478 /** 479 * Returns the value type to convert strings back into. The default is int. 480 * 481 * @since 3.0 482 */ 483 484 public Class getValueTypeClass() 485 { 486 return _valueTypeClass; 487 } 488 489 /** @since 3.0 */ 490 491 public boolean isIntegerNumber() 492 { 493 NumberStrategy strategy = (NumberStrategy) _numberAdaptors.getStrategy(_valueTypeClass); 494 if (strategy == null) 495 return false; 496 497 return strategy.getNumberType() == NUMBER_TYPE_INTEGER; 498 } 499 500 protected String getDefaultScriptPath() 501 { 502 return "/org/apache/tapestry/valid/NumberValidator.script"; 503 } 504 }