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 }