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