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