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 }