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