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 }