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