mirror of
https://github.com/unicode-org/icu.git
synced 2025-04-10 15:42:14 +00:00
ICU-22894 MF2, ICU4J: implements configuring the error handling behavior
This commit is contained in:
parent
5991c9392c
commit
b3bf9f8c27
5 changed files with 179 additions and 17 deletions
|
@ -352,7 +352,7 @@ class DateTimeFormatterFactory implements FormatterFactory {
|
|||
toFormat, new PlainStringFormattedValue("{|" + toFormat + "|}"));
|
||||
}
|
||||
} else if (toFormat instanceof Temporal) {
|
||||
toFormat = JavaTimeConverters.temporalToCalendar((Temporal) toFormat);
|
||||
toFormat = JavaTimeConverters.temporalToCalendar((Temporal) toFormat);
|
||||
}
|
||||
// Not an else-if here, because the `Temporal` conditions before make `toFormat` a `Calendar`
|
||||
if (toFormat instanceof Calendar) {
|
||||
|
|
|
@ -29,6 +29,7 @@ import com.ibm.icu.message2.MFDataModel.SelectMessage;
|
|||
import com.ibm.icu.message2.MFDataModel.StringPart;
|
||||
import com.ibm.icu.message2.MFDataModel.VariableRef;
|
||||
import com.ibm.icu.message2.MFDataModel.Variant;
|
||||
import com.ibm.icu.message2.MessageFormatter.ErrorHandlingBehavior;
|
||||
import com.ibm.icu.util.Calendar;
|
||||
import com.ibm.icu.util.CurrencyAmount;
|
||||
|
||||
|
@ -39,6 +40,7 @@ import com.ibm.icu.util.CurrencyAmount;
|
|||
// TODO: move this in the MessageFormatter?
|
||||
class MFDataModelFormatter {
|
||||
private final Locale locale;
|
||||
private final ErrorHandlingBehavior errorHandlingBehavior;
|
||||
private final MFDataModel.Message dm;
|
||||
|
||||
private final MFFunctionRegistry standardFunctions;
|
||||
|
@ -46,8 +48,13 @@ class MFDataModelFormatter {
|
|||
private static final MFFunctionRegistry EMPTY_REGISTY = MFFunctionRegistry.builder().build();
|
||||
|
||||
MFDataModelFormatter(
|
||||
MFDataModel.Message dm, Locale locale, MFFunctionRegistry customFunctionRegistry) {
|
||||
MFDataModel.Message dm,
|
||||
Locale locale,
|
||||
ErrorHandlingBehavior errorHandlingBehavior,
|
||||
MFFunctionRegistry customFunctionRegistry) {
|
||||
this.locale = locale;
|
||||
this.errorHandlingBehavior = errorHandlingBehavior == null
|
||||
? ErrorHandlingBehavior.BEST_EFFORT : errorHandlingBehavior;
|
||||
this.dm = dm;
|
||||
this.customFunctions =
|
||||
customFunctionRegistry == null ? EMPTY_REGISTY : customFunctionRegistry;
|
||||
|
@ -94,21 +101,23 @@ class MFDataModelFormatter {
|
|||
if (dm instanceof MFDataModel.PatternMessage) {
|
||||
MFDataModel.PatternMessage pm = (MFDataModel.PatternMessage) dm;
|
||||
variables = resolveDeclarations(pm.declarations, arguments);
|
||||
if (pm.pattern == null) {
|
||||
fatalFormattingError("The PatternMessage is null.");
|
||||
}
|
||||
patternToRender = pm.pattern;
|
||||
} else if (dm instanceof MFDataModel.SelectMessage) {
|
||||
MFDataModel.SelectMessage sm = (MFDataModel.SelectMessage) dm;
|
||||
variables = resolveDeclarations(sm.declarations, arguments);
|
||||
patternToRender = findBestMatchingPattern(sm, variables, arguments);
|
||||
if (patternToRender == null) {
|
||||
fatalFormattingError("Cannor find a match for the selector.");
|
||||
}
|
||||
} else {
|
||||
fatalFormattingError("Unknown message type.");
|
||||
// formattingError throws, so the return does not actually happen
|
||||
return "ERROR!";
|
||||
}
|
||||
|
||||
if (patternToRender == null) {
|
||||
return "ERROR!";
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (MFDataModel.PatternPart part : patternToRender.parts) {
|
||||
if (part instanceof MFDataModel.StringPart) {
|
||||
|
@ -175,7 +184,7 @@ class MFDataModelFormatter {
|
|||
// spec: Append `rv` as the last element of the list `res`.
|
||||
res.add(rs);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown selector type: " + functionName);
|
||||
fatalFormattingError("Unknown selector type: " + functionName);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,7 +192,7 @@ class MFDataModelFormatter {
|
|||
// or we have thrown an exception.
|
||||
// But just in case someone removes the throw above?
|
||||
if (res.size() != selectors.size()) {
|
||||
throw new IllegalArgumentException(
|
||||
fatalFormattingError(
|
||||
"Something went wrong, not enough selector functions, "
|
||||
+ res.size() + " vs. " + selectors.size());
|
||||
}
|
||||
|
@ -322,8 +331,7 @@ class MFDataModelFormatter {
|
|||
// And should do that only once, when building the data model.
|
||||
if (patternToRender == null) {
|
||||
// If there was a case with all entries in the keys `*` this should not happen
|
||||
throw new IllegalArgumentException(
|
||||
"The selection went wrong, cannot select any option.");
|
||||
fatalFormattingError("The selection went wrong, cannot select any option.");
|
||||
}
|
||||
|
||||
return patternToRender;
|
||||
|
@ -394,7 +402,7 @@ class MFDataModelFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
private static void fatalFormattingError(String message) {
|
||||
private static void fatalFormattingError(String message) throws IllegalArgumentException {
|
||||
throw new IllegalArgumentException(message);
|
||||
}
|
||||
|
||||
|
@ -412,7 +420,7 @@ class MFDataModelFormatter {
|
|||
functionName = customFunctions.getDefaultFormatterNameForType(clazz);
|
||||
}
|
||||
if (functionName == null) {
|
||||
throw new IllegalArgumentException(
|
||||
fatalFormattingError(
|
||||
"Object to format without a function, and unknown type: "
|
||||
+ toFormat.getClass().getName());
|
||||
}
|
||||
|
@ -528,11 +536,17 @@ class MFDataModelFormatter {
|
|||
|
||||
FormatterFactory funcFactory = getFormattingFunctionFactoryByName(toFormat, functionName);
|
||||
if (funcFactory == null) {
|
||||
if (errorHandlingBehavior == ErrorHandlingBehavior.STRICT) {
|
||||
fatalFormattingError("unable to find function at " + fallbackString);
|
||||
}
|
||||
return new FormattedPlaceholder(expression, new PlainStringFormattedValue(fallbackString));
|
||||
}
|
||||
Formatter ff = funcFactory.createFormatter(locale, options);
|
||||
String res = ff.formatToString(toFormat, arguments);
|
||||
if (res == null) {
|
||||
if (errorHandlingBehavior == ErrorHandlingBehavior.STRICT) {
|
||||
fatalFormattingError("unable to format string at " + fallbackString);
|
||||
}
|
||||
res = fallbackString;
|
||||
}
|
||||
|
||||
|
|
|
@ -143,6 +143,7 @@ import java.util.Map;
|
|||
public class MessageFormatter {
|
||||
private final Locale locale;
|
||||
private final String pattern;
|
||||
private final ErrorHandlingBehavior errorHandlingBehavior;
|
||||
private final MFFunctionRegistry functionRegistry;
|
||||
private final MFDataModel.Message dataModel;
|
||||
private final MFDataModelFormatter modelFormatter;
|
||||
|
@ -150,6 +151,7 @@ public class MessageFormatter {
|
|||
private MessageFormatter(Builder builder) {
|
||||
this.locale = builder.locale;
|
||||
this.functionRegistry = builder.functionRegistry;
|
||||
this.errorHandlingBehavior = builder.errorHandlingBehavior;
|
||||
if ((builder.pattern == null && builder.dataModel == null)
|
||||
|| (builder.pattern != null && builder.dataModel != null)) {
|
||||
throw new IllegalArgumentException(
|
||||
|
@ -171,7 +173,7 @@ public class MessageFormatter {
|
|||
+ "Error: " + pe.getMessage() + "\n");
|
||||
}
|
||||
}
|
||||
modelFormatter = new MFDataModelFormatter(dataModel, locale, functionRegistry);
|
||||
modelFormatter = new MFDataModelFormatter(dataModel, locale, errorHandlingBehavior, functionRegistry);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -201,6 +203,20 @@ public class MessageFormatter {
|
|||
return locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link ErrorHandlingBehavior} to use when encountering errors in
|
||||
* the current {@code MessageFormatter}.
|
||||
*
|
||||
* @return the error handling behavior.
|
||||
*
|
||||
* @internal ICU 76 technology preview
|
||||
* @deprecated This API is for technology preview only.
|
||||
*/
|
||||
@Deprecated
|
||||
public ErrorHandlingBehavior getErrorHandlingBehavior() {
|
||||
return errorHandlingBehavior;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pattern (the serialized message in MessageFormat 2 syntax) of
|
||||
* the current {@code MessageFormatter}.
|
||||
|
@ -271,6 +287,37 @@ public class MessageFormatter {
|
|||
throw new RuntimeException("Not yet implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines how the formatting errors will be handled at runtime.
|
||||
*
|
||||
* <p>Parsing errors and data model errors always throw and will not be affected by this setting.<br>
|
||||
* But resolution errors and formatting errors will either try to fallback (if possible) or throw,
|
||||
* depending on this setting.</p>
|
||||
*
|
||||
* <p>Used in conjunction with the
|
||||
* {@link MessageFormatter.Builder#setErrorHandlingBehavior(ErrorHandlingBehavior)} method.</p>
|
||||
*
|
||||
* @internal ICU 76 technology preview
|
||||
* @deprecated This API is for technology preview only.
|
||||
*/
|
||||
@Deprecated
|
||||
public static enum ErrorHandlingBehavior {
|
||||
/**
|
||||
* Suppress errors and return best-effort output.
|
||||
*
|
||||
* @internal ICU 76 technology preview
|
||||
* @deprecated This API is for technology preview only.
|
||||
*/
|
||||
BEST_EFFORT,
|
||||
/**
|
||||
* Signal all {@code MessageFormat} errors by throwing a {@link RuntimeException}.
|
||||
*
|
||||
* @internal ICU 76 technology preview
|
||||
* @deprecated This API is for technology preview only.
|
||||
*/
|
||||
STRICT
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code Builder} used to build instances of {@link MessageFormatter}.
|
||||
*
|
||||
|
@ -281,6 +328,7 @@ public class MessageFormatter {
|
|||
public static class Builder {
|
||||
private Locale locale = Locale.getDefault(Locale.Category.FORMAT);
|
||||
private String pattern = null;
|
||||
private ErrorHandlingBehavior errorHandlingBehavior = ErrorHandlingBehavior.BEST_EFFORT;
|
||||
private MFFunctionRegistry functionRegistry = MFFunctionRegistry.builder().build();
|
||||
private MFDataModel.Message dataModel = null;
|
||||
|
||||
|
@ -319,6 +367,23 @@ public class MessageFormatter {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ErrorHandlingBehavior} to use when encountering errors at formatting time.
|
||||
*
|
||||
* <p>The default value is {@code ErrorHandlingBehavior.BEST_EFFORT}, trying to fallback.</p>
|
||||
*
|
||||
* @param the error handling behavior to use.
|
||||
* @return the builder, for fluent use.
|
||||
*
|
||||
* @internal ICU 76 technology preview
|
||||
* @deprecated This API is for technology preview only.
|
||||
*/
|
||||
@Deprecated
|
||||
public Builder setErrorHandlingBehavior(ErrorHandlingBehavior errorHandlingBehavior) {
|
||||
this.errorHandlingBehavior = errorHandlingBehavior;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an instance of {@link MFFunctionRegistry} that should register any
|
||||
* custom functions used by the message.
|
||||
|
|
|
@ -574,4 +574,85 @@ public class MessageFormat2Test extends CoreTestFmwk {
|
|||
assertEquals("test local vars loop", "Count = 23, OffCount = 21, and delta=2.",
|
||||
mfVar2.formatToString(Args.of("count", 23, "delta", 2)));
|
||||
}
|
||||
|
||||
// Needs more tests. Ported from the equivalent test in ICU4C
|
||||
@Test
|
||||
public void testFormatterAPI() {
|
||||
String result;
|
||||
Map<String, Object> messageArguments = new HashMap<>();
|
||||
|
||||
// Check that constructing the formatter fails
|
||||
// if there's a syntax error
|
||||
String pattern = "{{}";
|
||||
MessageFormatter.Builder mfBuilder = MessageFormatter.builder();
|
||||
MessageFormatter mf;
|
||||
try {
|
||||
mf = mfBuilder
|
||||
// This shouldn't matter, since there's a syntax error
|
||||
.setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.BEST_EFFORT)
|
||||
.setPattern(pattern)
|
||||
.build();
|
||||
errln("error expected");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue("", e.getMessage().contains("Parse error"));
|
||||
}
|
||||
|
||||
/*
|
||||
Parsing is done when setPattern() is called,
|
||||
so setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.STRICT) or setSuppressErrors must be called
|
||||
_before_ setPattern() to get the right behavior,
|
||||
and if either method is called after setting a pattern,
|
||||
setPattern() has to be called again.
|
||||
*/
|
||||
|
||||
// Should get the same behavior with strict errors
|
||||
try {
|
||||
mf = mfBuilder.setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.STRICT)
|
||||
// Force re-parsing, as above comment
|
||||
.setPattern(pattern)
|
||||
.build();
|
||||
errln("error expected");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue("", e.getMessage().contains("Parse error"));
|
||||
}
|
||||
|
||||
// Try the same thing for a pattern with a resolution error
|
||||
pattern = "{{{$x}}}";
|
||||
// Check that a pattern with a resolution error gives fallback output
|
||||
mf = mfBuilder
|
||||
.setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.BEST_EFFORT)
|
||||
.setPattern(pattern)
|
||||
.build();
|
||||
result = mf.formatToString(messageArguments);
|
||||
assertEquals("", "{$x}", result);
|
||||
|
||||
try {
|
||||
// Check that we do get an error with strict errors
|
||||
mf = mfBuilder
|
||||
.setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.STRICT)
|
||||
.build();
|
||||
// U_ASSERT(U_SUCCESS(errorCode));
|
||||
result = mf.formatToString(messageArguments);
|
||||
errln("error expected");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertTrue("", e.getMessage().contains("unable to find function"));
|
||||
}
|
||||
|
||||
// Finally, check a valid pattern
|
||||
pattern = "hello";
|
||||
mf = mfBuilder
|
||||
.setPattern(pattern)
|
||||
.setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.BEST_EFFORT)
|
||||
.build();
|
||||
result = mf.formatToString(messageArguments);
|
||||
assertEquals("", "hello", result);
|
||||
|
||||
// Check that behavior is the same with strict errors
|
||||
mf = mfBuilder
|
||||
.setErrorHandlingBehavior(MessageFormatter.ErrorHandlingBehavior.STRICT)
|
||||
.build();
|
||||
result = mf.formatToString(messageArguments);
|
||||
assertEquals("", "hello", result);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -85,8 +85,9 @@ public class TestUtils {
|
|||
// "params": [{"name": "exp"}, { "value": { "date": 1722746637000 } }]
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
Param pair = params[i];
|
||||
if (pair.value instanceof Map) {
|
||||
Map innerMap = (Map) pair.value;
|
||||
if (pair.value instanceof Map<?, ?>) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> innerMap = (Map<String, Object>) pair.value;
|
||||
if (innerMap.size() == 1 && innerMap.containsKey("date") && innerMap.get("date") instanceof Double) {
|
||||
Long dateValue = Double.valueOf((Double) innerMap.get("date")).longValue();
|
||||
params[i] = new Param(pair.name, new Date(dateValue));
|
||||
|
@ -103,8 +104,9 @@ public class TestUtils {
|
|||
// "params": [{"name": "val"}, {"value": {"decimal": "1234567890123456789.987654321"}}]
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
Param pair = params[i];
|
||||
if (pair.value instanceof Map) {
|
||||
Map innerMap = (Map) pair.value;
|
||||
if (pair.value instanceof Map<?, ?>) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> innerMap = (Map<String, Object>) pair.value;
|
||||
if (innerMap.size() == 1 && innerMap.containsKey("decimal")
|
||||
&& innerMap.get("decimal") instanceof String) {
|
||||
String decimalValue = (String) innerMap.get("decimal");
|
||||
|
|
Loading…
Add table
Reference in a new issue