MFDataModelFormatter.java
// © 2022 and later: Unicode, Inc. and others.
// License & terms of use: https://www.unicode.org/copyright.html
package com.ibm.icu.message2;
import com.ibm.icu.message2.MFDataModel.CatchallKey;
import com.ibm.icu.message2.MFDataModel.Declaration;
import com.ibm.icu.message2.MFDataModel.Expression;
import com.ibm.icu.message2.MFDataModel.FunctionExpression;
import com.ibm.icu.message2.MFDataModel.FunctionRef;
import com.ibm.icu.message2.MFDataModel.InputDeclaration;
import com.ibm.icu.message2.MFDataModel.Literal;
import com.ibm.icu.message2.MFDataModel.LiteralExpression;
import com.ibm.icu.message2.MFDataModel.LiteralOrCatchallKey;
import com.ibm.icu.message2.MFDataModel.LiteralOrVariableRef;
import com.ibm.icu.message2.MFDataModel.LocalDeclaration;
import com.ibm.icu.message2.MFDataModel.Option;
import com.ibm.icu.message2.MFDataModel.Pattern;
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.BidiIsolation;
import com.ibm.icu.message2.MessageFormatter.ErrorHandlingBehavior;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.CurrencyAmount;
import java.time.DayOfWeek;
import java.time.Month;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Takes an {@link MFDataModel} and formats it to a {@link String} (and later on we will also
* implement formatting to a {@code FormattedMessage}).
*/
// TODO: move this in the MessageFormatter?
class MFDataModelFormatter {
// Bidi controls. For code readability only.
private static final char LRI = '\u2066'; // LEFT-TO-RIGHT ISOLATE (LRI)
private static final char RLI = '\u2067'; // RIGHT-TO-LEFT ISOLATE (RLI)
private static final char FSI = '\u2068'; // FIRST STRONG ISOLATE (FSI)
private static final char PDI = '\u2069'; // POP DIRECTIONAL ISOLATE (PDI)
private final Locale locale;
private final ErrorHandlingBehavior errorHandlingBehavior;
private final BidiIsolation bidiIsolation;
private final MFDataModel.Message dm;
private final MFFunctionRegistry standardFunctions;
private final MFFunctionRegistry customFunctions;
private static final MFFunctionRegistry EMPTY_REGISTY = MFFunctionRegistry.builder().build();
MFDataModelFormatter(
MFDataModel.Message dm,
Locale locale,
ErrorHandlingBehavior errorHandlingBehavior,
BidiIsolation bidiIsolation,
MFFunctionRegistry customFunctionRegistry) {
this.locale = locale;
this.errorHandlingBehavior =
errorHandlingBehavior == null
? ErrorHandlingBehavior.BEST_EFFORT
: errorHandlingBehavior;
this.bidiIsolation = bidiIsolation == null ? BidiIsolation.NONE : bidiIsolation;
this.dm = dm;
this.customFunctions =
customFunctionRegistry == null ? EMPTY_REGISTY : customFunctionRegistry;
standardFunctions =
MFFunctionRegistry.builder()
// Date/time formatting. No selection.
.setFunction("datetime", new DateTimeFunctionFactory("datetime"))
.setFunction("date", new DateTimeFunctionFactory("date"))
.setFunction("time", new DateTimeFunctionFactory("time"))
.setDefaultFunctionNameForType(Date.class, "datetime")
.setDefaultFunctionNameForType(Calendar.class, "datetime")
.setDefaultFunctionNameForType(java.util.Calendar.class, "datetime")
.setDefaultFunctionNameForType(Temporal.class, "datetime")
.setDefaultFunctionNameForType(DayOfWeek.class, "date")
.setDefaultFunctionNameForType(Month.class, "date")
// Number formatting and selection
.setFunction("number", new NumberFunctionFactory("number"))
.setFunction("integer", new NumberFunctionFactory("integer"))
.setFunction("currency", new NumberFunctionFactory("currency"))
.setFunction("percent", new NumberFunctionFactory("percent"))
.setFunction("offset", new NumberFunctionFactory("offset"))
.setDefaultFunctionNameForType(Integer.class, "number")
.setDefaultFunctionNameForType(Double.class, "number")
.setDefaultFunctionNameForType(Number.class, "number")
.setDefaultFunctionNameForType(CurrencyAmount.class, "currency")
// Function that returns "to string" and selects on string equality
.setFunction("string", new TextFunctionFactory())
.setDefaultFunctionNameForType(String.class, "string")
.setDefaultFunctionNameForType(CharSequence.class, "string")
// Register some custom selector
.setFunction("icu:gender", new TextFunctionFactory())
.build();
}
String format(Map<String, Object> arguments) {
MFDataModel.Pattern patternToRender = null;
MapWithNfcKeys nfcArguments = new MapWithNfcKeys(arguments);
MapWithNfcKeys variables;
if (dm instanceof MFDataModel.PatternMessage) {
MFDataModel.PatternMessage pm = (MFDataModel.PatternMessage) dm;
variables = resolveDeclarations(pm.declarations, nfcArguments);
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, nfcArguments);
patternToRender = findBestMatchingPattern(sm, variables, nfcArguments);
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!";
}
Directionality msgdir = Directionality.LTR;
StringBuilder result = new StringBuilder();
for (MFDataModel.PatternPart part : patternToRender.parts) {
if (part instanceof MFDataModel.StringPart) {
MFDataModel.StringPart strPart = (StringPart) part;
result.append(strPart.value);
} else if (part instanceof MFDataModel.Expression) {
FormattedPlaceholder formattedExpression =
formatExpression((Expression) part, variables, nfcArguments);
if (this.bidiIsolation == BidiIsolation.DEFAULT) {
implementBiDiDefault(result, msgdir, formattedExpression);
} else {
result.append(formattedExpression.getFormattedValue().toString());
}
} else if (part instanceof MFDataModel.Markup) {
// Ignore, we don't output markup to string
} else {
fatalFormattingError("Unknown part type: " + part);
}
}
return result.toString();
}
private void implementBiDiDefault(
StringBuilder result, Directionality msgdir, FormattedPlaceholder formattedExpression) {
String fmt = formattedExpression.getFormattedValue().toString();
Directionality dir = formattedExpression.getDirectionality();
boolean isolate = formattedExpression.getIsolate();
switch (dir) {
case LTR:
if (msgdir == Directionality.LTR && !isolate) {
result.append(fmt);
} else {
result.append(LRI).append(fmt).append(PDI);
}
break;
case RTL:
result.append(RLI).append(fmt).append(PDI);
break;
default:
result.append(FSI).append(fmt).append(PDI);
break;
}
}
private Pattern findBestMatchingPattern(
SelectMessage sm, MapWithNfcKeys variables, MapWithNfcKeys arguments) {
Pattern patternToRender = null;
// ====================================
// spec: ### Resolve Selectors
// ====================================
// Collect all the selector functions in an array, to reuse
List<Expression> selectors = sm.selectors;
// spec: Let `res` be a new empty list of resolved values that support selection.
List<ResolvedSelector> res = new ArrayList<>(selectors.size());
// spec: For each _selector_ `sel`, in source order,
for (Expression sel : selectors) {
// spec: Let `rv` be the resolved value of `sel`.
FormattedPlaceholder fph = null;
if (sel instanceof MFDataModel.VariableExpression) {
// If it is a `VariableExpression` then it is already resolved and in `variables`
String key = ((MFDataModel.VariableExpression) sel).arg.name;
fph = (FormattedPlaceholder) variables.get(key);
}
// Was not a `VariableExpression` or in `variables`
if (fph == null) {
fph = formatExpression(sel, variables, arguments);
}
String functionName = null;
Object argument = null;
MapWithNfcKeys options = new MapWithNfcKeys();
if (fph.getInput() instanceof ResolvedExpression) {
ResolvedExpression re = (ResolvedExpression) fph.getInput();
argument = re.argument;
functionName = re.functionName;
options.putAll(re.options);
} else if (fph.getInput() instanceof MFDataModel.VariableExpression) {
MFDataModel.VariableExpression ve = (MFDataModel.VariableExpression) fph.getInput();
argument = resolveLiteralOrVariable(ve.arg, variables, arguments);
if (ve.function instanceof FunctionRef) {
functionName = ((FunctionRef) ve.function).name;
}
} else if (fph.getInput() instanceof LiteralExpression) {
LiteralExpression le = (LiteralExpression) fph.getInput();
argument = le.arg;
if (le.function instanceof FunctionRef) {
functionName = ((FunctionRef) le.function).name;
}
}
FunctionFactory funcFactory = standardFunctions.getFunction(functionName);
if (funcFactory == null) {
funcFactory = customFunctions.getFunction(functionName);
}
// spec: If selection is supported for `rv`:
if (funcFactory != null) {
Function selectorFunction = funcFactory.create(locale, options.getMap());
ResolvedSelector rs = new ResolvedSelector(argument, options, selectorFunction);
// spec: Append `rv` as the last element of the list `res`.
res.add(rs);
} else {
fatalFormattingError("Unknown selector type: " + functionName);
}
}
// This should not be possible, we added one function for each selector,
// or we have thrown an exception.
// But just in case someone removes the throw above?
if (res.size() != selectors.size()) {
fatalFormattingError(
"Something went wrong, not enough selector functions, "
+ res.size()
+ " vs. "
+ selectors.size());
}
// ====================================
// spec: ### Resolve Preferences
// ====================================
// spec: Let `pref` be a new empty list of lists of strings.
List<List<String>> pref = new ArrayList<>();
// spec: For each index `i` in `res`:
for (int i = 0; i < res.size(); i++) {
// spec: Let `keys` be a new empty list of strings.
List<String> keys = new ArrayList<>();
// spec: For each _variant_ `var` of the message:
for (Variant var : sm.variants) {
// spec: Let `key` be the `var` key at position `i`.
LiteralOrCatchallKey key = var.keys.get(i);
// spec: If `key` is not the catch-all key `'*'`:
if (key instanceof CatchallKey) {
keys.add(CatchallKey.AS_KEY_STRING);
} else if (key instanceof Literal) {
// spec: Assert that `key` is a _literal_.
// spec: Let `ks` be the resolved value of `key`.
String ks = ((Literal) key).value;
// spec: Append `ks` as the last element of the list `keys`.
keys.add(ks);
} else {
fatalFormattingError("Literal expected, but got " + key);
}
}
// spec: Let `rv` be the resolved value at index `i` of `res`.
ResolvedSelector rv = res.get(i);
// spec: Let `matches` be the result of calling the method MatchSelectorKeys(`rv`,
// `keys`)
List<String> matches = matchSelectorKeys(rv, keys);
// spec: Append `matches` as the last element of the list `pref`.
pref.add(matches);
}
// ====================================
// spec: ### Filter Variants
// ====================================
// spec: Let `vars` be a new empty list of _variants_.
List<Variant> vars = new ArrayList<>();
// spec: For each _variant_ `var` of the message:
for (Variant var : sm.variants) {
// spec: For each index `i` in `pref`:
int found = 0;
for (int i = 0; i < pref.size(); i++) {
// spec: Let `key` be the `var` key at position `i`.
LiteralOrCatchallKey key = var.keys.get(i);
// spec: If `key` is the catch-all key `'*'`:
if (key instanceof CatchallKey) {
// spec: Continue the inner loop on `pref`.
found++;
continue;
}
// spec: Assert that `key` is a _literal_.
if (!(key instanceof Literal)) {
fatalFormattingError("Literal expected");
}
// spec: Let `ks` be the resolved value of `key`.
String ks = ((Literal) key).value;
// spec: Let `matches` be the list of strings at index `i` of `pref`.
List<String> matches = pref.get(i);
// spec: If `matches` includes `ks`:
if (matches.contains(ks)) {
// spec: Continue the inner loop on `pref`.
found++;
continue;
} else {
// spec: Else:
// spec: Continue the outer loop on message _variants_.
break;
}
}
if (found == pref.size()) {
// spec: Append `var` as the last element of the list `vars`.
vars.add(var);
}
}
// ====================================
// spec: ### Sort Variants
// ====================================
// spec: Let `sortable` be a new empty list of (integer, _variant_) tuples.
List<IntVarTuple> sortable = new ArrayList<>();
// spec: For each _variant_ `var` of `vars`:
for (Variant var : vars) {
// spec: Let `tuple` be a new tuple (-1, `var`).
IntVarTuple tuple = new IntVarTuple(-1, var);
// spec: Append `tuple` as the last element of the list `sortable`.
sortable.add(tuple);
}
// spec: Let `len` be the integer count of items in `pref`.
int len = pref.size();
// spec: Let `i` be `len` - 1.
int i = len - 1;
// spec: While `i` >= 0:
while (i >= 0) {
// spec: Let `matches` be the list of strings at index `i` of `pref`.
List<String> matches = pref.get(i);
// spec: Let `minpref` be the integer count of items in `matches`.
int minpref = matches.size();
// spec: For each tuple `tuple` of `sortable`:
for (IntVarTuple tuple : sortable) {
// spec: Let `matchpref` be an integer with the value `minpref`.
int matchpref = minpref;
// spec: Let `key` be the `tuple` _variant_ key at position `i`.
LiteralOrCatchallKey key = tuple.variant.keys.get(i);
// spec: If `key` is not the catch-all key `'*'`:
if (!(key instanceof CatchallKey)) {
// spec: Assert that `key` is a _literal_.
if (!(key instanceof Literal)) {
fatalFormattingError("Literal expected");
}
// spec: Let `ks` be the resolved value of `key`.
String ks = ((Literal) key).value;
// spec: Let `matchpref` be the integer position of `ks` in `matches`.
matchpref = matches.indexOf(ks);
}
// spec: Set the `tuple` integer value as `matchpref`.
tuple.integer = matchpref;
}
// spec: Set `sortable` to be the result of calling the method `SortVariants(sortable)`.
sortable.sort(MFDataModelFormatter::sortVariants);
// spec: Set `i` to be `i` - 1.
i--;
}
// spec: Let `var` be the _variant_ element of the first element of `sortable`.
IntVarTuple var = sortable.get(0);
// spec: Select the _pattern_ of `var`.
patternToRender = var.variant.value;
// 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
fatalFormattingError("The selection went wrong, cannot select any option.");
}
return patternToRender;
}
/* spec:
* `SortVariants` is a method whose single argument is
* a list of (integer, _variant_) tuples.
* It returns a list of (integer, _variant_) tuples.
* Any implementation of `SortVariants` is acceptable
* as long as it satisfies the following requirements:
*
* 1. Let `sortable` be an arbitrary list of (integer, _variant_) tuples.
* 1. Let `sorted` be `SortVariants(sortable)`.
* 1. `sorted` is the result of sorting `sortable` using the following comparator:
* 1. `(i1, v1)` <= `(i2, v2)` if and only if `i1 <= i2`.
* 1. The sort is stable (pairs of tuples from `sortable` that are equal
* in their first element have the same relative order in `sorted`).
*/
private static int sortVariants(IntVarTuple o1, IntVarTuple o2) {
int result = Integer.compare(o1.integer, o2.integer);
if (result != 0) {
return result;
}
List<LiteralOrCatchallKey> v1 = o1.variant.keys;
List<LiteralOrCatchallKey> v2 = o1.variant.keys;
if (v1.size() != v2.size()) {
fatalFormattingError("The number of keys is not equal.");
}
for (int i = 0; i < v1.size(); i++) {
LiteralOrCatchallKey k1 = v1.get(i);
LiteralOrCatchallKey k2 = v2.get(i);
String s1 = k1 instanceof Literal ? ((Literal) k1).value : CatchallKey.AS_KEY_STRING;
String s2 = k2 instanceof Literal ? ((Literal) k2).value : CatchallKey.AS_KEY_STRING;
int cmp = s1.compareTo(s2);
if (cmp != 0) {
return cmp;
}
}
return 0;
}
/**
* spec: The method MatchSelectorKeys is determined by the implementation. It takes as arguments
* a resolved _selector_ value `rv` and a list of string keys `keys`, and returns a list of
* string keys in preferential order. The returned list MUST contain only unique elements of the
* input list `keys`. The returned list MAY be empty. The most-preferred key is first, with each
* successive key appearing in order by decreasing preference.
*/
@SuppressWarnings("static-method")
private List<String> matchSelectorKeys(ResolvedSelector rv, List<String> keys) {
return rv.selectorFunction.matches(rv.argument, keys, rv.options.getMap());
}
private static class ResolvedSelector {
final Object argument;
final MapWithNfcKeys options;
final Function selectorFunction;
public ResolvedSelector(
Object argument, MapWithNfcKeys options, Function selectorFunction) {
this.argument = argument;
this.options = new MapWithNfcKeys(options);
this.selectorFunction = selectorFunction;
}
}
private static void fatalFormattingError(String message) throws IllegalArgumentException {
throw new IllegalArgumentException(message);
}
private FunctionFactory getFormattingFunctionFactoryByName(
Object toFormat, String functionName) {
// Get a function name from the type of the object to format
if (functionName == null || functionName.isEmpty()) {
if (toFormat == null) {
// The object to format is null, and no function provided.
return null;
}
Class<?> clazz = toFormat.getClass();
functionName = standardFunctions.getDefaultFunctionNameForType(clazz);
if (functionName == null) {
functionName = customFunctions.getDefaultFunctionNameForType(clazz);
}
if (functionName == null) {
fatalFormattingError(
"Object to format without a function, and unknown type: "
+ toFormat.getClass().getName());
}
}
FunctionFactory func = standardFunctions.getFunction(functionName);
if (func == null) {
func = customFunctions.getFunction(functionName);
}
return func;
}
private static Object resolveLiteralOrVariable(
LiteralOrVariableRef value, MapWithNfcKeys localVars, MapWithNfcKeys arguments) {
if (value instanceof Literal) {
String val = ((Literal) value).value;
// "The resolution of a text or literal MUST resolve to a string."
// https://github.com/unicode-org/message-format-wg/blob/main/spec/formatting.md#literal-resolution
return val;
} else if (value instanceof VariableRef) {
String varName = ((VariableRef) value).name;
Object val = localVars.get(varName);
if (val == null) {
val = localVars.get(varName);
}
if (val == null) {
val = arguments.get(StringUtils.toNfc(varName));
}
return val;
}
return value;
}
private static MapWithNfcKeys convertOptions(
Map<String, Option> options, MapWithNfcKeys localVars, MapWithNfcKeys arguments) {
MapWithNfcKeys result = new MapWithNfcKeys();
for (Option option : options.values()) {
result.put(option.name, resolveLiteralOrVariable(option.value, localVars, arguments));
}
return result;
}
/**
* Formats an expression.
*
* @param expression the expression to format
* @param variables local variables, created from declarations (`.input` and `.local`)
* @param arguments the arguments passed at runtime to be formatted (`mf.format(arguments)`)
*/
private FormattedPlaceholder formatExpression(
Expression expression, MapWithNfcKeys variables, MapWithNfcKeys arguments) {
FunctionRef function = null; // function name
String functionName = null;
Object toFormat = null;
Map<String, Object> options = new HashMap<>();
String fallbackString = "{\uFFFD}";
if (expression instanceof MFDataModel.VariableExpression) {
MFDataModel.VariableExpression varPart = (MFDataModel.VariableExpression) expression;
fallbackString = "{$" + varPart.arg.name + "}";
function = varPart.function; // function name & options
Object resolved = resolveLiteralOrVariable(varPart.arg, variables, arguments);
if (resolved instanceof FormattedPlaceholder) {
Object input = ((FormattedPlaceholder) resolved).getInput();
if (input instanceof ResolvedExpression) {
ResolvedExpression re = (ResolvedExpression) input;
toFormat = re.argument;
functionName = re.functionName;
options.putAll(re.options);
} else {
toFormat = input;
}
} else {
toFormat = resolved;
}
} else if (expression
instanceof MFDataModel.FunctionExpression) { // Function without arguments
MFDataModel.FunctionExpression fe = (FunctionExpression) expression;
fallbackString = "{:" + fe.function.name + "}";
function = fe.function;
} else if (expression instanceof MFDataModel.LiteralExpression) {
MFDataModel.LiteralExpression le = (LiteralExpression) expression;
function = le.function;
fallbackString = "{|" + le.arg.value + "|}";
toFormat = resolveLiteralOrVariable(le.arg, variables, arguments);
} else if (expression instanceof MFDataModel.Markup) {
// No output on markup, for now (we only format to string)
return new FormattedPlaceholder(expression, new PlainStringFormattedValue(""));
} else {
if (expression == null) {
fatalFormattingError("unexpected null expression");
} else {
fatalFormattingError("unknown expression type " + expression.getClass().getName());
}
}
if (function instanceof FunctionRef) {
FunctionRef fa = (FunctionRef) function;
functionName = fa.name;
MapWithNfcKeys newOptions = convertOptions(fa.options, variables, arguments);
options.putAll(newOptions.getMap());
}
FunctionFactory 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));
}
// TODO 78: hack.
// How do we pass the error handling policy to formatter / selector functions?
// I am afraid a clean solution for this would require some changes in the public APIs
// And it is too late for that.
options.put("icu:impl:errorPolicy", this.errorHandlingBehavior.name());
Function ff = funcFactory.create(locale, options);
FormattedPlaceholder resultToWrap = ff.format(toFormat, arguments.getMap());
String res = resultToWrap == null ? null : resultToWrap.toString();
if (res == null) {
if (errorHandlingBehavior == ErrorHandlingBehavior.STRICT) {
fatalFormattingError("unable to format string at " + fallbackString);
}
res = fallbackString;
}
if (resultToWrap != null) {
toFormat = resultToWrap.getInput();
}
ResolvedExpression resExpression = new ResolvedExpression(toFormat, functionName, options);
if (resultToWrap == null) {
return new FormattedPlaceholder(resExpression, new PlainStringFormattedValue(res));
}
// We wrap the result in a ResolvedExpression, but also propagate the direction info
return new FormattedPlaceholder(
resExpression,
new PlainStringFormattedValue(res),
resultToWrap.getDirectionality(),
resultToWrap.getIsolate());
}
static class ResolvedExpression implements Expression {
final Object argument;
final String functionName;
final Map<String, Object> options;
public ResolvedExpression(
Object argument, String functionName, Map<String, Object> options) {
this.argument = argument;
this.functionName = StringUtils.toNfc(functionName);
this.options = options;
}
}
private MapWithNfcKeys resolveDeclarations(
List<MFDataModel.Declaration> declarations, MapWithNfcKeys arguments) {
MapWithNfcKeys variables = new MapWithNfcKeys();
String name;
Expression value;
if (declarations != null) {
for (Declaration declaration : declarations) {
if (declaration instanceof InputDeclaration) {
name = ((InputDeclaration) declaration).name;
value = ((InputDeclaration) declaration).value;
} else if (declaration instanceof LocalDeclaration) {
name = ((LocalDeclaration) declaration).name;
value = ((LocalDeclaration) declaration).value;
} else {
continue;
}
try {
// There it no need to succeed in solving everything.
// For example there is no problem is `$b` is not defined below:
// .local $a = {$b :number}
// {{ Hello {$user}! }}
FormattedPlaceholder fmt = formatExpression(value, variables, arguments);
// If it works, all good
variables.put(StringUtils.toNfc(name), fmt);
} catch (IllegalArgumentException e) {
if (this.errorHandlingBehavior == ErrorHandlingBehavior.STRICT) {
throw (e);
}
} catch (Exception e) {
// It's OK to ignore the failure in this context, see comment above.
}
}
}
return variables;
}
private static class IntVarTuple {
int integer;
final Variant variant;
public IntVarTuple(int integer, Variant variant) {
this.integer = integer;
this.variant = variant;
}
}
/*
* I considered extending a HashMap.
* But then we would need to override all the methods that use keys:
* `compute`, `computeIfAbsent`, `computeIfPresent`, `containsKey`, `getOrDefault`,
* `merge`, `put`, `putIfAbsent`, `remove`, `replace`, and so on.
* If we don't and some refactoring in the code above starts using one of
* the methods that was not overridden then it will bypass the normalization
* and will create a map with mixed keys (some not normalized).
*/
private static class MapWithNfcKeys {
private final Map<String, Object> theMap = new HashMap<>();
Map<String, Object> getMap() {
return theMap;
}
MapWithNfcKeys() {
super();
}
MapWithNfcKeys(MapWithNfcKeys org) {
super();
theMap.putAll(org.getMap());
}
MapWithNfcKeys(Map<String, Object> orgMap) {
super();
if (orgMap != null) {
for (Map.Entry<String, Object> e : orgMap.entrySet()) {
this.put(StringUtils.toNfc(e.getKey()), e.getValue());
}
}
}
public Object put(String key, Object value) {
return theMap.put(StringUtils.toNfc(key), value);
}
public void putAll(Map<? extends String, ? extends Object> m) {
theMap.putAll(m);
}
public Object get(String key) {
return theMap.get(key);
}
}
}