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