ExtendedMessageFormat.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      https://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.lang3.text;

  18. import java.text.Format;
  19. import java.text.MessageFormat;
  20. import java.text.ParsePosition;
  21. import java.util.ArrayList;
  22. import java.util.Collection;
  23. import java.util.Locale;
  24. import java.util.Map;
  25. import java.util.Objects;

  26. import org.apache.commons.lang3.LocaleUtils;
  27. import org.apache.commons.lang3.StringUtils;
  28. import org.apache.commons.lang3.Validate;

  29. /**
  30.  * Extends {@link java.text.MessageFormat} to allow pluggable/additional formatting
  31.  * options for embedded format elements.  Client code should specify a registry
  32.  * of {@link FormatFactory} instances associated with {@link String}
  33.  * format names.  This registry will be consulted when the format elements are
  34.  * parsed from the message pattern.  In this way custom patterns can be specified,
  35.  * and the formats supported by {@link java.text.MessageFormat} can be overridden
  36.  * at the format and/or format style level (see MessageFormat).  A "format element"
  37.  * embedded in the message pattern is specified (<strong>()?</strong> signifies optionality):<br>
  38.  * <code>{</code><em>argument-number</em><strong>(</strong>{@code ,}<em>format-name</em><b>
  39.  * (</b>{@code ,}<em>format-style</em><strong>)?)?</strong><code>}</code>
  40.  *
  41.  * <p>
  42.  * <em>format-name</em> and <em>format-style</em> values are trimmed of surrounding whitespace
  43.  * in the manner of {@link java.text.MessageFormat}.  If <em>format-name</em> denotes
  44.  * {@code FormatFactory formatFactoryInstance} in {@code registry}, a {@link Format}
  45.  * matching <em>format-name</em> and <em>format-style</em> is requested from
  46.  * {@code formatFactoryInstance}.  If this is successful, the {@link Format}
  47.  * found is used for this format element.
  48.  * </p>
  49.  *
  50.  * <p><strong>NOTICE:</strong> The various subformat mutator methods are considered unnecessary; they exist on the parent
  51.  * class to allow the type of customization which it is the job of this class to provide in
  52.  * a configurable fashion.  These methods have thus been disabled and will throw
  53.  * {@link UnsupportedOperationException} if called.
  54.  * </p>
  55.  *
  56.  * <p>Limitations inherited from {@link java.text.MessageFormat}:</p>
  57.  * <ul>
  58.  * <li>When using "choice" subformats, support for nested formatting instructions is limited
  59.  *     to that provided by the base class.</li>
  60.  * <li>Thread-safety of {@link Format}s, including {@link MessageFormat} and thus
  61.  *     {@link ExtendedMessageFormat}, is not guaranteed.</li>
  62.  * </ul>
  63.  *
  64.  * @since 2.4
  65.  * @deprecated As of <a href="https://commons.apache.org/proper/commons-lang/changes-report.html#a3.6">3.6</a>, use Apache Commons Text
  66.  * <a href="https://commons.apache.org/proper/commons-text/javadocs/api-release/org/apache/commons/text/ExtendedMessageFormat.html">
  67.  * ExtendedMessageFormat</a>.
  68.  */
  69. @Deprecated
  70. public class ExtendedMessageFormat extends MessageFormat {

  71.     private static final long serialVersionUID = -2362048321261811743L;
  72.     private static final String EMPTY_PATTERN = StringUtils.EMPTY;
  73.     private static final char START_FMT = ',';
  74.     private static final char END_FE = '}';
  75.     private static final char START_FE = '{';
  76.     private static final char QUOTE = '\'';

  77.     /**
  78.      * To pattern string.
  79.      */
  80.     private String toPattern;

  81.     /**
  82.      * Our registry of FormatFactory.
  83.      */
  84.     private final Map<String, ? extends FormatFactory> registry;

  85.     /**
  86.      * Create a new ExtendedMessageFormat for the default locale.
  87.      *
  88.      * @param pattern  the pattern to use, not null
  89.      * @throws IllegalArgumentException in case of a bad pattern.
  90.      */
  91.     public ExtendedMessageFormat(final String pattern) {
  92.         this(pattern, Locale.getDefault());
  93.     }

  94.     /**
  95.      * Create a new ExtendedMessageFormat.
  96.      *
  97.      * @param pattern  the pattern to use, not null
  98.      * @param locale  the locale to use, not null
  99.      * @throws IllegalArgumentException in case of a bad pattern.
  100.      */
  101.     public ExtendedMessageFormat(final String pattern, final Locale locale) {
  102.         this(pattern, locale, null);
  103.     }

  104.     /**
  105.      * Create a new ExtendedMessageFormat.
  106.      *
  107.      * @param pattern  the pattern to use, not null.
  108.      * @param locale  the locale to use.
  109.      * @param registry  the registry of format factories, may be null.
  110.      * @throws IllegalArgumentException in case of a bad pattern.
  111.      */
  112.     public ExtendedMessageFormat(final String pattern, final Locale locale, final Map<String, ? extends FormatFactory> registry) {
  113.         super(EMPTY_PATTERN);
  114.         setLocale(LocaleUtils.toLocale(locale));
  115.         this.registry = registry;
  116.         applyPattern(pattern);
  117.     }

  118.     /**
  119.      * Create a new ExtendedMessageFormat for the default locale.
  120.      *
  121.      * @param pattern  the pattern 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(final String pattern, final Map<String, ? extends FormatFactory> registry) {
  126.         this(pattern, Locale.getDefault(), registry);
  127.     }

  128.     /**
  129.      * Consume a quoted string, adding it to {@code appendTo} if
  130.      * specified.
  131.      *
  132.      * @param pattern pattern to parse
  133.      * @param pos current parse position
  134.      * @param appendTo optional StringBuilder to append
  135.      * @return {@code appendTo}
  136.      */
  137.     private StringBuilder appendQuotedString(final String pattern, final ParsePosition pos,
  138.             final StringBuilder appendTo) {
  139.         assert pattern.toCharArray()[pos.getIndex()] == QUOTE :
  140.             "Quoted string must start with quote character";

  141.         // handle quote character at the beginning of the string
  142.         if (appendTo != null) {
  143.             appendTo.append(QUOTE);
  144.         }
  145.         next(pos);

  146.         final int start = pos.getIndex();
  147.         final char[] c = pattern.toCharArray();
  148.         for (int i = pos.getIndex(); i < pattern.length(); i++) {
  149.             if (c[pos.getIndex()] == QUOTE) {
  150.                 next(pos);
  151.                 return appendTo == null ? null : appendTo.append(c, start,
  152.                         pos.getIndex() - start);
  153.             }
  154.             next(pos);
  155.         }
  156.         throw new IllegalArgumentException(
  157.                 "Unterminated quoted string at position " + start);
  158.     }

  159.     /**
  160.      * Apply the specified pattern.
  161.      *
  162.      * @param pattern String
  163.      */
  164.     @Override
  165.     public final void applyPattern(final String pattern) {
  166.         if (registry == null) {
  167.             super.applyPattern(pattern);
  168.             toPattern = super.toPattern();
  169.             return;
  170.         }
  171.         final ArrayList<Format> foundFormats = new ArrayList<>();
  172.         final ArrayList<String> foundDescriptions = new ArrayList<>();
  173.         final StringBuilder stripCustom = new StringBuilder(pattern.length());

  174.         final ParsePosition pos = new ParsePosition(0);
  175.         final char[] c = pattern.toCharArray();
  176.         int fmtCount = 0;
  177.         while (pos.getIndex() < pattern.length()) {
  178.             switch (c[pos.getIndex()]) {
  179.             case QUOTE:
  180.                 appendQuotedString(pattern, pos, stripCustom);
  181.                 break;
  182.             case START_FE:
  183.                 fmtCount++;
  184.                 seekNonWs(pattern, pos);
  185.                 final int start = pos.getIndex();
  186.                 final int index = readArgumentIndex(pattern, next(pos));
  187.                 stripCustom.append(START_FE).append(index);
  188.                 seekNonWs(pattern, pos);
  189.                 Format format = null;
  190.                 String formatDescription = null;
  191.                 if (c[pos.getIndex()] == START_FMT) {
  192.                     formatDescription = parseFormatDescription(pattern,
  193.                             next(pos));
  194.                     format = getFormat(formatDescription);
  195.                     if (format == null) {
  196.                         stripCustom.append(START_FMT).append(formatDescription);
  197.                     }
  198.                 }
  199.                 foundFormats.add(format);
  200.                 foundDescriptions.add(format == null ? null : formatDescription);
  201.                 Validate.isTrue(foundFormats.size() == fmtCount);
  202.                 Validate.isTrue(foundDescriptions.size() == fmtCount);
  203.                 if (c[pos.getIndex()] != END_FE) {
  204.                     throw new IllegalArgumentException(
  205.                             "Unreadable format element at position " + start);
  206.                 }
  207.                 // falls-through
  208.             default:
  209.                 stripCustom.append(c[pos.getIndex()]);
  210.                 next(pos);
  211.             }
  212.         }
  213.         super.applyPattern(stripCustom.toString());
  214.         toPattern = insertFormats(super.toPattern(), foundDescriptions);
  215.         if (containsElements(foundFormats)) {
  216.             final Format[] origFormats = getFormats();
  217.             // only loop over what we know we have, as MessageFormat on Java 1.3
  218.             // seems to provide an extra format element:
  219.             int i = 0;
  220.             for (final Format f : foundFormats) {
  221.                 if (f != null) {
  222.                     origFormats[i] = f;
  223.                 }
  224.                 i++;
  225.             }
  226.             super.setFormats(origFormats);
  227.         }
  228.     }

  229.     /**
  230.      * Learn whether the specified Collection contains non-null elements.
  231.      * @param coll to check
  232.      * @return {@code true} if some Object was found, {@code false} otherwise.
  233.      */
  234.     private boolean containsElements(final Collection<?> coll) {
  235.         if (coll == null || coll.isEmpty()) {
  236.             return false;
  237.         }
  238.         return coll.stream().anyMatch(Objects::nonNull);
  239.     }

  240.     @Override
  241.     public boolean equals(final Object obj) {
  242.         if (this == obj) {
  243.             return true;
  244.         }
  245.         if (!super.equals(obj)) {
  246.             return false;
  247.         }
  248.         if (!(obj instanceof ExtendedMessageFormat)) {
  249.             return false;
  250.         }
  251.         final ExtendedMessageFormat other = (ExtendedMessageFormat) obj;
  252.         return Objects.equals(registry, other.registry) && Objects.equals(toPattern, other.toPattern);
  253.     }

  254.     /**
  255.      * Gets a custom format from a format description.
  256.      *
  257.      * @param desc String
  258.      * @return Format
  259.      */
  260.     private Format getFormat(final String desc) {
  261.         if (registry != null) {
  262.             String name = desc;
  263.             String args = null;
  264.             final int i = desc.indexOf(START_FMT);
  265.             if (i > 0) {
  266.                 name = desc.substring(0, i).trim();
  267.                 args = desc.substring(i + 1).trim();
  268.             }
  269.             final FormatFactory factory = registry.get(name);
  270.             if (factory != null) {
  271.                 return factory.getFormat(name, args, getLocale());
  272.             }
  273.         }
  274.         return null;
  275.     }

  276.     /**
  277.      * Consume quoted string only
  278.      *
  279.      * @param pattern pattern to parse
  280.      * @param pos current parse position
  281.      */
  282.     private void getQuotedString(final String pattern, final ParsePosition pos) {
  283.         appendQuotedString(pattern, pos, null);
  284.     }

  285.     @Override
  286.     public int hashCode() {
  287.         final int prime = 31;
  288.         final int result = super.hashCode();
  289.         return prime * result + Objects.hash(registry, toPattern);
  290.     }

  291.     /**
  292.      * Insert formats back into the pattern for toPattern() support.
  293.      *
  294.      * @param pattern source
  295.      * @param customPatterns The custom patterns to re-insert, if any
  296.      * @return full pattern
  297.      */
  298.     private String insertFormats(final String pattern, final ArrayList<String> customPatterns) {
  299.         if (!containsElements(customPatterns)) {
  300.             return pattern;
  301.         }
  302.         final StringBuilder sb = new StringBuilder(pattern.length() * 2);
  303.         final ParsePosition pos = new ParsePosition(0);
  304.         int fe = -1;
  305.         int depth = 0;
  306.         while (pos.getIndex() < pattern.length()) {
  307.             final char c = pattern.charAt(pos.getIndex());
  308.             switch (c) {
  309.             case QUOTE:
  310.                 appendQuotedString(pattern, pos, sb);
  311.                 break;
  312.             case START_FE:
  313.                 depth++;
  314.                 sb.append(START_FE).append(readArgumentIndex(pattern, next(pos)));
  315.                 // do not look for custom patterns when they are embedded, e.g. in a choice
  316.                 if (depth == 1) {
  317.                     fe++;
  318.                     final String customPattern = customPatterns.get(fe);
  319.                     if (customPattern != null) {
  320.                         sb.append(START_FMT).append(customPattern);
  321.                     }
  322.                 }
  323.                 break;
  324.             case END_FE:
  325.                 depth--;
  326.                 // falls-through
  327.             default:
  328.                 sb.append(c);
  329.                 next(pos);
  330.             }
  331.         }
  332.         return sb.toString();
  333.     }

  334.     /**
  335.      * Convenience method to advance parse position by 1
  336.      *
  337.      * @param pos ParsePosition
  338.      * @return {@code pos}
  339.      */
  340.     private ParsePosition next(final ParsePosition pos) {
  341.         pos.setIndex(pos.getIndex() + 1);
  342.         return pos;
  343.     }

  344.     /**
  345.      * Parse the format component of a format element.
  346.      *
  347.      * @param pattern string to parse
  348.      * @param pos current parse position
  349.      * @return Format description String
  350.      */
  351.     private String parseFormatDescription(final String pattern, final ParsePosition pos) {
  352.         final int start = pos.getIndex();
  353.         seekNonWs(pattern, pos);
  354.         final int text = pos.getIndex();
  355.         int depth = 1;
  356.         for (; pos.getIndex() < pattern.length(); next(pos)) {
  357.             switch (pattern.charAt(pos.getIndex())) {
  358.             case START_FE:
  359.                 depth++;
  360.                 break;
  361.             case END_FE:
  362.                 depth--;
  363.                 if (depth == 0) {
  364.                     return pattern.substring(text, pos.getIndex());
  365.                 }
  366.                 break;
  367.             case QUOTE:
  368.                 getQuotedString(pattern, pos);
  369.                 break;
  370.             default:
  371.                 break;
  372.             }
  373.         }
  374.         throw new IllegalArgumentException(
  375.                 "Unterminated format element at position " + start);
  376.     }

  377.     /**
  378.      * Reads the argument index from the current format element
  379.      *
  380.      * @param pattern pattern to parse
  381.      * @param pos current parse position
  382.      * @return argument index
  383.      */
  384.     private int readArgumentIndex(final String pattern, final ParsePosition pos) {
  385.         final int start = pos.getIndex();
  386.         seekNonWs(pattern, pos);
  387.         final StringBuilder result = new StringBuilder();
  388.         boolean error = false;
  389.         for (; !error && pos.getIndex() < pattern.length(); next(pos)) {
  390.             char c = pattern.charAt(pos.getIndex());
  391.             if (Character.isWhitespace(c)) {
  392.                 seekNonWs(pattern, pos);
  393.                 c = pattern.charAt(pos.getIndex());
  394.                 if (c != START_FMT && c != END_FE) {
  395.                     error = true;
  396.                     continue;
  397.                 }
  398.             }
  399.             if ((c == START_FMT || c == END_FE) && result.length() > 0) {
  400.                 try {
  401.                     return Integer.parseInt(result.toString());
  402.                 } catch (final NumberFormatException ignored) {
  403.                     // we've already ensured only digits, so unless something
  404.                     // outlandishly large was specified we should be okay.
  405.                 }
  406.             }
  407.             error = !Character.isDigit(c);
  408.             result.append(c);
  409.         }
  410.         if (error) {
  411.             throw new IllegalArgumentException(
  412.                     "Invalid format argument index at position " + start + ": "
  413.                             + pattern.substring(start, pos.getIndex()));
  414.         }
  415.         throw new IllegalArgumentException(
  416.                 "Unterminated format element at position " + start);
  417.     }

  418.     /**
  419.      * Consume whitespace from the current parse position.
  420.      *
  421.      * @param pattern String to read
  422.      * @param pos current position
  423.      */
  424.     private void seekNonWs(final String pattern, final ParsePosition pos) {
  425.         int len;
  426.         final char[] buffer = pattern.toCharArray();
  427.         do {
  428.             len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex());
  429.             pos.setIndex(pos.getIndex() + len);
  430.         } while (len > 0 && pos.getIndex() < pattern.length());
  431.     }

  432.     /**
  433.      * Throws UnsupportedOperationException - see class Javadoc for details.
  434.      *
  435.      * @param formatElementIndex format element index
  436.      * @param newFormat the new format
  437.      * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
  438.      */
  439.     @Override
  440.     public void setFormat(final int formatElementIndex, final Format newFormat) {
  441.         throw new UnsupportedOperationException();
  442.     }

  443.     /**
  444.      * Throws UnsupportedOperationException - see class Javadoc for details.
  445.      *
  446.      * @param argumentIndex argument index
  447.      * @param newFormat the new format
  448.      * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
  449.      */
  450.     @Override
  451.     public void setFormatByArgumentIndex(final int argumentIndex, final Format newFormat) {
  452.         throw new UnsupportedOperationException();
  453.     }

  454.     /**
  455.      * Throws UnsupportedOperationException - see class Javadoc for details.
  456.      *
  457.      * @param newFormats new formats
  458.      * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
  459.      */
  460.     @Override
  461.     public void setFormats(final Format[] newFormats) {
  462.         throw new UnsupportedOperationException();
  463.     }

  464.     /**
  465.      * Throws UnsupportedOperationException - see class Javadoc for details.
  466.      *
  467.      * @param newFormats new formats
  468.      * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
  469.      */
  470.     @Override
  471.     public void setFormatsByArgumentIndex(final Format[] newFormats) {
  472.         throw new UnsupportedOperationException();
  473.     }

  474.     /**
  475.      * {@inheritDoc}
  476.      */
  477.     @Override
  478.     public String toPattern() {
  479.         return toPattern;
  480.     }
  481. }