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