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