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