Skip to content

Commit b3bf9f8

Browse files
committed
ICU-22894 MF2, ICU4J: implements configuring the error handling behavior
1 parent 5991c93 commit b3bf9f8

File tree

5 files changed

+179
-17
lines changed

5 files changed

+179
-17
lines changed

icu4j/main/core/src/main/java/com/ibm/icu/message2/DateTimeFormatterFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ public FormattedPlaceholder format(Object toFormat, Map<String, Object> variable
352352
toFormat, new PlainStringFormattedValue("{|" + toFormat + "|}"));
353353
}
354354
} else if (toFormat instanceof Temporal) {
355-
toFormat = JavaTimeConverters.temporalToCalendar((Temporal) toFormat);
355+
toFormat = JavaTimeConverters.temporalToCalendar((Temporal) toFormat);
356356
}
357357
// Not an else-if here, because the `Temporal` conditions before make `toFormat` a `Calendar`
358358
if (toFormat instanceof Calendar) {

icu4j/main/core/src/main/java/com/ibm/icu/message2/MFDataModelFormatter.java

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.ibm.icu.message2.MFDataModel.StringPart;
3030
import com.ibm.icu.message2.MFDataModel.VariableRef;
3131
import com.ibm.icu.message2.MFDataModel.Variant;
32+
import com.ibm.icu.message2.MessageFormatter.ErrorHandlingBehavior;
3233
import com.ibm.icu.util.Calendar;
3334
import com.ibm.icu.util.CurrencyAmount;
3435

@@ -39,15 +40,21 @@
3940
// TODO: move this in the MessageFormatter?
4041
class MFDataModelFormatter {
4142
private final Locale locale;
43+
private final ErrorHandlingBehavior errorHandlingBehavior;
4244
private final MFDataModel.Message dm;
4345

4446
private final MFFunctionRegistry standardFunctions;
4547
private final MFFunctionRegistry customFunctions;
4648
private static final MFFunctionRegistry EMPTY_REGISTY = MFFunctionRegistry.builder().build();
4749

4850
MFDataModelFormatter(
49-
MFDataModel.Message dm, Locale locale, MFFunctionRegistry customFunctionRegistry) {
51+
MFDataModel.Message dm,
52+
Locale locale,
53+
ErrorHandlingBehavior errorHandlingBehavior,
54+
MFFunctionRegistry customFunctionRegistry) {
5055
this.locale = locale;
56+
this.errorHandlingBehavior = errorHandlingBehavior == null
57+
? ErrorHandlingBehavior.BEST_EFFORT : errorHandlingBehavior;
5158
this.dm = dm;
5259
this.customFunctions =
5360
customFunctionRegistry == null ? EMPTY_REGISTY : customFunctionRegistry;
@@ -94,21 +101,23 @@ String format(Map<String, Object> arguments) {
94101
if (dm instanceof MFDataModel.PatternMessage) {
95102
MFDataModel.PatternMessage pm = (MFDataModel.PatternMessage) dm;
96103
variables = resolveDeclarations(pm.declarations, arguments);
104+
if (pm.pattern == null) {
105+
fatalFormattingError("The PatternMessage is null.");
106+
}
97107
patternToRender = pm.pattern;
98108
} else if (dm instanceof MFDataModel.SelectMessage) {
99109
MFDataModel.SelectMessage sm = (MFDataModel.SelectMessage) dm;
100110
variables = resolveDeclarations(sm.declarations, arguments);
101111
patternToRender = findBestMatchingPattern(sm, variables, arguments);
112+
if (patternToRender == null) {
113+
fatalFormattingError("Cannor find a match for the selector.");
114+
}
102115
} else {
103116
fatalFormattingError("Unknown message type.");
104117
// formattingError throws, so the return does not actually happen
105118
return "ERROR!";
106119
}
107120

108-
if (patternToRender == null) {
109-
return "ERROR!";
110-
}
111-
112121
StringBuilder result = new StringBuilder();
113122
for (MFDataModel.PatternPart part : patternToRender.parts) {
114123
if (part instanceof MFDataModel.StringPart) {
@@ -175,15 +184,15 @@ private Pattern findBestMatchingPattern(
175184
// spec: Append `rv` as the last element of the list `res`.
176185
res.add(rs);
177186
} else {
178-
throw new IllegalArgumentException("Unknown selector type: " + functionName);
187+
fatalFormattingError("Unknown selector type: " + functionName);
179188
}
180189
}
181190

182191
// This should not be possible, we added one function for each selector,
183192
// or we have thrown an exception.
184193
// But just in case someone removes the throw above?
185194
if (res.size() != selectors.size()) {
186-
throw new IllegalArgumentException(
195+
fatalFormattingError(
187196
"Something went wrong, not enough selector functions, "
188197
+ res.size() + " vs. " + selectors.size());
189198
}
@@ -322,8 +331,7 @@ private Pattern findBestMatchingPattern(
322331
// And should do that only once, when building the data model.
323332
if (patternToRender == null) {
324333
// If there was a case with all entries in the keys `*` this should not happen
325-
throw new IllegalArgumentException(
326-
"The selection went wrong, cannot select any option.");
334+
fatalFormattingError("The selection went wrong, cannot select any option.");
327335
}
328336

329337
return patternToRender;
@@ -394,7 +402,7 @@ public ResolvedSelector(
394402
}
395403
}
396404

397-
private static void fatalFormattingError(String message) {
405+
private static void fatalFormattingError(String message) throws IllegalArgumentException {
398406
throw new IllegalArgumentException(message);
399407
}
400408

@@ -412,7 +420,7 @@ private FormatterFactory getFormattingFunctionFactoryByName(
412420
functionName = customFunctions.getDefaultFormatterNameForType(clazz);
413421
}
414422
if (functionName == null) {
415-
throw new IllegalArgumentException(
423+
fatalFormattingError(
416424
"Object to format without a function, and unknown type: "
417425
+ toFormat.getClass().getName());
418426
}
@@ -528,11 +536,17 @@ private FormattedPlaceholder formatExpression(
528536

529537
FormatterFactory funcFactory = getFormattingFunctionFactoryByName(toFormat, functionName);
530538
if (funcFactory == null) {
539+
if (errorHandlingBehavior == ErrorHandlingBehavior.STRICT) {
540+
fatalFormattingError("unable to find function at " + fallbackString);
541+
}
531542
return new FormattedPlaceholder(expression, new PlainStringFormattedValue(fallbackString));
532543
}
533544
Formatter ff = funcFactory.createFormatter(locale, options);
534545
String res = ff.formatToString(toFormat, arguments);
535546
if (res == null) {
547+
if (errorHandlingBehavior == ErrorHandlingBehavior.STRICT) {
548+
fatalFormattingError("unable to format string at " + fallbackString);
549+
}
536550
res = fallbackString;
537551
}
538552

icu4j/main/core/src/main/java/com/ibm/icu/message2/MessageFormatter.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,15 @@
143143
public class MessageFormatter {
144144
private final Locale locale;
145145
private final String pattern;
146+
private final ErrorHandlingBehavior errorHandlingBehavior;
146147
private final MFFunctionRegistry functionRegistry;
147148
private final MFDataModel.Message dataModel;
148149
private final MFDataModelFormatter modelFormatter;
149150

150151
private MessageFormatter(Builder builder) {
151152
this.locale = builder.locale;
152153
this.functionRegistry = builder.functionRegistry;
154+
this.errorHandlingBehavior = builder.errorHandlingBehavior;
153155
if ((builder.pattern == null && builder.dataModel == null)
154156
|| (builder.pattern != null && builder.dataModel != null)) {
155157
throw new IllegalArgumentException(
@@ -171,7 +173,7 @@ private MessageFormatter(Builder builder) {
171173
+ "Error: " + pe.getMessage() + "\n");
172174
}
173175
}
174-
modelFormatter = new MFDataModelFormatter(dataModel, locale, functionRegistry);
176+
modelFormatter = new MFDataModelFormatter(dataModel, locale, errorHandlingBehavior, functionRegistry);
175177
}
176178

177179
/**
@@ -201,6 +203,20 @@ public Locale getLocale() {
201203
return locale;
202204
}
203205

206+
/**
207+
* Get the {@link ErrorHandlingBehavior} to use when encountering errors in
208+
* the current {@code MessageFormatter}.
209+
*
210+
* @return the error handling behavior.
211+
*
212+
* @internal ICU 76 technology preview
213+
* @deprecated This API is for technology preview only.
214+
*/
215+
@Deprecated
216+
public ErrorHandlingBehavior getErrorHandlingBehavior() {
217+
return errorHandlingBehavior;
218+
}
219+
204220
/**
205221
* Get the pattern (the serialized message in MessageFormat 2 syntax) of
206222
* the current {@code MessageFormatter}.
@@ -271,6 +287,37 @@ public FormattedMessage format(Map<String, Object> arguments) {
271287
throw new RuntimeException("Not yet implemented.");
272288
}
273289

290+
/**
291+
* Determines how the formatting errors will be handled at runtime.
292+
*
293+
* <p>Parsing errors and data model errors always throw and will not be affected by this setting.<br>
294+
* But resolution errors and formatting errors will either try to fallback (if possible) or throw,
295+
* depending on this setting.</p>
296+
*
297+
* <p>Used in conjunction with the
298+
* {@link MessageFormatter.Builder#setErrorHandlingBehavior(ErrorHandlingBehavior)} method.</p>
299+
*
300+
* @internal ICU 76 technology preview
301+
* @deprecated This API is for technology preview only.
302+
*/
303+
@Deprecated
304+
public static enum ErrorHandlingBehavior {
305+
/**
306+
* Suppress errors and return best-effort output.
307+
*
308+
* @internal ICU 76 technology preview
309+
* @deprecated This API is for technology preview only.
310+
*/
311+
BEST_EFFORT,
312+
/**
313+
* Signal all {@code MessageFormat} errors by throwing a {@link RuntimeException}.
314+
*
315+
* @internal ICU 76 technology preview
316+
* @deprecated This API is for technology preview only.
317+
*/
318+
STRICT
319+
}
320+
274321
/**
275322
* A {@code Builder} used to build instances of {@link MessageFormatter}.
276323
*
@@ -281,6 +328,7 @@ public FormattedMessage format(Map<String, Object> arguments) {
281328
public static class Builder {
282329
private Locale locale = Locale.getDefault(Locale.Category.FORMAT);
283330
private String pattern = null;
331+
private ErrorHandlingBehavior errorHandlingBehavior = ErrorHandlingBehavior.BEST_EFFORT;
284332
private MFFunctionRegistry functionRegistry = MFFunctionRegistry.builder().build();
285333
private MFDataModel.Message dataModel = null;
286334

@@ -319,6 +367,23 @@ public Builder setPattern(String pattern) {
319367
return this;
320368
}
321369

370+
/**
371+
* Sets the {@link ErrorHandlingBehavior} to use when encountering errors at formatting time.
372+
*
373+
* <p>The default value is {@code ErrorHandlingBehavior.BEST_EFFORT}, trying to fallback.</p>
374+
*
375+
* @param the error handling behavior to use.
376+
* @return the builder, for fluent use.
377+
*
378+
* @internal ICU 76 technology preview
379+
* @deprecated This API is for technology preview only.
380+
*/
381+
@Deprecated
382+
public Builder setErrorHandlingBehavior(ErrorHandlingBehavior errorHandlingBehavior) {
383+
this.errorHandlingBehavior = errorHandlingBehavior;
384+
return this;
385+
}
386+
322387
/**
323388
* Sets an instance of {@link MFFunctionRegistry} that should register any
324389
* custom functions used by the message.

icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/MessageFormat2Test.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,4 +574,85 @@ public void testVariableOptionsInSelectorWithLocalVar() {
574574
assertEquals("test local vars loop", "Count = 23, OffCount = 21, and delta=2.",
575575
mfVar2.formatToString(Args.of("count", 23, "delta", 2)));
576576
}
577+
578+
// Needs more tests. Ported from the equivalent test in ICU4C
579+
@Test
580+
public void testFormatterAPI() {
581+
String result;
582+
Map<String, Object> messageArguments = new HashMap<>();
583+
584+
// Check that constructing the formatter fails
585+
// if there's a syntax error
586+
String pattern = "{{}";
587+
MessageFormatter.Builder mfBuilder = MessageFormatter.builder();
588+
MessageFormatter mf;
589+
try {
590+
mf = mfBuilder
591+
// This shouldn't matter, since there's a syntax error
592+
.setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.BEST_EFFORT)
593+
.setPattern(pattern)
594+
.build();
595+
errln("error expected");
596+
} catch (IllegalArgumentException e) {
597+
assertTrue("", e.getMessage().contains("Parse error"));
598+
}
599+
600+
/*
601+
Parsing is done when setPattern() is called,
602+
so setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.STRICT) or setSuppressErrors must be called
603+
_before_ setPattern() to get the right behavior,
604+
and if either method is called after setting a pattern,
605+
setPattern() has to be called again.
606+
*/
607+
608+
// Should get the same behavior with strict errors
609+
try {
610+
mf = mfBuilder.setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.STRICT)
611+
// Force re-parsing, as above comment
612+
.setPattern(pattern)
613+
.build();
614+
errln("error expected");
615+
} catch (IllegalArgumentException e) {
616+
assertTrue("", e.getMessage().contains("Parse error"));
617+
}
618+
619+
// Try the same thing for a pattern with a resolution error
620+
pattern = "{{{$x}}}";
621+
// Check that a pattern with a resolution error gives fallback output
622+
mf = mfBuilder
623+
.setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.BEST_EFFORT)
624+
.setPattern(pattern)
625+
.build();
626+
result = mf.formatToString(messageArguments);
627+
assertEquals("", "{$x}", result);
628+
629+
try {
630+
// Check that we do get an error with strict errors
631+
mf = mfBuilder
632+
.setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.STRICT)
633+
.build();
634+
// U_ASSERT(U_SUCCESS(errorCode));
635+
result = mf.formatToString(messageArguments);
636+
errln("error expected");
637+
} catch (IllegalArgumentException e) {
638+
assertTrue("", e.getMessage().contains("unable to find function"));
639+
}
640+
641+
// Finally, check a valid pattern
642+
pattern = "hello";
643+
mf = mfBuilder
644+
.setPattern(pattern)
645+
.setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.BEST_EFFORT)
646+
.build();
647+
result = mf.formatToString(messageArguments);
648+
assertEquals("", "hello", result);
649+
650+
// Check that behavior is the same with strict errors
651+
mf = mfBuilder
652+
.setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.STRICT)
653+
.build();
654+
result = mf.formatToString(messageArguments);
655+
assertEquals("", "hello", result);
656+
}
657+
577658
}

icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/TestUtils.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ static void rewriteDates(Param[] params) {
8585
// "params": [{"name": "exp"}, { "value": { "date": 1722746637000 } }]
8686
for (int i = 0; i < params.length; i++) {
8787
Param pair = params[i];
88-
if (pair.value instanceof Map) {
89-
Map innerMap = (Map) pair.value;
88+
if (pair.value instanceof Map<?, ?>) {
89+
@SuppressWarnings("unchecked")
90+
Map<String, Object> innerMap = (Map<String, Object>) pair.value;
9091
if (innerMap.size() == 1 && innerMap.containsKey("date") && innerMap.get("date") instanceof Double) {
9192
Long dateValue = Double.valueOf((Double) innerMap.get("date")).longValue();
9293
params[i] = new Param(pair.name, new Date(dateValue));
@@ -103,8 +104,9 @@ static void rewriteDecimals(Param[] params) {
103104
// "params": [{"name": "val"}, {"value": {"decimal": "1234567890123456789.987654321"}}]
104105
for (int i = 0; i < params.length; i++) {
105106
Param pair = params[i];
106-
if (pair.value instanceof Map) {
107-
Map innerMap = (Map) pair.value;
107+
if (pair.value instanceof Map<?, ?>) {
108+
@SuppressWarnings("unchecked")
109+
Map<String, Object> innerMap = (Map<String, Object>) pair.value;
108110
if (innerMap.size() == 1 && innerMap.containsKey("decimal")
109111
&& innerMap.get("decimal") instanceof String) {
110112
String decimalValue = (String) innerMap.get("decimal");

0 commit comments

Comments
 (0)