javax.faces.convert.NumberConverter Maven / Gradle / Ivy
Show all versions of jsf-api Show documentation
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package javax.faces.convert;
import javax.el.ValueExpression;
import javax.faces.component.UIComponent;
import javax.faces.component.PartialStateHolder;
import javax.faces.context.FacesContext;
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;
/**
* {@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 = "javax.faces.Number";
/**
* The message identifier of the {@link javax.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 =
"javax.faces.converter.NumberConverter.CURRENCY";
/**
* The message identifier of the {@link javax.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. HA
* {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 =
"javax.faces.converter.NumberConverter.NUMBER";
/**
* The message identifier of the {@link javax.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 =
"javax.faces.converter.NumberConverter.PATTERN";
/**
* The message identifier of the {@link javax.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 =
"javax.faces.converter.NumberConverter.PERCENT";
/**
* The message identifier of the {@link javax.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 =
"javax.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
.
*/
public String getCurrencyCode() {
return (this.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
.
*/
public String getCurrencySymbol() {
return (this.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
.
*/
public boolean isGroupingUsed() {
return (this.groupingUsed != null ? this.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
.
*/
public boolean isIntegerOnly() {
return (this.integerOnly != null ? this.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.
*/
public int getMaxFractionDigits() {
return (this.maxFractionDigits != null ? this.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.
*/
public int getMaxIntegerDigits() {
return (this.maxIntegerDigits != null ? this.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.
*/
public int getMinFractionDigits() {
return (this.minFractionDigits != null ? this.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.
*/
public int getMinIntegerDigits() {
return (this.minIntegerDigits != null ? this.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 javax.faces.component.UIViewRoot} for the current request
* will be utilized.
*/
public Locale getLocale() {
if (this.locale == null) {
this.locale =
getLocale(FacesContext.getCurrentInstance());
}
return (this.locale);
}
/**
* Set the Locale
to be used when parsing numbers.
* If set to null
, the Locale
stored in the
* {@link javax.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.
*/
public String getPattern() {
return (this.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
.
*/
public String getType() {
return (this.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}
*/
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}
*/
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
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;
}
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 = false;
public boolean isTransient() {
return (transientFlag);
}
public void setTransient(boolean transientFlag) {
this.transientFlag = transientFlag;
}
private boolean initialState;
public void markInitialState() {
initialState = true;
}
public boolean initialStateMarked() {
return initialState;
}
public void clearInitialState() {
initialState = false;
}
}