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