All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.icu.message2.MFDataModelFormatter Maven / Gradle / Ivy

Go to download

International Component for Unicode for Java (ICU4J) is a mature, widely used Java library providing Unicode and Globalization support

The newest version!
// © 2022 and later: Unicode, Inc. and others.
// License & terms of use: https://www.unicode.org/copyright.html

package com.ibm.icu.message2;

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;

import com.ibm.icu.message2.MFDataModel.Annotation;
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.FunctionAnnotation;
import com.ibm.icu.message2.MFDataModel.FunctionExpression;
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.ErrorHandlingBehavior;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.CurrencyAmount;

/**
 * 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 {
    private final Locale locale;
    private final ErrorHandlingBehavior errorHandlingBehavior;
    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,
            MFFunctionRegistry customFunctionRegistry) {
        this.locale = locale;
        this.errorHandlingBehavior = errorHandlingBehavior == null
                ? ErrorHandlingBehavior.BEST_EFFORT : errorHandlingBehavior;
        this.dm = dm;
        this.customFunctions =
                customFunctionRegistry == null ? EMPTY_REGISTY : customFunctionRegistry;

        standardFunctions =
                MFFunctionRegistry.builder()
                        // Date/time formatting
                        .setFormatter("datetime", new DateTimeFormatterFactory("datetime"))
                        .setFormatter("date", new DateTimeFormatterFactory("date"))
                        .setFormatter("time", new DateTimeFormatterFactory("time"))
                        .setDefaultFormatterNameForType(Date.class, "datetime")
                        .setDefaultFormatterNameForType(Calendar.class, "datetime")
                        .setDefaultFormatterNameForType(java.util.Calendar.class, "datetime")
                        .setDefaultFormatterNameForType(Temporal.class, "datetime")

                        // Number formatting
                        .setFormatter("number", new NumberFormatterFactory("number"))
                        .setFormatter("integer", new NumberFormatterFactory("integer"))
                        .setDefaultFormatterNameForType(Integer.class, "number")
                        .setDefaultFormatterNameForType(Double.class, "number")
                        .setDefaultFormatterNameForType(Number.class, "number")
                        .setDefaultFormatterNameForType(CurrencyAmount.class, "number")

                        // Format that returns "to string"
                        .setFormatter("string", new IdentityFormatterFactory())
                        .setDefaultFormatterNameForType(String.class, "string")
                        .setDefaultFormatterNameForType(CharSequence.class, "string")

                        // Register the standard selectors
                        .setSelector("number", new NumberFormatterFactory("number"))
                        .setSelector("integer", new NumberFormatterFactory("integer"))
                        .setSelector("string", new TextSelectorFactory())
                        .setSelector("icu:gender", new TextSelectorFactory())
                        .build();
    }

    String format(Map arguments) {
        MFDataModel.Pattern patternToRender = null;
        if (arguments == null) {
            arguments = new HashMap<>();
        }

        Map variables;
        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!";
        }

        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, arguments);
                result.append(formattedExpression.getFormattedValue().toString());
            } else if (part instanceof MFDataModel.Markup) {
                // Ignore
            } else {
                fatalFormattingError("Unknown part type: " + part);
            }
        }
        return result.toString();
    }

    private Pattern findBestMatchingPattern(
            SelectMessage sm, Map variables, Map arguments) {
        Pattern patternToRender = null;

        // ====================================
        // spec: ### Resolve Selectors
        // ====================================

        // Collect all the selector functions in an array, to reuse
        List selectors = sm.selectors;
        // spec: Let `res` be a new empty list of resolved values that support selection.
        List 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 = formatExpression(sel, variables, arguments);
            String functionName = null;
            Object argument = null;
            Map options = new HashMap<>();
            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.annotation instanceof FunctionAnnotation) {
                    functionName = ((FunctionAnnotation) ve.annotation).name;
                }
            } else if (fph.getInput() instanceof LiteralExpression) {
                LiteralExpression le = (LiteralExpression) fph.getInput();
                argument = le.arg;
                if (le.annotation instanceof FunctionAnnotation) {
                    functionName = ((FunctionAnnotation) le.annotation).name;
                }
            }
            SelectorFactory funcFactory = standardFunctions.getSelector(functionName);
            if (funcFactory == null) {
                funcFactory = customFunctions.getSelector(functionName);
            }
            // spec: If selection is supported for `rv`:
            if (funcFactory != null) {
                Selector selectorFunction = funcFactory.createSelector(locale, options);
                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> 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 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("*");
                } 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 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 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 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 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 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 v1 = o1.variant.keys;
        List 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 : "*";
            String s2 = k2 instanceof Literal ? ((Literal) k2).value : "*";
            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 matchSelectorKeys(ResolvedSelector rv, List keys) {
        return rv.selectorFunction.matches(rv.argument, keys, rv.options);
    }

    private static class ResolvedSelector {
        final Object argument;
        final Map options;
        final Selector selectorFunction;

        public ResolvedSelector(
                Object argument, Map options, Selector selectorFunction) {
            this.argument = argument;
            this.options = new HashMap<>(options);
            this.selectorFunction = selectorFunction;
        }
    }

    private static void fatalFormattingError(String message) throws IllegalArgumentException {
        throw new IllegalArgumentException(message);
    }

    private FormatterFactory 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.getDefaultFormatterNameForType(clazz);
            if (functionName == null) {
                functionName = customFunctions.getDefaultFormatterNameForType(clazz);
            }
            if (functionName == null) {
                fatalFormattingError(
                        "Object to format without a function, and unknown type: "
                                + toFormat.getClass().getName());
            }
        }

        FormatterFactory func = standardFunctions.getFormatter(functionName);
        if (func == null) {
            func = customFunctions.getFormatter(functionName);
        }
        return func;
    }

    private static Object resolveLiteralOrVariable(
            LiteralOrVariableRef value,
            Map localVars,
            Map 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(varName);
            }
            return val;
        }
        return value;
    }

    private static Map convertOptions(
            Map options,
            Map localVars,
            Map arguments) {
        Map result = new HashMap<>();
        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, Map variables, Map arguments) {

        Annotation annotation = null; // function name
        String functionName = null;
        Object toFormat = null;
        Map options = new HashMap<>();
        String fallbackString = "{\uFFFD}";

        if (expression instanceof MFDataModel.VariableExpression) {
            MFDataModel.VariableExpression varPart = (MFDataModel.VariableExpression) expression;
            fallbackString = "{$" + varPart.arg.name + "}";
            annotation = varPart.annotation; // 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.annotation.name + "}";
            annotation = fe.annotation;
        } else if (expression instanceof MFDataModel.LiteralExpression) {
            MFDataModel.LiteralExpression le = (LiteralExpression) expression;
            annotation = le.annotation;
            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 (annotation instanceof FunctionAnnotation) {
            FunctionAnnotation fa = (FunctionAnnotation) annotation;
            if (functionName != null && !functionName.equals(fa.name)) {
                fatalFormattingError(
                        "invalid function overrides, '" + functionName + "' <> '" + fa.name + "'");
            }
            functionName = fa.name;
            Map newOptions = convertOptions(fa.options, variables, arguments);
            options.putAll(newOptions);
        }

        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;
        }

        ResolvedExpression resExpression = new ResolvedExpression(toFormat, functionName, options);
        return new FormattedPlaceholder(resExpression, new PlainStringFormattedValue(res));
    }

    static class ResolvedExpression implements Expression {
        final Object argument;
        final String functionName;
        final Map options;

        public ResolvedExpression(
                Object argument, String functionName, Map options) {
            this.argument = argument;
            this.functionName = functionName;
            this.options = options;
        }
    }

    private Map resolveDeclarations(
            List declarations, Map arguments) {
        Map variables = new HashMap<>();
        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(name, fmt);
                } 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;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy