/*******************************************************************************
* Copyright (c) 2007, 2014 compeople AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* compeople AG - initial API and implementation
*******************************************************************************/
package org.eclipse.riena.ui.ridgets.validation;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.Locale;
import com.ibm.icu.text.NumberFormat;
import org.osgi.service.log.LogService;
import org.eclipse.core.databinding.conversion.IConverter;
import org.eclipse.core.databinding.conversion.NumberToStringConverter;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExecutableExtension;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.equinox.log.Logger;
import org.eclipse.osgi.util.NLS;
import org.eclipse.riena.core.Log4r;
import org.eclipse.riena.core.util.ArraysUtil;
import org.eclipse.riena.core.util.PropertiesUtils;
import org.eclipse.riena.ui.ridgets.nls.Messages;
/**
* A range check rule for a Number.
* <p>
* Checks if a given string could be safely converted to a given number type and if the input is in a given range. A value of <tt>null</tt> or an empty String
* is treated as zero.
* <p>
* This rule does not support partial correctness checking.
*
* @see ValidRangeAllowEmpty
*/
public class ValidRange extends ValidDecimal implements IExecutableExtension {
/**
* Value to request unlimited precision in method {@link #toBigDecimal(Number, int)}
*
* @since 5.0
*/
protected static final int PRECISION_UNLIMITED = 0;
private Number min;
private Number max;
private final IConverter converter;
/**
* Constructs a range check rule for the default locate, with the range set to (0, 0).
*/
public ValidRange() {
this(0, 0);
}
/**
* Constructs a range check rule for the default locale, with the range set to (min, max).
*
* @param min
* the minimum value
* @param max
* the maximum value
*
* @throws some_kind_of_runtime_exception
* if <tt>min >= max</tt>.
* @throws some_kind_of_runtime_exception
* if a parameter is <tt>null</tt>.
* @throws some_kind_of_runtime_exception
* if a parameter <tt>min</tt> and <tt>max</tt> do not belong to the same class.
*/
public ValidRange(final Number min, final Number max) {
this(min, max, Locale.getDefault());
}
/**
* Constructs a range check rule for the given locale, with the range set to (min, max).
*
* @param min
* the minimum value
* @param max
* the maximum value
* @param locale
* the Locale to use for number formatting; never null.
*
* @throws some_kind_of_runtime_exception
* if <tt>min >= max</tt>.
* @throws some_kind_of_runtime_exception
* if a parameter is <tt>null</tt>.
* @throws some_kind_of_runtime_exception
* if a parameter <tt>min</tt> and <tt>max</tt> do not belong to the same class.
*/
public ValidRange(final Number min, final Number max, final Locale locale) {
this(min, max, locale, null);
}
/**
* Constructs a range check rule for the given locale, with the range set to (min, max).
*
* @param min
* the minimum value
* @param max
* the maximum value
* @param converter
* a IConverter capable of converting the range min and max numbers to a string (only used for the error reporting)
*
* @throws some_kind_of_runtime_exception
* if <tt>min >= max</tt>.
* @throws some_kind_of_runtime_exception
* if a parameter is <tt>null</tt>.
* @throws some_kind_of_runtime_exception
* if a parameter <tt>min</tt> and <tt>max</tt> do not belong to the same class.
*
* @since 4.0
*/
public ValidRange(final Number min, final Number max, final IConverter converter) {
this(min, max, Locale.getDefault(), converter);
}
/**
* Constructs a range check rule for the given locale, with the range set to (min, max).
*
* @param min
* the minimum value
* @param max
* the maximum value
* @param locale
* the Locale to use for number formatting; never null.
* @param converter
* a IConverter capable of converting the range min and max numbers to a string (only used for the error reporting)
*
* @throws some_kind_of_runtime_exception
* if <tt>min >= max</tt>.
* @throws some_kind_of_runtime_exception
* if a parameter is <tt>null</tt>.
* @throws some_kind_of_runtime_exception
* if a parameter <tt>min</tt> and <tt>max</tt> do not belong to the same class.
*
* @since 4.0
*/
public ValidRange(final Number min, final Number max, final Locale locale, final IConverter converter) {
this(min, max, locale, converter, DEFAULT_NUMBER_OF_FRACTION_DIGITS, DEFAULT_MAX_LENGTH);
}
/**
* Constructs a range check rule for the given locale, with the range set to (min, max).
*
* @param min
* the minimum value
* @param max
* the maximum value
* @param locale
* the Locale to use for number formatting; never null.
* @param converter
* a IConverter capable of converting the range min and max numbers to a string (only used for the error reporting)
* @param numberOfFractionDigits
* number of fraction digits.
* @param maxLength
* number of integer digits.
*
* @throws some_kind_of_runtime_exception
* if <tt>min >= max</tt>.
* @throws some_kind_of_runtime_exception
* if a parameter is <tt>null</tt>.
* @throws some_kind_of_runtime_exception
* if a parameter <tt>min</tt> and <tt>max</tt> do not belong to the same class.
*
* @since 4.0
*/
public ValidRange(final Number min, final Number max, final Locale locale, final IConverter converter, final int numberOfFractionDigits, final int maxLength) {
super(true, numberOfFractionDigits, maxLength, true, locale);
Assert.isNotNull(min, "parameter min must not be null"); //$NON-NLS-1$
Assert.isNotNull(max, "parameter max must not be null"); //$NON-NLS-1$
Assert.isLegal(min.getClass().equals(max.getClass()), "min and max must be of the same class. (min = " //$NON-NLS-1$
+ min.getClass().getName() + ", max = " + max.getClass().getName()); //$NON-NLS-1$
this.min = min;
this.max = max;
Assert.isLegal(toBigDecimal(this.min).compareTo(toBigDecimal(this.max)) <= 0, "min " + this.min + " must be smaller or equal max " //$NON-NLS-1$ //$NON-NLS-2$
+ this.max);
this.converter = converter;
}
/**
* Returns an IStatus instance; never null.
*
* @param value
* a <tt>String</tt> instance or <tt>null</tt>, where <tt>null</tt> is treated as zero.
*
* @see org.eclipse.riena.ui.ridgets.validation.ValidDecimal#validate(java.lang.Object)
*/
@Override
public IStatus validate(final Object value) {
final IStatus validDecimalStatus = super.validate(value);
if (!validDecimalStatus.isOK()) {
return validDecimalStatus;
}
Assert.isLegal(value == null || value instanceof String);
BigDecimal currentValue = BigDecimal.ZERO;
if (value != null) {
final String string = Utils.removeWhitespace((String) value);
if (string.length() > 0) {
final DecimalFormat format = getFormat();
synchronized (format) { // format not thread safe!
format.setParseBigDecimal(true);
try {
currentValue = (BigDecimal) format.parse(string);
} catch (final ParseException pex) {
// should never occur, as super.validate(Object) will
// make this method return earlier
final Logger logger = Log4r.getLogger(ValidRange.class);
final String message = NLS.bind(Messages.ValidRange_error_cannotParse, string);
logger.log(LogService.LOG_ERROR, message, pex);
return ValidationRuleStatus.error(true, message);
}
}
}
}
if (validateRange(currentValue)) {
final String message = NLS.bind(Messages.ValidRange_error_outOfRange, new Object[] { convert(currentValue), convert(min), convert(max) });
return ValidationRuleStatus.error(true, message);
}
return ValidationRuleStatus.ok();
}
private String convert(final Number number) {
if (converter != null) {
return (String) converter.convert(number);
}
final NumberFormat numberInstance = NumberFormat.getNumberInstance(getLocale());
numberInstance.setGroupingUsed(isGroupingInMessage());
return (String) NumberToStringConverter.fromBigDecimal(numberInstance).convert(toBigDecimal(number));
}
/**
* @since 3.0
*/
protected boolean validateRange(final BigDecimal value) {
return value.compareTo(toBigDecimal(min)) < 0 || value.compareTo(toBigDecimal(max)) > 0;
}
/**
* Convert a number to BigDecimal.
*
* @param number
* a number.
*
* @return a BigDecimal instance.
*/
protected BigDecimal toBigDecimal(final Number number) {
return toBigDecimal(number, 0);
}
/**
* Convert a number to BigDecimal.
*
* @param number
* a number.
*
* @param value
* the value used for the precision of the number to convert.
*
* @return a BigDecimal instance.
* @since 3.0
*/
protected BigDecimal toBigDecimal(final Number number, final BigDecimal value) {
return toBigDecimal(number, value.precision());
}
/**
* Convert a number to BigDecimal.
*
* @param number
* a number.
*
* @param precision
* The number of digits to be used for an operation. A value of {@link #PRECISION_UNLIMITED} indicates that unlimited precision (as many digits
* as are required) will be used. Note that leading zeros (in the coefficient of a number) are never significant.
*
* @return a BigDecimal instance.
* @since 3.0
*/
protected BigDecimal toBigDecimal(final Number number, final int precision) {
if (number instanceof BigDecimal) {
return (BigDecimal) number;
} else if (number instanceof BigInteger) {
return new BigDecimal((BigInteger) number);
} else if (precision == PRECISION_UNLIMITED && number instanceof Double) {
return new BigDecimal(Double.toString((Double) number));
} else if (precision == PRECISION_UNLIMITED && number instanceof Float) {
return new BigDecimal(Float.toString((Float) number));
} else if (number instanceof Float || number instanceof Double) {
return new BigDecimal(number.doubleValue(), new MathContext(precision));
} else {
return new BigDecimal(number.longValue());
}
}
/**
* This method is called on a newly constructed extension for validation. After creating a new instance of {@code ValidRange} this method is called to
* initialize the instance. The arguments for initialization are in the parameter {@code data}. Is the data a string the arguments are separated with ','.
* The order of the arguments in data is equivalent to the order of the parameters of one of the constructors.<br>
* If data has more than two arguments. The last arguments are used to set the Local for this validator.
*
*
* @see org.eclipse.core.runtime.IExecutableExtension#setInitializationData(org.eclipse.core.runtime.IConfigurationElement, java.lang.String,
* java.lang.Object)
* @see org.eclipse.riena.ui.ridgets.validation.ValidDecimal#setLocale(java.lang.String[])
*/
@Override
public void setInitializationData(final IConfigurationElement config, final String propertyName, final Object data) throws CoreException {
if (data instanceof String) {
final String[] args = PropertiesUtils.asArray(data);
if (args.length > 0) {
this.min = new BigDecimal(args[0]);
}
if (args.length > 1) {
this.max = new BigDecimal(args[1]);
}
if (args.length > 2) {
final String[] localArgs = ArraysUtil.copyRange(args, 2, args.length);
setLocale(localArgs);
}
}
}
}