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    }