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