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
015package org.apache.tapestry.valid;
016
017import java.math.BigDecimal;
018import java.math.BigInteger;
019import java.util.HashMap;
020import java.util.Map;
021
022import org.apache.hivemind.ApplicationRuntimeException;
023import org.apache.hivemind.lib.util.StrategyRegistry;
024import org.apache.hivemind.lib.util.StrategyRegistryImpl;
025import org.apache.hivemind.util.PropertyUtils;
026import org.apache.tapestry.IMarkupWriter;
027import org.apache.tapestry.IRequestCycle;
028import org.apache.tapestry.Tapestry;
029import 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
039public 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}