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 java.math.BigDecimal;
018 import java.math.BigInteger;
019 import java.util.HashMap;
020 import java.util.Map;
021
022 import org.apache.hivemind.ApplicationRuntimeException;
023 import org.apache.hivemind.lib.util.StrategyRegistry;
024 import org.apache.hivemind.lib.util.StrategyRegistryImpl;
025 import org.apache.hivemind.util.PropertyUtils;
026 import org.apache.tapestry.IMarkupWriter;
027 import org.apache.tapestry.IRequestCycle;
028 import org.apache.tapestry.Tapestry;
029 import org.apache.tapestry.form.IFormComponent;
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(
343 "NumberValidator.no-adaptor-for-field",
344 field,
345 _valueTypeClass.getName()));
346
347 return result;
348 }
349
350 /**
351 * Returns an strategy for the given type.
352 * <p>
353 * Note: this method exists only for testing purposes. It is not meant to be invoked by user
354 * code and is subject to change at any time.
355 *
356 * @param type
357 * the type (a Number subclass) for which to return an adaptor
358 * @return the adaptor, or null if no such adaptor may be found
359 * @since 3.0
360 */
361 public static NumberStrategy getStrategy(Class type)
362 {
363 return (NumberStrategy) _numberAdaptors.getStrategy(type);
364 }
365
366 public Object toObject(IFormComponent field, String value) throws ValidatorException
367 {
368 if (checkRequired(field, value))
369 return null;
370
371 NumberStrategy adaptor = getStrategy(field);
372 Number result = null;
373
374 try
375 {
376 result = adaptor.parse(value);
377 }
378 catch (NumberFormatException ex)
379 {
380 throw new ValidatorException(buildInvalidNumericFormatMessage(field),
381 ValidationConstraint.NUMBER_FORMAT);
382 }
383
384 if (_minimum != null && adaptor.compare(result, _minimum) < 0)
385 throw new ValidatorException(buildNumberTooSmallMessage(field, _minimum),
386 ValidationConstraint.TOO_SMALL);
387
388 if (_maximum != null && adaptor.compare(result, _maximum) > 0)
389 throw new ValidatorException(buildNumberTooLargeMessage(field, _maximum),
390 ValidationConstraint.TOO_LARGE);
391
392 return result;
393 }
394
395 public Number getMaximum()
396 {
397 return _maximum;
398 }
399
400 public boolean getHasMaximum()
401 {
402 return _maximum != null;
403 }
404
405 public void setMaximum(Number maximum)
406 {
407 _maximum = maximum;
408 }
409
410 public Number getMinimum()
411 {
412 return _minimum;
413 }
414
415 public boolean getHasMinimum()
416 {
417 return _minimum != null;
418 }
419
420 public void setMinimum(Number minimum)
421 {
422 _minimum = minimum;
423 }
424
425 /**
426 * @since 2.2
427 */
428
429 public void renderValidatorContribution(IFormComponent field, IMarkupWriter writer,
430 IRequestCycle cycle)
431 {
432 if (!isClientScriptingEnabled())
433 return;
434
435 if (!(isRequired() || _minimum != null || _maximum != null))
436 return;
437
438 Map symbols = new HashMap();
439
440 if (isRequired())
441 symbols.put("requiredMessage", buildRequiredMessage(field));
442
443 if (isIntegerNumber())
444 symbols.put("formatMessage", buildInvalidIntegerFormatMessage(field));
445 else
446 symbols.put("formatMessage", buildInvalidNumericFormatMessage(field));
447
448 if (_minimum != null || _maximum != null)
449 symbols.put("rangeMessage", buildRangeMessage(field, _minimum, _maximum));
450
451 processValidatorScript(getScriptPath(), cycle, field, symbols);
452 }
453
454 /**
455 * Sets the value type from a string type name. The name may be a scalar numeric type, a fully
456 * qualified class name, or the name of a numeric wrapper type from java.lang (with the package
457 * name omitted).
458 *
459 * @since 3.0
460 */
461
462 public void setValueType(String typeName)
463 {
464 Class typeClass = (Class) TYPES.get(typeName);
465
466 if (typeClass == null)
467 throw new ApplicationRuntimeException(Tapestry.format(
468 "NumberValidator.unknown-type",
469 typeName));
470
471 _valueTypeClass = typeClass;
472 }
473
474 /** @since 3.0 * */
475
476 public void setValueTypeClass(Class valueTypeClass)
477 {
478 _valueTypeClass = valueTypeClass;
479 }
480
481 /**
482 * Returns the value type to convert strings back into. The default is int.
483 *
484 * @since 3.0
485 */
486
487 public Class getValueTypeClass()
488 {
489 return _valueTypeClass;
490 }
491
492 /** @since 3.0 */
493
494 public boolean isIntegerNumber()
495 {
496 NumberStrategy strategy = (NumberStrategy) _numberAdaptors.getStrategy(_valueTypeClass);
497 if (strategy == null)
498 return false;
499
500 return strategy.getNumberType() == NUMBER_TYPE_INTEGER;
501 }
502
503 protected String getDefaultScriptPath()
504 {
505 return "/org/apache/tapestry/valid/NumberValidator.script";
506 }
507 }