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

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

Go to download

Jakarta Faces defines an MVC framework for building user interfaces for web applications, including UI components, state management, event handing, input validation, page navigation, and support for internationalization and accessibility.

There is a newer version: 4.1.2
Show newest version
/*
 * 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 - 2025 Weber Informatics LLC | Privacy Policy