From 43cf3f491e7b9b42dc4a5fa4d7bc00cef1d38f7e Mon Sep 17 00:00:00 2001 From: Matthew Jason Benson Date: Tue, 30 Oct 2007 15:06:54 +0000 Subject: [PATCH] [LANG-362] Add ExtendedMessageFormat git-svn-id: https://svn.apache.org/repos/asf/commons/proper/lang/trunk@590106 13f79535-47bb-0310-9956-ffa450edef68 --- .../commons/lang/text/ChoiceMetaFormat.java | 72 ++++ .../commons/lang/text/DateMetaFormat.java | 57 +++ .../lang/text/DateMetaFormatSupport.java | 231 ++++++++++++ .../lang/text/DefaultMetaFormatFactory.java | 114 ++++++ .../lang/text/ExtendedMessageFormat.java | 344 ++++++++++++++++++ .../commons/lang/text/MetaFormatSupport.java | 131 +++++++ .../lang/text/NameKeyedMetaFormat.java | 163 +++++++++ .../commons/lang/text/NumberMetaFormat.java | 133 +++++++ .../commons/lang/text/TimeMetaFormat.java | 70 ++++ .../lang/text/AbstractMessageFormatTest.java | 294 +++++++++++++++ .../ExtendedMessageFormatBaselineTest.java | 40 ++ .../lang/text/MessageFormatExtensionTest.java | 120 ++++++ .../commons/lang/text/MessageFormatTest.java | 22 ++ .../commons/lang/text/TextTestSuite.java | 3 + 14 files changed, 1794 insertions(+) create mode 100644 src/java/org/apache/commons/lang/text/ChoiceMetaFormat.java create mode 100644 src/java/org/apache/commons/lang/text/DateMetaFormat.java create mode 100644 src/java/org/apache/commons/lang/text/DateMetaFormatSupport.java create mode 100644 src/java/org/apache/commons/lang/text/DefaultMetaFormatFactory.java create mode 100644 src/java/org/apache/commons/lang/text/ExtendedMessageFormat.java create mode 100644 src/java/org/apache/commons/lang/text/MetaFormatSupport.java create mode 100644 src/java/org/apache/commons/lang/text/NameKeyedMetaFormat.java create mode 100644 src/java/org/apache/commons/lang/text/NumberMetaFormat.java create mode 100644 src/java/org/apache/commons/lang/text/TimeMetaFormat.java create mode 100644 src/test/org/apache/commons/lang/text/AbstractMessageFormatTest.java create mode 100644 src/test/org/apache/commons/lang/text/ExtendedMessageFormatBaselineTest.java create mode 100644 src/test/org/apache/commons/lang/text/MessageFormatExtensionTest.java create mode 100644 src/test/org/apache/commons/lang/text/MessageFormatTest.java 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 + * Formats. + * + * @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();
+     * 
+ *

+ */ + public static class Builder { + private HashMap keyedFormats = new HashMap(); + + /** + * Add the specified format with the specified string key. + * + * @param key + * @param format + * @return + */ + public Builder put(String key, Format format) { + keyedFormats.put(key, format); + return this; + } + + /** + * Render the {@link NameKeyedMetaFormat} instance from this Builder. + * + * @return NameKeyedMetaFormat + */ + public NameKeyedMetaFormat toNameKeyedMetaFormat() { + return new NameKeyedMetaFormat(keyedFormats); + } + } + + private Map/* */keyedFormats = new HashMap(); + + /** + * Create a new NameKeyedMetaFormat. + */ + public NameKeyedMetaFormat(Map keyedFormats) { + this.keyedFormats = keyedFormats; + } + + /* + * (non-Javadoc) + * + * @see org.apache.commons.lang.text.MetaFormatSupport#format(java.lang.Object, + * java.lang.StringBuffer, java.text.FieldPosition) + */ + public StringBuffer format(Object obj, StringBuffer toAppendTo, + FieldPosition pos) { + int start = toAppendTo.length(); + // first try to match a sans-subformat format: + for (Iterator iter = iterateKeys(); iter.hasNext();) { + Object key = iter.next(); + if (ObjectUtils.equals(keyedFormats.get(key), obj)) { + return toAppendTo.append(key); + } + } + // now try again with subformats: + for (Iterator iter = iterateKeys(); iter.hasNext();) { + Object key = iter.next(); + try { + ((Format) keyedFormats.get(key)).format(obj, toAppendTo, pos); + if (toAppendTo.length() > start) { + toAppendTo.insert(start, ','); + } + return toAppendTo.insert(start, key); + } catch (Exception e) { + continue; + } + } + throw new IllegalArgumentException("Cannot format " + obj); + } + + /* + * (non-Javadoc) + * + * @see org.apache.commons.lang.text.MetaFormatSupport#parseObject(java.lang.String, + * java.text.ParsePosition) + */ + public Object parseObject(String source, ParsePosition pos) { + int start = pos.getIndex(); + boolean subformat = false; + for (; pos.getIndex() < source.length(); next(pos)) { + char c = source.charAt(pos.getIndex()); + if (c == TRIGGER_SUBFORMAT) { + subformat = true; + break; + } + if (c == TRIGGER_END) { + break; + } + } + String key = source.substring(start, pos.getIndex()); + Format format = (Format) keyedFormats.get(key); + if (format == null) { + format = (Format) keyedFormats.get(key.trim()); + if (format == null) { + pos.setErrorIndex(start); + return null; + } + } + if (subformat) { + return format.parseObject(source, next(pos)); + } + return format; + } + + /** + * Extension point to alter the iteration order of the delegate format keys. + * + * @return Iterator. + */ + protected Iterator iterateKeys() { + return keyedFormats.keySet().iterator(); + } + +} diff --git a/src/java/org/apache/commons/lang/text/NumberMetaFormat.java b/src/java/org/apache/commons/lang/text/NumberMetaFormat.java new file mode 100644 index 000000000..4c2f6fd49 --- /dev/null +++ b/src/java/org/apache/commons/lang/text/NumberMetaFormat.java @@ -0,0 +1,133 @@ +/* + * 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.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParsePosition; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Stock "number" MetaFormat. + * + * @see {@link ExtendedMessageFormat} + * @author Matt Benson + * @since 2.4 + * @version $Id$ + */ +public class NumberMetaFormat extends MetaFormatSupport { + private static final long serialVersionUID = -5876397363537288952L; + private static final String DEFAULT = ""; + private static final String INTEGER = "integer"; + private static final String CURRENCY = "currency"; + private static final String PERCENT = "percent"; + + private Locale locale; + + private transient Map subformats; + private transient Map reverseSubformats; + private transient DecimalFormatSymbols decimalFormatSymbols; + + /** + * Create a new NumberMetaFormat. + */ + public NumberMetaFormat() { + this(Locale.getDefault()); + } + + /** + * Create a new NumberMetaFormat. + * + * @param locale + */ + public NumberMetaFormat(Locale locale) { + super(); + this.locale = locale; + } + + /* + * (non-Javadoc) + * + * @see org.apache.commons.lang.text.AbstractMetaFormat#format(java.lang.Object, + * java.lang.StringBuffer, java.text.FieldPosition) + */ + public StringBuffer format(Object obj, StringBuffer toAppendTo, + FieldPosition pos) { + initialize(); + String subformat = (String) reverseSubformats.get(obj); + if (subformat != null) { + return toAppendTo.append(subformat); + } + if (obj instanceof DecimalFormat) { + DecimalFormat df = (DecimalFormat) obj; + if (df.getDecimalFormatSymbols().equals(decimalFormatSymbols)) { + return toAppendTo.append(df.toPattern()); + } + } + throw new IllegalArgumentException(); + } + + /* + * (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(); + initialize(); + Object result = subformats.get(subformat); + if (result != null) { + return result; + } + return new DecimalFormat(subformat, decimalFormatSymbols); + } + + /** + * Get the locale in use by this 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; }