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