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