Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 101 additions & 31 deletions api/src/main/java/jakarta/faces/convert/DateTimeConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,18 @@
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.time.chrono.IsoChronology;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.FormatStyle;
import java.time.format.ResolverStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQuery;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Pattern;

import jakarta.faces.component.PartialStateHolder;
import jakarta.faces.component.UIComponent;
Expand Down Expand Up @@ -86,6 +91,12 @@ public class DateTimeConverter
private static final String STYLE_FULL = "full";
private static final TimeZone TIMEZONE_DEFAULT = TimeZone.getTimeZone("GMT");

private static final Pattern ESCAPED_DATE_TIME_PATTERN = Pattern.compile("'[^']*+'");
private static final Pattern FIXED_WIDTH_WHITESPACE =
Pattern.compile("[\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]");
private static final Pattern ZERO_WIDTH_WHITESPACE =
Pattern.compile("[\u200b-\u200d\u2060\ufeff]");

private String _dateStyle;
private Locale _locale;
private String _pattern;
Expand Down Expand Up @@ -113,17 +124,18 @@ public Object getAsObject(FacesContext facesContext, UIComponent uiComponent, St
{
if (isJava8DateTimeFormatter())
{
DateTimeFormatter format = getDateTimeFormatter();
DateTimeFormatter format = getDateTimeFormatter(true);
String toParse = normalizeWhitespace(value);
try
{
TemporalQuery tq = getTemporalQuery();
if (tq != null)
{
return format.parse(value, tq);
return format.parse(toParse, tq);
}
else
{
return format.parse(value);
return format.parse(toParse);
}
}
catch (Exception e)
Expand All @@ -143,7 +155,7 @@ else if (TYPE_OFFSET_TIME.equals(type) || TYPE_OFFSET_DATE_TIME.equals(type))
{
currentDate = ZonedDateTime.now();
}
Object[] args = new Object[]{value,
Object[] args = new Object[]{toParse,
format.format(currentDate),MessageUtils.getLabel(facesContext, uiComponent)};

if(type.equals(TYPE_LOCAL_DATE))
Expand Down Expand Up @@ -225,7 +237,7 @@ public String getAsString(FacesContext facesContext, UIComponent uiComponent, Ob

if (isJava8DateTimeFormatter())
{
DateTimeFormatter format = getDateTimeFormatter();
DateTimeFormatter format = getDateTimeFormatter(false);

if (value instanceof TemporalAccessor accessor)
{
Expand Down Expand Up @@ -300,14 +312,19 @@ else if (type.equals(TYPE_BOTH))
return format;
}

private DateTimeFormatter getDateTimeFormatter()
/**
* @param forParsing {@code true} for {@link #getAsObject}; when {@code localDate}, {@code localDateTime}, or
* {@code localTime} without an explicit pattern, use a whitespace-normalized localized pattern.
*/
private DateTimeFormatter getDateTimeFormatter(boolean forParsing)
{
DateTimeFormatter formatter = null;
DateTimeFormatter formatter;
String type = getType();
String pattern = getPattern();
Locale locale = getLocale();

if (pattern != null && pattern.length() > 0)
{
Locale locale = getLocale();
if (locale == null)
{
formatter = DateTimeFormatter.ofPattern(pattern);
Expand All @@ -316,52 +333,105 @@ private DateTimeFormatter getDateTimeFormatter()
{
formatter = DateTimeFormatter.ofPattern(pattern, locale);
}
return formatter;
}
else

if (forParsing
&& (TYPE_LOCAL_DATE.equals(type) || TYPE_LOCAL_DATE_TIME.equals(type) || TYPE_LOCAL_TIME.equals(type)))
{
if (TYPE_LOCAL_DATE.equals(type))
{
formatter = DateTimeFormatter.ofLocalizedDate(calcFormatStyle(getDateStyle()));
formatter = createLocalizedParseFormatter(calcFormatStyle(getDateStyle()), null, locale);
}
else if (TYPE_LOCAL_DATE_TIME.equals(type) )
else if (TYPE_LOCAL_DATE_TIME.equals(type))
{
String timeStyle = getTimeStyle();
if (timeStyle != null && timeStyle.length() > 0)
{
formatter = DateTimeFormatter.ofLocalizedDateTime(
calcFormatStyle(getDateStyle()), calcFormatStyle(timeStyle));
formatter = createLocalizedParseFormatter(
calcFormatStyle(getDateStyle()), calcFormatStyle(timeStyle), locale);
}
else
{
formatter = DateTimeFormatter.ofLocalizedDateTime(
calcFormatStyle(getDateStyle()));
FormatStyle ds = calcFormatStyle(getDateStyle());
formatter = createLocalizedParseFormatter(ds, ds, locale);
}
}
else if (TYPE_LOCAL_TIME.equals(type) )
{
formatter = DateTimeFormatter.ofLocalizedTime(calcFormatStyle(getTimeStyle()));
}
else if (TYPE_OFFSET_TIME.equals(type))
{
formatter = DateTimeFormatter.ISO_OFFSET_TIME;
}
else if (TYPE_OFFSET_DATE_TIME.equals(type))
else
{
formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
formatter = createLocalizedParseFormatter(null, calcFormatStyle(getTimeStyle()), locale);
}
else if (TYPE_ZONED_DATE_TIME.equals(type))
return formatter;
}

if (TYPE_LOCAL_DATE.equals(type))
{
formatter = DateTimeFormatter.ofLocalizedDate(calcFormatStyle(getDateStyle()));
}
else if (TYPE_LOCAL_DATE_TIME.equals(type))
{
String timeStyle = getTimeStyle();
if (timeStyle != null && timeStyle.length() > 0)
{
formatter = DateTimeFormatter.ISO_ZONED_DATE_TIME;
formatter = DateTimeFormatter.ofLocalizedDateTime(
calcFormatStyle(getDateStyle()), calcFormatStyle(timeStyle));
}

Locale locale = getLocale();
if (locale != null)
else
{
formatter = formatter.withLocale(locale);
formatter = DateTimeFormatter.ofLocalizedDateTime(
calcFormatStyle(getDateStyle()));
}
}
else if (TYPE_LOCAL_TIME.equals(type))
{
formatter = DateTimeFormatter.ofLocalizedTime(calcFormatStyle(getTimeStyle()));
}
else if (TYPE_OFFSET_TIME.equals(type))
{
formatter = DateTimeFormatter.ISO_OFFSET_TIME;
}
else if (TYPE_OFFSET_DATE_TIME.equals(type))
{
formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
}
else if (TYPE_ZONED_DATE_TIME.equals(type))
{
formatter = DateTimeFormatter.ISO_ZONED_DATE_TIME;
}
else
{
throw new ConverterException("invalid type '" + _type + '\'');
}

if (locale != null)
{
formatter = formatter.withLocale(locale);
}
return formatter;
}

private static DateTimeFormatter createLocalizedParseFormatter(
FormatStyle dateStyle, FormatStyle timeStyle, Locale locale)
{
Locale loc = locale != null ? locale : Locale.getDefault();
String localizedPattern = DateTimeFormatterBuilder.getLocalizedDateTimePattern(
dateStyle, timeStyle, IsoChronology.INSTANCE, loc);
String normalizedPattern = normalizeWhitespace(localizedPattern);
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder().appendPattern(normalizedPattern);
if (!ESCAPED_DATE_TIME_PATTERN.matcher(normalizedPattern).replaceAll("").contains("uu"))
{
builder.parseDefaulting(ChronoField.ERA, 1);
}
return builder.toFormatter(loc)
.withChronology(IsoChronology.INSTANCE)
.withResolverStyle(ResolverStyle.STRICT);
}

private static String normalizeWhitespace(CharSequence text)
{
String normalized = FIXED_WIDTH_WHITESPACE.matcher(text).replaceAll(" ");
return ZERO_WIDTH_WHITESPACE.matcher(normalized).replaceAll("");
}

/**
* According to java8 api, parse() also receives a TemporalQuery parameter that works as a qualifier to decide
Expand Down
124 changes: 79 additions & 45 deletions api/src/main/java/jakarta/faces/convert/NumberConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.text.ParsePosition;
import java.util.Currency;
import java.util.Locale;
import java.util.regex.Pattern;

import jakarta.el.ValueExpression;
import jakarta.faces.component.PartialStateHolder;
Expand Down Expand Up @@ -61,6 +62,11 @@ public class NumberConverter implements Converter<Number>, PartialStateHolder
public static final String PATTERN_ID = "jakarta.faces.converter.NumberConverter.PATTERN";
public static final String PERCENT_ID = "jakarta.faces.converter.NumberConverter.PERCENT";

private static final Pattern FIXED_WIDTH_WHITESPACE =
Pattern.compile("[\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]");
private static final Pattern ZERO_WIDTH_WHITESPACE =
Pattern.compile("[\u200b-\u200d\u2060\ufeff]");

private String _currencyCode;
private String _currencySymbol;
private Locale _locale;
Expand Down Expand Up @@ -93,7 +99,7 @@ public Number getAsObject(FacesContext facesContext, UIComponent uiComponent, St
{
return null;
}

NumberFormat format = getNumberFormat(facesContext);
format.setParseIntegerOnly(_integerOnly);

Expand All @@ -116,65 +122,87 @@ public Number getAsObject(FacesContext facesContext, UIComponent uiComponent, St
}
}

DecimalFormatSymbols dfs = df.getDecimalFormatSymbols();
boolean changed = false;
if(dfs.getGroupingSeparator() == '\u00a0')
{
dfs.setGroupingSeparator(' ');
df.setDecimalFormatSymbols(dfs);
value = value.replace('\u00a0', ' ');
changed = true;
}

formatCurrency(format);

try
{
DecimalFormatSymbols symbols = df.getDecimalFormatSymbols();
char origGroupingSep = symbols.getGroupingSeparator();
String origPrefix = df.getPositivePrefix();
String origSuffix = df.getPositiveSuffix();
String origNegPrefix = df.getNegativePrefix();
String origNegSuffix = df.getNegativeSuffix();

boolean hasFixedWidthWhitespace =
FIXED_WIDTH_WHITESPACE.matcher(String.valueOf(origGroupingSep)).matches()
|| FIXED_WIDTH_WHITESPACE.matcher(origPrefix).find()
|| FIXED_WIDTH_WHITESPACE.matcher(origSuffix).find()
|| FIXED_WIDTH_WHITESPACE.matcher(origNegPrefix).find()
|| FIXED_WIDTH_WHITESPACE.matcher(origNegSuffix).find();

if (hasFixedWidthWhitespace)
{
String normalizedValue = normalizeWhitespace(value);

if (FIXED_WIDTH_WHITESPACE.matcher(String.valueOf(origGroupingSep)).matches())
{
symbols.setGroupingSeparator(' ');
}

df.setDecimalFormatSymbols(symbols);
df.setPositivePrefix(normalizeWhitespace(origPrefix));
df.setPositiveSuffix(normalizeWhitespace(origSuffix));
df.setNegativePrefix(normalizeWhitespace(origNegPrefix));
df.setNegativeSuffix(normalizeWhitespace(origNegSuffix));

try
{
return parse(normalizedValue, format, destType);
}
catch (ParseException pe)
{
symbols.setGroupingSeparator(origGroupingSep);
df.setDecimalFormatSymbols(symbols);
df.setPositivePrefix(origPrefix);
df.setPositiveSuffix(origSuffix);
df.setNegativePrefix(origNegPrefix);
df.setNegativeSuffix(origNegSuffix);
}
}

return parse(value, format, destType);
}
catch (ParseException e)
{
if(changed)
if (getPattern() != null)
{
dfs.setGroupingSeparator('\u00a0');
df.setDecimalFormatSymbols(dfs);
throw new ConverterException(MessageUtils.getErrorMessage(facesContext,
PATTERN_ID,
new Object[]{value, "$###,###", MessageUtils.getLabel(facesContext, uiComponent)}));
}
try
else if (getType().equals("number"))
{
return parse(value, format, destType);
throw new ConverterException(MessageUtils.getErrorMessage(facesContext,
NUMBER_ID,
new Object[]{value, format.format(21),
MessageUtils.getLabel(facesContext, uiComponent)}));
}
catch (ParseException pe)
else if (getType().equals("currency"))
{
if (getPattern() != null)
{
throw new ConverterException(MessageUtils.getErrorMessage(facesContext,
PATTERN_ID,
new Object[]{value, "$###,###", MessageUtils.getLabel(facesContext, uiComponent)}));
}
else if (getType().equals("number"))
{
throw new ConverterException(MessageUtils.getErrorMessage(facesContext,
NUMBER_ID,
new Object[]{value, format.format(21),
MessageUtils.getLabel(facesContext, uiComponent)}));
}
else if (getType().equals("currency"))
{
throw new ConverterException(MessageUtils.getErrorMessage(facesContext,
CURRENCY_ID,
new Object[]{value, format.format(42.25),
MessageUtils.getLabel(facesContext, uiComponent)}));
}
else if (getType().equals("percent"))
{
throw new ConverterException(MessageUtils.getErrorMessage(facesContext,
PERCENT_ID,
new Object[]{value, format.format(.90),
MessageUtils.getLabel(facesContext, uiComponent)}));
}
throw new ConverterException(MessageUtils.getErrorMessage(facesContext,
CURRENCY_ID,
new Object[]{value, format.format(42.25),
MessageUtils.getLabel(facesContext, uiComponent)}));
}
else if (getType().equals("percent"))
{
throw new ConverterException(MessageUtils.getErrorMessage(facesContext,
PERCENT_ID,
new Object[]{value, format.format(.90),
MessageUtils.getLabel(facesContext, uiComponent)}));
}
}

return null;
}

Expand Down Expand Up @@ -579,6 +607,12 @@ private DecimalFormatSymbols getDecimalFormatSymbols()
{
return new DecimalFormatSymbols(getLocale());
}

private static String normalizeWhitespace(String text)
{
String normalized = FIXED_WIDTH_WHITESPACE.matcher(text).replaceAll(" ");
return ZERO_WIDTH_WHITESPACE.matcher(normalized).replaceAll("");
}

private boolean _initialStateMarked = false;

Expand Down
Loading
Loading