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