Skip to content

Commit 74eb57b

Browse files
committed
Merge #5399 from 4.0 into 4.1
2 parents cefab44 + 7e5ba9b commit 74eb57b

File tree

4 files changed

+511
-36
lines changed

4 files changed

+511
-36
lines changed

impl/src/main/java/jakarta/faces/convert/DateTimeConverter.java

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ public class DateTimeConverter implements Converter, PartialStateHolder {
185185
);
186186

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

189191
// ------------------------------------------------------ Instance Variables
190192

@@ -387,7 +389,7 @@ public Object getAsObject(FacesContext context, UIComponent component, String va
387389
Locale locale = getLocale(context);
388390

389391
// Create and configure the parser to be used
390-
parser = getDateFormat(locale);
392+
parser = getDateFormat(locale, true);
391393
if (timeZone != null) {
392394
parser.setTimeZone(timeZone);
393395
}
@@ -428,21 +430,32 @@ private static class FormatWrapper {
428430
private final DateFormat dateFormat;
429431
private final DateTimeFormatter dateTimeFormatter;
430432
private final TemporalQuery<Object> from;
433+
private final boolean normalizeWhitespaceOnParse;
431434

432435
private FormatWrapper(DateFormat dataFormat) {
433436
this.dateFormat = dataFormat;
434437
dateTimeFormatter = null;
435438
from = null;
439+
normalizeWhitespaceOnParse = false;
436440
}
437441

438-
private FormatWrapper(DateTimeFormatter dateTimeFormatter, TemporalQuery<Object> from) {
442+
private FormatWrapper(DateTimeFormatter dateTimeFormatter, TemporalQuery<Object> from, boolean normalizeWhitespaceOnParse) {
439443
dateFormat = null;
440444
this.dateTimeFormatter = dateTimeFormatter;
441445
this.from = from;
446+
this.normalizeWhitespaceOnParse = normalizeWhitespaceOnParse;
442447
}
443448

444449
private Object parse(CharSequence text) throws ParseException {
445-
return dateFormat != null ? dateFormat.parse((String) text) : dateTimeFormatter.parse(text, from);
450+
if (dateFormat != null) {
451+
return dateFormat.parse((String) text);
452+
}
453+
454+
if (normalizeWhitespaceOnParse) {
455+
text = normalizeWhitespace(text);
456+
}
457+
458+
return dateTimeFormatter.parse(text, from);
446459
}
447460

448461
private String format(Object obj) {
@@ -488,7 +501,7 @@ public String getAsString(FacesContext context, UIComponent component, Object va
488501
Locale locale = getLocale(context);
489502

490503
// Create and configure the formatter to be used
491-
FormatWrapper formatter = getDateFormat(locale);
504+
FormatWrapper formatter = getDateFormat(locale, false);
492505
if (null != timeZone) {
493506
formatter.setTimeZone(timeZone);
494507
}
@@ -513,9 +526,11 @@ public String getAsString(FacesContext context, UIComponent component, Object va
513526
* </p>
514527
*
515528
* @param locale The <code>Locale</code> used to select formatting and parsing conventions
529+
* @param forParsing {@code true} if the result will be used for parsing user input (enables whitespace normalization),
530+
* {@code false} if it will be used for formatting output
516531
* @throws ConverterException if no instance can be created
517532
*/
518-
private FormatWrapper getDateFormat(Locale locale) {
533+
private FormatWrapper getDateFormat(Locale locale, boolean forParsing) {
519534

520535
// PENDING(craigmcc) - Implement pooling if needed for performance?
521536

@@ -541,11 +556,11 @@ private FormatWrapper getDateFormat(Locale locale) {
541556
} else if (type.equals("time")) {
542557
df = DateFormat.getTimeInstance(getStyle(timeStyle), locale);
543558
} else if (type.equals("localDate")) {
544-
dtfBuilder = new DateTimeFormatterBuilder().appendLocalized(getFormatStyle(dateStyle), null);
559+
dtfBuilder = createLocalizedBuilder(getFormatStyle(dateStyle), null, locale, forParsing);
545560
} else if (type.equals("localDateTime")) {
546-
dtfBuilder = new DateTimeFormatterBuilder().appendLocalized(getFormatStyle(dateStyle), getFormatStyle(timeStyle));
561+
dtfBuilder = createLocalizedBuilder(getFormatStyle(dateStyle), getFormatStyle(timeStyle), locale, forParsing);
547562
} else if (type.equals("localTime")) {
548-
dtfBuilder = new DateTimeFormatterBuilder().appendLocalized(null, getFormatStyle(timeStyle));
563+
dtfBuilder = createLocalizedBuilder(null, getFormatStyle(timeStyle), locale, forParsing);
549564
} else if (type.equals("offsetTime")) {
550565
dtf = DateTimeFormatter.ISO_OFFSET_TIME.withLocale(locale);
551566
} else if (type.equals("offsetDateTime")) {
@@ -570,7 +585,7 @@ private FormatWrapper getDateFormat(Locale locale) {
570585
}
571586

572587
if (dtf != null) {
573-
return new FormatWrapper(dtf, fromJavaTime);
588+
return new FormatWrapper(dtf, fromJavaTime, forParsing);
574589
}
575590
}
576591

@@ -624,7 +639,27 @@ private static int getStyle(String name) {
624639
throw new ConverterException("Invalid style '" + name + '\'');
625640
}
626641

627-
private static FormatStyle getFormatStyle(String name) {
642+
/**
643+
* Returns a {@link DateTimeFormatterBuilder} with the localized date/time pattern appended. When {@code forParsing} is {@code true}, the pattern is
644+
* normalized to replace fixed-width whitespace (such as NNBSP U+202F) with regular spaces and strip zero-width characters, so that user input with regular
645+
* spaces is accepted.
646+
*/
647+
private static DateTimeFormatterBuilder createLocalizedBuilder(FormatStyle dateStyle, FormatStyle timeStyle, Locale locale, boolean forParsing) {
648+
if (forParsing) {
649+
var localizedPattern = DateTimeFormatterBuilder.getLocalizedDateTimePattern(dateStyle, timeStyle, IsoChronology.INSTANCE, locale);
650+
var normalizedPattern = normalizeWhitespace(localizedPattern);
651+
return new DateTimeFormatterBuilder().appendPattern(normalizedPattern);
652+
}
653+
654+
return new DateTimeFormatterBuilder().appendLocalized(dateStyle, timeStyle);
655+
}
656+
657+
private static String normalizeWhitespace(CharSequence text) {
658+
String normalized = FIXED_WIDTH_WHITESPACE.matcher(text).replaceAll(" ");
659+
return ZERO_WIDTH_WHITESPACE.matcher(normalized).replaceAll("");
660+
}
661+
662+
private static FormatStyle getFormatStyle(String name) {
628663
if (null != name) {
629664
switch (name) {
630665
case "default":

impl/src/main/java/jakarta/faces/convert/NumberConverter.java

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
import java.text.NumberFormat;
2424
import java.text.ParseException;
2525
import java.util.Locale;
26+
import java.util.regex.Pattern;
2627

27-
import jakarta.el.ValueExpression;
2828
import jakarta.faces.component.PartialStateHolder;
2929
import jakarta.faces.component.UIComponent;
3030
import jakarta.faces.context.FacesContext;
@@ -171,7 +171,8 @@ public class NumberConverter implements Converter, PartialStateHolder {
171171
*/
172172
public static final String STRING_ID = "jakarta.faces.converter.STRING";
173173

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

176177
// ------------------------------------------------------ Instance Variables
177178

@@ -542,45 +543,74 @@ public Object getAsObject(FacesContext context, UIComponent component, String va
542543
configureCurrency(parser);
543544
}
544545
parser.setParseIntegerOnly(isIntegerOnly());
545-
boolean groupSepChanged = false;
546-
// BEGIN HACK 4510618
547-
// This lovely bit of code is for a workaround in some
548-
// oddities in the JDK's parsing code.
549-
// See: http://bugs.sun.com/view_bug.do?bug_id=4510618
546+
547+
// Normalize all fixed-width whitespace in the formatter's symbols, prefix,
548+
// and suffix to regular spaces, and normalize the user input to match.
549+
// This handles NBSP/NNBSP in grouping separators (e.g. fr-FR) and in
550+
// currency prefixes/suffixes (e.g. Brazilian Real "R$\u00A0") at the same time.
551+
// See: https://github.com/eclipse-ee4j/mojarra/issues/5399
550552
if (parser instanceof DecimalFormat) {
551-
DecimalFormat dParser = (DecimalFormat) parser;
553+
var dParser = (DecimalFormat) parser;
552554

553555
// Take a small hit in performance to avoid a loss in
554556
// precision due to DecimalFormat.parse() returning Double
555-
ValueExpression ve = component.getValueExpression("value");
557+
var ve = component.getValueExpression("value");
556558
if (ve != null) {
557-
Class<?> expectedType = ve.getType(context.getELContext());
559+
var expectedType = ve.getType(context.getELContext());
558560
if (expectedType != null && expectedType.isAssignableFrom(BigDecimal.class)) {
559561
dParser.setParseBigDecimal(true);
560562
}
561563
}
562-
DecimalFormatSymbols symbols = dParser.getDecimalFormatSymbols();
563-
if (symbols.getGroupingSeparator() == '\u00a0') {
564-
groupSepChanged = true;
565-
String tValue;
566-
if (value.contains(NBSP)) {
567-
tValue = value.replace('\u00a0', ' ');
568-
} else {
569-
tValue = value;
564+
565+
var symbols = dParser.getDecimalFormatSymbols();
566+
var origGroupingSep = symbols.getGroupingSeparator();
567+
// TODO: uncomment in Faces 5.0: var origMonetaryGroupingSep = symbols.getMonetaryGroupingSeparator();
568+
var origPrefix = dParser.getPositivePrefix();
569+
var origSuffix = dParser.getPositiveSuffix();
570+
var origNegPrefix = dParser.getNegativePrefix();
571+
var origNegSuffix = dParser.getNegativeSuffix();
572+
573+
boolean hasFixedWidthWhitespace =
574+
FIXED_WIDTH_WHITESPACE.matcher(String.valueOf(origGroupingSep)).matches() ||
575+
// TODO: uncomment in Faces 5.0: FIXED_WIDTH_WHITESPACE.matcher(String.valueOf(origMonetaryGroupingSep)).matches() ||
576+
FIXED_WIDTH_WHITESPACE.matcher(origPrefix).find() ||
577+
FIXED_WIDTH_WHITESPACE.matcher(origSuffix).find() ||
578+
FIXED_WIDTH_WHITESPACE.matcher(origNegPrefix).find() ||
579+
FIXED_WIDTH_WHITESPACE.matcher(origNegSuffix).find();
580+
581+
if (hasFixedWidthWhitespace) {
582+
var normalizedValue = normalizeWhitespace(value);
583+
584+
if (FIXED_WIDTH_WHITESPACE.matcher(String.valueOf(origGroupingSep)).matches()) {
585+
symbols.setGroupingSeparator(' ');
570586
}
571-
symbols.setGroupingSeparator(' ');
587+
588+
// TODO: uncomment in Faces 5.0:
589+
// if (FIXED_WIDTH_WHITESPACE.matcher(String.valueOf(origMonetaryGroupingSep)).matches()) {
590+
// symbols.setMonetaryGroupingSeparator(' ');
591+
// }
592+
572593
dParser.setDecimalFormatSymbols(symbols);
594+
dParser.setPositivePrefix(normalizeWhitespace(origPrefix));
595+
dParser.setPositiveSuffix(normalizeWhitespace(origSuffix));
596+
dParser.setNegativePrefix(normalizeWhitespace(origNegPrefix));
597+
dParser.setNegativeSuffix(normalizeWhitespace(origNegSuffix));
598+
573599
try {
574-
return dParser.parse(tValue);
575-
} catch (ParseException pe) {
576-
if (groupSepChanged) {
577-
symbols.setGroupingSeparator('\u00a0');
578-
dParser.setDecimalFormatSymbols(symbols);
579-
}
600+
return dParser.parse(normalizedValue);
601+
}
602+
catch (ParseException pe) {
603+
// Restore original symbols and fall through to regular parsing
604+
symbols.setGroupingSeparator(origGroupingSep);
605+
// TODO: uncomment in Faces 5.0: symbols.setMonetaryGroupingSeparator(origMonetaryGroupingSep);
606+
dParser.setDecimalFormatSymbols(symbols);
607+
dParser.setPositivePrefix(origPrefix);
608+
dParser.setPositiveSuffix(origSuffix);
609+
dParser.setNegativePrefix(origNegPrefix);
610+
dParser.setNegativeSuffix(origNegSuffix);
580611
}
581612
}
582613
}
583-
// END HACK 4510618
584614

585615
// Perform the requested parsing
586616
returnValue = parser.parse(value);
@@ -856,6 +886,11 @@ else if (type.equals("currency")) {
856886

857887
}
858888

889+
private static String normalizeWhitespace(String text) {
890+
String normalized = FIXED_WIDTH_WHITESPACE.matcher(text).replaceAll(" ");
891+
return ZERO_WIDTH_WHITESPACE.matcher(normalized).replaceAll("");
892+
}
893+
859894
// ----------------------------------------------------- StateHolder Methods
860895

861896
@Override

0 commit comments

Comments
 (0)