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;
}
}