MessagePatternUtil.java

// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 *   Copyright (C) 2011-2014, International Business Machines
 *   Corporation and others.  All Rights Reserved.
 *******************************************************************************
 *   created on: 2011jul14
 *   created by: Markus W. Scherer
 */

package com.ibm.icu.text;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Utilities for working with a MessagePattern. Intended for use in tools when convenience is more
 * important than minimizing runtime and object creations.
 *
 * <p>This class only has static methods. Each of the nested classes is immutable and thread-safe.
 *
 * <p>This class and its nested classes are not intended for public subclassing.
 *
 * @stable ICU 49
 * @author Markus Scherer
 */
public final class MessagePatternUtil {

    // Private constructor preventing object instantiation
    private MessagePatternUtil() {}

    /**
     * Factory method, builds and returns a MessageNode from a MessageFormat pattern string.
     *
     * @param patternString a MessageFormat pattern string
     * @return a MessageNode or a ComplexArgStyleNode
     * @throws IllegalArgumentException if the MessagePattern is empty or does not represent a
     *     MessageFormat pattern
     * @stable ICU 49
     */
    public static MessageNode buildMessageNode(String patternString) {
        return buildMessageNode(new MessagePattern(patternString));
    }

    /**
     * Factory method, builds and returns a MessageNode from a MessagePattern.
     *
     * @param pattern a parsed MessageFormat pattern string
     * @return a MessageNode or a ComplexArgStyleNode
     * @throws IllegalArgumentException if the MessagePattern is empty or does not represent a
     *     MessageFormat pattern
     * @stable ICU 49
     */
    public static MessageNode buildMessageNode(MessagePattern pattern) {
        int limit = pattern.countParts() - 1;
        if (limit < 0) {
            throw new IllegalArgumentException("The MessagePattern is empty");
        } else if (pattern.getPartType(0) != MessagePattern.Part.Type.MSG_START) {
            throw new IllegalArgumentException(
                    "The MessagePattern does not represent a MessageFormat pattern");
        }
        return buildMessageNode(pattern, 0, limit);
    }

    /**
     * Common base class for all elements in a tree of nodes returned by {@link
     * MessagePatternUtil#buildMessageNode(MessagePattern)}. This class and all subclasses are
     * immutable and thread-safe.
     *
     * @stable ICU 49
     */
    public static class Node {
        private Node() {}
    }

    /**
     * A Node representing a parsed MessageFormat pattern string.
     *
     * @stable ICU 49
     */
    public static class MessageNode extends Node {
        /**
         * @return the list of MessageContentsNode nodes that this message contains
         * @stable ICU 49
         */
        public List<MessageContentsNode> getContents() {
            return list;
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 49
         */
        @Override
        public String toString() {
            return list.toString();
        }

        private MessageNode() {
            super();
        }

        private void addContentsNode(MessageContentsNode node) {
            if (node instanceof TextNode && !list.isEmpty()) {
                // Coalesce adjacent text nodes.
                MessageContentsNode lastNode = list.get(list.size() - 1);
                if (lastNode instanceof TextNode) {
                    TextNode textNode = (TextNode) lastNode;
                    textNode.text = textNode.text + ((TextNode) node).text;
                    return;
                }
            }
            list.add(node);
        }

        private MessageNode freeze() {
            list = Collections.unmodifiableList(list);
            return this;
        }

        private volatile List<MessageContentsNode> list = new ArrayList<MessageContentsNode>();
    }

    /**
     * A piece of MessageNode contents. Use getType() to determine the type and the actual Node
     * subclass.
     *
     * @stable ICU 49
     */
    public static class MessageContentsNode extends Node {
        /**
         * The type of a piece of MessageNode contents.
         *
         * @stable ICU 49
         */
        public enum Type {
            /**
             * This is a TextNode containing literal text (downcast and call getText()).
             *
             * @stable ICU 49
             */
            TEXT,
            /**
             * This is an ArgNode representing a message argument (downcast and use specific
             * methods).
             *
             * @stable ICU 49
             */
            ARG,
            /**
             * This Node represents a place in a plural argument's variant where the formatted
             * (plural-offset) value is to be put.
             *
             * @stable ICU 49
             */
            REPLACE_NUMBER
        }

        /**
         * Returns the type of this piece of MessageNode contents.
         *
         * @stable ICU 49
         */
        public Type getType() {
            return type;
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 49
         */
        @Override
        public String toString() {
            // Note: There is no specific subclass for REPLACE_NUMBER
            // because it would not provide any additional API.
            // Therefore we have a little bit of REPLACE_NUMBER-specific code
            // here in the contents-node base class.
            return "{REPLACE_NUMBER}";
        }

        private MessageContentsNode(Type type) {
            super();
            this.type = type;
        }

        private static MessageContentsNode createReplaceNumberNode() {
            return new MessageContentsNode(Type.REPLACE_NUMBER);
        }

        private Type type;
    }

    /**
     * Literal text, a piece of MessageNode contents.
     *
     * @stable ICU 49
     */
    public static class TextNode extends MessageContentsNode {
        /**
         * @return the literal text at this point in the message
         * @stable ICU 49
         */
        public String getText() {
            return text;
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 49
         */
        @Override
        public String toString() {
            return "«" + text + "»";
        }

        private TextNode(String text) {
            super(Type.TEXT);
            this.text = text;
        }

        private String text;
    }

    /**
     * A piece of MessageNode contents representing a message argument and its details.
     *
     * @stable ICU 49
     */
    public static class ArgNode extends MessageContentsNode {
        /**
         * @return the argument type
         * @stable ICU 49
         */
        public MessagePattern.ArgType getArgType() {
            return argType;
        }

        /**
         * @return the argument name string (the decimal-digit string if the argument has a number)
         * @stable ICU 49
         */
        public String getName() {
            return name;
        }

        /**
         * @return the argument number, or -1 if none (for a named argument)
         * @stable ICU 49
         */
        public int getNumber() {
            return number;
        }

        /**
         * @return the argument type string, or null if none was specified
         * @stable ICU 49
         */
        public String getTypeName() {
            return typeName;
        }

        /**
         * @return the simple-argument style string, or null if no style is specified and for other
         *     argument types
         * @stable ICU 49
         */
        public String getSimpleStyle() {
            return style;
        }

        /**
         * @return the complex-argument-style object, or null if the argument type is NONE_ARG or
         *     SIMPLE_ARG
         * @stable ICU 49
         */
        public ComplexArgStyleNode getComplexStyle() {
            return complexStyle;
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 49
         */
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append('{').append(name);
            if (argType != MessagePattern.ArgType.NONE) {
                sb.append(',').append(typeName);
                if (argType == MessagePattern.ArgType.SIMPLE) {
                    if (style != null) {
                        sb.append(',').append(style);
                    }
                } else {
                    sb.append(',').append(complexStyle.toString());
                }
            }
            return sb.append('}').toString();
        }

        private ArgNode() {
            super(Type.ARG);
        }

        private static ArgNode createArgNode() {
            return new ArgNode();
        }

        private MessagePattern.ArgType argType;
        private String name;
        private int number = -1;
        private String typeName;
        private String style;
        private ComplexArgStyleNode complexStyle;
    }

    /**
     * A Node representing details of the argument style of a complex argument. (Which is a
     * choice/plural/select argument which selects among nested messages.)
     *
     * @stable ICU 49
     */
    public static class ComplexArgStyleNode extends Node {
        /**
         * @return the argument type (same as getArgType() on the parent ArgNode)
         * @stable ICU 49
         */
        public MessagePattern.ArgType getArgType() {
            return argType;
        }

        /**
         * @return true if this is a plural style with an explicit offset
         * @stable ICU 49
         */
        public boolean hasExplicitOffset() {
            return explicitOffset;
        }

        /**
         * @return the plural offset, or 0 if this is not a plural style or the offset is explicitly
         *     or implicitly 0
         * @stable ICU 49
         */
        public double getOffset() {
            return offset;
        }

        /**
         * @return the list of variants: the nested messages with their selection criteria
         * @stable ICU 49
         */
        public List<VariantNode> getVariants() {
            return list;
        }

        /**
         * Separates the variants by type. Intended for use with plural and select argument styles,
         * not useful for choice argument styles.
         *
         * <p>Both parameters are used only for output, and are first cleared.
         *
         * @param numericVariants Variants with numeric-value selectors (if any) are added here. Can
         *     be null for a select argument style.
         * @param keywordVariants Variants with keyword selectors, except "other", are added here.
         *     For a plural argument, if this list is empty after the call, then all variants except
         *     "other" have explicit values and PluralRules need not be called.
         * @return the "other" variant (the first one if there are several), null if none (choice
         *     style)
         * @stable ICU 49
         */
        public VariantNode getVariantsByType(
                List<VariantNode> numericVariants, List<VariantNode> keywordVariants) {
            if (numericVariants != null) {
                numericVariants.clear();
            }
            keywordVariants.clear();
            VariantNode other = null;
            for (VariantNode variant : list) {
                if (variant.isSelectorNumeric()) {
                    numericVariants.add(variant);
                } else if ("other".equals(variant.getSelector())) {
                    if (other == null) {
                        // Return the first "other" variant. (MessagePattern allows duplicates.)
                        other = variant;
                    }
                } else {
                    keywordVariants.add(variant);
                }
            }
            return other;
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 49
         */
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append('(').append(argType.toString()).append(" style) ");
            if (hasExplicitOffset()) {
                sb.append("offset:").append(offset).append(' ');
            }
            return sb.append(list.toString()).toString();
        }

        private ComplexArgStyleNode(MessagePattern.ArgType argType) {
            super();
            this.argType = argType;
        }

        private void addVariant(VariantNode variant) {
            list.add(variant);
        }

        private ComplexArgStyleNode freeze() {
            list = Collections.unmodifiableList(list);
            return this;
        }

        private MessagePattern.ArgType argType;
        private double offset;
        private boolean explicitOffset;
        private volatile List<VariantNode> list = new ArrayList<VariantNode>();
    }

    /**
     * A Node representing a nested message (nested inside an argument) with its selection
     * criterion.
     *
     * @stable ICU 49
     */
    public static class VariantNode extends Node {
        /**
         * Returns the selector string. For example: A plural/select keyword ("few"), a plural
         * explicit value ("=1"), a choice comparison operator ("#").
         *
         * @return the selector string
         * @stable ICU 49
         */
        public String getSelector() {
            return selector;
        }

        /**
         * @return true for choice variants and for plural explicit values
         * @stable ICU 49
         */
        public boolean isSelectorNumeric() {
            return numericValue != MessagePattern.NO_NUMERIC_VALUE;
        }

        /**
         * @return the selector's numeric value, or NO_NUMERIC_VALUE if !isSelectorNumeric()
         * @stable ICU 49
         */
        public double getSelectorValue() {
            return numericValue;
        }

        /**
         * @return the nested message
         * @stable ICU 49
         */
        public MessageNode getMessage() {
            return msgNode;
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 49
         */
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            if (isSelectorNumeric()) {
                sb.append(numericValue).append(" (").append(selector).append(") {");
            } else {
                sb.append(selector).append(" {");
            }
            return sb.append(msgNode.toString()).append('}').toString();
        }

        private VariantNode() {
            super();
        }

        private String selector;
        private double numericValue = MessagePattern.NO_NUMERIC_VALUE;
        private MessageNode msgNode;
    }

    private static MessageNode buildMessageNode(MessagePattern pattern, int start, int limit) {
        int prevPatternIndex = pattern.getPart(start).getLimit();
        MessageNode node = new MessageNode();
        for (int i = start + 1; ; ++i) {
            MessagePattern.Part part = pattern.getPart(i);
            int patternIndex = part.getIndex();
            if (prevPatternIndex < patternIndex) {
                node.addContentsNode(
                        new TextNode(
                                pattern.getPatternString()
                                        .substring(prevPatternIndex, patternIndex)));
            }
            if (i == limit) {
                break;
            }
            MessagePattern.Part.Type partType = part.getType();
            if (partType == MessagePattern.Part.Type.ARG_START) {
                int argLimit = pattern.getLimitPartIndex(i);
                node.addContentsNode(buildArgNode(pattern, i, argLimit));
                i = argLimit;
                part = pattern.getPart(i);
            } else if (partType == MessagePattern.Part.Type.REPLACE_NUMBER) {
                node.addContentsNode(MessageContentsNode.createReplaceNumberNode());
                // else: ignore SKIP_SYNTAX and INSERT_CHAR parts.
            }
            prevPatternIndex = part.getLimit();
        }
        return node.freeze();
    }

    private static ArgNode buildArgNode(MessagePattern pattern, int start, int limit) {
        ArgNode node = ArgNode.createArgNode();
        MessagePattern.Part part = pattern.getPart(start);
        MessagePattern.ArgType argType = node.argType = part.getArgType();
        part = pattern.getPart(++start); // ARG_NAME or ARG_NUMBER
        node.name = pattern.getSubstring(part);
        if (part.getType() == MessagePattern.Part.Type.ARG_NUMBER) {
            node.number = part.getValue();
        }
        ++start;
        switch (argType) {
            case SIMPLE:
                // ARG_TYPE
                node.typeName = pattern.getSubstring(pattern.getPart(start++));
                if (start < limit) {
                    // ARG_STYLE
                    node.style = pattern.getSubstring(pattern.getPart(start));
                }
                break;
            case CHOICE:
                node.typeName = "choice";
                node.complexStyle = buildChoiceStyleNode(pattern, start, limit);
                break;
            case PLURAL:
                node.typeName = "plural";
                node.complexStyle = buildPluralStyleNode(pattern, start, limit, argType);
                break;
            case SELECT:
                node.typeName = "select";
                node.complexStyle = buildSelectStyleNode(pattern, start, limit);
                break;
            case SELECTORDINAL:
                node.typeName = "selectordinal";
                node.complexStyle = buildPluralStyleNode(pattern, start, limit, argType);
                break;
            default:
                // NONE type, nothing else to do
                break;
        }
        return node;
    }

    private static ComplexArgStyleNode buildChoiceStyleNode(
            MessagePattern pattern, int start, int limit) {
        ComplexArgStyleNode node = new ComplexArgStyleNode(MessagePattern.ArgType.CHOICE);
        while (start < limit) {
            int valueIndex = start;
            MessagePattern.Part part = pattern.getPart(start);
            double value = pattern.getNumericValue(part);
            start += 2;
            int msgLimit = pattern.getLimitPartIndex(start);
            VariantNode variant = new VariantNode();
            variant.selector = pattern.getSubstring(pattern.getPart(valueIndex + 1));
            variant.numericValue = value;
            variant.msgNode = buildMessageNode(pattern, start, msgLimit);
            node.addVariant(variant);
            start = msgLimit + 1;
        }
        return node.freeze();
    }

    private static ComplexArgStyleNode buildPluralStyleNode(
            MessagePattern pattern, int start, int limit, MessagePattern.ArgType argType) {
        ComplexArgStyleNode node = new ComplexArgStyleNode(argType);
        MessagePattern.Part offset = pattern.getPart(start);
        if (offset.getType().hasNumericValue()) {
            node.explicitOffset = true;
            node.offset = pattern.getNumericValue(offset);
            ++start;
        }
        while (start < limit) {
            MessagePattern.Part selector = pattern.getPart(start++);
            double value = MessagePattern.NO_NUMERIC_VALUE;
            MessagePattern.Part part = pattern.getPart(start);
            if (part.getType().hasNumericValue()) {
                value = pattern.getNumericValue(part);
                ++start;
            }
            int msgLimit = pattern.getLimitPartIndex(start);
            VariantNode variant = new VariantNode();
            variant.selector = pattern.getSubstring(selector);
            variant.numericValue = value;
            variant.msgNode = buildMessageNode(pattern, start, msgLimit);
            node.addVariant(variant);
            start = msgLimit + 1;
        }
        return node.freeze();
    }

    private static ComplexArgStyleNode buildSelectStyleNode(
            MessagePattern pattern, int start, int limit) {
        ComplexArgStyleNode node = new ComplexArgStyleNode(MessagePattern.ArgType.SELECT);
        while (start < limit) {
            MessagePattern.Part selector = pattern.getPart(start++);
            int msgLimit = pattern.getLimitPartIndex(start);
            VariantNode variant = new VariantNode();
            variant.selector = pattern.getSubstring(selector);
            variant.msgNode = buildMessageNode(pattern, start, msgLimit);
            node.addVariant(variant);
            start = msgLimit + 1;
        }
        return node.freeze();
    }
}