All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jakarta.faces.convert.NumberConverter Maven / Gradle / Ivy

/*
 * Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package jakarta.faces.convert;

import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

import jakarta.el.ValueExpression;
import jakarta.faces.component.PartialStateHolder;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;

/**
 * 

* {@link Converter} implementation for java.lang.Number * values. *

* *

* The getAsObject() method parses a String into an java.lang.Double or * java.lang.Long, according to the following algorithm: *

*
    *
  • If the specified String is null, return a null. Otherwise, trim leading and trailing whitespace * before proceeding.
  • *
  • If the specified String - after trimming - has a zero length, return null.
  • *
  • If the locale property is not null, use that Locale for managing parsing. Otherwise, * use the Locale from the UIViewRoot.
  • *
  • If a pattern has been specified, its syntax must conform the rules specified by * java.text.DecimalFormat. Such a pattern will be used to parse, and the type property will * be ignored.
  • *
  • If a pattern has not been specified, parsing will be based on the type property, which * expects a currency, a number, or a percent. The parse pattern for currencies, numbers, and percentages is determined * by calling the getCurrencyInstance(), getNumberInstance(), or * getPercentInstance() method of the java.text.NumberFormat class, passing in the selected * Locale.
  • *
  • If the integerOnly property has been set to true, only the integer portion of the String will be * parsed. See the JavaDocs for the setParseIntegerOnly() method of the java.text.NumberFormat * class for more information.
  • *
* *

* The getAsString() method expects a value of type java.lang.Number (or a subclass), and * creates a formatted String according to the following algorithm: *

*
    *
  • If the specified value is null, return a zero-length String.
  • *
  • If the specified value is a String, return it unmodified.
  • *
  • If the locale property is not null, use that Locale for managing formatting. Otherwise, * use the Locale from the FacesContext.
  • *
  • If a pattern has been specified, its syntax must conform the rules specified by * java.text.DecimalFormat. Such a pattern will be used to format, and the type property * (along with related formatting options described in the next paragraph) will be ignored.
  • *
  • If a pattern has not been specified, formatting will be based on the type property, * which formats the value as a currency, a number, or a percent. The format pattern for currencies, numbers, and * percentages is determined by calling the percentages is determined by calling the getCurrencyInstance(), * getNumberInstance(), or getPercentInstance() method of the * java.text.NumberFormat class, passing in the selected Locale. In addition, the following * properties will be applied to the format pattern, if specified: *
      *
    • If the groupingUsed property is true, the setGroupingUsed(true) method on * the corresponding NumberFormat instance will be called.
    • *
    • The minimum and maximum number of digits in the integer and fractional portions of the result will be configured * based on any values set for the maxFractionDigits, maxIntegerDigits, * minFractionDigits, and minIntegerDigits properties.
    • *
    • If the type is set to currency, it is also possible to configure the currency symbol to be used, * using either the currencyCode or currencySymbol properties. If both are set, the value for * currencyCode takes precedence on a JDK 1.4 (or later) JVM; otherwise, the value for * currencySymbol takes precedence.
    • *
    *
  • *
*/ public class NumberConverter implements Converter, PartialStateHolder { // ------------------------------------------------------ Manifest Constants /** *

* The standard converter id for this converter. *

*/ public static final String CONVERTER_ID = "jakarta.faces.Number"; /** *

* The message identifier of the {@link jakarta.faces.application.FacesMessage} to be created if the conversion to * Number fails. The message format string for this message may optionally include the following * placeholders: *

    *
  • {0} replaced by the unconverted value.
  • *
  • {1} replaced by an example value.
  • *
  • {2} replaced by a String whose value is the label of the input component that produced * this message.
  • *
*/ public static final String CURRENCY_ID = "jakarta.faces.converter.NumberConverter.CURRENCY"; /** *

* The message identifier of the {@link jakarta.faces.application.FacesMessage} to be created if the conversion to * Number fails. The message format string for this message may optionally include the following * placeholders: *

    *
  • {0} replaced by the unconverted value.
  • *
  • {1} replaced by an example value.
  • *
  • {2} replaced by a String whose value is the label of the input component that produced * this message.
  • *
*/ public static final String NUMBER_ID = "jakarta.faces.converter.NumberConverter.NUMBER"; /** *

* The message identifier of the {@link jakarta.faces.application.FacesMessage} to be created if the conversion to * Number fails. The message format string for this message may optionally include the following * placeholders: *

    *
  • {0} replaced by the unconverted value.
  • *
  • {1} replaced by an example value.
  • *
  • {2} replaced by a String whose value is the label of the input component that produced * this message.
  • *
*/ public static final String PATTERN_ID = "jakarta.faces.converter.NumberConverter.PATTERN"; /** *

* The message identifier of the {@link jakarta.faces.application.FacesMessage} to be created if the conversion to * Number fails. The message format string for this message may optionally include the following * placeholders: *

    *
  • {0} replaced by the unconverted value.
  • *
  • {1} replaced by an example value.
  • *
  • {2} replaced by a String whose value is the label of the input component that produced * this message.
  • *
*/ public static final String PERCENT_ID = "jakarta.faces.converter.NumberConverter.PERCENT"; /** *

* The message identifier of the {@link jakarta.faces.application.FacesMessage} to be created if the conversion of the * Number value to String fails. The message format string for this message may optionally * include the following placeholders: *

    *
  • {0} relaced by the unconverted value.
  • *
  • {1} replaced by a String whose value is the label of the input component that produced * this message.
  • *
*/ public static final String STRING_ID = "jakarta.faces.converter.STRING"; private static final String NBSP = "\u00a0"; // ------------------------------------------------------ Instance Variables private String currencyCode = null; private String currencySymbol = null; private Boolean groupingUsed = true; private Boolean integerOnly = false; private Integer maxFractionDigits; private Integer maxIntegerDigits; private Integer minFractionDigits; private Integer minIntegerDigits; private Locale locale = null; private String pattern = null; private String type = "number"; // -------------------------------------------------------------- Properties /** *

* Return the ISO 4217 currency code used by getAsString() with a type of * currency. If not set, the value used will be based on the formatting Locale. *

* * @return the currency code */ public String getCurrencyCode() { return currencyCode; } /** *

* Set the ISO 4217 currency code used by getAsString() with a type of currency. *

* * @param currencyCode The new currency code */ public void setCurrencyCode(String currencyCode) { clearInitialState(); this.currencyCode = currencyCode; } /** *

* Return the currency symbol used by getAsString() with a type of currency. If * not set, the value used will be based on the formatting Locale. *

* * @return the currency symbol */ public String getCurrencySymbol() { return currencySymbol; } /** *

* Set the currency symbol used by getAsString() with a type of currency. *

* * @param currencySymbol The new currency symbol */ public void setCurrencySymbol(String currencySymbol) { clearInitialState(); this.currencySymbol = currencySymbol; } /** *

* Return true if getAsString should include grouping separators if necessary. If not * modified, the default value is true. *

* * @return whether or not grouping is used */ public boolean isGroupingUsed() { return groupingUsed != null ? groupingUsed : true; } /** *

* Set the flag indicating whether getAsString() should include grouping separators if necessary. *

* * @param groupingUsed The new grouping used flag */ public void setGroupingUsed(boolean groupingUsed) { clearInitialState(); this.groupingUsed = groupingUsed; } /** *

* Return true if only the integer portion of the given value should be returned from * getAsObject(). If not modified, the default value is false. *

* * @return whether or not this is integer only */ public boolean isIntegerOnly() { return integerOnly != null ? integerOnly : false; } /** *

* Set to true if only the integer portion of the given value should be returned from * getAsObject(). *

* * @param integerOnly The new integer-only flag */ public void setIntegerOnly(boolean integerOnly) { clearInitialState(); this.integerOnly = integerOnly; } /** *

* Return the maximum number of digits getAsString() should render in the fraction portion of the result. *

* * @return the maximum fraction digits */ public int getMaxFractionDigits() { return maxFractionDigits != null ? maxFractionDigits : 0; } /** *

* Set the maximum number of digits getAsString() should render in the fraction portion of the result. If * not set, the number of digits depends on the value being converted. *

* * @param maxFractionDigits The new limit */ public void setMaxFractionDigits(int maxFractionDigits) { clearInitialState(); this.maxFractionDigits = maxFractionDigits; } /** *

* Return the maximum number of digits getAsString() should render in the integer portion of the result. *

* * @return the max integer digits */ public int getMaxIntegerDigits() { return maxIntegerDigits != null ? maxIntegerDigits : 0; } /** *

* Set the maximum number of digits getAsString() should render in the integer portion of the result. If * not set, the number of digits depends on the value being converted. *

* * @param maxIntegerDigits The new limit */ public void setMaxIntegerDigits(int maxIntegerDigits) { clearInitialState(); this.maxIntegerDigits = maxIntegerDigits; } /** *

* Return the minimum number of digits getAsString() should render in the fraction portion of the result. *

* * @return the min fraction digits */ public int getMinFractionDigits() { return minFractionDigits != null ? minFractionDigits : 0; } /** *

* Set the minimum number of digits getAsString() should render in the fraction portion of the result. If * not set, the number of digits depends on the value being converted. *

* * @param minFractionDigits The new limit */ public void setMinFractionDigits(int minFractionDigits) { clearInitialState(); this.minFractionDigits = minFractionDigits; } /** *

* Return the minimum number of digits getAsString() should render in the integer portion of the result. *

* * @return the minimum integer digits */ public int getMinIntegerDigits() { return minIntegerDigits != null ? minIntegerDigits : 0; } /** *

* Set the minimum number of digits getAsString() should render in the integer portion of the result. If * not set, the number of digits depends on the value being converted. *

* * @param minIntegerDigits The new limit */ public void setMinIntegerDigits(int minIntegerDigits) { clearInitialState(); this.minIntegerDigits = minIntegerDigits; } /** *

* Return the Locale to be used when parsing numbers. If this value is null, the * Locale stored in the {@link jakarta.faces.component.UIViewRoot} for the current request will be * utilized. *

* * @return the {@code Locale} for this converter */ public Locale getLocale() { if (locale == null) { locale = getLocale(FacesContext.getCurrentInstance()); } return locale; } /** *

* Set the Locale to be used when parsing numbers. If set to null, the Locale * stored in the {@link jakarta.faces.component.UIViewRoot} for the current request will be utilized. *

* * @param locale The new Locale (or null) */ public void setLocale(Locale locale) { clearInitialState(); this.locale = locale; } /** *

* Return the format pattern to be used when formatting and parsing numbers. *

* * @return the pattern */ public String getPattern() { return pattern; } /** *

* Set the format pattern to be used when formatting and parsing numbers. Valid values are those supported by * java.text.DecimalFormat. An invalid value will cause a {@link ConverterException} when * getAsObject() or getAsString() is called. *

* * @param pattern The new format pattern */ public void setPattern(String pattern) { clearInitialState(); this.pattern = pattern; } /** *

* Return the number type to be used when formatting and parsing numbers. If not modified, the default type is * number. *

* * @return the type */ public String getType() { return type; } /** *

* Set the number type to be used when formatting and parsing numbers. Valid values are currency, * number, or percent. An invalid value will cause a {@link ConverterException} when * getAsObject() or getAsString() is called. *

* * @param type The new number style */ public void setType(String type) { clearInitialState(); this.type = type; } // ------------------------------------------------------- Converter Methods /** * @throws ConverterException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ @Override public Object getAsObject(FacesContext context, UIComponent component, String value) { if (context == null || component == null) { throw new NullPointerException(); } Object returnValue = null; NumberFormat parser = null; try { // If the specified value is null or zero-length, return null if (value == null) { return null; } value = value.trim(); if (value.length() < 1) { return null; } // Identify the Locale to use for parsing Locale locale = getLocale(context); // Create and configure the parser to be used parser = getNumberFormat(locale); if (pattern != null && pattern.length() != 0 || "currency".equals(type)) { configureCurrency(parser); } parser.setParseIntegerOnly(isIntegerOnly()); boolean groupSepChanged = false; // BEGIN HACK 4510618 // This lovely bit of code is for a workaround in some // oddities in the JDK's parsing code. // See: http://bugs.sun.com/view_bug.do?bug_id=4510618 if (parser instanceof DecimalFormat) { DecimalFormat dParser = (DecimalFormat) parser; // Take a small hit in performance to avoid a loss in // precision due to DecimalFormat.parse() returning Double ValueExpression ve = component.getValueExpression("value"); if (ve != null) { Class expectedType = ve.getType(context.getELContext()); if (expectedType != null && expectedType.isAssignableFrom(BigDecimal.class)) { dParser.setParseBigDecimal(true); } } DecimalFormatSymbols symbols = dParser.getDecimalFormatSymbols(); if (symbols.getGroupingSeparator() == '\u00a0') { groupSepChanged = true; String tValue; if (value.contains(NBSP)) { tValue = value.replace('\u00a0', ' '); } else { tValue = value; } symbols.setGroupingSeparator(' '); dParser.setDecimalFormatSymbols(symbols); try { return dParser.parse(tValue); } catch (ParseException pe) { if (groupSepChanged) { symbols.setGroupingSeparator('\u00a0'); dParser.setDecimalFormatSymbols(symbols); } } } } // END HACK 4510618 // Perform the requested parsing returnValue = parser.parse(value); } catch (ParseException e) { if (pattern != null) { throw new ConverterException(MessageFactory.getMessage(context, PATTERN_ID, value, "#,##0.0#", MessageFactory.getLabel(context, component)), e); } else if (type.equals("currency")) { throw new ConverterException( MessageFactory.getMessage(context, CURRENCY_ID, value, parser.format(99.99), MessageFactory.getLabel(context, component)), e); } else if (type.equals("number")) { throw new ConverterException( MessageFactory.getMessage(context, NUMBER_ID, value, parser.format(99), MessageFactory.getLabel(context, component)), e); } else if (type.equals("percent")) { throw new ConverterException( MessageFactory.getMessage(context, PERCENT_ID, value, parser.format(.75), MessageFactory.getLabel(context, component)), e); } } catch (Exception e) { throw new ConverterException(e); } return returnValue; } /** * @throws ConverterException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ @Override public String getAsString(FacesContext context, UIComponent component, Object value) { if (context == null || component == null) { throw new NullPointerException(); } try { // If the specified value is null, return a zero-length String if (value == null) { return ""; } // If the incoming value is still a string, play nice // and return the value unmodified if (value instanceof String) { return (String) value; } // Identify the Locale to use for formatting Locale locale = getLocale(context); // Create and configure the formatter to be used NumberFormat formatter = getNumberFormat(locale); if (pattern != null && pattern.length() != 0 || "currency".equals(type)) { configureCurrency(formatter); } configureFormatter(formatter); // Perform the requested formatting return formatter.format(value); } catch (ConverterException e) { throw new ConverterException(MessageFactory.getMessage(context, STRING_ID, value, MessageFactory.getLabel(context, component)), e); } catch (Exception e) { throw new ConverterException(MessageFactory.getMessage(context, STRING_ID, value, MessageFactory.getLabel(context, component)), e); } } // --------------------------------------------------------- Private Methods private static Class currencyClass; static { try { currencyClass = Class.forName("java.util.Currency"); // container's runtime is J2SE 1.4 or greater } catch (Exception ignored) { } } private static final Class[] GET_INSTANCE_PARAM_TYPES = new Class[] { String.class }; /** * * Override the formatting locale's default currency symbol with the specified currency code (specified via the * "currencyCode" attribute) or currency symbol (specified via the "currencySymbol" attribute). *

* *

* If both "currencyCode" and "currencySymbol" are present, "currencyCode" takes precedence over "currencySymbol" if the * java.util.Currency class is defined in the container's runtime (that is, if the container's runtime is J2SE 1.4 or * greater), and "currencySymbol" takes precendence over "currencyCode" otherwise. *

* *

* If only "currencyCode" is given, it is used as a currency symbol if java.util.Currency is not defined. *

* *
     * Example:
     * 
     * JDK    "currencyCode" "currencySymbol" Currency symbol being displayed
     * -----------------------------------------------------------------------
     * all         ---            ---         Locale's default currency symbol
     * 
     * <1.4        EUR            ---         EUR
     * >=1.4       EUR            ---         Locale's currency symbol for Euro
     * 
     * all         ---           \u20AC       \u20AC
     * 
     * <1.4        EUR           \u20AC       \u20AC
     * >=1.4       EUR           \u20AC       Locale's currency symbol for Euro
     * 
* * @param formatter The NumberFormatter to be configured */ private void configureCurrency(NumberFormat formatter) throws Exception { // Implementation copied from JSTL's FormatNumberSupport.setCurrency() String code = null; String symbol = null; if (currencyCode == null && currencySymbol == null) { return; } if (currencyCode != null && currencySymbol != null) { if (currencyClass != null) { code = currencyCode; } else { symbol = currencySymbol; } } else if (currencyCode == null) { symbol = currencySymbol; } else { if (currencyClass != null) { code = currencyCode; } else { symbol = currencyCode; } } if (code != null) { Object[] methodArgs = new Object[1]; /* * java.util.Currency.getInstance() */ Method m = currencyClass.getMethod("getInstance", GET_INSTANCE_PARAM_TYPES); methodArgs[0] = code; Object currency = m.invoke(null, methodArgs); /* * java.text.NumberFormat.setCurrency() */ Class[] paramTypes = new Class[1]; paramTypes[0] = currencyClass; Class numberFormatClass = Class.forName("java.text.NumberFormat"); m = numberFormatClass.getMethod("setCurrency", paramTypes); methodArgs[0] = currency; m.invoke(formatter, methodArgs); } else { /* * Let potential ClassCastException propagate up (will almost never happen) */ DecimalFormat df = (DecimalFormat) formatter; DecimalFormatSymbols dfs = df.getDecimalFormatSymbols(); dfs.setCurrencySymbol(symbol); df.setDecimalFormatSymbols(dfs); } } /** *

* Configure the specified NumberFormat based on the formatting properties that have been set. *

* * @param formatter The NumberFormat instance to configure */ private void configureFormatter(NumberFormat formatter) { formatter.setGroupingUsed(groupingUsed); if (isMaxIntegerDigitsSet()) { formatter.setMaximumIntegerDigits(maxIntegerDigits); } if (isMinIntegerDigitsSet()) { formatter.setMinimumIntegerDigits(minIntegerDigits); } if (isMaxFractionDigitsSet()) { formatter.setMaximumFractionDigits(maxFractionDigits); } if (isMinFractionDigitsSet()) { formatter.setMinimumFractionDigits(minFractionDigits); } } private boolean isMaxIntegerDigitsSet() { return maxIntegerDigits != null; } private boolean isMinIntegerDigitsSet() { return minIntegerDigits != null; } private boolean isMaxFractionDigitsSet() { return maxFractionDigits != null; } private boolean isMinFractionDigitsSet() { return minFractionDigits != null; } /** *

* Return the Locale we will use for localizing our formatting and parsing processing. *

* * @param context The {@link FacesContext} for the current request */ private Locale getLocale(FacesContext context) { // PENDING(craigmcc) - JSTL localization context? Locale locale = this.locale; if (locale == null) { locale = context.getViewRoot().getLocale(); } return locale; } /** *

* Return a NumberFormat instance to use for formatting and parsing in this {@link Converter}. *

* * @param locale The Locale used to select formatting and parsing conventions * @throws ConverterException if no instance can be created */ private NumberFormat getNumberFormat(Locale locale) { if (pattern == null && type == null) { throw new IllegalArgumentException("Either pattern or type must" + " be specified."); } // PENDING(craigmcc) - Implement pooling if needed for performance? // If pattern is specified, type is ignored if (pattern != null) { DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale); return new DecimalFormat(pattern, symbols); } // Create an instance based on the specified type else if (type.equals("currency")) { return NumberFormat.getCurrencyInstance(locale); } else if (type.equals("number")) { return NumberFormat.getNumberInstance(locale); } else if (type.equals("percent")) { return NumberFormat.getPercentInstance(locale); } else { // PENDING(craigmcc) - i18n throw new ConverterException(new IllegalArgumentException(type)); } } // ----------------------------------------------------- StateHolder Methods @Override public Object saveState(FacesContext context) { if (context == null) { throw new NullPointerException(); } if (!initialStateMarked()) { Object values[] = new Object[11]; values[0] = currencyCode; values[1] = currencySymbol; values[2] = groupingUsed; values[3] = integerOnly; values[4] = maxFractionDigits; values[5] = maxIntegerDigits; values[6] = minFractionDigits; values[7] = minIntegerDigits; values[8] = locale; values[9] = pattern; values[10] = type; return values; } return null; } @Override public void restoreState(FacesContext context, Object state) { if (context == null) { throw new NullPointerException(); } if (state != null) { Object values[] = (Object[]) state; currencyCode = (String) values[0]; currencySymbol = (String) values[1]; groupingUsed = (Boolean) values[2]; integerOnly = (Boolean) values[3]; maxFractionDigits = (Integer) values[4]; maxIntegerDigits = (Integer) values[5]; minFractionDigits = (Integer) values[6]; minIntegerDigits = (Integer) values[7]; locale = (Locale) values[8]; pattern = (String) values[9]; type = (String) values[10]; } } private boolean transientFlag; @Override public boolean isTransient() { return transientFlag; } @Override public void setTransient(boolean transientFlag) { this.transientFlag = transientFlag; } private boolean initialState; @Override public void markInitialState() { initialState = true; } @Override public boolean initialStateMarked() { return initialState; } @Override public void clearInitialState() { initialState = false; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy