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}