blob: 929affc0fbff08b5387d89ffe51616363803d9cf [file] [log] [blame]
/*
*******************************************************************************
* Copyright (C) 2011-2012, 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.
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
* @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
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
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
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
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.
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public static class Node {
private Node() {}
}
/**
* A Node representing a parsed MessageFormat pattern string.
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public static class MessageNode extends Node {
/**
* @return the list of MessageContentsNode nodes that this message contains
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public List<MessageContentsNode> getContents() {
return list;
}
/**
* {@inheritDoc}
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
@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 List<MessageContentsNode> list = new ArrayList<MessageContentsNode>();
}
/**
* A piece of MessageNode contents.
* Use getType() to determine the type and the actual Node subclass.
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public static class MessageContentsNode extends Node {
/**
* The type of a piece of MessageNode contents.
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public enum Type {
/**
* This is a TextNode containing literal text (downcast and call getText()).
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
TEXT,
/**
* This is an ArgNode representing a message argument
* (downcast and use specific methods).
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
ARG,
/**
* This Node represents a place in a plural argument's variant where
* the formatted (plural-offset) value is to be put.
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
REPLACE_NUMBER
}
/**
* Returns the type of this piece of MessageNode contents.
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public Type getType() {
return type;
}
/**
* {@inheritDoc}
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
@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.
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public static class TextNode extends MessageContentsNode {
/**
* @return the literal text at this point in the message
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public String getText() {
return text;
}
/**
* {@inheritDoc}
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
@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.
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public static class ArgNode extends MessageContentsNode {
/**
* @return the argument type
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public MessagePattern.ArgType getArgType() {
return argType;
}
/**
* @return the argument name string (the decimal-digit string if the argument has a number)
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public String getName() {
return name;
}
/**
* @return the argument number, or -1 if none (for a named argument)
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public int getNumber() {
return number;
}
/**
* @return the argument type string, or null if none was specified
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public String getTypeName() {
return typeName;
}
/**
* @return the simple-argument style string,
* or null if no style is specified and for other argument types
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public String getSimpleStyle() {
return style;
}
/**
* @return the complex-argument-style object,
* or null if the argument type is NONE_ARG or SIMPLE_ARG
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public ComplexArgStyleNode getComplexStyle() {
return complexStyle;
}
/**
* {@inheritDoc}
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
@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.)
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public static class ComplexArgStyleNode extends Node {
/**
* @return the argument type (same as getArgType() on the parent ArgNode)
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public MessagePattern.ArgType getArgType() {
return argType;
}
/**
* @return true if this is a plural style with an explicit offset
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
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
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public double getOffset() {
return offset;
}
/**
* @return the list of variants: the nested messages with their selection criteria
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
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)
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
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}
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
@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 List<VariantNode> list = new ArrayList<VariantNode>();
}
/**
* A Node representing a nested message (nested inside an argument)
* with its selection criterium.
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
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
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public String getSelector() {
return selector;
}
/**
* @return true for choice variants and for plural explicit values
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public boolean isSelectorNumeric() {
return numericValue != MessagePattern.NO_NUMERIC_VALUE;
}
/**
* @return the selector's numeric value, or NO_NUMERIC_VALUE if !isSelectorNumeric()
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public double getSelectorValue() {
return numericValue;
}
/**
* @return the nested message
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
public MessageNode getMessage() {
return msgNode;
}
/**
* {@inheritDoc}
* @draft ICU 49
* @provisional This API might change or be removed in a future release.
*/
@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();
}
}