1 // © 2016 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html#License
3 /*
4 *******************************************************************************
5 *   Copyright (C) 2011-2014, International Business Machines
6 *   Corporation and others.  All Rights Reserved.
7 *******************************************************************************
8 *   created on: 2011jul14
9 *   created by: Markus W. Scherer
10 */
11 
12 package com.ibm.icu.text;
13 
14 import java.util.ArrayList;
15 import java.util.Collections;
16 import java.util.List;
17 
18 /**
19  * Utilities for working with a MessagePattern.
20  * Intended for use in tools when convenience is more important than
21  * minimizing runtime and object creations.
22  *
23  * <p>This class only has static methods.
24  * Each of the nested classes is immutable and thread-safe.
25  *
26  * <p>This class and its nested classes are not intended for public subclassing.
27  * @stable ICU 49
28  * @author Markus Scherer
29  */
30 public final class MessagePatternUtil {
31 
32     // Private constructor preventing object instantiation
MessagePatternUtil()33     private MessagePatternUtil() {
34     }
35 
36     /**
37      * Factory method, builds and returns a MessageNode from a MessageFormat pattern string.
38      * @param patternString a MessageFormat pattern string
39      * @return a MessageNode or a ComplexArgStyleNode
40      * @throws IllegalArgumentException if the MessagePattern is empty
41      *         or does not represent a MessageFormat pattern
42      * @stable ICU 49
43      */
buildMessageNode(String patternString)44     public static MessageNode buildMessageNode(String patternString) {
45         return buildMessageNode(new MessagePattern(patternString));
46     }
47 
48     /**
49      * Factory method, builds and returns a MessageNode from a MessagePattern.
50      * @param pattern a parsed MessageFormat pattern string
51      * @return a MessageNode or a ComplexArgStyleNode
52      * @throws IllegalArgumentException if the MessagePattern is empty
53      *         or does not represent a MessageFormat pattern
54      * @stable ICU 49
55      */
buildMessageNode(MessagePattern pattern)56     public static MessageNode buildMessageNode(MessagePattern pattern) {
57         int limit = pattern.countParts() - 1;
58         if (limit < 0) {
59             throw new IllegalArgumentException("The MessagePattern is empty");
60         } else if (pattern.getPartType(0) != MessagePattern.Part.Type.MSG_START) {
61             throw new IllegalArgumentException(
62             "The MessagePattern does not represent a MessageFormat pattern");
63         }
64         return buildMessageNode(pattern, 0, limit);
65     }
66 
67     /**
68      * Common base class for all elements in a tree of nodes
69      * returned by {@link MessagePatternUtil#buildMessageNode(MessagePattern)}.
70      * This class and all subclasses are immutable and thread-safe.
71      * @stable ICU 49
72      */
73     public static class Node {
Node()74         private Node() {}
75     }
76 
77     /**
78      * A Node representing a parsed MessageFormat pattern string.
79      * @stable ICU 49
80      */
81     public static class MessageNode extends Node {
82         /**
83          * @return the list of MessageContentsNode nodes that this message contains
84          * @stable ICU 49
85          */
getContents()86         public List<MessageContentsNode> getContents() {
87             return list;
88         }
89         /**
90          * {@inheritDoc}
91          * @stable ICU 49
92          */
93         @Override
toString()94         public String toString() {
95             return list.toString();
96         }
97 
MessageNode()98         private MessageNode() {
99             super();
100         }
addContentsNode(MessageContentsNode node)101         private void addContentsNode(MessageContentsNode node) {
102             if (node instanceof TextNode && !list.isEmpty()) {
103                 // Coalesce adjacent text nodes.
104                 MessageContentsNode lastNode = list.get(list.size() - 1);
105                 if (lastNode instanceof TextNode) {
106                     TextNode textNode = (TextNode)lastNode;
107                     textNode.text = textNode.text + ((TextNode)node).text;
108                     return;
109                 }
110             }
111             list.add(node);
112         }
freeze()113         private MessageNode freeze() {
114             list = Collections.unmodifiableList(list);
115             return this;
116         }
117 
118         private volatile List<MessageContentsNode> list = new ArrayList<MessageContentsNode>();
119     }
120 
121     /**
122      * A piece of MessageNode contents.
123      * Use getType() to determine the type and the actual Node subclass.
124      * @stable ICU 49
125      */
126     public static class MessageContentsNode extends Node {
127         /**
128          * The type of a piece of MessageNode contents.
129          * @stable ICU 49
130          */
131         public enum Type {
132             /**
133              * This is a TextNode containing literal text (downcast and call getText()).
134              * @stable ICU 49
135              */
136             TEXT,
137             /**
138              * This is an ArgNode representing a message argument
139              * (downcast and use specific methods).
140              * @stable ICU 49
141              */
142             ARG,
143             /**
144              * This Node represents a place in a plural argument's variant where
145              * the formatted (plural-offset) value is to be put.
146              * @stable ICU 49
147              */
148             REPLACE_NUMBER
149         }
150         /**
151          * Returns the type of this piece of MessageNode contents.
152          * @stable ICU 49
153          */
getType()154         public Type getType() {
155             return type;
156         }
157         /**
158          * {@inheritDoc}
159          * @stable ICU 49
160          */
161         @Override
toString()162         public String toString() {
163             // Note: There is no specific subclass for REPLACE_NUMBER
164             // because it would not provide any additional API.
165             // Therefore we have a little bit of REPLACE_NUMBER-specific code
166             // here in the contents-node base class.
167             return "{REPLACE_NUMBER}";
168         }
169 
MessageContentsNode(Type type)170         private MessageContentsNode(Type type) {
171             super();
172             this.type = type;
173         }
createReplaceNumberNode()174         private static MessageContentsNode createReplaceNumberNode() {
175             return new MessageContentsNode(Type.REPLACE_NUMBER);
176         }
177 
178         private Type type;
179     }
180 
181     /**
182      * Literal text, a piece of MessageNode contents.
183      * @stable ICU 49
184      */
185     public static class TextNode extends MessageContentsNode {
186         /**
187          * @return the literal text at this point in the message
188          * @stable ICU 49
189          */
getText()190         public String getText() {
191             return text;
192         }
193         /**
194          * {@inheritDoc}
195          * @stable ICU 49
196          */
197         @Override
toString()198         public String toString() {
199             return "«" + text + "»";
200         }
201 
TextNode(String text)202         private TextNode(String text) {
203             super(Type.TEXT);
204             this.text = text;
205         }
206 
207         private String text;
208     }
209 
210     /**
211      * A piece of MessageNode contents representing a message argument and its details.
212      * @stable ICU 49
213      */
214     public static class ArgNode extends MessageContentsNode {
215         /**
216          * @return the argument type
217          * @stable ICU 49
218          */
getArgType()219         public MessagePattern.ArgType getArgType() {
220             return argType;
221         }
222         /**
223          * @return the argument name string (the decimal-digit string if the argument has a number)
224          * @stable ICU 49
225          */
getName()226         public String getName() {
227             return name;
228         }
229         /**
230          * @return the argument number, or -1 if none (for a named argument)
231          * @stable ICU 49
232          */
getNumber()233         public int getNumber() {
234             return number;
235         }
236         /**
237          * @return the argument type string, or null if none was specified
238          * @stable ICU 49
239          */
getTypeName()240         public String getTypeName() {
241             return typeName;
242         }
243         /**
244          * @return the simple-argument style string,
245          *         or null if no style is specified and for other argument types
246          * @stable ICU 49
247          */
getSimpleStyle()248         public String getSimpleStyle() {
249             return style;
250         }
251         /**
252          * @return the complex-argument-style object,
253          *         or null if the argument type is NONE_ARG or SIMPLE_ARG
254          * @stable ICU 49
255          */
getComplexStyle()256         public ComplexArgStyleNode getComplexStyle() {
257             return complexStyle;
258         }
259         /**
260          * {@inheritDoc}
261          * @stable ICU 49
262          */
263         @Override
toString()264         public String toString() {
265             StringBuilder sb = new StringBuilder();
266             sb.append('{').append(name);
267             if (argType != MessagePattern.ArgType.NONE) {
268                 sb.append(',').append(typeName);
269                 if (argType == MessagePattern.ArgType.SIMPLE) {
270                     if (style != null) {
271                         sb.append(',').append(style);
272                     }
273                 } else {
274                     sb.append(',').append(complexStyle.toString());
275                 }
276             }
277             return sb.append('}').toString();
278         }
279 
ArgNode()280         private ArgNode() {
281             super(Type.ARG);
282         }
createArgNode()283         private static ArgNode createArgNode() {
284             return new ArgNode();
285         }
286 
287         private MessagePattern.ArgType argType;
288         private String name;
289         private int number = -1;
290         private String typeName;
291         private String style;
292         private ComplexArgStyleNode complexStyle;
293     }
294 
295     /**
296      * A Node representing details of the argument style of a complex argument.
297      * (Which is a choice/plural/select argument which selects among nested messages.)
298      * @stable ICU 49
299      */
300     public static class ComplexArgStyleNode extends Node {
301         /**
302          * @return the argument type (same as getArgType() on the parent ArgNode)
303          * @stable ICU 49
304          */
getArgType()305         public MessagePattern.ArgType getArgType() {
306             return argType;
307         }
308         /**
309          * @return true if this is a plural style with an explicit offset
310          * @stable ICU 49
311          */
hasExplicitOffset()312         public boolean hasExplicitOffset() {
313             return explicitOffset;
314         }
315         /**
316          * @return the plural offset, or 0 if this is not a plural style or
317          *         the offset is explicitly or implicitly 0
318          * @stable ICU 49
319          */
getOffset()320         public double getOffset() {
321             return offset;
322         }
323         /**
324          * @return the list of variants: the nested messages with their selection criteria
325          * @stable ICU 49
326          */
getVariants()327         public List<VariantNode> getVariants() {
328             return list;
329         }
330         /**
331          * Separates the variants by type.
332          * Intended for use with plural and select argument styles,
333          * not useful for choice argument styles.
334          *
335          * <p>Both parameters are used only for output, and are first cleared.
336          * @param numericVariants Variants with numeric-value selectors (if any) are added here.
337          *        Can be null for a select argument style.
338          * @param keywordVariants Variants with keyword selectors, except "other", are added here.
339          *        For a plural argument, if this list is empty after the call, then
340          *        all variants except "other" have explicit values
341          *        and PluralRules need not be called.
342          * @return the "other" variant (the first one if there are several),
343          *         null if none (choice style)
344          * @stable ICU 49
345          */
getVariantsByType(List<VariantNode> numericVariants, List<VariantNode> keywordVariants)346         public VariantNode getVariantsByType(List<VariantNode> numericVariants,
347                                              List<VariantNode> keywordVariants) {
348             if (numericVariants != null) {
349                 numericVariants.clear();
350             }
351             keywordVariants.clear();
352             VariantNode other = null;
353             for (VariantNode variant : list) {
354                 if (variant.isSelectorNumeric()) {
355                     numericVariants.add(variant);
356                 } else if ("other".equals(variant.getSelector())) {
357                     if (other == null) {
358                         // Return the first "other" variant. (MessagePattern allows duplicates.)
359                         other = variant;
360                     }
361                 } else {
362                     keywordVariants.add(variant);
363                 }
364             }
365             return other;
366         }
367         /**
368          * {@inheritDoc}
369          * @stable ICU 49
370          */
371         @Override
toString()372         public String toString() {
373             StringBuilder sb = new StringBuilder();
374             sb.append('(').append(argType.toString()).append(" style) ");
375             if (hasExplicitOffset()) {
376                 sb.append("offset:").append(offset).append(' ');
377             }
378             return sb.append(list.toString()).toString();
379         }
380 
ComplexArgStyleNode(MessagePattern.ArgType argType)381         private ComplexArgStyleNode(MessagePattern.ArgType argType) {
382             super();
383             this.argType = argType;
384         }
addVariant(VariantNode variant)385         private void addVariant(VariantNode variant) {
386             list.add(variant);
387         }
freeze()388         private ComplexArgStyleNode freeze() {
389             list = Collections.unmodifiableList(list);
390             return this;
391         }
392 
393         private MessagePattern.ArgType argType;
394         private double offset;
395         private boolean explicitOffset;
396         private volatile List<VariantNode> list = new ArrayList<VariantNode>();
397     }
398 
399     /**
400      * A Node representing a nested message (nested inside an argument)
401      * with its selection criterium.
402      * @stable ICU 49
403      */
404     public static class VariantNode extends Node {
405         /**
406          * Returns the selector string.
407          * For example: A plural/select keyword ("few"), a plural explicit value ("=1"),
408          * a choice comparison operator ("#").
409          * @return the selector string
410          * @stable ICU 49
411          */
getSelector()412         public String getSelector() {
413             return selector;
414         }
415         /**
416          * @return true for choice variants and for plural explicit values
417          * @stable ICU 49
418          */
isSelectorNumeric()419         public boolean isSelectorNumeric() {
420             return numericValue != MessagePattern.NO_NUMERIC_VALUE;
421         }
422         /**
423          * @return the selector's numeric value, or NO_NUMERIC_VALUE if !isSelectorNumeric()
424          * @stable ICU 49
425          */
getSelectorValue()426         public double getSelectorValue() {
427             return numericValue;
428         }
429         /**
430          * @return the nested message
431          * @stable ICU 49
432          */
getMessage()433         public MessageNode getMessage() {
434             return msgNode;
435         }
436         /**
437          * {@inheritDoc}
438          * @stable ICU 49
439          */
440         @Override
toString()441         public String toString() {
442             StringBuilder sb = new StringBuilder();
443             if (isSelectorNumeric()) {
444                 sb.append(numericValue).append(" (").append(selector).append(") {");
445             } else {
446                 sb.append(selector).append(" {");
447             }
448             return sb.append(msgNode.toString()).append('}').toString();
449         }
450 
VariantNode()451         private VariantNode() {
452             super();
453         }
454 
455         private String selector;
456         private double numericValue = MessagePattern.NO_NUMERIC_VALUE;
457         private MessageNode msgNode;
458     }
459 
buildMessageNode(MessagePattern pattern, int start, int limit)460     private static MessageNode buildMessageNode(MessagePattern pattern, int start, int limit) {
461         int prevPatternIndex = pattern.getPart(start).getLimit();
462         MessageNode node = new MessageNode();
463         for (int i = start + 1;; ++i) {
464             MessagePattern.Part part = pattern.getPart(i);
465             int patternIndex = part.getIndex();
466             if (prevPatternIndex < patternIndex) {
467                 node.addContentsNode(
468                         new TextNode(pattern.getPatternString().substring(prevPatternIndex,
469                                      patternIndex)));
470             }
471             if (i == limit) {
472                 break;
473             }
474             MessagePattern.Part.Type partType = part.getType();
475             if (partType == MessagePattern.Part.Type.ARG_START) {
476                 int argLimit = pattern.getLimitPartIndex(i);
477                 node.addContentsNode(buildArgNode(pattern, i, argLimit));
478                 i = argLimit;
479                 part = pattern.getPart(i);
480             } else if (partType == MessagePattern.Part.Type.REPLACE_NUMBER) {
481                 node.addContentsNode(MessageContentsNode.createReplaceNumberNode());
482                 // else: ignore SKIP_SYNTAX and INSERT_CHAR parts.
483             }
484             prevPatternIndex = part.getLimit();
485         }
486         return node.freeze();
487     }
488 
buildArgNode(MessagePattern pattern, int start, int limit)489     private static ArgNode buildArgNode(MessagePattern pattern, int start, int limit) {
490         ArgNode node = ArgNode.createArgNode();
491         MessagePattern.Part part = pattern.getPart(start);
492         MessagePattern.ArgType argType = node.argType = part.getArgType();
493         part = pattern.getPart(++start);  // ARG_NAME or ARG_NUMBER
494         node.name = pattern.getSubstring(part);
495         if (part.getType() == MessagePattern.Part.Type.ARG_NUMBER) {
496             node.number = part.getValue();
497         }
498         ++start;
499         switch(argType) {
500         case SIMPLE:
501             // ARG_TYPE
502             node.typeName = pattern.getSubstring(pattern.getPart(start++));
503             if (start < limit) {
504                 // ARG_STYLE
505                 node.style = pattern.getSubstring(pattern.getPart(start));
506             }
507             break;
508         case CHOICE:
509             node.typeName = "choice";
510             node.complexStyle = buildChoiceStyleNode(pattern, start, limit);
511             break;
512         case PLURAL:
513             node.typeName = "plural";
514             node.complexStyle = buildPluralStyleNode(pattern, start, limit, argType);
515             break;
516         case SELECT:
517             node.typeName = "select";
518             node.complexStyle = buildSelectStyleNode(pattern, start, limit);
519             break;
520         case SELECTORDINAL:
521             node.typeName = "selectordinal";
522             node.complexStyle = buildPluralStyleNode(pattern, start, limit, argType);
523             break;
524         default:
525             // NONE type, nothing else to do
526             break;
527         }
528         return node;
529     }
530 
buildChoiceStyleNode(MessagePattern pattern, int start, int limit)531     private static ComplexArgStyleNode buildChoiceStyleNode(MessagePattern pattern,
532                                                             int start, int limit) {
533         ComplexArgStyleNode node = new ComplexArgStyleNode(MessagePattern.ArgType.CHOICE);
534         while (start < limit) {
535             int valueIndex = start;
536             MessagePattern.Part part = pattern.getPart(start);
537             double value = pattern.getNumericValue(part);
538             start += 2;
539             int msgLimit = pattern.getLimitPartIndex(start);
540             VariantNode variant = new VariantNode();
541             variant.selector = pattern.getSubstring(pattern.getPart(valueIndex + 1));
542             variant.numericValue = value;
543             variant.msgNode = buildMessageNode(pattern, start, msgLimit);
544             node.addVariant(variant);
545             start = msgLimit + 1;
546         }
547         return node.freeze();
548     }
549 
buildPluralStyleNode(MessagePattern pattern, int start, int limit, MessagePattern.ArgType argType)550     private static ComplexArgStyleNode buildPluralStyleNode(MessagePattern pattern,
551                                                             int start, int limit,
552                                                             MessagePattern.ArgType argType) {
553         ComplexArgStyleNode node = new ComplexArgStyleNode(argType);
554         MessagePattern.Part offset = pattern.getPart(start);
555         if (offset.getType().hasNumericValue()) {
556             node.explicitOffset = true;
557             node.offset = pattern.getNumericValue(offset);
558             ++start;
559         }
560         while (start < limit) {
561             MessagePattern.Part selector = pattern.getPart(start++);
562             double value = MessagePattern.NO_NUMERIC_VALUE;
563             MessagePattern.Part part = pattern.getPart(start);
564             if (part.getType().hasNumericValue()) {
565                 value = pattern.getNumericValue(part);
566                 ++start;
567             }
568             int msgLimit = pattern.getLimitPartIndex(start);
569             VariantNode variant = new VariantNode();
570             variant.selector = pattern.getSubstring(selector);
571             variant.numericValue = value;
572             variant.msgNode = buildMessageNode(pattern, start, msgLimit);
573             node.addVariant(variant);
574             start = msgLimit + 1;
575         }
576         return node.freeze();
577     }
578 
buildSelectStyleNode(MessagePattern pattern, int start, int limit)579     private static ComplexArgStyleNode buildSelectStyleNode(MessagePattern pattern,
580                                                             int start, int limit) {
581         ComplexArgStyleNode node = new ComplexArgStyleNode(MessagePattern.ArgType.SELECT);
582         while (start < limit) {
583             MessagePattern.Part selector = pattern.getPart(start++);
584             int msgLimit = pattern.getLimitPartIndex(start);
585             VariantNode variant = new VariantNode();
586             variant.selector = pattern.getSubstring(selector);
587             variant.msgNode = buildMessageNode(pattern, start, msgLimit);
588             node.addVariant(variant);
589             start = msgLimit + 1;
590         }
591         return node.freeze();
592     }
593 }
594