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    }