Java tutorial
/* * Copyright 2012 essendi it GmbH * Copyright 2014 Vaadin Ltd. * * 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.vaadin.ui; import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.HashMap; import java.util.Locale; import java.util.Map; import org.vaadin.ui.shared.numberfield.Constants; import org.vaadin.ui.shared.numberfield.NumberFieldAttributes; import org.vaadin.ui.shared.numberfield.NumberValidator; import com.vaadin.annotations.StyleSheet; import com.vaadin.data.Validator; import com.vaadin.data.Validator.InvalidValueException; import com.vaadin.data.util.converter.Converter.ConversionException; import com.vaadin.data.util.converter.StringToDoubleConverter; import com.vaadin.server.PaintException; import com.vaadin.server.PaintTarget; import com.vaadin.ui.TextField; /** * <p> * Provides a numeric field with automatic keystroke filtering and validation * for integer (123) and decimal numbers (12.3). The minus sign and * user-definable grouping and decimal separators are supported. * </p> * <p> * Inputs are validated on client- <b>and</b> server-side. The client-side * validator gets active on every keypress in the field. If the keypress would * lead to an invalid value, it is suppressed and the value remains unchanged. * The server-side validation is triggered when the field loses focus, see * {@link #addServerSideValidator()} for more details. To omit server-side * validation (which is enabled per default), you can call * {@link #removeServerSideValidator()}. * </p> * <p> * A user-entered value is formatted automatically when the field's focus is * lost. <code>NumberField</code> uses {@link DecimalFormat} for formatting and * send the formatted value of the input back to client. There's a number of * setters to define the format, see the code example below for a general view. * </p> * <p> * Some features in an usage example: <blockquote> * * <pre> * NumberField numField = new NumberField(); // NumberField extends TextField * numField.setDecimalAllowed(true); // not just integers (by default, decimals are * // allowed) * numField.setDecimalPrecision(2); // maximum 2 digits after the decimal separator * numField.setDecimalSeparator(','); // e.g. 1,5 * numField.setDecimalSeparatorAlwaysShown(true); // e.g. 12345 -> 12345, * numField.setMinimumFractionDigits(2); // e.g. 123,4 -> 123,40 * numField.setGroupingUsed(true); // use grouping (e.g. 12345 -> 12.345) * numField.setGroupingSeparator('.'); // use '.' as grouping separator * numField.setGroupingSize(3); // 3 digits between grouping separators: 12.345.678 * numField.setMinValue(0); // valid values must be >= 0 ... * numField.setMaxValue(999.9); // ... and <= 999.9 * numField.setErrorText("Invalid number format!"); // feedback message on bad * // input * numField.setNegativeAllowed(false); // prevent negative numbers (defaults to * // true) * numField.setValueIgnoreReadOnly("10"); // set the field's value, regardless * // whether it is read-only or not * numField.removeValidator(); // omit server-side validation * </pre> * * </blockquote> * </p> */ @StyleSheet("numberfield.css") @SuppressWarnings("serial") public class NumberField extends TextField { // Server-side validator private Validator numberValidator; // All NumberField instances share the same formatter and converter private static DecimalFormat decimalFormat = new DecimalFormat(); private static StringToDoubleConverter converter; // Settings needed both on server- and client-side private NumberFieldAttributes attributes = new NumberFieldAttributes(); // Settings needed only on server-side and therefore not part of // NumberFieldAttributes private String errorText = "Invalid number"; private int groupingSize; private int minimumFractionDigits; private boolean decimalSeparatorAlwaysShown; /** * <p> * Constructs an empty <code>NumberField</code> with no caption. The field * is bound to a server-side validator (see * {@link #addServerSideValidator()}) and immediate mode is set to true; * this is necessary for the validation of the field to occur immediately * when the input focus changes. * </p> * <p> * The decimal/grouping separator defaults to the user's local * decimal/grouping separator. Grouping (e.g. 12345 -> 12.345) is enabled, * the maximum precision to display after the decimal separator is set to 2 * per default. * </p> */ public NumberField() { super(); setImmediate(true); setDefaultValues(); } /** * <p> * Constructs an empty <code>NumberField</code> with given caption. The * field is bound to a server-side validator (see * {@link #addServerSideValidator()}) and immediate mode is set to true; * this is necessary for the validation of the field to occur immediately * when the input focus changes. * </p> * <p> * The decimal/grouping separator defaults to the user's local * decimal/grouping separator. Grouping (e.g. 12345 -> 12.345) is enabled, * the maximum precision to display after the decimal separator is set to 2 * per default. * </p> * * @param caption * Sets the component's caption {@code String}. */ public NumberField(String caption) { this(); setCaption(caption); } private void setDefaultValues() { setGroupingUsed(true); groupingSize = 3; setDecimalPrecision(2); minimumFractionDigits = 0; decimalSeparatorAlwaysShown = false; addServerSideValidator(); setNullSettingAllowed(true); // Set the decimal/grouping separator to the user's default locale attributes.setDecimalSeparator(DecimalFormatSymbols.getInstance().getDecimalSeparator()); attributes.setGroupingSeparator(DecimalFormatSymbols.getInstance().getGroupingSeparator()); // Member "attributes" defines some defaults as well! } @Override public void setConverter(Class<?> datamodelType) { if (datamodelType.isAssignableFrom(Double.class)) { if (converter == null) { createConverter(); } setConverter(converter); } else { super.setConverter(datamodelType); } } /** * Creates a converter that always uses US Locale. This is necessary because * localised internal values break validation. * * FIXME: This is a quick hack to fix problems with binding. Correct * solution would be to change the validation to respect localised values. */ private void createConverter() { converter = new StringToDoubleConverter() { @Override protected NumberFormat getFormat(Locale locale) { return super.getFormat(Locale.US); } @Override public Double convertToModel(String value, Class<? extends Double> targetType, Locale locale) throws com.vaadin.data.util.converter.Converter.ConversionException { return super.convertToModel(value, targetType, Locale.US); } @Override public String convertToPresentation(Double value, Class<? extends String> targetType, Locale locale) throws com.vaadin.data.util.converter.Converter.ConversionException { String result = super.convertToPresentation(value, targetType, Locale.US); if (result != null) { // remove thousand groupings result = result.replaceAll(",", ""); } return result; } }; } private void createNumberValidator() { // Create our server-side validator numberValidator = new Validator() { public boolean isValid(Object value) { return validateValue(value); } @Override public void validate(Object value) throws InvalidValueException { if (!isValid(value)) { throw new InvalidValueException(errorText); } } }; } private boolean validateValue(Object value) { if (value == null || "".equals(value)) { return !isRequired(); } // FIXME: This is a hack to get around converters. if (value instanceof Double) { value = BigDecimal.valueOf((Double) value).toPlainString(); } if (!(value instanceof String)) { return false; } final boolean isValid; if (isDecimalAllowed()) { isValid = NumberValidator.isValidDecimal((String) value, attributes, false); } else { isValid = NumberValidator.isValidInteger((String) value, attributes, false); } return isValid; } /** * Validates the field's value according to the validation rules determined * with the setters of this class (e.g. {@link #setDecimalAllowed(boolean)} * or {@link #setMaxValue(double)}). * * @return True, if validation succeeds; false, if validation fails. */ @Override public boolean isValid() { return validateValue(getValue()); } @Override public void paintContent(PaintTarget target) throws PaintException { super.paintContent(target); // Paint any component specific content by setting attributes. // These attributes can be read in the widget's updateFromUIDL(). // paintContent() is called when the contents of the component should be // painted in response to the component first being shown or having been // altered so that its visual representation is changed. target.addAttribute(Constants.ATTRIBUTE_ALLOW_DECIMALS, isDecimalAllowed()); target.addAttribute(Constants.ATTRIBUTE_ALLOW_NEGATIVES, isNegativeAllowed()); target.addAttribute(Constants.ATTRIBUTE_DECIMAL_PRECISION, getDecimalPrecision()); target.addAttribute(Constants.ATTRIBUTE_DECIMAL_SEPARATOR, getDecimalSeparator()); target.addAttribute(Constants.ATTRIBUTE_USE_GROUPING, isGroupingUsed()); target.addAttribute(Constants.ATTRIBUTE_GROUPING_SEPARATOR, getGroupingSeparator()); target.addAttribute(Constants.ATTRIBUTE_MIN_VALUE, getMinValue()); target.addAttribute(Constants.ATTRIBUTE_MAX_VALUE, getMaxValue()); // Use DecimalFormat to format the user-input and send the // formatted value back to client. target.addAttribute(Constants.ATTRIBUTE_SERVER_FORMATTED_VALUE, getValueAsFormattedDecimalNumber()); } private String getValueAsFormattedDecimalNumber() { Object value = getValue(); if (value == null || "".equals(value)) { return ""; } try { synchronized (decimalFormat) { setDecimalFormatToNumberFieldAttributes(); // Number valueAsNumber = decimalFormat.parse(value); Number valueAsNumber = Double.valueOf(value.toString()); return decimalFormat.format(valueAsNumber); } } catch (Exception e) { e.printStackTrace(); return value.toString(); } } @Override public void changeVariables(Object source, Map<String, Object> variables) { if (variables.containsKey("text") && !isReadOnly()) { // Workaround so that we can modify the values variables = new HashMap<String, Object>(variables); // Only do the setting if the string representation of the value // has been updated String newValue = (String) variables.get("text"); try { synchronized (decimalFormat) { setDecimalFormatToNumberFieldAttributes(); if (newValue != null && newValue.trim().equals("")) { variables.put("text", null); } else { Number valueAsNumber = decimalFormat.parse(newValue); variables.put("text", valueAsNumber.toString()); } } } catch (Exception e) { e.printStackTrace(); } } super.changeVariables(source, variables); } private void setDecimalFormatToNumberFieldAttributes() { synchronized (decimalFormat) { decimalFormat.setGroupingUsed(attributes.isGroupingUsed()); decimalFormat.setGroupingSize(groupingSize); decimalFormat.setMinimumFractionDigits(minimumFractionDigits); decimalFormat.setDecimalSeparatorAlwaysShown(decimalSeparatorAlwaysShown); // Adapt decimal format symbols DecimalFormatSymbols symbols = decimalFormat.getDecimalFormatSymbols(); symbols.setDecimalSeparator(attributes.getDecimalSeparator()); symbols.setGroupingSeparator(attributes.getGroupingSeparator()); decimalFormat.setDecimalFormatSymbols(symbols); } } /** * <p> * Binds the field to a server-side validator. The validation (is it a valid * integer/decimal number?) provides feedback about bad input. If the input * is recognized as invalid, an {@link InvalidValueException} is thrown, * which reports an error message and mark the field as invalid. The error * message can be set with {@link #setErrorText()}. * </p> * <p> * To have the validation done immediately when the field loses focus, * immediate mode should not be disabled (state is enabled per default). * </p> */ public void addServerSideValidator() { if (numberValidator == null) { createNumberValidator(); } super.addValidator(numberValidator); } /** * Removes the field's validator. * * @see #addServerSideValidator() */ public void removeServerSideValidator() { super.removeValidator(numberValidator); } /** * @param newValue * Sets the value of the field to a double. */ public void setValue(Double newValue) throws ReadOnlyException, ConversionException { if (newValue == null) { super.setValue(null); } else { // Use BigDecimal to avoid scientific notation and thus allow // NumberValidator to work properly String noExponent = BigDecimal.valueOf(newValue).toPlainString(); super.setValue(noExponent); } } /** * Sets the value of the field, regardless whether the field is in read-only * mode. * * @param newValue * The field's new String value. */ public void setValueIgnoreReadOnly(String newValue) { boolean wasReadOnly = isReadOnly(); if (isReadOnly()) { setReadOnly(false); } setValue(newValue); if (wasReadOnly) { setReadOnly(true); } } /** * Sets the value of the field, regardless whether the field is in read-only * mode. * * @param newValue * The field's new Double value. */ public void setValueIgnoreReadOnly(Double newValue) { boolean wasReadOnly = isReadOnly(); if (isReadOnly()) { setReadOnly(false); } setValue(newValue); if (wasReadOnly) { setReadOnly(true); } } /** * See {@link NumberFieldAttributes#setDecimalAllowed(boolean)}. * <p> * As a side-effect, the minimum number of digits allowed in the fraction * portion of a number is set to 0 if decimalAllowed is false (so that * {@link DecimalFormat} does the right thing). * </p> */ public void setDecimalAllowed(boolean decimalAllowed) { attributes.setDecimalAllowed(decimalAllowed); if (!decimalAllowed) { setMinimumFractionDigits(0); } markAsDirty(); } /** * See {@link NumberFieldAttributes#isDecimalAllowed()}. */ public boolean isDecimalAllowed() { return attributes.isDecimalAllowed(); } /** * @param text * The error text to display in case of an invalid field value.<br/> * Caution: If the argument is "" or <code>null</code>, the field * won't be recognizable as invalid! */ public void setErrorText(String text) { errorText = text; } /** * @return The error text displayed in case of an invalid field value. */ public String getErrorText() { return errorText; } /** * See {@link NumberFieldAttributes#isGroupingUsed()}. */ public boolean isGroupingUsed() { return attributes.isGroupingUsed(); } /** * See {@link NumberFieldAttributes#setGroupingUsed(boolean)}. */ public void setGroupingUsed(boolean group) { attributes.setGroupingUsed(group); markAsDirty(); } /** * See {@link DecimalFormat#getGroupingSize()}. */ public int getGroupingSize() { return groupingSize; } /** * See {@link DecimalFormat#setGroupingSize(int)}. */ public void setGroupingSize(int groupingSize) { this.groupingSize = groupingSize; markAsDirty(); } /** * See {@link DecimalFormat#getMinimumFractionDigits()}. */ public int getMinimumFractionDigits() { return minimumFractionDigits; } /** * See {@link DecimalFormat#setMinimumFractionDigits(int)}. */ public void setMinimumFractionDigits(int minimumDigits) { minimumFractionDigits = minimumDigits; markAsDirty(); } /** * See {@link DecimalFormat#isDecimalSeparatorAlwaysShown()}. */ public boolean isDecimalSeparatorAlwaysShown() { return decimalSeparatorAlwaysShown; } /** * See {@link DecimalFormat#setDecimalSeparatorAlwaysShown(boolean)}. */ public void setDecimalSeparatorAlwaysShown(boolean showAlways) { decimalSeparatorAlwaysShown = showAlways; markAsDirty(); } /** * See {@link NumberFieldAttributes#getDecimalPrecision()}. */ public int getDecimalPrecision() { return attributes.getDecimalPrecision(); } /** * See {@link NumberFieldAttributes#setDecimalPrecision(int)}. */ public void setDecimalPrecision(int maximumDigits) { attributes.setDecimalPrecision(maximumDigits); markAsDirty(); } /** * See {@link NumberFieldAttributes#getDecimalSeparator()}. */ public char getDecimalSeparator() { return attributes.getDecimalSeparator(); } /** * See {@link NumberFieldAttributes#getEscapedDecimalSeparator()}. */ public String getEscapedDecimalSeparator() { return attributes.getEscapedDecimalSeparator(); } /** * Sets the decimal separator. If the field has a value containing the old * decimal separator, it is replaced with the new one. */ public void setDecimalSeparator(char newSeparator) { replaceSeparatorInField(getDecimalSeparator(), newSeparator); attributes.setDecimalSeparator(newSeparator); markAsDirty(); } private void replaceSeparatorInField(char oldSeparator, char newSeparator) { String value = getValue(); if (value != null) { String valueWithReplacedSeparator = value.replace(oldSeparator, newSeparator); setValue(valueWithReplacedSeparator); } } /** * See {@link NumberFieldAttributes#getGroupingSeparator()}. */ public char getGroupingSeparator() { return attributes.getGroupingSeparator(); } /** * See {@link NumberFieldAttributes#getEscapedGroupingSeparator()}. */ public String getEscapedGroupingSeparator() { return attributes.getEscapedGroupingSeparator(); } /** * Sets the grouping separator. If the field has a value containing the old * grouping separator, it is replaced with the new one. */ public void setGroupingSeparator(char newSeparator) { replaceSeparatorInField(getGroupingSeparator(), newSeparator); attributes.setGroupingSeparator(newSeparator); markAsDirty(); } /** * See {@link NumberFieldAttributes#getMinValue()}. */ public double getMinValue() { return attributes.getMinValue(); } /** * See {@link NumberFieldAttributes#setMinValue(double)}. */ public void setMinValue(double minValue) { attributes.setMinValue(minValue); markAsDirty(); } /** * See {@link NumberFieldAttributes#getMaxValue()}. */ public double getMaxValue() { return attributes.getMaxValue(); } /** * See {@link NumberFieldAttributes#setMaxValue(double)}. */ public void setMaxValue(double maxValue) { attributes.setMaxValue(maxValue); markAsDirty(); } /** * See {@link NumberFieldAttributes#isNegativeAllowed()}. */ public boolean isNegativeAllowed() { return attributes.isNegativeAllowed(); } /** * See {@link NumberFieldAttributes#setNegativeAllowed(boolean)}. */ public void setNegativeAllowed(boolean negativeAllowed) { attributes.setNegativeAllowed(negativeAllowed); markAsDirty(); } /** * @return The field's value as a double value. If the field contains no * parsable number, 0.0 is returned. */ public double getDoubleValueDoNotThrow() { try { return Double.valueOf(getValueNonLocalized()); } catch (Exception e) { return 0.0; } } /** * @return The field's value as a string representation of a decimal number * with '.' as decimal separator and cutted grouping separators. * Example: If the field's value is "2.546,99", this method will * return "2546.99". If the field is empty, "0" is returned. */ public String getValueNonLocalized() { String value = getValue(); if (value == null || "".equals(value)) { return "0"; } String groupingSeparator = String.valueOf(getGroupingSeparator()); return value.replace(groupingSeparator, "").replace(getDecimalSeparator(), '.'); } /** * If the given value is the string representation of a decimal number with * '.' as decimal separator (e.g. 12345.67), this method will return the * value with replaced decimal seperator as it was set with * {@link #setDecimalSeparator(char)}. */ public String replacePointWithDecimalSeparator(String value) { return value.replace('.', getDecimalSeparator()); } public String getFormattedValue() { return getValueAsFormattedDecimalNumber(); } }