| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.commons.lang.text; |
| |
| import java.text.Format; |
| import java.text.MessageFormat; |
| import java.text.ParsePosition; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Iterator; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| import org.apache.commons.lang.Validate; |
| |
| /** |
| * Extends <code>java.text.MessageFormat</code> to allow pluggable/additional formatting |
| * options for embedded format elements. Client code should specify a registry |
| * of <code>FormatFactory</code> instances associated with <code>String</code> |
| * format names. This registry will be consulted when the format elements are |
| * parsed from the message pattern. In this way custom patterns can be specified, |
| * and the formats supported by <code>java.text.MessageFormat</code> can be overridden |
| * at the format and/or format style level (see MessageFormat). A "format element" |
| * embedded in the message pattern is specified (<b>()?</b> signifies optionality):<br /> |
| * <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> |
| * |
| * <p> |
| * <i>format-name</i> and <i>format-style</i> values are trimmed of surrounding whitespace |
| * in the manner of <code>java.text.MessageFormat</code>. If <i>format-name</i> denotes |
| * <code>FormatFactory formatFactoryInstance</code> in <code>registry</code>, a <code>Format</code> |
| * matching <i>format-name</i> and <i>format-style</i> is requested from |
| * <code>formatFactoryInstance</code>. If this is successful, the <code>Format</code> |
| * found is used for this format element. |
| * </p> |
| * |
| * <p>NOTICE: The various subformat mutator methods are considered unnecessary; they exist on the parent |
| * class to allow the type of customization which it is the job of this class to provide in |
| * a configurable fashion. These methods have thus been disabled and will throw |
| * <code>UnsupportedOperationException</code> if called. |
| * </p> |
| * |
| * <p>Limitations inherited from <code>java.text.MessageFormat</code>: |
| * <ul> |
| * <li>When using "choice" subformats, support for nested formatting instructions is limited |
| * to that provided by the base class.</li> |
| * <li>Thread-safety of <code>Format</code>s, including <code>MessageFormat</code> and thus |
| * <code>ExtendedMessageFormat</code>, is not guaranteed.</li> |
| * </ul> |
| * </p> |
| * |
| * @author Matt Benson |
| * @author Niall Pemberton |
| * @since 2.4 |
| * @version $Id$ |
| */ |
| public class ExtendedMessageFormat extends MessageFormat { |
| private static final long serialVersionUID = -2362048321261811743L; |
| |
| private static final String DUMMY_PATTERN = ""; |
| private static final String ESCAPED_QUOTE = "''"; |
| private static final char START_FMT = ','; |
| private static final char END_FE = '}'; |
| private static final char START_FE = '{'; |
| private static final char QUOTE = '\''; |
| |
| private String toPattern; |
| private Map registry; |
| |
| /** |
| * Create a new ExtendedMessageFormat for the default locale. |
| * |
| * @param pattern String |
| * @throws IllegalArgumentException in case of a bad pattern. |
| */ |
| public ExtendedMessageFormat(String pattern) { |
| this(pattern, Locale.getDefault()); |
| } |
| |
| /** |
| * Create a new ExtendedMessageFormat. |
| * |
| * @param pattern String |
| * @param locale Locale |
| * @throws IllegalArgumentException in case of a bad pattern. |
| */ |
| public ExtendedMessageFormat(String pattern, Locale locale) { |
| this(pattern, locale, null); |
| } |
| |
| /** |
| * Create a new ExtendedMessageFormat for the default locale. |
| * |
| * @param pattern String |
| * @param registry Registry of format factories: Map<String, FormatFactory> |
| * @throws IllegalArgumentException in case of a bad pattern. |
| */ |
| public ExtendedMessageFormat(String pattern, Map registry) { |
| this(pattern, Locale.getDefault(), registry); |
| } |
| |
| /** |
| * Create a new ExtendedMessageFormat. |
| * |
| * @param pattern String |
| * @param locale Locale |
| * @param registry Registry of format factories: Map<String, FormatFactory> |
| * @throws IllegalArgumentException in case of a bad pattern. |
| */ |
| public ExtendedMessageFormat(String pattern, Locale locale, Map registry) { |
| super(DUMMY_PATTERN); |
| setLocale(locale); |
| this.registry = registry; |
| applyPattern(pattern); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public String toPattern() { |
| return toPattern; |
| } |
| |
| /** |
| * Apply the specified pattern. |
| * |
| * @param pattern String |
| */ |
| public final void applyPattern(String pattern) { |
| if (registry == null) { |
| super.applyPattern(pattern); |
| toPattern = super.toPattern(); |
| return; |
| } |
| ArrayList foundFormats = new ArrayList(); |
| ArrayList foundDescriptions = new ArrayList(); |
| StringBuffer stripCustom = new StringBuffer(pattern.length()); |
| |
| ParsePosition pos = new ParsePosition(0); |
| char[] c = pattern.toCharArray(); |
| int fmtCount = 0; |
| while (pos.getIndex() < pattern.length()) { |
| switch (c[pos.getIndex()]) { |
| case QUOTE: |
| appendQuotedString(pattern, pos, stripCustom, true); |
| break; |
| case START_FE: |
| fmtCount++; |
| seekNonWs(pattern, pos); |
| int start = pos.getIndex(); |
| int index = readArgumentIndex(pattern, next(pos)); |
| stripCustom.append(START_FE).append(index); |
| seekNonWs(pattern, pos); |
| Format format = null; |
| String formatDescription = null; |
| if (c[pos.getIndex()] == START_FMT) { |
| formatDescription = parseFormatDescription(pattern, |
| next(pos)); |
| format = getFormat(formatDescription); |
| if (format == null) { |
| stripCustom.append(START_FMT).append(formatDescription); |
| } |
| } |
| foundFormats.add(format); |
| foundDescriptions.add(format == null ? null : formatDescription); |
| Validate.isTrue(foundFormats.size() == fmtCount); |
| Validate.isTrue(foundDescriptions.size() == fmtCount); |
| if (c[pos.getIndex()] != END_FE) { |
| throw new IllegalArgumentException( |
| "Unreadable format element at position " + start); |
| } |
| // fall through |
| default: |
| stripCustom.append(c[pos.getIndex()]); |
| next(pos); |
| } |
| } |
| super.applyPattern(stripCustom.toString()); |
| toPattern = insertFormats(super.toPattern(), foundDescriptions); |
| if (containsElements(foundFormats)) { |
| Format[] origFormats = getFormats(); |
| // only loop over what we know we have, as MessageFormat on Java 1.3 |
| // seems to provide an extra format element: |
| int i = 0; |
| for (Iterator it = foundFormats.iterator(); it.hasNext(); i++) { |
| Format f = (Format) it.next(); |
| if (f != null) { |
| origFormats[i] = f; |
| } |
| } |
| super.setFormats(origFormats); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| * @throws UnsupportedOperationException |
| */ |
| public void setFormat(int formatElementIndex, Format newFormat) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * @throws UnsupportedOperationException |
| */ |
| public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * @throws UnsupportedOperationException |
| */ |
| public void setFormats(Format[] newFormats) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * @throws UnsupportedOperationException |
| */ |
| public void setFormatsByArgumentIndex(Format[] newFormats) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * Get a custom format from a format description. |
| * |
| * @param desc String |
| * @return Format |
| */ |
| private Format getFormat(String desc) { |
| if (registry != null) { |
| String name = desc; |
| String args = null; |
| int i = desc.indexOf(START_FMT); |
| if (i > 0) { |
| name = desc.substring(0, i).trim(); |
| args = desc.substring(i + 1).trim(); |
| } |
| FormatFactory factory = (FormatFactory) registry.get(name); |
| if (factory != null) { |
| return factory.getFormat(name, args, getLocale()); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Read the argument index from the current format element |
| * |
| * @param pattern pattern to parse |
| * @param pos current parse position |
| * @return argument index |
| */ |
| private int readArgumentIndex(String pattern, ParsePosition pos) { |
| int start = pos.getIndex(); |
| seekNonWs(pattern, pos); |
| StringBuffer result = new StringBuffer(); |
| boolean error = false; |
| for (; !error && pos.getIndex() < pattern.length(); next(pos)) { |
| char c = pattern.charAt(pos.getIndex()); |
| if (Character.isWhitespace(c)) { |
| seekNonWs(pattern, pos); |
| c = pattern.charAt(pos.getIndex()); |
| if (c != START_FMT && c != END_FE) { |
| error = true; |
| continue; |
| } |
| } |
| if ((c == START_FMT || c == END_FE) && result.length() > 0) { |
| try { |
| return Integer.parseInt(result.toString()); |
| } catch (NumberFormatException e) { |
| // we've already ensured only digits, so unless something |
| // outlandishly large was specified we should be okay. |
| } |
| } |
| error = !Character.isDigit(c); |
| result.append(c); |
| } |
| if (error) { |
| throw new IllegalArgumentException( |
| "Invalid format argument index at position " + start + ": " |
| + pattern.substring(start, pos.getIndex())); |
| } |
| throw new IllegalArgumentException( |
| "Unterminated format element at position " + start); |
| } |
| |
| /** |
| * Parse the format component of a format element. |
| * |
| * @param pattern string to parse |
| * @param pos current parse position |
| * @return Format description String |
| */ |
| private String parseFormatDescription(String pattern, ParsePosition pos) { |
| int start = pos.getIndex(); |
| seekNonWs(pattern, pos); |
| int text = pos.getIndex(); |
| int depth = 1; |
| for (; pos.getIndex() < pattern.length(); next(pos)) { |
| switch (pattern.charAt(pos.getIndex())) { |
| case START_FE: |
| depth++; |
| break; |
| case END_FE: |
| depth--; |
| if (depth == 0) { |
| return pattern.substring(text, pos.getIndex()); |
| } |
| break; |
| case QUOTE: |
| getQuotedString(pattern, pos, false); |
| break; |
| } |
| } |
| throw new IllegalArgumentException( |
| "Unterminated format element at position " + start); |
| } |
| |
| /** |
| * Insert formats back into the pattern for toPattern() support. |
| * |
| * @param pattern source |
| * @param customPatterns The custom patterns to re-insert, if any |
| * @return full pattern |
| */ |
| private String insertFormats(String pattern, ArrayList customPatterns) { |
| if (!containsElements(customPatterns)) { |
| return pattern; |
| } |
| StringBuffer sb = new StringBuffer(pattern.length() * 2); |
| ParsePosition pos = new ParsePosition(0); |
| int fe = -1; |
| int depth = 0; |
| while (pos.getIndex() < pattern.length()) { |
| char c = pattern.charAt(pos.getIndex()); |
| switch (c) { |
| case QUOTE: |
| appendQuotedString(pattern, pos, sb, false); |
| break; |
| case START_FE: |
| depth++; |
| if (depth == 1) { |
| fe++; |
| sb.append(START_FE).append( |
| readArgumentIndex(pattern, next(pos))); |
| String customPattern = (String) customPatterns.get(fe); |
| if (customPattern != null) { |
| sb.append(START_FMT).append(customPattern); |
| } |
| } |
| break; |
| case END_FE: |
| depth--; |
| //fall through: |
| default: |
| sb.append(c); |
| next(pos); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| /** |
| * Consume whitespace from the current parse position. |
| * |
| * @param pattern String to read |
| * @param pos current position |
| */ |
| private void seekNonWs(String pattern, ParsePosition pos) { |
| int len = 0; |
| char[] buffer = pattern.toCharArray(); |
| do { |
| len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex()); |
| pos.setIndex(pos.getIndex() + len); |
| } while (len > 0 && pos.getIndex() < pattern.length()); |
| } |
| |
| /** |
| * Convenience method to advance parse position by 1 |
| * |
| * @param pos ParsePosition |
| * @return <code>pos</code> |
| */ |
| private ParsePosition next(ParsePosition pos) { |
| pos.setIndex(pos.getIndex() + 1); |
| return pos; |
| } |
| |
| /** |
| * Consume a quoted string, adding it to <code>appendTo</code> if |
| * specified. |
| * |
| * @param pattern pattern to parse |
| * @param pos current parse position |
| * @param appendTo optional StringBuffer to append |
| * @param escapingOn whether to process escaped quotes |
| * @return <code>appendTo</code> |
| */ |
| private StringBuffer appendQuotedString(String pattern, ParsePosition pos, |
| StringBuffer appendTo, boolean escapingOn) { |
| int start = pos.getIndex(); |
| char[] c = pattern.toCharArray(); |
| if (escapingOn && c[start] == QUOTE) { |
| return appendTo == null ? null : appendTo.append(QUOTE); |
| } |
| int lastHold = start; |
| for (int i = pos.getIndex(); i < pattern.length(); i++) { |
| if (escapingOn && pattern.substring(i).startsWith(ESCAPED_QUOTE)) { |
| appendTo.append(c, lastHold, pos.getIndex() - lastHold).append( |
| QUOTE); |
| pos.setIndex(i + ESCAPED_QUOTE.length()); |
| lastHold = pos.getIndex(); |
| continue; |
| } |
| switch (c[pos.getIndex()]) { |
| case QUOTE: |
| next(pos); |
| return appendTo == null ? null : appendTo.append(c, lastHold, |
| pos.getIndex() - lastHold); |
| default: |
| next(pos); |
| } |
| } |
| throw new IllegalArgumentException( |
| "Unterminated quoted string at position " + start); |
| } |
| |
| /** |
| * Consume quoted string only |
| * |
| * @param pattern pattern to parse |
| * @param pos current parse position |
| * @param escapingOn whether to process escaped quotes |
| */ |
| private void getQuotedString(String pattern, ParsePosition pos, |
| boolean escapingOn) { |
| appendQuotedString(pattern, pos, null, escapingOn); |
| } |
| |
| /** |
| * Learn whether the specified Collection contains non-null elements. |
| * @param coll to check |
| * @return <code>true</code> if some Object was found, <code>false</code> otherwise. |
| */ |
| private boolean containsElements(Collection coll) { |
| if (coll == null || coll.size() == 0) { |
| return false; |
| } |
| for (Iterator iter = coll.iterator(); iter.hasNext();) { |
| if (iter.next() != null) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |