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><strong>NOTICE:</strong> The various subformat mutator methods are considered unnecessary; they exist on the parent 059 * class to allow the type of customization which it is the job of this class to provide in 060 * a configurable fashion. These methods have thus been disabled and will throw 061 * {@link UnsupportedOperationException} if called. 062 * </p> 063 * 064 * <p>Limitations inherited from {@link java.text.MessageFormat}:</p> 065 * <ul> 066 * <li>When using "choice" subformats, support for nested formatting instructions is limited 067 * to that provided by the base class.</li> 068 * <li>Thread-safety of {@link Format}s, including {@link MessageFormat} and thus 069 * {@link ExtendedMessageFormat}, is not guaranteed.</li> 070 * </ul> 071 * 072 * @since 2.4 073 * @deprecated As of <a href="https://commons.apache.org/proper/commons-lang/changes-report.html#a3.6">3.6</a>, use Apache Commons Text 074 * <a href="https://commons.apache.org/proper/commons-text/javadocs/api-release/org/apache/commons/text/ExtendedMessageFormat.html"> 075 * ExtendedMessageFormat</a>. 076 */ 077@Deprecated 078public class ExtendedMessageFormat extends MessageFormat { 079 080 private static final long serialVersionUID = -2362048321261811743L; 081 private static final String EMPTY_PATTERN = StringUtils.EMPTY; 082 private static final char START_FMT = ','; 083 private static final char END_FE = '}'; 084 private static final char START_FE = '{'; 085 private static final char QUOTE = '\''; 086 087 /** 088 * To pattern string. 089 */ 090 private String toPattern; 091 092 /** 093 * Our registry of FormatFactory. 094 */ 095 private final Map<String, ? extends FormatFactory> registry; 096 097 /** 098 * Create a new ExtendedMessageFormat for the default locale. 099 * 100 * @param pattern the pattern to use, not null 101 * @throws IllegalArgumentException in case of a bad pattern. 102 */ 103 public ExtendedMessageFormat(final String pattern) { 104 this(pattern, Locale.getDefault()); 105 } 106 107 /** 108 * Create a new ExtendedMessageFormat. 109 * 110 * @param pattern the pattern to use, not null 111 * @param locale the locale to use, not null 112 * @throws IllegalArgumentException in case of a bad pattern. 113 */ 114 public ExtendedMessageFormat(final String pattern, final Locale locale) { 115 this(pattern, locale, null); 116 } 117 118 /** 119 * Create a new ExtendedMessageFormat. 120 * 121 * @param pattern the pattern to use, not null. 122 * @param locale the locale to use. 123 * @param registry the registry of format factories, may be null. 124 * @throws IllegalArgumentException in case of a bad pattern. 125 */ 126 public ExtendedMessageFormat(final String pattern, final Locale locale, final Map<String, ? extends FormatFactory> registry) { 127 super(EMPTY_PATTERN); 128 setLocale(LocaleUtils.toLocale(locale)); 129 this.registry = registry; 130 applyPattern(pattern); 131 } 132 133 /** 134 * Create a new ExtendedMessageFormat for the default locale. 135 * 136 * @param pattern the pattern to use, not null 137 * @param registry the registry of format factories, may be null 138 * @throws IllegalArgumentException in case of a bad pattern. 139 */ 140 public ExtendedMessageFormat(final String pattern, final Map<String, ? extends FormatFactory> registry) { 141 this(pattern, Locale.getDefault(), registry); 142 } 143 144 /** 145 * Consume a quoted string, adding it to {@code appendTo} if 146 * specified. 147 * 148 * @param pattern pattern to parse 149 * @param pos current parse position 150 * @param appendTo optional StringBuilder to append 151 * @return {@code appendTo} 152 */ 153 private StringBuilder appendQuotedString(final String pattern, final ParsePosition pos, 154 final StringBuilder appendTo) { 155 assert pattern.toCharArray()[pos.getIndex()] == QUOTE : 156 "Quoted string must start with quote character"; 157 158 // handle quote character at the beginning of the string 159 if (appendTo != null) { 160 appendTo.append(QUOTE); 161 } 162 next(pos); 163 164 final int start = pos.getIndex(); 165 final char[] c = pattern.toCharArray(); 166 for (int i = pos.getIndex(); i < pattern.length(); i++) { 167 if (c[pos.getIndex()] == QUOTE) { 168 next(pos); 169 return appendTo == null ? null : appendTo.append(c, start, 170 pos.getIndex() - start); 171 } 172 next(pos); 173 } 174 throw new IllegalArgumentException( 175 "Unterminated quoted string at position " + start); 176 } 177 178 /** 179 * Apply the specified pattern. 180 * 181 * @param pattern String 182 */ 183 @Override 184 public final void applyPattern(final String pattern) { 185 if (registry == null) { 186 super.applyPattern(pattern); 187 toPattern = super.toPattern(); 188 return; 189 } 190 final ArrayList<Format> foundFormats = new ArrayList<>(); 191 final ArrayList<String> foundDescriptions = new ArrayList<>(); 192 final StringBuilder stripCustom = new StringBuilder(pattern.length()); 193 194 final ParsePosition pos = new ParsePosition(0); 195 final char[] c = pattern.toCharArray(); 196 int fmtCount = 0; 197 while (pos.getIndex() < pattern.length()) { 198 switch (c[pos.getIndex()]) { 199 case QUOTE: 200 appendQuotedString(pattern, pos, stripCustom); 201 break; 202 case START_FE: 203 fmtCount++; 204 seekNonWs(pattern, pos); 205 final int start = pos.getIndex(); 206 final int index = readArgumentIndex(pattern, next(pos)); 207 stripCustom.append(START_FE).append(index); 208 seekNonWs(pattern, pos); 209 Format format = null; 210 String formatDescription = null; 211 if (c[pos.getIndex()] == START_FMT) { 212 formatDescription = parseFormatDescription(pattern, 213 next(pos)); 214 format = getFormat(formatDescription); 215 if (format == null) { 216 stripCustom.append(START_FMT).append(formatDescription); 217 } 218 } 219 foundFormats.add(format); 220 foundDescriptions.add(format == null ? null : formatDescription); 221 Validate.isTrue(foundFormats.size() == fmtCount); 222 Validate.isTrue(foundDescriptions.size() == fmtCount); 223 if (c[pos.getIndex()] != END_FE) { 224 throw new IllegalArgumentException( 225 "Unreadable format element at position " + start); 226 } 227 // falls-through 228 default: 229 stripCustom.append(c[pos.getIndex()]); 230 next(pos); 231 } 232 } 233 super.applyPattern(stripCustom.toString()); 234 toPattern = insertFormats(super.toPattern(), foundDescriptions); 235 if (containsElements(foundFormats)) { 236 final Format[] origFormats = getFormats(); 237 // only loop over what we know we have, as MessageFormat on Java 1.3 238 // seems to provide an extra format element: 239 int i = 0; 240 for (final Format f : foundFormats) { 241 if (f != null) { 242 origFormats[i] = f; 243 } 244 i++; 245 } 246 super.setFormats(origFormats); 247 } 248 } 249 250 /** 251 * Learn whether the specified Collection contains non-null elements. 252 * 253 * @param coll to check 254 * @return {@code true} if some Object was found, {@code false} otherwise. 255 */ 256 private boolean containsElements(final Collection<?> coll) { 257 if (coll == null || coll.isEmpty()) { 258 return false; 259 } 260 return coll.stream().anyMatch(Objects::nonNull); 261 } 262 263 @Override 264 public boolean equals(final Object obj) { 265 if (this == obj) { 266 return true; 267 } 268 if (!super.equals(obj)) { 269 return false; 270 } 271 if (!(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 * Consume quoted string only 303 * 304 * @param pattern pattern to parse 305 * @param pos current parse position 306 */ 307 private void getQuotedString(final String 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 int fe = -1; 332 int depth = 0; 333 while (pos.getIndex() < pattern.length()) { 334 final char c = pattern.charAt(pos.getIndex()); 335 switch (c) { 336 case QUOTE: 337 appendQuotedString(pattern, pos, sb); 338 break; 339 case START_FE: 340 depth++; 341 sb.append(START_FE).append(readArgumentIndex(pattern, next(pos))); 342 // do not look for custom patterns when they are embedded, e.g. in a choice 343 if (depth == 1) { 344 fe++; 345 final String customPattern = customPatterns.get(fe); 346 if (customPattern != null) { 347 sb.append(START_FMT).append(customPattern); 348 } 349 } 350 break; 351 case END_FE: 352 depth--; 353 // falls-through 354 default: 355 sb.append(c); 356 next(pos); 357 } 358 } 359 return sb.toString(); 360 } 361 362 /** 363 * Convenience method to advance parse position by 1 364 * 365 * @param pos ParsePosition 366 * @return {@code pos} 367 */ 368 private ParsePosition next(final ParsePosition pos) { 369 pos.setIndex(pos.getIndex() + 1); 370 return pos; 371 } 372 373 /** 374 * Parse the format component of a format element. 375 * 376 * @param pattern string to parse 377 * @param pos current parse position 378 * @return Format description String 379 */ 380 private String parseFormatDescription(final String pattern, final ParsePosition pos) { 381 final int start = pos.getIndex(); 382 seekNonWs(pattern, pos); 383 final int text = pos.getIndex(); 384 int depth = 1; 385 for (; pos.getIndex() < pattern.length(); next(pos)) { 386 switch (pattern.charAt(pos.getIndex())) { 387 case START_FE: 388 depth++; 389 break; 390 case END_FE: 391 depth--; 392 if (depth == 0) { 393 return pattern.substring(text, pos.getIndex()); 394 } 395 break; 396 case QUOTE: 397 getQuotedString(pattern, pos); 398 break; 399 default: 400 break; 401 } 402 } 403 throw new IllegalArgumentException( 404 "Unterminated format element at position " + start); 405 } 406 407 /** 408 * Reads the argument index from the current format element 409 * 410 * @param pattern pattern to parse 411 * @param pos current parse position 412 * @return argument index 413 */ 414 private int readArgumentIndex(final String pattern, final ParsePosition pos) { 415 final int start = pos.getIndex(); 416 seekNonWs(pattern, pos); 417 final StringBuilder result = new StringBuilder(); 418 boolean error = false; 419 for (; !error && pos.getIndex() < pattern.length(); next(pos)) { 420 char c = pattern.charAt(pos.getIndex()); 421 if (Character.isWhitespace(c)) { 422 seekNonWs(pattern, pos); 423 c = pattern.charAt(pos.getIndex()); 424 if (c != START_FMT && c != END_FE) { 425 error = true; 426 continue; 427 } 428 } 429 if ((c == START_FMT || c == END_FE) && result.length() > 0) { 430 try { 431 return Integer.parseInt(result.toString()); 432 } catch (final NumberFormatException ignored) { 433 // we've already ensured only digits, so unless something 434 // outlandishly large was specified we should be okay. 435 } 436 } 437 error = !Character.isDigit(c); 438 result.append(c); 439 } 440 if (error) { 441 throw new IllegalArgumentException( 442 "Invalid format argument index at position " + start + ": " 443 + pattern.substring(start, pos.getIndex())); 444 } 445 throw new IllegalArgumentException( 446 "Unterminated format element at position " + start); 447 } 448 449 /** 450 * Consume whitespace from the current parse position. 451 * 452 * @param pattern String to read 453 * @param pos current position 454 */ 455 private void seekNonWs(final String pattern, final ParsePosition pos) { 456 int len; 457 final char[] buffer = pattern.toCharArray(); 458 do { 459 len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex()); 460 pos.setIndex(pos.getIndex() + len); 461 } while (len > 0 && pos.getIndex() < pattern.length()); 462 } 463 464 /** 465 * Throws UnsupportedOperationException - see class Javadoc for details. 466 * 467 * @param formatElementIndex format element index 468 * @param newFormat the new format 469 * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat 470 */ 471 @Override 472 public void setFormat(final int formatElementIndex, final Format newFormat) { 473 throw new UnsupportedOperationException(); 474 } 475 476 /** 477 * Throws UnsupportedOperationException - see class Javadoc for details. 478 * 479 * @param argumentIndex argument index 480 * @param newFormat the new format 481 * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat 482 */ 483 @Override 484 public void setFormatByArgumentIndex(final int argumentIndex, final Format newFormat) { 485 throw new UnsupportedOperationException(); 486 } 487 488 /** 489 * Throws UnsupportedOperationException - see class Javadoc for details. 490 * 491 * @param newFormats new formats 492 * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat 493 */ 494 @Override 495 public void setFormats(final Format[] newFormats) { 496 throw new UnsupportedOperationException(); 497 } 498 499 /** 500 * Throws UnsupportedOperationException - see class Javadoc for details. 501 * 502 * @param newFormats new formats 503 * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat 504 */ 505 @Override 506 public void setFormatsByArgumentIndex(final Format[] newFormats) { 507 throw new UnsupportedOperationException(); 508 } 509 510 /** 511 * {@inheritDoc} 512 */ 513 @Override 514 public String toPattern() { 515 return toPattern; 516 } 517}