    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements.  See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache License, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License.  You may obtain a copy of the License at
    008     *
    009     *      http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the License for the specific language governing permissions and
    015     * limitations under the License.
    016     */
    017    package org.apache.commons.lang.text;
    018
    019    import java.text.Format;
    020    import java.text.MessageFormat;
    021    import java.text.ParsePosition;
    022    import java.util.ArrayList;
    023    import java.util.Collection;
    024    import java.util.Iterator;
    025    import java.util.Locale;
    026    import java.util.Map;
    027
    028    import org.apache.commons.lang.Validate;
    029
    030    /**
    031     * Extends <code>java.text.MessageFormat</code> to allow pluggable/additional formatting
    032     * options for embedded format elements.  Client code should specify a registry
    033     * of <code>FormatFactory</code> instances associated with <code>String</code>
    034     * format names.  This registry will be consulted when the format elements are
    035     * parsed from the message pattern.  In this way custom patterns can be specified,
    036     * and the formats supported by <code>java.text.MessageFormat</code> can be overridden
    037     * at the format and/or format style level (see MessageFormat).  A "format element"
    038     * embedded in the message pattern is specified (<b>()?</b> signifies optionality):<br />
    039     * <code>{</code><i>argument-number</i><b>(</b><code>,</code><i>format-name</i><b>(</b><code>,</code><i>format-style</i><b>)?)?</b><code>}</code>
    040     *
    041     * <p>
    042     * <i>format-name</i> and <i>format-style</i> values are trimmed of surrounding whitespace
    043     * in the manner of <code>java.text.MessageFormat</code>.  If <i>format-name</i> denotes
    044     * <code>FormatFactory formatFactoryInstance</code> in <code>registry</code>, a <code>Format</code>
    045     * matching <i>format-name</i> and <i>format-style</i> is requested from
    046     * <code>formatFactoryInstance</code>.  If this is successful, the <code>Format</code>
    047     * found is used for this format element.
    048     * </p>
    049     *
    050     * <p>NOTICE: The various subformat mutator methods are considered unnecessary; they exist on the parent
    051     * class to allow the type of customization which it is the job of this class to provide in
    052     * a configurable fashion.  These methods have thus been disabled and will throw
    053     * <code>UnsupportedOperationException</code> if called.
    054     * </p>
    055     *
    056     * <p>Limitations inherited from <code>java.text.MessageFormat</code>:
    057     * <ul>
    058     * <li>When using "choice" subformats, support for nested formatting instructions is limited
    059     *     to that provided by the base class.</li>
    060     * <li>Thread-safety of <code>Format</code>s, including <code>MessageFormat</code> and thus
    061     *     <code>ExtendedMessageFormat</code>, is not guaranteed.</li>
    062     * </ul>
    063     * </p>
    064     *
    065     * @author Apache Software Foundation
    066     * @author Matt Benson
    067     * @since 2.4
    068     * @version $Id: ExtendedMessageFormat.java 905636 2010-02-02 14:03:32Z niallp $
    069     */
    070    public class ExtendedMessageFormat extends MessageFormat {
    071        private static final long serialVersionUID = -2362048321261811743L;
    072
    073        private static final String DUMMY_PATTERN = "";
    074        private static final String ESCAPED_QUOTE = "''";
    075        private static final char START_FMT = ',';
    076        private static final char END_FE = '}';
    077        private static final char START_FE = '{';
    078        private static final char QUOTE = '\'';
    079
    080        private String toPattern;
    081        private final Map registry;
    082
    083        /**
    084         * Create a new ExtendedMessageFormat for the default locale.
    085         *
    086         * @param pattern  the pattern to use, not null
    087         * @throws IllegalArgumentException in case of a bad pattern.
    088         */
    089        public ExtendedMessageFormat(String pattern) {
    090            this(pattern, Locale.getDefault());
    091        }
    092
    093        /**
    094         * Create a new ExtendedMessageFormat.
    095         *
    096         * @param pattern  the pattern to use, not null
    097         * @param locale  the locale to use, not null
    098         * @throws IllegalArgumentException in case of a bad pattern.
    099         */
    100        public ExtendedMessageFormat(String pattern, Locale locale) {
    101            this(pattern, locale, null);
    102        }
    103
    104        /**
    105         * Create a new ExtendedMessageFormat for the default locale.
    106         *
    107         * @param pattern  the pattern to use, not null
    108         * @param registry  the registry of format factories, may be null
    109         * @throws IllegalArgumentException in case of a bad pattern.
    110         */
    111        public ExtendedMessageFormat(String pattern, Map registry) {
    112            this(pattern, Locale.getDefault(), registry);
    113        }
    114
    115        /**
    116         * Create a new ExtendedMessageFormat.
    117         *
    118         * @param pattern  the pattern to use, not null
    119         * @param locale  the locale to use, not null
    120         * @param registry  the registry of format factories, may be null
    121         * @throws IllegalArgumentException in case of a bad pattern.
    122         */
    123        public ExtendedMessageFormat(String pattern, Locale locale, Map registry) {
    124            super(DUMMY_PATTERN);
    125            setLocale(locale);
    126            this.registry = registry;
    127            applyPattern(pattern);
    128        }
    129
    130        /**
    131         * {@inheritDoc}
    132         */
    133        public String toPattern() {
    134            return toPattern;
    135        }
    136
    137        /**
    138         * Apply the specified pattern.
    139         *
    140         * @param pattern String
    141         */
    142        public final void applyPattern(String pattern) {
    143            if (registry == null) {
    144                super.applyPattern(pattern);
    145                toPattern = super.toPattern();
    146                return;
    147            }
    148            ArrayList foundFormats = new ArrayList();
    149            ArrayList foundDescriptions = new ArrayList();
    150            StringBuffer stripCustom = new StringBuffer(pattern.length());
    151
    152            ParsePosition pos = new ParsePosition(0);
    153            char[] c = pattern.toCharArray();
    154            int fmtCount = 0;
    155            while (pos.getIndex() < pattern.length()) {
    156                switch (c[pos.getIndex()]) {
    157                case QUOTE:
    158                    appendQuotedString(pattern, pos, stripCustom, true);
    159                    break;
    160                case START_FE:
    161                    fmtCount++;
    162                    seekNonWs(pattern, pos);
    163                    int start = pos.getIndex();
    164                    int index = readArgumentIndex(pattern, next(pos));
    165                    stripCustom.append(START_FE).append(index);
    166                    seekNonWs(pattern, pos);
    167                    Format format = null;
    168                    String formatDescription = null;
    169                    if (c[pos.getIndex()] == START_FMT) {
    170                        formatDescription = parseFormatDescription(pattern,
    171                                next(pos));
    172                        format = getFormat(formatDescription);
    173                        if (format == null) {
    174                            stripCustom.append(START_FMT).append(formatDescription);
    175                        }
    176                    }
    177                    foundFormats.add(format);
    178                    foundDescriptions.add(format == null ? null : formatDescription);
    179                    Validate.isTrue(foundFormats.size() == fmtCount);
    180                    Validate.isTrue(foundDescriptions.size() == fmtCount);
    181                    if (c[pos.getIndex()] != END_FE) {
    182                        throw new IllegalArgumentException(
    183                                "Unreadable format element at position " + start);
    184                    }
    185                    //$FALL-THROUGH$
    186                default:
    187                    stripCustom.append(c[pos.getIndex()]);
    188                    next(pos);
    189                }
    190            }
    191            super.applyPattern(stripCustom.toString());
    192            toPattern = insertFormats(super.toPattern(), foundDescriptions);
    193            if (containsElements(foundFormats)) {
    194                Format[] origFormats = getFormats();
    195                // only loop over what we know we have, as MessageFormat on Java 1.3
    196                // seems to provide an extra format element:
    197                int i = 0;
    198                for (Iterator it = foundFormats.iterator(); it.hasNext(); i++) {
    199                    Format f = (Format) it.next();
    200                    if (f != null) {
    201                        origFormats[i] = f;
    202                    }
    203                }
    204                super.setFormats(origFormats);
    205            }
    206        }
    207
    208        /**
    209         * {@inheritDoc}
    210         * @throws UnsupportedOperationException
    211         */
    212        public void setFormat(int formatElementIndex, Format newFormat) {
    213            throw new UnsupportedOperationException();
    214        }
    215
    216        /**
    217         * {@inheritDoc}
    218         * @throws UnsupportedOperationException
    219         */
    220        public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) {
    221            throw new UnsupportedOperationException();
    222        }
    223
    224        /**
    225         * {@inheritDoc}
    226         * @throws UnsupportedOperationException
    227         */
    228        public void setFormats(Format[] newFormats) {
    229            throw new UnsupportedOperationException();
    230        }
    231
    232        /**
    233         * {@inheritDoc}
    234         * @throws UnsupportedOperationException
    235         */
    236        public void setFormatsByArgumentIndex(Format[] newFormats) {
    237            throw new UnsupportedOperationException();
    238        }
    239
    240        /**
    241         * Get a custom format from a format description.
    242         *
    243         * @param desc String
    244         * @return Format
    245         */
    246        private Format getFormat(String desc) {
    247            if (registry != null) {
    248                String name = desc;
    249                String args = null;
    250                int i = desc.indexOf(START_FMT);
    251                if (i > 0) {
    252                    name = desc.substring(0, i).trim();
    253                    args = desc.substring(i + 1).trim();
    254                }
    255                FormatFactory factory = (FormatFactory) registry.get(name);
    256                if (factory != null) {
    257                    return factory.getFormat(name, args, getLocale());
    258                }
    259            }
    260            return null;
    261        }
    262
    263        /**
    264         * Read the argument index from the current format element
    265         *
    266         * @param pattern pattern to parse
    267         * @param pos current parse position
    268         * @return argument index
    269         */
    270        private int readArgumentIndex(String pattern, ParsePosition pos) {
    271            int start = pos.getIndex();
    272            seekNonWs(pattern, pos);
    273            StringBuffer result = new StringBuffer();
    274            boolean error = false;
    275            for (; !error && pos.getIndex() < pattern.length(); next(pos)) {
    276                char c = pattern.charAt(pos.getIndex());
    277                if (Character.isWhitespace(c)) {
    278                    seekNonWs(pattern, pos);
    279                    c = pattern.charAt(pos.getIndex());
    280                    if (c != START_FMT && c != END_FE) {
    281                        error = true;
    282                        continue;
    283                    }
    284                }
    285                if ((c == START_FMT || c == END_FE) && result.length() > 0) {
    286                    try {
    287                        return Integer.parseInt(result.toString());
    288                    } catch (NumberFormatException e) {
    289                        // we've already ensured only digits, so unless something
    290                        // outlandishly large was specified we should be okay.
    291                    }
    292                }
    293                error = !Character.isDigit(c);
    294                result.append(c);
    295            }
    296            if (error) {
    297                throw new IllegalArgumentException(
    298                        "Invalid format argument index at position " + start + ": "
    299                                + pattern.substring(start, pos.getIndex()));
    300            }
    301            throw new IllegalArgumentException(
    302                    "Unterminated format element at position " + start);
    303        }
    304
    305        /**
    306         * Parse the format component of a format element.
    307         *
    308         * @param pattern string to parse
    309         * @param pos current parse position
    310         * @return Format description String
    311         */
    312        private String parseFormatDescription(String pattern, ParsePosition pos) {
    313            int start = pos.getIndex();
    314            seekNonWs(pattern, pos);
    315            int text = pos.getIndex();
    316            int depth = 1;
    317            for (; pos.getIndex() < pattern.length(); next(pos)) {
    318                switch (pattern.charAt(pos.getIndex())) {
    319                case START_FE:
    320                    depth++;
    321                    break;
    322                case END_FE:
    323                    depth--;
    324                    if (depth == 0) {
    325                        return pattern.substring(text, pos.getIndex());
    326                    }
    327                    break;
    328                case QUOTE:
    329                    getQuotedString(pattern, pos, false);
    330                    break;
    331                }
    332            }
    333            throw new IllegalArgumentException(
    334                    "Unterminated format element at position " + start);
    335        }
    336
    337        /**
    338         * Insert formats back into the pattern for toPattern() support.
    339         *
    340         * @param pattern source
    341         * @param customPatterns The custom patterns to re-insert, if any
    342         * @return full pattern
    343         */
    344        private String insertFormats(String pattern, ArrayList customPatterns) {
    345            if (!containsElements(customPatterns)) {
    346                return pattern;
    347            }
    348            StringBuffer sb = new StringBuffer(pattern.length() * 2);
    349            ParsePosition pos = new ParsePosition(0);
    350            int fe = -1;
    351            int depth = 0;
    352            while (pos.getIndex() < pattern.length()) {
    353                char c = pattern.charAt(pos.getIndex());
    354                switch (c) {
    355                case QUOTE:
    356                    appendQuotedString(pattern, pos, sb, false);
    357                    break;
    358                case START_FE:
    359                    depth++;
    360                    if (depth == 1) {
    361                        fe++;
    362                        sb.append(START_FE).append(
    363                                readArgumentIndex(pattern, next(pos)));
    364                        String customPattern = (String) customPatterns.get(fe);
    365                        if (customPattern != null) {
    366                            sb.append(START_FMT).append(customPattern);
    367                        }
    368                    }
    369                    break;
    370                case END_FE:
    371                    depth--;
    372                    //$FALL-THROUGH$
    373                default:
    374                    sb.append(c);
    375                    next(pos);
    376                }
    377            }
    378            return sb.toString();
    379        }
    380
    381        /**
    382         * Consume whitespace from the current parse position.
    383         *
    384         * @param pattern String to read
    385         * @param pos current position
    386         */
    387        private void seekNonWs(String pattern, ParsePosition pos) {
    388            int len = 0;
    389            char[] buffer = pattern.toCharArray();
    390            do {
    391                len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex());
    392                pos.setIndex(pos.getIndex() + len);
    393            } while (len > 0 && pos.getIndex() < pattern.length());
    394        }
    395
    396        /**
    397         * Convenience method to advance parse position by 1
    398         *
    399         * @param pos ParsePosition
    400         * @return <code>pos</code>
    401         */
    402        private ParsePosition next(ParsePosition pos) {
    403            pos.setIndex(pos.getIndex() + 1);
    404            return pos;
    405        }
    406
    407        /**
    408         * Consume a quoted string, adding it to <code>appendTo</code> if
    409         * specified.
    410         *
    411         * @param pattern pattern to parse
    412         * @param pos current parse position
    413         * @param appendTo optional StringBuffer to append
    414         * @param escapingOn whether to process escaped quotes
    415         * @return <code>appendTo</code>
    416         */
    417        private StringBuffer appendQuotedString(String pattern, ParsePosition pos,
    418                StringBuffer appendTo, boolean escapingOn) {
    419            int start = pos.getIndex();
    420            char[] c = pattern.toCharArray();
    421            if (escapingOn && c[start] == QUOTE) {
    422                next(pos);
    423                return appendTo == null ? null : appendTo.append(QUOTE);
    424            }
    425            int lastHold = start;
    426            for (int i = pos.getIndex(); i < pattern.length(); i++) {
    427                if (escapingOn && pattern.substring(i).startsWith(ESCAPED_QUOTE)) {
    428                    appendTo.append(c, lastHold, pos.getIndex() - lastHold).append(
    429                            QUOTE);
    430                    pos.setIndex(i + ESCAPED_QUOTE.length());
    431                    lastHold = pos.getIndex();
    432                    continue;
    433                }
    434                switch (c[pos.getIndex()]) {
    435                case QUOTE:
    436                    next(pos);
    437                    return appendTo == null ? null : appendTo.append(c, lastHold,
    438                            pos.getIndex() - lastHold);
    439                default:
    440                    next(pos);
    441                }
    442            }
    443            throw new IllegalArgumentException(
    444                    "Unterminated quoted string at position " + start);
    445        }
    446
    447        /**
    448         * Consume quoted string only
    449         *
    450         * @param pattern pattern to parse
    451         * @param pos current parse position
    452         * @param escapingOn whether to process escaped quotes
    453         */
    454        private void getQuotedString(String pattern, ParsePosition pos,
    455                boolean escapingOn) {
    456            appendQuotedString(pattern, pos, null, escapingOn);
    457        }
    458
    459        /**
    460         * Learn whether the specified Collection contains non-null elements.
    461         * @param coll to check
    462         * @return <code>true</code> if some Object was found, <code>false</code> otherwise.
    463         */
    464        private boolean containsElements(Collection coll) {
    465            if (coll == null || coll.size() == 0) {
    466                return false;
    467            }
    468            for (Iterator iter = coll.iterator(); iter.hasNext();) {
    469                if (iter.next() != null) {
    470                    return true;
    471                }
    472            }
    473            return false;
    474        }
    475    }