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    }