001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.lang3.text; 018 019import java.text.Format; 020import java.text.MessageFormat; 021import java.text.ParsePosition; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Locale; 025import java.util.Map; 026import java.util.Objects; 027 028import org.apache.commons.lang3.LocaleUtils; 029import org.apache.commons.lang3.StringUtils; 030import org.apache.commons.lang3.Validate; 031 032/** 033 * Extends {@link java.text.MessageFormat} to allow pluggable/additional formatting 034 * options for embedded format elements. 035 * <p> 036 * Client code should specify a registry 037 * of {@link FormatFactory} instances associated with {@link String} 038 * format names. This registry will be consulted when the format elements are 039 * parsed from the message pattern. In this way custom patterns can be specified, 040 * and the formats supported by {@link java.text.MessageFormat} can be overridden 041 * at the format and/or format style level (see MessageFormat). A "format element" 042 * embedded in the message pattern is specified (<strong>()?</strong> signifies optionality): 043 * </p> 044 * <p> 045 * <code>{</code><em>argument-number</em><strong>(</strong>{@code ,}<em>format-name</em><b> 046 * (</b>{@code ,}<em>format-style</em><strong>)?)?</strong><code>}</code> 047 * </p> 048 * 049 * <p> 050 * <em>format-name</em> and <em>format-style</em> values are trimmed of surrounding whitespace 051 * in the manner of {@link java.text.MessageFormat}. If <em>format-name</em> denotes 052 * {@code FormatFactory formatFactoryInstance} in {@code registry}, a {@link Format} 053 * matching <em>format-name</em> and <em>format-style</em> is requested from 054 * {@code formatFactoryInstance}. If this is successful, the {@link Format} 055 * found is used for this format element. 056 * </p> 057 * 058 * <p> 059 * <strong>NOTICE:</strong> The various subformat mutator methods are considered unnecessary; they exist on the parent 060 * class to allow the type of customization which it is the job of this class to provide in 061 * a configurable fashion. These methods have thus been disabled and will throw 062 * {@link UnsupportedOperationException} if called. 063 * </p> 064 * 065 * <p> 066 * Limitations inherited from {@link java.text.MessageFormat}: 067 * </p> 068 * <ul> 069 * <li>When using "choice" subformats, support for nested formatting instructions is limited 070 * to that provided by the base class.</li> 071 * <li>Thread-safety of {@link Format}s, including {@link MessageFormat} and thus 072 * {@link ExtendedMessageFormat}, is not guaranteed.</li> 073 * </ul> 074 * 075 * @since 2.4 076 * @deprecated As of <a href="https://commons.apache.org/proper/commons-lang/changes-report.html#a3.6">3.6</a>, use Apache Commons Text 077 * <a href="https://commons.apache.org/proper/commons-text/javadocs/api-release/org/apache/commons/text/ExtendedMessageFormat.html"> 078 * ExtendedMessageFormat</a>. 079 */ 080@Deprecated 081public class ExtendedMessageFormat extends MessageFormat { 082 083 private static final long serialVersionUID = -2362048321261811743L; 084 private static final String EMPTY_PATTERN = StringUtils.EMPTY; 085 private static final char START_FMT = ','; 086 private static final char END_FE = '}'; 087 private static final char START_FE = '{'; 088 private static final char QUOTE = '\''; 089 090 /** 091 * To pattern string. 092 */ 093 private String toPattern; 094 095 /** 096 * Our registry of FormatFactory. 097 */ 098 private final Map<String, ? extends FormatFactory> registry; 099 100 /** 101 * Create a new ExtendedMessageFormat for the default locale. 102 * 103 * @param pattern The pattern to use, not null 104 * @throws IllegalArgumentException Thrown in case of a bad pattern. 105 */ 106 public ExtendedMessageFormat(final String pattern) { 107 this(pattern, Locale.getDefault()); 108 } 109 110 /** 111 * Create a new ExtendedMessageFormat. 112 * 113 * @param pattern The pattern to use, not null 114 * @param locale The locale to use, not null 115 * @throws IllegalArgumentException Thrown in case of a bad pattern. 116 */ 117 public ExtendedMessageFormat(final String pattern, final Locale locale) { 118 this(pattern, locale, null); 119 } 120 121 /** 122 * Create a new ExtendedMessageFormat. 123 * 124 * @param pattern The pattern to use, not null. 125 * @param locale The locale to use. 126 * @param registry The registry of format factories, may be null. 127 * @throws IllegalArgumentException Thrown in case of a bad pattern. 128 */ 129 public ExtendedMessageFormat(final String pattern, final Locale locale, final Map<String, ? extends FormatFactory> registry) { 130 super(EMPTY_PATTERN); 131 setLocale(LocaleUtils.toLocale(locale)); 132 this.registry = registry; 133 applyPattern(pattern); 134 } 135 136 /** 137 * Create a new ExtendedMessageFormat for the default locale. 138 * 139 * @param pattern The pattern to use, not null 140 * @param registry The registry of format factories, may be null 141 * @throws IllegalArgumentException Thrown in case of a bad pattern. 142 */ 143 public ExtendedMessageFormat(final String pattern, final Map<String, ? extends FormatFactory> registry) { 144 this(pattern, Locale.getDefault(), registry); 145 } 146 147 /** 148 * Consume a quoted string, adding it to {@code appendTo} if 149 * specified. 150 * 151 * @param pattern pattern to parse, as a char array created once by the caller (avoids copying 152 * the entire pattern for every token parsed) 153 * @param pos current parse position 154 * @param appendTo optional StringBuilder to append 155 * @return {@code appendTo} 156 */ 157 private StringBuilder appendQuotedString(final char[] pattern, final ParsePosition pos, 158 final StringBuilder appendTo) { 159 assert pattern[pos.getIndex()] == QUOTE : 160 "Quoted string must start with quote character"; 161 162 // handle quote character at the beginning of the string 163 if (appendTo != null) { 164 appendTo.append(QUOTE); 165 } 166 next(pos); 167 168 final int start = pos.getIndex(); 169 for (int i = pos.getIndex(); i < pattern.length; i++) { 170 if (pattern[pos.getIndex()] == QUOTE) { 171 next(pos); 172 return appendTo == null ? null : appendTo.append(pattern, start, 173 pos.getIndex() - start); 174 } 175 next(pos); 176 } 177 throw new IllegalArgumentException( 178 "Unterminated quoted string at position " + start); 179 } 180 181 /** 182 * Apply the specified pattern. 183 * 184 * @param pattern String 185 */ 186 @Override 187 public final void applyPattern(final String pattern) { 188 if (registry == null) { 189 super.applyPattern(pattern); 190 toPattern = super.toPattern(); 191 return; 192 } 193 final ArrayList<Format> foundFormats = new ArrayList<>(); 194 final ArrayList<String> foundDescriptions = new ArrayList<>(); 195 final StringBuilder stripCustom = new StringBuilder(pattern.length()); 196 197 final ParsePosition pos = new ParsePosition(0); 198 final char[] c = pattern.toCharArray(); 199 int fmtCount = 0; 200 while (pos.getIndex() < pattern.length()) { 201 switch (c[pos.getIndex()]) { 202 case QUOTE: 203 appendQuotedString(c, pos, stripCustom); 204 break; 205 case START_FE: 206 fmtCount++; 207 seekNonWs(c, pos); 208 final int start = pos.getIndex(); 209 final int index = readArgumentIndex(pattern, c, next(pos)); 210 stripCustom.append(START_FE).append(index); 211 seekNonWs(c, pos); 212 Format format = null; 213 String formatDescription = null; 214 if (c[pos.getIndex()] == START_FMT) { 215 formatDescription = parseFormatDescription(pattern, c, 216 next(pos)); 217 format = getFormat(formatDescription); 218 if (format == null) { 219 stripCustom.append(START_FMT).append(formatDescription); 220 } 221 } 222 foundFormats.add(format); 223 foundDescriptions.add(format == null ? null : formatDescription); 224 Validate.isTrue(foundFormats.size() == fmtCount); 225 Validate.isTrue(foundDescriptions.size() == fmtCount); 226 if (c[pos.getIndex()] != END_FE) { 227 throw new IllegalArgumentException( 228 "Unreadable format element at position " + start); 229 } 230 // falls-through 231 default: 232 stripCustom.append(c[pos.getIndex()]); 233 next(pos); 234 } 235 } 236 super.applyPattern(stripCustom.toString()); 237 toPattern = insertFormats(super.toPattern(), foundDescriptions); 238 if (containsElements(foundFormats)) { 239 final Format[] origFormats = getFormats(); 240 // only loop over what we know we have, as MessageFormat on Java 1.3 241 // seems to provide an extra format element: 242 int i = 0; 243 for (final Format f : foundFormats) { 244 if (f != null) { 245 origFormats[i] = f; 246 } 247 i++; 248 } 249 super.setFormats(origFormats); 250 } 251 } 252 253 /** 254 * Learn whether the specified Collection contains non-null elements. 255 * 256 * @param coll to check 257 * @return {@code true} if some Object was found, {@code false} otherwise. 258 */ 259 private boolean containsElements(final Collection<?> coll) { 260 if (coll == null || coll.isEmpty()) { 261 return false; 262 } 263 return coll.stream().anyMatch(Objects::nonNull); 264 } 265 266 @Override 267 public boolean equals(final Object obj) { 268 if (this == obj) { 269 return true; 270 } 271 if (!super.equals(obj) || !(obj instanceof ExtendedMessageFormat)) { 272 return false; 273 } 274 final ExtendedMessageFormat other = (ExtendedMessageFormat) obj; 275 return Objects.equals(registry, other.registry) && Objects.equals(toPattern, other.toPattern); 276 } 277 278 /** 279 * Gets a custom format from a format description. 280 * 281 * @param desc String 282 * @return Format 283 */ 284 private Format getFormat(final String desc) { 285 if (registry != null) { 286 String name = desc; 287 String args = null; 288 final int i = desc.indexOf(START_FMT); 289 if (i > 0) { 290 name = desc.substring(0, i).trim(); 291 args = desc.substring(i + 1).trim(); 292 } 293 final FormatFactory factory = registry.get(name); 294 if (factory != null) { 295 return factory.getFormat(name, args, getLocale()); 296 } 297 } 298 return null; 299 } 300 301 /** 302 * Gets to the end of the quoted string by advancing the parse position. 303 * 304 * @param pattern pattern to parse, as a char array created once by the caller 305 * @param pos current parse position 306 */ 307 private void getQuotedString(final char[] pattern, final ParsePosition pos) { 308 appendQuotedString(pattern, pos, null); 309 } 310 311 @Override 312 public int hashCode() { 313 final int prime = 31; 314 final int result = super.hashCode(); 315 return prime * result + Objects.hash(registry, toPattern); 316 } 317 318 /** 319 * Insert formats back into the pattern for toPattern() support. 320 * 321 * @param pattern source 322 * @param customPatterns The custom patterns to re-insert, if any 323 * @return full pattern 324 */ 325 private String insertFormats(final String pattern, final ArrayList<String> customPatterns) { 326 if (!containsElements(customPatterns)) { 327 return pattern; 328 } 329 final StringBuilder sb = new StringBuilder(pattern.length() * 2); 330 final ParsePosition pos = new ParsePosition(0); 331 final char[] chars = pattern.toCharArray(); 332 int fe = -1; 333 int depth = 0; 334 while (pos.getIndex() < pattern.length()) { 335 final char c = pattern.charAt(pos.getIndex()); 336 switch (c) { 337 case QUOTE: 338 appendQuotedString(chars, pos, sb); 339 break; 340 case START_FE: 341 depth++; 342 sb.append(START_FE).append(readArgumentIndex(pattern, chars, next(pos))); 343 // do not look for custom patterns when they are embedded, e.g. in a choice 344 if (depth == 1) { 345 fe++; 346 final String customPattern = customPatterns.get(fe); 347 if (customPattern != null) { 348 sb.append(START_FMT).append(customPattern); 349 } 350 } 351 break; 352 case END_FE: 353 depth--; 354 // falls-through 355 default: 356 sb.append(c); 357 next(pos); 358 } 359 } 360 return sb.toString(); 361 } 362 363 /** 364 * Convenience method to advance parse position by 1 365 * 366 * @param pos ParsePosition 367 * @return {@code pos} 368 */ 369 private ParsePosition next(final ParsePosition pos) { 370 pos.setIndex(pos.getIndex() + 1); 371 return pos; 372 } 373 374 /** 375 * Parse the format component of a format element. 376 * 377 * @param pattern string to parse 378 * @param chars the pattern as a char array created once by the caller 379 * @param pos current parse position 380 * @return Format description String 381 */ 382 private String parseFormatDescription(final String pattern, final char[] chars, final ParsePosition pos) { 383 final int start = pos.getIndex(); 384 seekNonWs(chars, pos); 385 final int text = pos.getIndex(); 386 int depth = 1; 387 while (pos.getIndex() < pattern.length()) { 388 switch (pattern.charAt(pos.getIndex())) { 389 case START_FE: 390 depth++; 391 next(pos); 392 break; 393 case END_FE: 394 depth--; 395 if (depth == 0) { 396 return pattern.substring(text, pos.getIndex()); 397 } 398 next(pos); 399 break; 400 case QUOTE: 401 getQuotedString(chars, pos); 402 break; 403 default: 404 next(pos); 405 break; 406 } 407 } 408 throw new IllegalArgumentException( 409 "Unterminated format element at position " + start); 410 } 411 412 /** 413 * Reads the argument index from the current format element 414 * 415 * @param pattern pattern to parse 416 * @param chars the pattern as a char array created once by the caller 417 * @param pos current parse position 418 * @return argument index 419 */ 420 private int readArgumentIndex(final String pattern, final char[] chars, final ParsePosition pos) { 421 final int start = pos.getIndex(); 422 seekNonWs(chars, pos); 423 final StringBuilder result = new StringBuilder(); 424 boolean error = false; 425 for (; !error && pos.getIndex() < pattern.length(); next(pos)) { 426 char c = pattern.charAt(pos.getIndex()); 427 if (Character.isWhitespace(c)) { 428 seekNonWs(chars, pos); 429 if (pos.getIndex() >= pattern.length()) { 430 break; 431 } 432 c = pattern.charAt(pos.getIndex()); 433 if (c != START_FMT && c != END_FE) { 434 error = true; 435 continue; 436 } 437 } 438 if ((c == START_FMT || c == END_FE) && result.length() > 0) { 439 try { 440 return Integer.parseInt(result.toString()); 441 } catch (final NumberFormatException ignored) { 442 // we've already ensured only digits, so unless something 443 // outlandishly large was specified we should be okay. 444 } 445 } 446 error = !Character.isDigit(c); 447 result.append(c); 448 } 449 if (error) { 450 throw new IllegalArgumentException("Invalid format argument index at position " + start + ": " + pattern.substring(start, pos.getIndex())); 451 } 452 throw new IllegalArgumentException("Unterminated format element at position " + start); 453 } 454 455 /** 456 * Consume whitespace from the current parse position. 457 * 458 * @param buffer the pattern to read, as a char array created once by the caller (avoids 459 * copying the entire pattern on every call) 460 * @param pos current position 461 */ 462 private void seekNonWs(final char[] buffer, final ParsePosition pos) { 463 while (pos.getIndex() < buffer.length) { 464 final int len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex()); 465 if (len == 0) { 466 break; 467 } 468 pos.setIndex(pos.getIndex() + len); 469 } 470 } 471 472 /** 473 * Sets no format and always throws {@link UnsupportedOperationException}. See the class Javadoc for details. 474 * 475 * @param formatElementIndex format element index 476 * @param newFormat The new format 477 * @throws UnsupportedOperationException Thrown because this operation is not supported. 478 */ 479 @Override 480 public void setFormat(final int formatElementIndex, final Format newFormat) { 481 throw new UnsupportedOperationException(); 482 } 483 484 /** 485 * Sets no format and always throws {@link UnsupportedOperationException}. See the class Javadoc for details. 486 * 487 * @param argumentIndex argument index 488 * @param newFormat The new format 489 * @throws UnsupportedOperationException Thrown because this operation is not supported. 490 */ 491 @Override 492 public void setFormatByArgumentIndex(final int argumentIndex, final Format newFormat) { 493 throw new UnsupportedOperationException(); 494 } 495 496 /** 497 * Sets no format and always throws {@link UnsupportedOperationException}. See the class Javadoc for details. 498 * 499 * @param newFormats new formats 500 * @throws UnsupportedOperationException Thrown because this operation is not supported. 501 */ 502 @Override 503 public void setFormats(final Format[] newFormats) { 504 throw new UnsupportedOperationException(); 505 } 506 507 /** 508 * Sets no format and always throws {@link UnsupportedOperationException}. See the class Javadoc for details. 509 * 510 * @param newFormats new formats 511 * @throws UnsupportedOperationException Thrown because this operation is not supported. 512 */ 513 @Override 514 public void setFormatsByArgumentIndex(final Format[] newFormats) { 515 throw new UnsupportedOperationException(); 516 } 517 518 /** 519 * {@inheritDoc} 520 */ 521 @Override 522 public String toPattern() { 523 return toPattern; 524 } 525}