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