blob: c48fb45683697767957a33a8cbc2e3c0e4834b21 [file] [log] [blame]
// Copyright 2004 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry.valid;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.tapestry.ApplicationRuntimeException;
import org.apache.tapestry.IMarkupWriter;
import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.Tapestry;
import org.apache.tapestry.form.IFormComponent;
import org.apache.tapestry.util.AdaptorRegistry;
/**
* Simple validation for standard number classes. This is probably insufficient
* for anything tricky and application specific, such as parsing currency.
*
* @author Howard Lewis Ship
* @version $Id$
* @since 1.0.8
*
**/
public class NumberValidator extends BaseValidator
{
private static final Map TYPES = new HashMap();
static {
TYPES.put("boolean", boolean.class);
TYPES.put("Boolean", Boolean.class);
TYPES.put("java.lang.Boolean", Boolean.class);
TYPES.put("char", char.class);
TYPES.put("Character", Character.class);
TYPES.put("java.lang.Character", Character.class);
TYPES.put("short", short.class);
TYPES.put("Short", Short.class);
TYPES.put("java.lang.Short", Short.class);
TYPES.put("int", int.class);
TYPES.put("Integer", Integer.class);
TYPES.put("java.lang.Integer", Integer.class);
TYPES.put("long", long.class);
TYPES.put("Long", Long.class);
TYPES.put("java.lang.Long", Long.class);
TYPES.put("float", float.class);
TYPES.put("Float", Float.class);
TYPES.put("java.lang.Float", Float.class);
TYPES.put("byte", byte.class);
TYPES.put("Byte", Byte.class);
TYPES.put("java.lang.Byte", Byte.class);
TYPES.put("double", double.class);
TYPES.put("Double", Double.class);
TYPES.put("java.lang.Double", Double.class);
TYPES.put("java.math.BigInteger", BigInteger.class);
TYPES.put("java.math.BigDecimal", BigDecimal.class);
}
private static final Set INT_TYPES = new HashSet();
private Class _valueTypeClass = int.class;
private boolean _zeroIsNull;
private Number _minimum;
private Number _maximum;
private String _scriptPath = "/org/apache/tapestry/valid/NumberValidator.script";
private String _invalidNumericFormatMessage;
private String _invalidIntegerFormatMessage;
private String _numberTooSmallMessage;
private String _numberTooLargeMessage;
private String _numberRangeMessage;
private static AdaptorRegistry _numberAdaptors = new AdaptorRegistry();
public final static int NUMBER_TYPE_INTEGER = 0;
public final static int NUMBER_TYPE_REAL = 1;
/**
* This class is not meant for use outside of NumberValidator; it
* is public only to fascilitate some unit testing.
*
*/
public static abstract class NumberAdaptor
{
/**
* Parses a non-empty {@link String} into the correct subclass of
* {@link Number}.
*
* @throws NumberFormatException if the String can not be parsed.
**/
abstract public Number parse(String value);
/**
* Indicates the type of the number represented -- integer or real.
* The information is used to build the client-side validator.
* This method could return a boolean, but returns an int to allow
* future extensions of the validator.
*
* @return one of the predefined number types
**/
abstract public int getNumberType();
public int compare(Number left, Number right)
{
if (!left.getClass().equals(right.getClass()))
right = coerce(right);
Comparable lc = (Comparable) left;
return lc.compareTo(right);
}
/**
* Invoked when comparing two Numbers of different types.
* The number is cooerced from its ordinary type to
* the correct type for comparison.
*
* @since 3.0
*/
protected abstract Number coerce(Number number);
}
private static abstract class IntegerNumberAdaptor extends NumberAdaptor
{
public int getNumberType()
{
return NUMBER_TYPE_INTEGER;
}
}
private static abstract class RealNumberAdaptor extends NumberAdaptor
{
public int getNumberType()
{
return NUMBER_TYPE_REAL;
}
}
private static class ByteAdaptor extends IntegerNumberAdaptor
{
public Number parse(String value)
{
return new Byte(value);
}
protected Number coerce(Number number)
{
return new Byte(number.byteValue());
}
}
private static class ShortAdaptor extends IntegerNumberAdaptor
{
public Number parse(String value)
{
return new Short(value);
}
protected Number coerce(Number number)
{
return new Short(number.shortValue());
}
}
private static class IntAdaptor extends IntegerNumberAdaptor
{
public Number parse(String value)
{
return new Integer(value);
}
protected Number coerce(Number number)
{
return new Integer(number.intValue());
}
}
private static class LongAdaptor extends IntegerNumberAdaptor
{
public Number parse(String value)
{
return new Long(value);
}
protected Number coerce(Number number)
{
return new Long(number.longValue());
}
}
private static class FloatAdaptor extends RealNumberAdaptor
{
public Number parse(String value)
{
return new Float(value);
}
protected Number coerce(Number number)
{
return new Float(number.floatValue());
}
}
private static class DoubleAdaptor extends RealNumberAdaptor
{
public Number parse(String value)
{
return new Double(value);
}
protected Number coerce(Number number)
{
return new Double(number.doubleValue());
}
}
private static class BigDecimalAdaptor extends RealNumberAdaptor
{
public Number parse(String value)
{
return new BigDecimal(value);
}
protected Number coerce(Number number)
{
return new BigDecimal(number.doubleValue());
}
}
private static class BigIntegerAdaptor extends IntegerNumberAdaptor
{
public Number parse(String value)
{
return new BigInteger(value);
}
protected Number coerce(Number number)
{
return new BigInteger(number.toString());
}
}
static {
NumberAdaptor byteAdaptor = new ByteAdaptor();
NumberAdaptor shortAdaptor = new ShortAdaptor();
NumberAdaptor intAdaptor = new IntAdaptor();
NumberAdaptor longAdaptor = new LongAdaptor();
NumberAdaptor floatAdaptor = new FloatAdaptor();
NumberAdaptor doubleAdaptor = new DoubleAdaptor();
_numberAdaptors.register(Byte.class, byteAdaptor);
_numberAdaptors.register(byte.class, byteAdaptor);
_numberAdaptors.register(Short.class, shortAdaptor);
_numberAdaptors.register(short.class, shortAdaptor);
_numberAdaptors.register(Integer.class, intAdaptor);
_numberAdaptors.register(int.class, intAdaptor);
_numberAdaptors.register(Long.class, longAdaptor);
_numberAdaptors.register(long.class, longAdaptor);
_numberAdaptors.register(Float.class, floatAdaptor);
_numberAdaptors.register(float.class, floatAdaptor);
_numberAdaptors.register(Double.class, doubleAdaptor);
_numberAdaptors.register(double.class, doubleAdaptor);
_numberAdaptors.register(BigDecimal.class, new BigDecimalAdaptor());
_numberAdaptors.register(BigInteger.class, new BigIntegerAdaptor());
}
public String toString(IFormComponent field, Object value)
{
if (value == null)
return null;
if (_zeroIsNull)
{
Number number = (Number) value;
if (number.doubleValue() == 0.0)
return null;
}
return value.toString();
}
private NumberAdaptor getAdaptor(IFormComponent field)
{
NumberAdaptor result = getAdaptor(_valueTypeClass);
if (result == null)
throw new ApplicationRuntimeException(
Tapestry.format(
"NumberValidator.no-adaptor-for-field",
field,
_valueTypeClass.getName()));
return result;
}
/**
* Returns an adaptor for the given type.
*
* <p>
* Note: this method exists only for testing purposes. It is not meant to
* be invoked by user code and is subject to change at any time.
*
* @param type the type (a Number subclass) for which to return an adaptor
* @return the adaptor, or null if no such adaptor may be found
* @since 3.0
*/
public static NumberAdaptor getAdaptor(Class type)
{
return (NumberAdaptor) _numberAdaptors.getAdaptor(type);
}
public Object toObject(IFormComponent field, String value) throws ValidatorException
{
if (checkRequired(field, value))
return null;
NumberAdaptor adaptor = getAdaptor(field);
Number result = null;
try
{
result = adaptor.parse(value);
}
catch (NumberFormatException ex)
{
throw new ValidatorException(
buildInvalidNumericFormatMessage(field),
ValidationConstraint.NUMBER_FORMAT);
}
if (_minimum != null && adaptor.compare(result, _minimum) < 0)
throw new ValidatorException(
buildNumberTooSmallMessage(field),
ValidationConstraint.TOO_SMALL);
if (_maximum != null && adaptor.compare(result, _maximum) > 0)
throw new ValidatorException(
buildNumberTooLargeMessage(field),
ValidationConstraint.TOO_LARGE);
return result;
}
public Number getMaximum()
{
return _maximum;
}
public boolean getHasMaximum()
{
return _maximum != null;
}
public void setMaximum(Number maximum)
{
_maximum = maximum;
}
public Number getMinimum()
{
return _minimum;
}
public boolean getHasMinimum()
{
return _minimum != null;
}
public void setMinimum(Number minimum)
{
_minimum = minimum;
}
/**
* If true, then when rendering, a zero is treated as a non-value, and null is returned.
* If false, the default, then zero is rendered as zero.
*
**/
public boolean getZeroIsNull()
{
return _zeroIsNull;
}
public void setZeroIsNull(boolean zeroIsNull)
{
_zeroIsNull = zeroIsNull;
}
/**
*
* @since 2.2
*
**/
public void renderValidatorContribution(
IFormComponent field,
IMarkupWriter writer,
IRequestCycle cycle)
{
if (!isClientScriptingEnabled())
return;
if (!(isRequired() || _minimum != null || _maximum != null))
return;
Map symbols = new HashMap();
if (isRequired())
symbols.put("requiredMessage", buildRequiredMessage(field));
if (isIntegerNumber())
symbols.put("formatMessage", buildInvalidIntegerFormatMessage(field));
else
symbols.put("formatMessage", buildInvalidNumericFormatMessage(field));
if (_minimum != null || _maximum != null)
symbols.put("rangeMessage", buildRangeMessage(field));
processValidatorScript(_scriptPath, cycle, field, symbols);
}
private String buildRangeMessage(IFormComponent field)
{
if (_minimum != null && _maximum != null)
return buildNumberRangeMessage(field);
if (_maximum != null)
return buildNumberTooLargeMessage(field);
return buildNumberTooSmallMessage(field);
}
/**
* @since 2.2
*
**/
public String getScriptPath()
{
return _scriptPath;
}
/**
* Allows a developer to use the existing validation logic with a different client-side
* script. This is often sufficient to allow application-specific error presentation
* (perhaps by using DHTML to update the content of a &lt;span&gt; tag, or to use
* a more sophisticated pop-up window than <code>window.alert()</code>).
*
* @since 2.2
*
**/
public void setScriptPath(String scriptPath)
{
_scriptPath = scriptPath;
}
/** Sets the value type from a string type name. The name may be
* a scalar numeric type, a fully qualified class name, or the name
* of a numeric wrapper type from java.lang (with the package name omitted).
*
* @since 3.0
*
**/
public void setValueType(String typeName)
{
Class typeClass = (Class) TYPES.get(typeName);
if (typeClass == null)
throw new ApplicationRuntimeException(
Tapestry.format("NumberValidator.unknown-type", typeName));
_valueTypeClass = typeClass;
}
/** @since 3.0 **/
public void setValueTypeClass(Class valueTypeClass)
{
_valueTypeClass = valueTypeClass;
}
/**
*
* Returns the value type to convert strings back into. The default is int.
*
* @since 3.0
*
**/
public Class getValueTypeClass()
{
return _valueTypeClass;
}
/** @since 3.0 */
public String getInvalidNumericFormatMessage()
{
return _invalidNumericFormatMessage;
}
/** @since 3.0 */
public String getInvalidIntegerFormatMessage()
{
return _invalidIntegerFormatMessage;
}
/** @since 3.0 */
public String getNumberRangeMessage()
{
return _numberRangeMessage;
}
/** @since 3.0 */
public String getNumberTooLargeMessage()
{
return _numberTooLargeMessage;
}
/** @since 3.0 */
public String getNumberTooSmallMessage()
{
return _numberTooSmallMessage;
}
/**
* Overrides the <code>invalid-numeric-format</code> bundle key.
* Parameter {0} is the display name of the field.
*
* @since 3.0
*/
public void setInvalidNumericFormatMessage(String string)
{
_invalidNumericFormatMessage = string;
}
/**
* Overrides the <code>invalid-int-format</code> bundle key.
* Parameter {0} is the display name of the field.
*
* @since 3.0
*/
public void setInvalidIntegerFormatMessage(String string)
{
_invalidIntegerFormatMessage = string;
}
/** @since 3.0 */
protected String buildInvalidNumericFormatMessage(IFormComponent field)
{
String pattern =
getPattern(
getInvalidNumericFormatMessage(),
"invalid-numeric-format",
field.getPage().getLocale());
return formatString(pattern, field.getDisplayName());
}
/** @since 3.0 */
protected String buildInvalidIntegerFormatMessage(IFormComponent field)
{
String pattern =
getPattern(
getInvalidIntegerFormatMessage(),
"invalid-int-format",
field.getPage().getLocale());
return formatString(pattern, field.getDisplayName());
}
/**
* Overrides the <code>number-range</code> bundle key.
* Parameter [0} is the display name of the field.
* Parameter {1} is the minimum value.
* Parameter {2} is the maximum value.
*
* @since 3.0
*/
public void setNumberRangeMessage(String string)
{
_numberRangeMessage = string;
}
protected String buildNumberRangeMessage(IFormComponent field)
{
String pattern =
getPattern(_numberRangeMessage, "number-range", field.getPage().getLocale());
return formatString(pattern, new Object[] { field.getDisplayName(), _minimum, _maximum });
}
/**
* Overrides the <code>number-too-large</code> bundle key.
* Parameter {0} is the display name of the field.
* Parameter {1} is the maximum allowed value.
*
* @since 3.0
*/
public void setNumberTooLargeMessage(String string)
{
_numberTooLargeMessage = string;
}
/** @since 3.0 */
protected String buildNumberTooLargeMessage(IFormComponent field)
{
String pattern =
getPattern(_numberTooLargeMessage, "number-too-large", field.getPage().getLocale());
return formatString(pattern, field.getDisplayName(), _maximum);
}
/**
* Overrides the <code>number-too-small</code> bundle key.
* Parameter {0} is the display name of the field.
* Parameter {1} is the minimum allowed value.
*
* @since 3.0
*
*/
public void setNumberTooSmallMessage(String string)
{
_numberTooSmallMessage = string;
}
/** @since 3.0 */
protected String buildNumberTooSmallMessage(IFormComponent field)
{
String pattern =
getPattern(_numberTooSmallMessage, "number-too-small", field.getPage().getLocale());
return formatString(pattern, field.getDisplayName(), _minimum);
}
/** @since 3.0 */
public boolean isIntegerNumber()
{
NumberAdaptor result = (NumberAdaptor) _numberAdaptors.getAdaptor(_valueTypeClass);
if (result == null)
return false;
return result.getNumberType() == NUMBER_TYPE_INTEGER;
}
}