diff --git a/src/java/org/apache/commons/lang/text/ChoiceMetaFormat.java b/src/java/org/apache/commons/lang/text/ChoiceMetaFormat.java
new file mode 100644
index 000000000..b38e2f2f6
--- /dev/null
+++ b/src/java/org/apache/commons/lang/text/ChoiceMetaFormat.java
@@ -0,0 +1,72 @@
+/*
+ * 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.ChoiceFormat;
+import java.text.FieldPosition;
+import java.text.ParsePosition;
+
+/**
+ * Stock "choice" MetaFormat.
+ *
+ * @see {@link ExtendedMessageFormat}
+ * @author Matt Benson
+ * @since 2.4
+ * @version $Id$
+ */
+public class ChoiceMetaFormat extends MetaFormatSupport {
+ private static final long serialVersionUID = 3802197832963795129L;
+
+ /**
+ * Singleton-usable instance.
+ */
+ public static final ChoiceMetaFormat INSTANCE = new ChoiceMetaFormat();
+
+ /**
+ * Create a new ChoiceMetaFormat.
+ */
+ public ChoiceMetaFormat() {
+ super();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.text.Format#format(java.lang.Object, java.lang.StringBuffer,
+ * java.text.FieldPosition)
+ */
+ public StringBuffer format(Object obj, StringBuffer toAppendTo,
+ FieldPosition pos) {
+ if (obj instanceof ChoiceFormat) {
+ return toAppendTo.append(((ChoiceFormat) obj).toPattern());
+ }
+ throw new IllegalArgumentException(String.valueOf(obj));
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.text.Format#parseObject(java.lang.String,
+ * java.text.ParsePosition)
+ */
+ public Object parseObject(String source, ParsePosition pos) {
+ int start = pos.getIndex();
+ seekFormatElementEnd(source, pos);
+ return new ChoiceFormat(source.substring(start, pos.getIndex()));
+ }
+
+}
diff --git a/src/java/org/apache/commons/lang/text/DateMetaFormat.java b/src/java/org/apache/commons/lang/text/DateMetaFormat.java
new file mode 100644
index 000000000..64163bae5
--- /dev/null
+++ b/src/java/org/apache/commons/lang/text/DateMetaFormat.java
@@ -0,0 +1,57 @@
+/*
+ * 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.DateFormat;
+import java.util.Locale;
+
+/**
+ * Stock "date" MetaFormat.
+ *
+ * @see {@link ExtendedMessageFormat}
+ * @author Matt Benson
+ * @since 2.4
+ * @version $Id$
+ */
+public class DateMetaFormat extends DateMetaFormatSupport {
+ private static final long serialVersionUID = -4732179430347600208L;
+
+ /**
+ * Create a new DateMetaFormat.
+ */
+ public DateMetaFormat() {
+ super();
+ }
+
+ /**
+ * Create a new DateMetaFormat.
+ *
+ * @param locale
+ */
+ public DateMetaFormat(Locale locale) {
+ super(locale);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.commons.lang.text.AbstractDateMetaFormat#createSubformatInstance(int)
+ */
+ protected DateFormat createSubformatInstance(int style) {
+ return DateFormat.getDateInstance(style, getLocale());
+ }
+}
diff --git a/src/java/org/apache/commons/lang/text/DateMetaFormatSupport.java b/src/java/org/apache/commons/lang/text/DateMetaFormatSupport.java
new file mode 100644
index 000000000..866c809b2
--- /dev/null
+++ b/src/java/org/apache/commons/lang/text/DateMetaFormatSupport.java
@@ -0,0 +1,231 @@
+/*
+ * 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.DateFormat;
+import java.text.DateFormatSymbols;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * date/time metaFormat support.
+ * @see {@link ExtendedMessageFormat}
+ * @author Matt Benson
+ * @since 2.4
+ * @version $Id$
+ */
+public abstract class DateMetaFormatSupport extends MetaFormatSupport {
+ /** "Default" subformat name */
+ protected static final String DEFAULT = "";
+
+ /** "Short" subformat name */
+ protected static final String SHORT = "short";
+
+ /** "Medium" subformat name */
+ protected static final String MEDIUM = "medium";
+
+ /** "Long" subformat name */
+ protected static final String LONG = "long";
+
+ /** "Full" subformat name */
+ protected static final String FULL = "full";
+
+ private Locale locale;
+ private boolean handlePatterns = true;
+
+ private transient boolean initialized;
+ private transient Map styleMap;
+ private transient Map inverseStyleMap;
+ private transient Map subformats;
+ private transient Map reverseSubformats;
+ private transient DateFormatSymbols dateFormatSymbols;
+
+ /**
+ * Create a new AbstractDateMetaFormat.
+ */
+ public DateMetaFormatSupport() {
+ this(Locale.getDefault());
+ }
+
+ /**
+ * Create a new AbstractDateMetaFormat.
+ *
+ * @param locale
+ */
+ public DateMetaFormatSupport(Locale locale) {
+ super();
+ this.locale = locale;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.text.Format#format(java.lang.Object, java.lang.StringBuffer,
+ * java.text.FieldPosition)
+ */
+ public StringBuffer format(Object obj, StringBuffer toAppendTo,
+ FieldPosition pos) {
+ String subformat = getSubformatName(obj);
+ if (subformat != null) {
+ return toAppendTo.append(subformat);
+ }
+ if (isHandlePatterns() && obj instanceof SimpleDateFormat) {
+ SimpleDateFormat sdf = (SimpleDateFormat) obj;
+ if (sdf.getDateFormatSymbols().equals(dateFormatSymbols)) {
+ return toAppendTo.append(sdf.toPattern());
+ }
+ }
+ throw new IllegalArgumentException(String.valueOf(obj));
+ }
+
+ private String getSubformatName(Object subformat) {
+ initialize();
+ if (reverseSubformats.containsKey(subformat)) {
+ return (String) inverseStyleMap.get(reverseSubformats
+ .get(subformat));
+ }
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.text.Format#parseObject(java.lang.String,
+ * java.text.ParsePosition)
+ */
+ public Object parseObject(String source, ParsePosition pos) {
+ int start = pos.getIndex();
+ seekFormatElementEnd(source, pos);
+ if (pos.getErrorIndex() >= 0) {
+ return null;
+ }
+ String subformat = source.substring(start, pos.getIndex()).trim();
+ Object result = getSubformat(subformat);
+ if (result != null) {
+ return result;
+ }
+ if (isHandlePatterns()) {
+ return new SimpleDateFormat(subformat, getLocale());
+ }
+ pos.setErrorIndex(start);
+ return null;
+ }
+
+ private Format getSubformat(String subformat) {
+ initialize();
+ if (!styleMap.containsKey(subformat)) {
+ return null;
+ }
+ initialize();
+ return (Format) subformats.get(styleMap.get(subformat));
+ }
+
+ /**
+ * Get the locale in use by this {@link DateMetaFormatSupport}.
+ *
+ * @return Locale
+ */
+ public Locale getLocale() {
+ return locale;
+ }
+
+ private synchronized void initialize() {
+ if (!initialized) {
+ styleMap = createStyleMap();
+ inverseStyleMap = createInverseStyleMap();
+ subformats = new HashMap();
+ reverseSubformats = new HashMap();
+ for (Iterator iter = styleMap.values().iterator(); iter.hasNext();) {
+ Integer style = (Integer) iter.next();
+ if (subformats.containsKey(style)) {
+ continue;
+ }
+ Format sf = createSubformatInstance(style.intValue());
+ subformats.put(style, sf);
+ if (inverseStyleMap.containsKey(style)) {
+ reverseSubformats.put(sf, style);
+ }
+ }
+ dateFormatSymbols = new DateFormatSymbols(getLocale());
+ }
+ initialized = true;
+ }
+
+ /**
+ * Create a subformat for the given DateFormat
style
+ * constant.
+ *
+ * @param style
+ * @return a DateFormat instance.
+ */
+ protected abstract DateFormat createSubformatInstance(int style);
+
+ /**
+ * Get whether this metaformat can parse date/time pattern formats in
+ * addition to named formats.
+ *
+ * @return boolean.
+ */
+ public boolean isHandlePatterns() {
+ return handlePatterns;
+ }
+
+ /**
+ * Set whether this metaformat can parse date/time pattern formats in
+ * addition to named formats.
+ *
+ * @param handlePatterns
+ * the boolean handlePatterns to set.
+ * @return this
for fluent usage.
+ */
+ public DateMetaFormatSupport setHandlePatterns(boolean handlePatterns) {
+ this.handlePatterns = handlePatterns;
+ return this;
+ }
+
+ /**
+ * Create the style map.
+ *
+ * @return Map
+ */
+ protected Map createStyleMap() {
+ HashMap result = new HashMap();
+ result.put(SHORT, new Integer(DateFormat.SHORT));
+ result.put(MEDIUM, new Integer(DateFormat.MEDIUM));
+ result.put(LONG, new Integer(DateFormat.LONG));
+ result.put(FULL, new Integer(DateFormat.FULL));
+ result.put(DEFAULT, new Integer(DateFormat.DEFAULT));
+ return result;
+ }
+
+ /**
+ * Create the inverse style map.
+ *
+ * @return Map
+ */
+ protected Map createInverseStyleMap() {
+ Map invertMe = createStyleMap();
+ invertMe.remove(DEFAULT);
+ return invert(invertMe);
+ }
+}
diff --git a/src/java/org/apache/commons/lang/text/DefaultMetaFormatFactory.java b/src/java/org/apache/commons/lang/text/DefaultMetaFormatFactory.java
new file mode 100644
index 000000000..2255773f6
--- /dev/null
+++ b/src/java/org/apache/commons/lang/text/DefaultMetaFormatFactory.java
@@ -0,0 +1,114 @@
+/*
+ * 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.ParsePosition;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.Validate;
+
+/**
+ * Factory methods to produce metaformat instances that behave like
+ * java.text.MessageFormat.
+ *
+ * @author Matt Benson
+ * @since 2.4
+ * @version $Id$
+ */
+/* package-private */ class DefaultMetaFormatFactory {
+
+ /** Number key */
+ public static final String NUMBER_KEY = "number";
+
+ /** Date key */
+ public static final String DATE_KEY = "date";
+
+ /** Time key */
+ public static final String TIME_KEY = "time";
+
+ /** Choice key */
+ public static final String CHOICE_KEY = "choice";
+
+ private static final String[] NO_SUBFORMAT_KEYS = new String[] {
+ NUMBER_KEY, DATE_KEY, TIME_KEY };
+
+ private static final String[] NO_PATTERN_KEYS = new String[] { NUMBER_KEY,
+ DATE_KEY, TIME_KEY, CHOICE_KEY };
+
+ private static final String[] PATTERN_KEYS = new String[] { DATE_KEY,
+ TIME_KEY };
+
+ private static class OrderedNameKeyedMetaFormat extends NameKeyedMetaFormat {
+ private static final long serialVersionUID = -7688772075239431055L;
+
+ private List keys;
+
+ private OrderedNameKeyedMetaFormat(String[] names, Format[] formats) {
+ super(createMap(names, formats));
+ this.keys = Arrays.asList(names);
+ }
+
+ private static Map createMap(String[] names, Format[] formats) {
+ Validate.isTrue(ArrayUtils.isSameLength(names, formats));
+ HashMap result = new HashMap(names.length);
+ for (int i = 0; i < names.length; i++) {
+ result.put(names[i], formats[i]);
+ }
+ return result;
+ }
+
+ protected Iterator iterateKeys() {
+ return keys.iterator();
+ }
+ }
+
+ /**
+ * Get a default metaformat for the specified Locale.
+ *
+ * @param locale
+ * the Locale for the resulting Format instance.
+ * @return Format
+ */
+ public static Format getFormat(final Locale locale) {
+ Format nmf = new NumberMetaFormat(locale);
+ Format dmf = new DateMetaFormat(locale).setHandlePatterns(false);
+ Format tmf = new TimeMetaFormat(locale).setHandlePatterns(false);
+
+ return new MultiFormat(new Format[] {
+ new OrderedNameKeyedMetaFormat(NO_SUBFORMAT_KEYS, new Format[] {
+ getDefaultFormat(nmf), getDefaultFormat(dmf),
+ getDefaultFormat(tmf) }),
+ new OrderedNameKeyedMetaFormat(NO_PATTERN_KEYS, new Format[] {
+ nmf, dmf, tmf, ChoiceMetaFormat.INSTANCE }),
+ new OrderedNameKeyedMetaFormat(PATTERN_KEYS,
+ new Format[] { new DateMetaFormat(locale),
+ new TimeMetaFormat(locale) }) });
+ }
+
+ private static Format getDefaultFormat(Format metaformat) {
+ ParsePosition pos = new ParsePosition(0);
+ Object o = metaformat.parseObject("", pos);
+ return pos.getErrorIndex() < 0 ? (Format) o : null;
+ }
+}
diff --git a/src/java/org/apache/commons/lang/text/ExtendedMessageFormat.java b/src/java/org/apache/commons/lang/text/ExtendedMessageFormat.java
new file mode 100644
index 000000000..89667a3fe
--- /dev/null
+++ b/src/java/org/apache/commons/lang/text/ExtendedMessageFormat.java
@@ -0,0 +1,344 @@
+/*
+ * 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.Locale;
+
+import org.apache.commons.lang.Validate;
+
+/**
+ * Extends MessageFormat
to allow pluggable/additional formatting
+ * options for embedded format elements; requires a "meta-format", i.e. a
+ * Format
capable of parsing and formatting other
+ * Format
s.
+ *
+ * @author Matt Benson
+ * @since 2.4
+ * @version $Id$
+ */
+public class ExtendedMessageFormat extends MessageFormat {
+ private static final long serialVersionUID = -2362048321261811743L;
+
+ /**
+ * Get a default meta-format for the default Locale. This will produce
+ * behavior identical to a java.lang.MessageFormat
using the
+ * default locale.
+ *
+ * @return Format
+ */
+ public static Format createDefaultMetaFormat() {
+ return createDefaultMetaFormat(Locale.getDefault());
+ }
+
+ /**
+ * Get a default meta-format for the specified Locale. This will produce
+ * behavior identical to a java.lang.MessageFormat
using
+ * locale
.
+ *
+ * @param locale
+ * the Locale for the resulting Format instance.
+ * @return Format
+ */
+ public static Format createDefaultMetaFormat(Locale locale) {
+ return DefaultMetaFormatFactory.getFormat(locale);
+ }
+
+ private static class Parser {
+ 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 stripFormats(String pattern) {
+ StringBuffer sb = new StringBuffer(pattern.length());
+ ParsePosition pos = new ParsePosition(0);
+ while (pos.getIndex() < pattern.length()) {
+ switch (pattern.charAt(pos.getIndex())) {
+ case QUOTE:
+ appendQuotedString(pattern, pos, sb, true);
+ break;
+ case START_FE:
+ int start = pos.getIndex();
+ readArgumentIndex(pattern, next(pos));
+ sb.append(pattern, start, pos.getIndex());
+ if (pattern.charAt(pos.getIndex()) == START_FMT) {
+ eatFormat(pattern, next(pos));
+ }
+ if (pattern.charAt(pos.getIndex()) != END_FE) {
+ throw new IllegalArgumentException(
+ "Unreadable format element at position "
+ + start);
+ }
+ // fall through
+ default:
+ sb.append(pattern.charAt(pos.getIndex()));
+ next(pos);
+ }
+ }
+ return sb.toString();
+ }
+
+ private String insertFormats(String pattern, Format[] formats,
+ Format metaFormat) {
+ if (formats == null || formats.length == 0) {
+ return pattern;
+ }
+ StringBuffer sb = new StringBuffer(pattern.length() * 2);
+ ParsePosition pos = new ParsePosition(0);
+ int fe = -1;
+ while (pos.getIndex() < pattern.length()) {
+ char c = pattern.charAt(pos.getIndex());
+ switch (c) {
+ case QUOTE:
+ appendQuotedString(pattern, pos, sb, false);
+ break;
+ case START_FE:
+ fe++;
+ sb.append(START_FE).append(
+ readArgumentIndex(pattern, next(pos)));
+ if (formats[fe] != null) {
+ sb.append(START_FMT).append(
+ metaFormat.format(formats[fe]));
+ }
+ break;
+ default:
+ sb.append(pattern.charAt(pos.getIndex()));
+ next(pos);
+ }
+ }
+ return sb.toString();
+ }
+
+ private Format[] parseFormats(String pattern, Format metaFormat) {
+ ArrayList result = new ArrayList();
+ ParsePosition pos = new ParsePosition(0);
+ while (pos.getIndex() < pattern.length()) {
+ switch (pattern.charAt(pos.getIndex())) {
+ case QUOTE:
+ getQuotedString(pattern, next(pos), true);
+ break;
+ case START_FE:
+ int start = pos.getIndex();
+ readArgumentIndex(pattern, next(pos));
+ if (pattern.charAt(pos.getIndex()) == START_FMT) {
+ seekNonWs(pattern, next(pos));
+ result.add(metaFormat.parseObject(pattern, pos));
+ }
+ seekNonWs(pattern, pos);
+ if (pattern.charAt(pos.getIndex()) != END_FE) {
+ throw new IllegalArgumentException(
+ "Unreadable format element at position "
+ + start);
+ }
+ // fall through
+ default:
+ next(pos);
+ }
+ }
+ return (Format[]) result.toArray(new Format[result.size()]);
+ }
+
+ 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());
+ }
+
+ private ParsePosition next(ParsePosition pos) {
+ pos.setIndex(pos.getIndex() + 1);
+ return pos;
+ }
+
+ private String readArgumentIndex(String pattern, ParsePosition pos) {
+ int start = pos.getIndex();
+ for (; pos.getIndex() < pattern.length(); next(pos)) {
+ char c = pattern.charAt(pos.getIndex());
+ if (c == START_FMT || c == END_FE) {
+ return pattern.substring(start, pos.getIndex());
+ }
+ if (!Character.isDigit(c)) {
+ throw new IllegalArgumentException(
+ "Invalid format argument index at position "
+ + start);
+ }
+ }
+ throw new IllegalArgumentException(
+ "Unterminated format element at position " + start);
+ }
+
+ private StringBuffer appendQuotedString(String pattern,
+ ParsePosition pos, StringBuffer appendTo, boolean escapingOn) {
+ int start = pos.getIndex();
+ if (escapingOn && pattern.charAt(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(pattern, lastHold, pos.getIndex()).append(
+ QUOTE);
+ pos.setIndex(i + ESCAPED_QUOTE.length());
+ lastHold = pos.getIndex();
+ continue;
+ }
+ switch (pattern.charAt(pos.getIndex())) {
+ case QUOTE:
+ next(pos);
+ return appendTo == null ? null : appendTo.append(pattern,
+ lastHold, pos.getIndex());
+ default:
+ next(pos);
+ }
+ }
+ throw new IllegalArgumentException(
+ "Unterminated quoted string at position " + start);
+ }
+
+ private void getQuotedString(String pattern, ParsePosition pos,
+ boolean escapingOn) {
+ appendQuotedString(pattern, pos, null, escapingOn);
+ }
+
+ private void eatFormat(String pattern, ParsePosition pos) {
+ int start = 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;
+ }
+ break;
+ case QUOTE:
+ getQuotedString(pattern, pos, false);
+ break;
+ }
+ }
+ throw new IllegalArgumentException(
+ "Unterminated format element at position " + start);
+ }
+ }
+
+ private static final Parser PARSER = new Parser();
+
+ private Format metaFormat;
+ private String strippedPattern;
+
+ /**
+ * Create a new ExtendedMessageFormat.
+ *
+ * @param pattern
+ * @param metaFormat
+ * @throws IllegalArgumentException
+ * if metaFormat
is null
or in
+ * case of a bad pattern.
+ */
+ public ExtendedMessageFormat(String pattern, Format metaFormat) {
+ /*
+ * We have to do some acrobatics here: the call to the super constructor
+ * will invoke applyPattern(), but we don't want to apply the pattern
+ * until we've installed our custom metaformat. So we check for that in
+ * our (final) applyPattern implementation, and re-call at the end of
+ * this constructor.
+ */
+ super(pattern);
+ setMetaFormat(metaFormat);
+ applyPattern(pattern);
+ }
+
+ /**
+ * Apply the specified pattern.
+ *
+ * @param pattern
+ * pattern String
+ */
+ public final void applyPattern(String pattern) {
+ if (metaFormat == null) {
+ return;
+ }
+ applyPatternPre(pattern);
+ strippedPattern = PARSER.stripFormats(pattern);
+ super.applyPattern(strippedPattern);
+ setFormats(PARSER.parseFormats(pattern, metaFormat));
+ applyPatternPost(pattern);
+ }
+
+ /**
+ * Pre-execution hook that allows subclasses to customize the behavior of
+ * the final applyPattern implementation.
+ *
+ * @param pattern
+ */
+ protected void applyPatternPre(String pattern) {
+ // noop
+ }
+
+ /**
+ * Post-execution hook that allows subclasses to customize the behavior of
+ * the final applyPattern implementation.
+ *
+ * @param pattern
+ */
+ protected void applyPatternPost(String pattern) {
+ // noop
+ }
+
+ /**
+ * Render the pattern from the current state of the
+ * ExtendedMessageFormat
.
+ *
+ * @return pattern String
+ */
+ public String toPattern() {
+ return PARSER.insertFormats(strippedPattern, getFormats(), metaFormat);
+ }
+
+ /**
+ * Get the meta-format currently configured.
+ *
+ * @return Format.
+ */
+ public synchronized Format getMetaFormat() {
+ return metaFormat;
+ }
+
+ /**
+ * Set the meta-format. Has no effect until a subsequent call to
+ * {@link #applyPattern(String)}.
+ *
+ * @param metaFormat
+ * the Format metaFormat to set.
+ */
+ public synchronized void setMetaFormat(Format metaFormat) {
+ Validate.notNull(metaFormat, "metaFormat is null");
+ this.metaFormat = metaFormat;
+ }
+
+}
diff --git a/src/java/org/apache/commons/lang/text/MetaFormatSupport.java b/src/java/org/apache/commons/lang/text/MetaFormatSupport.java
new file mode 100644
index 000000000..f1df7824b
--- /dev/null
+++ b/src/java/org/apache/commons/lang/text/MetaFormatSupport.java
@@ -0,0 +1,131 @@
+/*
+ * 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.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * metaFormat support.
+ *
+ * @see {@link ExtendedMessageFormat}
+ * @author Matt Benson
+ * @since 2.4
+ * @version $Id$
+ */
+public abstract class MetaFormatSupport extends Format {
+
+ private static final char END_FE = '}';
+ private static final char START_FE = '{';
+ private static final char QUOTE = '\'';
+
+ /**
+ * Invert the specified Map.
+ *
+ * @param map
+ * the Map to invert.
+ * @return a new Map instance.
+ * @throws NullPointerException
+ * if map
is null
.
+ */
+ protected Map invert(Map map) {
+ Map result = new HashMap(map.size());
+ for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) {
+ Map.Entry entry = (Map.Entry) iter.next();
+ result.put(entry.getValue(), entry.getKey());
+ }
+ return result;
+ }
+
+ /**
+ * Find the end of the subformat.
+ *
+ * @param source
+ * @param pos
+ */
+ protected void seekFormatElementEnd(String source, ParsePosition pos) {
+ int depth = 1;
+ boolean quote = false;
+ for (; pos.getIndex() < source.length(); next(pos)) {
+ switch (source.charAt(pos.getIndex())) {
+ case QUOTE:
+ quote ^= true;
+ break;
+ case START_FE:
+ depth += quote ? 0 : 1;
+ break;
+ case END_FE:
+ depth -= quote ? 0 : 1;
+ if (depth == 0) {
+ return;
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Advance the parse index by 1.
+ *
+ * @param pos
+ * the ParsePosition to advance.
+ * @return pos
+ */
+ protected ParsePosition next(ParsePosition pos) {
+ pos.setIndex(pos.getIndex() + 1);
+ return pos;
+ }
+
+ // provide default javadoc >;)
+ /**
+ * Parse an object from the specified String and ParsePosition. If an error
+ * occurs pos.getErrorIndex()
will contain a value >= zero,
+ * indicating the index at which the parse error occurred.
+ *
+ * @param source
+ * String to parse
+ * @param pos
+ * ParsePosition marking index into source
+ * @return Object parsed
+ */
+ public abstract Object parseObject(String source, ParsePosition pos);
+
+ /**
+ * Format the specified object, appending to the given StringBuffer, and
+ * optionally respecting the specified FieldPosition.
+ *
+ * @param obj
+ * the object to format
+ * @param toAppendTo
+ * the StringBuffer to which the formatted object should be
+ * appended
+ * @param pos
+ * FieldPosition associated with obj
+ * @return toAppendTo
+ * @throws NullPointerException
+ * if toAppendTo
or pos
is
+ * null
+ * @throws IllegalArgumentException
+ * if unable to format obj
+ */
+ public abstract StringBuffer format(Object obj, StringBuffer toAppendTo,
+ FieldPosition pos);
+}
diff --git a/src/java/org/apache/commons/lang/text/NameKeyedMetaFormat.java b/src/java/org/apache/commons/lang/text/NameKeyedMetaFormat.java
new file mode 100644
index 000000000..9caaf4d0c
--- /dev/null
+++ b/src/java/org/apache/commons/lang/text/NameKeyedMetaFormat.java
@@ -0,0 +1,163 @@
+/*
+ * 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.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.commons.lang.ObjectUtils;
+
+/**
+ * Basic metaFormat that requires enough configuration information to
+ * parse/format other Formats for use by ExtendedMessageFormat.
+ *
+ * @see {@link ExtendedMessageFormat}
+ * @author Matt Benson
+ * @since 2.4
+ * @version $Id$
+ */
+public class NameKeyedMetaFormat extends MetaFormatSupport {
+ private static final long serialVersionUID = 5963121202601122213L;
+
+ private static final char TRIGGER_END = '}';
+ private static final char TRIGGER_SUBFORMAT = ',';
+
+ /**
+ * Provides a builder with a fluent interface. Example:
+ *
+ *
+ *
+ *
+ * NameKeyedMetaFormat nkmf = new NameKeyedMetaFormat.Builder().put("foo",
+ * new FooFormat()).put("bar", new BarFormat())
+ * .put("baz", new BazFormat()).toNameKeyedMetaFormat();
+ *
NumberMetaFormat
.
+ *
+ * @return Locale
+ */
+ public Locale getLocale() {
+ return locale;
+ }
+
+ private synchronized void initialize() {
+ if (subformats == null) {
+ subformats = new HashMap();
+ subformats.put(DEFAULT, NumberFormat.getInstance(getLocale()));
+ subformats.put(INTEGER, NumberFormat
+ .getIntegerInstance(getLocale()));
+ subformats.put(CURRENCY, NumberFormat
+ .getCurrencyInstance(getLocale()));
+ subformats.put(PERCENT, NumberFormat
+ .getPercentInstance(getLocale()));
+
+ reverseSubformats = invert(subformats);
+ decimalFormatSymbols = new DecimalFormatSymbols(getLocale());
+ }
+ }
+}
diff --git a/src/java/org/apache/commons/lang/text/TimeMetaFormat.java b/src/java/org/apache/commons/lang/text/TimeMetaFormat.java
new file mode 100644
index 000000000..2da0d0cf9
--- /dev/null
+++ b/src/java/org/apache/commons/lang/text/TimeMetaFormat.java
@@ -0,0 +1,70 @@
+/*
+ * 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.DateFormat;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Stock "time" MetaFormat.
+ *
+ * @see {@link ExtendedMessageFormat}
+ * @author Matt Benson
+ * @since 2.4
+ * @version $Id$
+ */
+public class TimeMetaFormat extends DateMetaFormatSupport {
+ private static final long serialVersionUID = -4959095416302142342L;
+
+ /**
+ * Create a new TimeMetaFormat.
+ */
+ public TimeMetaFormat() {
+ super();
+ }
+
+ /**
+ * Create a new NumberMetaFormat.
+ *
+ * @param locale
+ */
+ public TimeMetaFormat(Locale locale) {
+ super(locale);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.commons.lang.text.AbstractDateMetaFormat#createSubformatInstance(int)
+ */
+ protected DateFormat createSubformatInstance(int style) {
+ return DateFormat.getTimeInstance(style, getLocale());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.commons.lang.text.AbstractDateMetaFormat#createReverseStyleMap()
+ */
+ protected Map createInverseStyleMap() {
+ Map invertMe = createStyleMap();
+ invertMe.remove(DEFAULT);
+ invertMe.remove(FULL);
+ return invert(invertMe);
+ }
+}
diff --git a/src/test/org/apache/commons/lang/text/AbstractMessageFormatTest.java b/src/test/org/apache/commons/lang/text/AbstractMessageFormatTest.java
new file mode 100644
index 000000000..cf93e0e21
--- /dev/null
+++ b/src/test/org/apache/commons/lang/text/AbstractMessageFormatTest.java
@@ -0,0 +1,294 @@
+/*
+ * 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.DateFormat;
+import java.text.MessageFormat;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+import junit.framework.TestCase;
+
+/**
+ * Abstract testcase to verify behavior of default-configuration
+ * ExtendedMessageFormat vs. MessageFormat.
+ *
+ * @author Matt Benson
+ * @since 2.4
+ * @version $Id$
+ */
+public abstract class AbstractMessageFormatTest extends TestCase {
+ protected static final Object[] NUMBERS = { new Double(0.1),
+ new Double(1.1), new Double(2.1) };
+
+ protected static final Object[] DATES = {
+ new GregorianCalendar(1970, Calendar.JANUARY, 01, 0, 15, 20)
+ .getTime(),
+ new GregorianCalendar(1970, Calendar.FEBRUARY, 02, 12, 30, 35)
+ .getTime(),
+ new GregorianCalendar(1970, Calendar.MARCH, 03, 18, 45, 50)
+ .getTime() };
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see junit.framework.TestCase#setUp()
+ */
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ protected abstract MessageFormat createMessageFormat(String pattern);
+
+ protected void doAssertions(String expected, String pattern, Object[] args) {
+ doAssertions(expected, pattern, args, pattern);
+ }
+
+ protected void doAssertions(String expected, String pattern, Object[] args,
+ String toPattern) {
+ MessageFormat f = createMessageFormat(pattern);
+ assertEquals(expected, f.format(args));
+ assertEquals(toPattern, f.toPattern());
+ }
+
+ public void testPlain() {
+ StringBuffer pattern = new StringBuffer();
+ for (int i = 0; i < NUMBERS.length; i++) {
+ if (i > 0) {
+ pattern.append("; ");
+ }
+ pattern.append("Object ").append(i).append(": ").append(NUMBERS[i]);
+ }
+ String p = pattern.toString();
+ doAssertions(p, p, NUMBERS);
+ }
+
+ public void testSimple() {
+ doAssertions("Object 0: 0.1; Object 1: 1.1; Object 2: 2.1",
+ "Object 0: {0}; Object 1: {1}; Object 2: {2}", NUMBERS);
+ }
+
+ public void testNumber() {
+ doAssertions(
+ "Number 0: 0.1; Number 1: 1.1; Number 2: 2.1",
+ "Number 0: {0,number}; Number 1: {1,number}; Number 2: {2,number}",
+ NUMBERS);
+ }
+
+ public void testNumberLooseFormatting() {
+ doAssertions(
+ "Number 0: 0.1; Number 1: 1.1; Number 2: 2.1",
+ "Number 0: {0, number }; Number 1: {1, number }; Number 2: {2, number }",
+ NUMBERS,
+ "Number 0: {0,number}; Number 1: {1,number}; Number 2: {2,number}");
+ }
+
+ public void testInteger() {
+ doAssertions(
+ "Number 0: 0; Number 1: 1; Number 2: 2",
+ "Number 0: {0,number,integer}; Number 1: {1,number,integer}; Number 2: {2,number,integer}",
+ NUMBERS);
+ }
+
+ public void testIntegerLooseFormatting() {
+ doAssertions(
+ "Number 0: 0; Number 1: 1; Number 2: 2",
+ "Number 0: {0, number , integer }; Number 1: {1, number , integer }; Number 2: {2, number , integer }",
+ NUMBERS,
+ "Number 0: {0,number,integer}; Number 1: {1,number,integer}; Number 2: {2,number,integer}");
+ }
+
+ public void testCurrency() {
+ doAssertions(
+ "Number 0: $0.10; Number 1: $1.10; Number 2: $2.10",
+ "Number 0: {0,number,currency}; Number 1: {1,number,currency}; Number 2: {2,number,currency}",
+ NUMBERS);
+ }
+
+ public void testPercent() {
+ doAssertions(
+ "Number 0: 10%; Number 1: 110%; Number 2: 210%",
+ "Number 0: {0,number,percent}; Number 1: {1,number,percent}; Number 2: {2,number,percent}",
+ NUMBERS);
+ }
+
+ public void testNumberPattern() {
+ doAssertions(
+ "Number 0: 000.100; Number 1: 001.100; Number 2: 002.100",
+ "Number 0: {0,number,#000.000}; Number 1: {1,number,#000.000}; Number 2: {2,number,#000.000}",
+ NUMBERS);
+ }
+
+ public void testDate() {
+ doAssertions(
+ "Date 0: Jan 1, 1970; Date 1: Feb 2, 1970; Date 2: Mar 3, 1970",
+ "Date 0: {0,date}; Date 1: {1,date}; Date 2: {2,date}", DATES);
+ }
+
+ public void testDateLooseFormatting() {
+ doAssertions(
+ "Date 0: Jan 1, 1970; Date 1: Feb 2, 1970; Date 2: Mar 3, 1970",
+ "Date 0: {0, date }; Date 1: {1, date }; Date 2: {2, date }",
+ DATES, "Date 0: {0,date}; Date 1: {1,date}; Date 2: {2,date}");
+ }
+
+ public void testShortDate() {
+ doAssertions(
+ "Date 0: 1/1/70; Date 1: 2/2/70; Date 2: 3/3/70",
+ "Date 0: {0,date,short}; Date 1: {1,date,short}; Date 2: {2,date,short}",
+ DATES);
+ }
+
+ public void testShortDateLooseFormatting() {
+ doAssertions(
+ "Date 0: 1/1/70; Date 1: 2/2/70; Date 2: 3/3/70",
+ "Date 0: {0, date , short }; Date 1: {1, date , short }; Date 2: {2, date , short }",
+ DATES,
+ "Date 0: {0,date,short}; Date 1: {1,date,short}; Date 2: {2,date,short}");
+ }
+
+ public void testMediumDate() {
+ doAssertions(
+ "Date 0: Jan 1, 1970; Date 1: Feb 2, 1970; Date 2: Mar 3, 1970",
+ "Date 0: {0,date,medium}; Date 1: {1,date,medium}; Date 2: {2,date,medium}",
+ DATES, "Date 0: {0,date}; Date 1: {1,date}; Date 2: {2,date}");
+ }
+
+ public void testLongDate() {
+ doAssertions(
+ "Date 0: January 1, 1970; Date 1: February 2, 1970; Date 2: March 3, 1970",
+ "Date 0: {0,date,long}; Date 1: {1,date,long}; Date 2: {2,date,long}",
+ DATES);
+ }
+
+ public void testFullDate() {
+ doAssertions(
+ "Date 0: Thursday, January 1, 1970; Date 1: Monday, February 2, 1970; Date 2: Tuesday, March 3, 1970",
+ "Date 0: {0,date,full}; Date 1: {1,date,full}; Date 2: {2,date,full}",
+ DATES);
+ }
+
+ public void testDatePattern() {
+ doAssertions(
+ "Date 0: AD1970.1; Date 1: AD1970.33; Date 2: AD1970.62",
+ "Date 0: {0,date,Gyyyy.D}; Date 1: {1,date,Gyyyy.D}; Date 2: {2,date,Gyyyy.D}",
+ DATES);
+ }
+
+ public void testTime() {
+ doAssertions(
+ "Time 0: 12:15:20 AM; Time 1: 12:30:35 PM; Time 2: 6:45:50 PM",
+ "Time 0: {0,time}; Time 1: {1,time}; Time 2: {2,time}", DATES);
+ }
+
+ public void testShortTime() {
+ doAssertions(
+ "Time 0: 12:15 AM; Time 1: 12:30 PM; Time 2: 6:45 PM",
+ "Time 0: {0,time,short}; Time 1: {1,time,short}; Time 2: {2,time,short}",
+ DATES);
+ }
+
+ public void testMediumTime() {
+ doAssertions(
+ "Time 0: 12:15:20 AM; Time 1: 12:30:35 PM; Time 2: 6:45:50 PM",
+ "Time 0: {0,time,medium}; Time 1: {1,time,medium}; Time 2: {2,time,medium}",
+ DATES, "Time 0: {0,time}; Time 1: {1,time}; Time 2: {2,time}");
+ }
+
+ public void testLongTime() {
+ DateFormat df = DateFormat.getTimeInstance(DateFormat.LONG);
+ StringBuffer expected = new StringBuffer();
+ for (int i = 0; i < DATES.length; i++) {
+ if (i > 0) {
+ expected.append("; ");
+ }
+ expected.append("Time ").append(i).append(": ").append(
+ df.format(DATES[i]));
+ }
+ doAssertions(
+ expected.toString(),
+ "Time 0: {0,time,long}; Time 1: {1,time,long}; Time 2: {2,time,long}",
+ DATES);
+ }
+
+ public void testFullTime() {
+ DateFormat df = DateFormat.getTimeInstance(DateFormat.FULL);
+ StringBuffer expected = new StringBuffer();
+ for (int i = 0; i < DATES.length; i++) {
+ if (i > 0) {
+ expected.append("; ");
+ }
+ expected.append("Time ").append(i).append(": ").append(
+ df.format(DATES[i]));
+ }
+ doAssertions(
+ expected.toString(),
+ "Time 0: {0,time,full}; Time 1: {1,time,full}; Time 2: {2,time,full}",
+ DATES,
+ "Time 0: {0,time,long}; Time 1: {1,time,long}; Time 2: {2,time,long}");
+ }
+
+ public void testTimePattern() {
+ doAssertions(
+ "Time 0: AM01520; Time 1: PM123035; Time 2: PM184550",
+ "Time 0: {0,time,aHms}; Time 1: {1,time,aHms}; Time 2: {2,time,aHms}",
+ DATES,
+ "Time 0: {0,date,aHms}; Time 1: {1,date,aHms}; Time 2: {2,date,aHms}");
+ }
+
+ public void testChoice() {
+ String choice = "0.0#x|1.0#y|2.0#z";
+ StringBuffer pattern = new StringBuffer();
+ for (int i = 0; i < 3; i++) {
+ if (i > 0) {
+ pattern.append("; ");
+ }
+ pattern.append("Choice ").append(i).append(": {").append(i).append(
+ ",choice,").append(choice).append("}");
+ }
+ doAssertions("Choice 0: x; Choice 1: y; Choice 2: z", pattern
+ .toString(), NUMBERS);
+ }
+
+ public void testChoiceLooseFormatting() {
+ String choice = "0.0#x |1.0#y |2.0#z ";
+ StringBuffer pattern = new StringBuffer();
+ for (int i = 0; i < 3; i++) {
+ if (i > 0) {
+ pattern.append("; ");
+ }
+ pattern.append("Choice ").append(i).append(": {").append(i).append(
+ ",choice,").append(choice).append("}");
+ }
+ doAssertions("Choice 0: x ; Choice 1: y ; Choice 2: z ", pattern
+ .toString(), NUMBERS);
+ }
+
+ public void testChoiceRecursive() {
+ String choice = "0.0#{0}|1.0#{1}|2.0#{2}";
+ StringBuffer pattern = new StringBuffer();
+ for (int i = 0; i < 3; i++) {
+ if (i > 0) {
+ pattern.append("; ");
+ }
+ pattern.append("Choice ").append(i).append(": {").append(i).append(
+ ",choice,").append(choice).append("}");
+ }
+ doAssertions("Choice 0: 0.1; Choice 1: 1.1; Choice 2: 2.1", pattern
+ .toString(), NUMBERS);
+ }
+}
diff --git a/src/test/org/apache/commons/lang/text/ExtendedMessageFormatBaselineTest.java b/src/test/org/apache/commons/lang/text/ExtendedMessageFormatBaselineTest.java
new file mode 100644
index 000000000..9c3bdb6e4
--- /dev/null
+++ b/src/test/org/apache/commons/lang/text/ExtendedMessageFormatBaselineTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.MessageFormat;
+import java.util.Locale;
+
+/**
+ * Baseline tests for {@link ExtendedMessageFormat}
+ *
+ * @author Matt Benson
+ * @since 2.4
+ * @version $Id$
+ */
+public class ExtendedMessageFormatBaselineTest extends AbstractMessageFormatTest {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.commons.lang.text.AbstractMessageFormatTest#createMessageFormat(java.lang.String)
+ */
+ protected MessageFormat createMessageFormat(String pattern) {
+ return new ExtendedMessageFormat(pattern, ExtendedMessageFormat.createDefaultMetaFormat(Locale.US));
+ }
+
+}
diff --git a/src/test/org/apache/commons/lang/text/MessageFormatExtensionTest.java b/src/test/org/apache/commons/lang/text/MessageFormatExtensionTest.java
new file mode 100644
index 000000000..e842bf3b5
--- /dev/null
+++ b/src/test/org/apache/commons/lang/text/MessageFormatExtensionTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.FieldPosition;
+import java.text.Format;
+import java.text.MessageFormat;
+import java.text.ParsePosition;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+
+/**
+ * Extension tests for {@link ExtendedMessageFormat}
+ *
+ * @author Matt Benson
+ * @since 2.4
+ * @version $Id$
+ */
+public class MessageFormatExtensionTest extends AbstractMessageFormatTest {
+
+ static class ProperNameCapitalizationFormat extends Format {
+ private static final long serialVersionUID = -6081911520622186866L;
+ private static final StrMatcher MATCH = StrMatcher
+ .charSetMatcher(" ,.");
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.text.Format#format(java.lang.Object,
+ * java.lang.StringBuffer, java.text.FieldPosition)
+ */
+ public StringBuffer format(Object obj, StringBuffer toAppendTo,
+ FieldPosition fpos) {
+ char[] buffer = String.valueOf(obj).toCharArray();
+ ParsePosition pos = new ParsePosition(0);
+ while (pos.getIndex() < buffer.length) {
+ char c = buffer[pos.getIndex()];
+ if (Character.isLowerCase(c)) {
+ c = Character.toUpperCase(c);
+ }
+ if (Character.isUpperCase(c)) {
+ toAppendTo.append(c);
+ next(pos);
+ }
+ int start = pos.getIndex();
+ seekDelimiter(buffer, pos);
+ toAppendTo.append(new String(buffer, start, pos.getIndex()
+ - start).toLowerCase());
+ }
+ return toAppendTo;
+ }
+
+ /**
+ * Unable to do much; return the String.
+ */
+ public Object parseObject(String source, ParsePosition pos) {
+ return source.substring(pos.getIndex());
+ }
+
+ private static void seekDelimiter(char[] buffer, ParsePosition pos) {
+ for (; pos.getIndex() < buffer.length
+ && MATCH.isMatch(buffer, pos.getIndex()) == 0; next(pos))
+ ;
+ if (pos.getIndex() >= buffer.length) {
+ return;
+ }
+ int len = 0;
+ do {
+ len = MATCH.isMatch(buffer, pos.getIndex());
+ pos.setIndex(pos.getIndex() + len);
+ } while (len > 0 && pos.getIndex() < buffer.length);
+ }
+
+ private static void next(ParsePosition pos) {
+ pos.setIndex(pos.getIndex() + 1);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.commons.lang.text.AbstractMessageFormatTest#createMessageFormat(java.lang.String)
+ */
+ protected MessageFormat createMessageFormat(String pattern) {
+ return new ExtendedMessageFormat(pattern, new MultiFormat.Builder()
+ .add(ExtendedMessageFormat.createDefaultMetaFormat(Locale.US)).add(
+ new NameKeyedMetaFormat.Builder().put("properName",
+ new ProperNameCapitalizationFormat())
+ .toNameKeyedMetaFormat()).toMultiFormat());
+ }
+
+ public void testProperName() {
+ doAssertions("John Q. Public; John Q. Public",
+ "{0,properName}; {1,properName}", new String[] {
+ "JOHN Q. PUBLIC", "john q. public" });
+ }
+
+ public void testMixed() {
+ doAssertions("John Q. Public was born on Thursday, January 1, 1970.",
+ "{0,properName} was born on {1,date,full}.", new Object[] {
+ "john q. public",
+ new GregorianCalendar(1970, Calendar.JANUARY, 01, 0,
+ 15, 20).getTime() });
+ }
+}
diff --git a/src/test/org/apache/commons/lang/text/MessageFormatTest.java b/src/test/org/apache/commons/lang/text/MessageFormatTest.java
new file mode 100644
index 000000000..4836001fc
--- /dev/null
+++ b/src/test/org/apache/commons/lang/text/MessageFormatTest.java
@@ -0,0 +1,22 @@
+package org.apache.commons.lang.text;
+
+import java.text.MessageFormat;
+import java.util.Locale;
+
+/**
+ * Baseline tests for java.text.MessageFormat.
+ *
+ * @author Matt Benson
+ * @since 2.4
+ * @version $Id$
+ */
+public class MessageFormatTest extends AbstractMessageFormatTest {
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.commons.lang.text.AbstractMessageFormatTest#createMessageFormat(java.lang.String)
+ */
+ protected MessageFormat createMessageFormat(String pattern) {
+ return new MessageFormat(pattern, Locale.US);
+ }
+}
diff --git a/src/test/org/apache/commons/lang/text/TextTestSuite.java b/src/test/org/apache/commons/lang/text/TextTestSuite.java
index cbcb1b126..b0c8dbdc8 100644
--- a/src/test/org/apache/commons/lang/text/TextTestSuite.java
+++ b/src/test/org/apache/commons/lang/text/TextTestSuite.java
@@ -57,6 +57,9 @@ public class TextTestSuite extends TestCase {
suite.addTest(StrSubstitutorTest.suite());
suite.addTest(StrTokenizerTest.suite());
suite.addTestSuite(MultiFormatTest.class);
+ suite.addTestSuite(MessageFormatTest.class);
+ suite.addTestSuite(ExtendedMessageFormatBaselineTest.class);
+ suite.addTestSuite(MessageFormatExtensionTest.class);
return suite;
}