/******************************************************************************* * 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.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.ParseException; import java.util.Locale; import org.eclipse.core.databinding.validation.IValidator; 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.osgi.util.NLS; import org.eclipse.riena.core.util.ArraysUtil; import org.eclipse.riena.core.util.PropertiesUtils; import org.eclipse.riena.ui.ridgets.nls.Messages; /** * Checks if a given string could be safely converted to a decimal number, that is a number with a fraction part. <br> * <br> * * This rule supports partial correctness checking. Partial correct means that we do not treat missing fraction as error, where "missing fraction" * means there no fraction digit. */ public class ValidDecimal implements IValidator, IExecutableExtension { /** * @since 4.0 */ protected static final int DEFAULT_MAX_LENGTH = 15; /** * @since 4.0 */ protected static final int DEFAULT_NUMBER_OF_FRACTION_DIGITS = 2; private static final char FRENCH_GROUPING_SEPARATOR = (char) 0xA0; private boolean partialCheckSupported; private DecimalFormat format; private DecimalFormatSymbols symbols; private int numberOfFractionDigits; private int maxLength; private Locale locale; private boolean groupingInMessage = true; /** * Constructs a decimal type check rule with no partialChecking and the default {@code Locale}. * */ public ValidDecimal() { this(Locale.getDefault()); } /** * Constructs a decimal type check rule with no partialChecking and the given {@code Locale}.<br> * <b>Note:</b> this constructor sets {@code maxLength} to 15 and {@code numberOfFractionDigits} to 2 * * @param locale * the {@code Locale} to use for number formatting; never null. */ public ValidDecimal(final Locale locale) { this(false, locale); } /** * Constructs a decimal type check rule. * * @param partialCheckSupported * <tt>true</tt> if partial checking is required. * @param locale * the {@code Locale} to use for number formatting; never null */ public ValidDecimal(final boolean partialCheckSupported, final Locale locale) { this(partialCheckSupported, DEFAULT_NUMBER_OF_FRACTION_DIGITS, DEFAULT_MAX_LENGTH, false, locale); } /** * Constructs a decimal type check rule. * * @param partialCheckSupported * <tt>true</tt> if partial checking is required. * @param numberOfFractionDigits * number of fraction digits. * @param maxLength * number of integer digits. * @param withSign * use sign or not. * @param locale * the {@code Locale} to use for number formatting; never null */ public ValidDecimal(final boolean partialCheckSupported, final int numberOfFractionDigits, final int maxLength, final boolean withSign, final Locale locale) { Assert.isNotNull(locale); this.partialCheckSupported = partialCheckSupported; this.numberOfFractionDigits = numberOfFractionDigits; this.maxLength = maxLength; // TODO: Configure format with withSign?! this.locale = locale; } /** * Configure whether the numbers, cited in the validation status messages should be grouped or not. * <p> * The default for this setting is <code>true</code>. * * @param groupingInMessage * the groupingInMessage to set * @since 4.0 */ public void setGroupingInMessage(final boolean groupingInMessage) { this.groupingInMessage = groupingInMessage; } /** * @return the groupingInMessage <code>true</code> if this validator will use grouping in its status messages. * @since 4.0 */ protected boolean isGroupingInMessage() { return groupingInMessage; } /** * @return the locale that is used in this validator * @since 4.0 */ protected Locale getLocale() { return locale; } /** * Validates the given object. If the object is no String instance, a {@link ValidationFailure} will be thrown. The rule validates if the given object is a * string, a well formed decimal according to the rule's {@linkplain Locale}. * * @param object * the object to validate, must be of type String. */ public IStatus validate(final Object value) { if (value != null) { if (!(value instanceof String)) { throw new ValidationFailure("ValidCharacters can only validate objects of type String."); //$NON-NLS-1$ } final String string = Utils.removeWhitespace((String) value); if (string.length() > 0) { final ScanResult scanned = scan(string); if (!partialCheckSupported) { if (scanned.decimalSeparatorIndex < 0) { final Character decSep = Character.valueOf(getSymbols().getDecimalSeparator()); final String message = NLS.bind(Messages.ValidDecimal_error_noDecSep, decSep, string); return ValidationRuleStatus.error(true, message); } // test if grouping character is behind decimal separator: if (scanned.groupingSeparatorIndex > scanned.decimalSeparatorIndex) { final Character groupSep = Character.valueOf(getSymbols().getGroupingSeparator()); final Character decSep = Character.valueOf(getSymbols().getDecimalSeparator()); final String message = NLS.bind(Messages.ValidDecimal_error_trailingGroupSep, new Object[] { groupSep, decSep, string }); return ValidationRuleStatus.error(true, message); } } // test if alien character present: if (scanned.lastAlienCharIndex > -1) { final String message = NLS.bind(Messages.ValidDecimal_error_alienChar, Character.valueOf(scanned.lastAlienCharacter), string); return ValidationRuleStatus.error(true, message); } try { synchronized (getFormat()) {// NumberFormat not thread-safe! getFormat().parse(string); } } catch (final ParseException e) { final String message = NLS.bind(Messages.ValidDecimal_error_cannotParse, string); return ValidationRuleStatus.error(true, message); } if (scanned.length > maxLength) { final String message = NLS.bind(Messages.ValidDecimal_error_maxLength, string, maxLength); return ValidationRuleStatus.error(true, message); } if (scanned.fractionDigits > numberOfFractionDigits) { final String message = NLS.bind(Messages.ValidDecimal_error_numberOfFractionDigits, string, numberOfFractionDigits); return ValidationRuleStatus.error(true, message); } } } return ValidationRuleStatus.ok(); } /** * Contains the result of the {@link ValidDecimal#scan(String)} method. */ protected static final class ScanResult { /** * The index of the decimal-separator character. If more than one is present, this will hold the last index. */ protected int decimalSeparatorIndex = -1; /** * The index of the last grouping-separator character found. */ protected int groupingSeparatorIndex = -1; /** * The index of the last minus sign character found. */ protected int minusSignIndex = -1; /** * The last alien character found. Where "alien" means no digit, minus-sign, decimal-separator or grouping-separator. In case the * grouping-character is <tt>(char)0xa0</tt>, like for the French locale's NumberFormat, whitespace is not considered alien either. * * @see Character#isDigit(char) * @see Character#isWhitespace(char) * @see DecimalFormatSymbols */ protected char lastAlienCharacter; /** * The index of the last alien character found. Where "alien" means no digit, minus-sign, decimal-separator or grouping-separator. In case the * grouping-character is <tt>(char)0xa0</tt>, like for the French locale's NumberFormat, whitespace is not considered alien either. * * @see Character#isDigit(char) * @see Character#isWhitespace(char) * @see DecimalFormatSymbols */ protected int lastAlienCharIndex = -1; // The number of digits before the decimal separator protected int length = 0; // The number of digits behind the decimal separator protected int fractionDigits = 0; private ScanResult() { // empty } } /** * Scans the parameter's String instance and returns Information about indexes of different characters. * * @param string * the string * @return a ScanResult instance */ protected ScanResult scan(final String string) { final ScanResult result = new ScanResult(); final boolean acceptWhitespaceAsGroupingSeparator = Character.isWhitespace(getSymbols().getGroupingSeparator()) || getSymbols().getGroupingSeparator() == FRENCH_GROUPING_SEPARATOR; final char minusSign = getSymbols().getMinusSign(); for (int t = 0; t < string.length(); ++t) { final char currentChar = string.charAt(t); if (currentChar == getSymbols().getDecimalSeparator()) { result.decimalSeparatorIndex = t; } else if (currentChar == getSymbols().getGroupingSeparator() || (Character.isWhitespace(currentChar) && acceptWhitespaceAsGroupingSeparator)) { result.groupingSeparatorIndex = t; } else if (currentChar == minusSign) { result.minusSignIndex = t; } else if (!Character.isDigit(currentChar)) { result.lastAlienCharacter = currentChar; result.lastAlienCharIndex = t; } else if (Character.isDigit(currentChar)) { if (result.decimalSeparatorIndex == -1) { result.length++; } else { result.fractionDigits++; } } } return result; } /** * Gets this rule's NumberFormat to parse a string. Accessing the format must be synchronized, as it is not thread safe. * * @see DecimalFormatSymbols * @return a {@linkplain DecimalFormat} instance */ protected DecimalFormat getFormat() { if (format == null) { format = (DecimalFormat) DecimalFormat.getInstance(locale); format.setMaximumFractionDigits(numberOfFractionDigits); format.setMaximumIntegerDigits(maxLength); } return format; } /** * Gets the s DecimalFormatSymbols of this rule's {@link #format}. Changes on the symbols will change the rule's format accordingly. As DecimalFormat is not * thread safe, changes to the symbols must be properly synchronized with accessing the format. * * @see #getFormat() */ protected DecimalFormatSymbols getSymbols() { if (symbols == null) { symbols = getFormat().getDecimalFormatSymbols(); } return symbols; } /** * Creates and sets the {@code Locale} for this validator. * * @param localeArgs * language, country, variant * @since 3.0 */ protected void setLocale(final String[] localeArgs) { if (localeArgs.length > 0) { final String language = localeArgs[0]; final String country = localeArgs.length > 1 ? localeArgs[1] : ""; //$NON-NLS-1$ final String variant = localeArgs.length > 2 ? localeArgs[2] : ""; //$NON-NLS-1$ setLocale(new Locale(language, country, variant)); } } private void setLocale(final Locale locale) { this.locale = locale; } /** * This method is called on a newly constructed extension for validation. After creating a new instance of {@code ValidDecimal} 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. * * * @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[]) */ public void setInitializationData(final IConfigurationElement config, final String propertyName, final Object data) throws CoreException { if (data instanceof String) { final String[] args = PropertiesUtils.asArray(data); int localStart = 0; if (args.length > 0) { if (args[0].equals(Boolean.TRUE.toString())) { this.partialCheckSupported = true; localStart++; } else if (args[0].equals(Boolean.FALSE.toString())) { this.partialCheckSupported = false; localStart++; } } if ((args.length > 1) && (args[1].length() > 0)) { try { this.numberOfFractionDigits = Integer.parseInt(args[1]); localStart++; if ((args.length > 2) && (args[2].length() > 0)) { this.maxLength = Integer.parseInt(args[2]); localStart++; if ((args.length > 3) && (args[3].length() > 0)) { if (args[3].equals(Boolean.TRUE.toString())) { // TODO: Configure format with withSign?! localStart++; } else if (args[3].equals(Boolean.FALSE.toString())) { // TODO: Configure format with withSign?! localStart++; } } } } catch (final NumberFormatException e1) { } } if (args.length > localStart) { final String[] localArgs = ArraysUtil.copyRange(args, localStart, args.length); setLocale(localArgs); } } } }