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