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.  *      http://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.ObjectUtils;
  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 (<b>()?</b> signifies optionality):<br>
  38.  * <code>{</code><em>argument-number</em><b>(</b>{@code ,}<em>format-name</em><b>
  39.  * (</b>{@code ,}<em>format-style</em><b>)?)?</b><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><b>NOTICE:</b> 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 3.6, 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> instead
  68.  */
  69. @Deprecated
  70. public class ExtendedMessageFormat extends MessageFormat {
  71.     private static final long serialVersionUID = -2362048321261811743L;
  72.     private static final int HASH_SEED = 31;

  73.     private static final String DUMMY_PATTERN = "";
  74.     private static final char START_FMT = ',';
  75.     private static final char END_FE = '}';
  76.     private static final char START_FE = '{';
  77.     private static final char QUOTE = '\'';

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

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

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

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

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

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

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

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

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

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

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

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

  241.     /**
  242.      * Check if this extended message format is equal to another object.
  243.      *
  244.      * @param obj the object to compare to
  245.      * @return true if this object equals the other, otherwise false
  246.      */
  247.     @Override
  248.     public boolean equals(final Object obj) {
  249.         if (obj == this) {
  250.             return true;
  251.         }
  252.         if (obj == null) {
  253.             return false;
  254.         }
  255.         if (!super.equals(obj)) {
  256.             return false;
  257.         }
  258.         if (ObjectUtils.notEqual(getClass(), obj.getClass())) {
  259.           return false;
  260.         }
  261.         final ExtendedMessageFormat rhs = (ExtendedMessageFormat) obj;
  262.         if (ObjectUtils.notEqual(toPattern, rhs.toPattern)) {
  263.             return false;
  264.         }
  265.         return !ObjectUtils.notEqual(registry, rhs.registry);
  266.     }

  267.     /**
  268.      * Gets a custom format from a format description.
  269.      *
  270.      * @param desc String
  271.      * @return Format
  272.      */
  273.     private Format getFormat(final String desc) {
  274.         if (registry != null) {
  275.             String name = desc;
  276.             String args = null;
  277.             final int i = desc.indexOf(START_FMT);
  278.             if (i > 0) {
  279.                 name = desc.substring(0, i).trim();
  280.                 args = desc.substring(i + 1).trim();
  281.             }
  282.             final FormatFactory factory = registry.get(name);
  283.             if (factory != null) {
  284.                 return factory.getFormat(name, args, getLocale());
  285.             }
  286.         }
  287.         return null;
  288.     }

  289.     /**
  290.      * Consume quoted string only
  291.      *
  292.      * @param pattern pattern to parse
  293.      * @param pos current parse position
  294.      */
  295.     private void getQuotedString(final String pattern, final ParsePosition pos) {
  296.         appendQuotedString(pattern, pos, null);
  297.     }

  298.     /**
  299.      * {@inheritDoc}
  300.      */
  301.     @Override
  302.     public int hashCode() {
  303.         int result = super.hashCode();
  304.         result = HASH_SEED * result + Objects.hashCode(registry);
  305.         result = HASH_SEED * result + Objects.hashCode(toPattern);
  306.         return result;
  307.     }

  308.     /**
  309.      * Insert formats back into the pattern for toPattern() support.
  310.      *
  311.      * @param pattern source
  312.      * @param customPatterns The custom patterns to re-insert, if any
  313.      * @return full pattern
  314.      */
  315.     private String insertFormats(final String pattern, final ArrayList<String> customPatterns) {
  316.         if (!containsElements(customPatterns)) {
  317.             return pattern;
  318.         }
  319.         final StringBuilder sb = new StringBuilder(pattern.length() * 2);
  320.         final ParsePosition pos = new ParsePosition(0);
  321.         int fe = -1;
  322.         int depth = 0;
  323.         while (pos.getIndex() < pattern.length()) {
  324.             final char c = pattern.charAt(pos.getIndex());
  325.             switch (c) {
  326.             case QUOTE:
  327.                 appendQuotedString(pattern, pos, sb);
  328.                 break;
  329.             case START_FE:
  330.                 depth++;
  331.                 sb.append(START_FE).append(readArgumentIndex(pattern, next(pos)));
  332.                 // do not look for custom patterns when they are embedded, e.g. in a choice
  333.                 if (depth == 1) {
  334.                     fe++;
  335.                     final String customPattern = customPatterns.get(fe);
  336.                     if (customPattern != null) {
  337.                         sb.append(START_FMT).append(customPattern);
  338.                     }
  339.                 }
  340.                 break;
  341.             case END_FE:
  342.                 depth--;
  343.                 //$FALL-THROUGH$
  344.             default:
  345.                 sb.append(c);
  346.                 next(pos);
  347.             }
  348.         }
  349.         return sb.toString();
  350.     }

  351.     /**
  352.      * Convenience method to advance parse position by 1
  353.      *
  354.      * @param pos ParsePosition
  355.      * @return {@code pos}
  356.      */
  357.     private ParsePosition next(final ParsePosition pos) {
  358.         pos.setIndex(pos.getIndex() + 1);
  359.         return pos;
  360.     }

  361.     /**
  362.      * Parse the format component of a format element.
  363.      *
  364.      * @param pattern string to parse
  365.      * @param pos current parse position
  366.      * @return Format description String
  367.      */
  368.     private String parseFormatDescription(final String pattern, final ParsePosition pos) {
  369.         final int start = pos.getIndex();
  370.         seekNonWs(pattern, pos);
  371.         final int text = pos.getIndex();
  372.         int depth = 1;
  373.         for (; pos.getIndex() < pattern.length(); next(pos)) {
  374.             switch (pattern.charAt(pos.getIndex())) {
  375.             case START_FE:
  376.                 depth++;
  377.                 break;
  378.             case END_FE:
  379.                 depth--;
  380.                 if (depth == 0) {
  381.                     return pattern.substring(text, pos.getIndex());
  382.                 }
  383.                 break;
  384.             case QUOTE:
  385.                 getQuotedString(pattern, pos);
  386.                 break;
  387.             default:
  388.                 break;
  389.             }
  390.         }
  391.         throw new IllegalArgumentException(
  392.                 "Unterminated format element at position " + start);
  393.     }

  394.     /**
  395.      * Read the argument index from the current format element
  396.      *
  397.      * @param pattern pattern to parse
  398.      * @param pos current parse position
  399.      * @return argument index
  400.      */
  401.     private int readArgumentIndex(final String pattern, final ParsePosition pos) {
  402.         final int start = pos.getIndex();
  403.         seekNonWs(pattern, pos);
  404.         final StringBuilder result = new StringBuilder();
  405.         boolean error = false;
  406.         for (; !error && pos.getIndex() < pattern.length(); next(pos)) {
  407.             char c = pattern.charAt(pos.getIndex());
  408.             if (Character.isWhitespace(c)) {
  409.                 seekNonWs(pattern, pos);
  410.                 c = pattern.charAt(pos.getIndex());
  411.                 if (c != START_FMT && c != END_FE) {
  412.                     error = true;
  413.                     continue;
  414.                 }
  415.             }
  416.             if ((c == START_FMT || c == END_FE) && result.length() > 0) {
  417.                 try {
  418.                     return Integer.parseInt(result.toString());
  419.                 } catch (final NumberFormatException ignored) {
  420.                     // we've already ensured only digits, so unless something
  421.                     // outlandishly large was specified we should be okay.
  422.                 }
  423.             }
  424.             error = !Character.isDigit(c);
  425.             result.append(c);
  426.         }
  427.         if (error) {
  428.             throw new IllegalArgumentException(
  429.                     "Invalid format argument index at position " + start + ": "
  430.                             + pattern.substring(start, pos.getIndex()));
  431.         }
  432.         throw new IllegalArgumentException(
  433.                 "Unterminated format element at position " + start);
  434.     }

  435.     /**
  436.      * Consume whitespace from the current parse position.
  437.      *
  438.      * @param pattern String to read
  439.      * @param pos current position
  440.      */
  441.     private void seekNonWs(final String pattern, final ParsePosition pos) {
  442.         int len;
  443.         final char[] buffer = pattern.toCharArray();
  444.         do {
  445.             len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex());
  446.             pos.setIndex(pos.getIndex() + len);
  447.         } while (len > 0 && pos.getIndex() < pattern.length());
  448.     }

  449.     /**
  450.      * Throws UnsupportedOperationException - see class Javadoc for details.
  451.      *
  452.      * @param formatElementIndex format element index
  453.      * @param newFormat the new format
  454.      * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
  455.      */
  456.     @Override
  457.     public void setFormat(final int formatElementIndex, final Format newFormat) {
  458.         throw new UnsupportedOperationException();
  459.     }

  460.     /**
  461.      * Throws UnsupportedOperationException - see class Javadoc for details.
  462.      *
  463.      * @param argumentIndex argument index
  464.      * @param newFormat the new format
  465.      * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
  466.      */
  467.     @Override
  468.     public void setFormatByArgumentIndex(final int argumentIndex, final Format newFormat) {
  469.         throw new UnsupportedOperationException();
  470.     }

  471.     /**
  472.      * Throws UnsupportedOperationException - see class Javadoc for details.
  473.      *
  474.      * @param newFormats new formats
  475.      * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
  476.      */
  477.     @Override
  478.     public void setFormats(final Format[] newFormats) {
  479.         throw new UnsupportedOperationException();
  480.     }

  481.     /**
  482.      * Throws UnsupportedOperationException - see class Javadoc for details.
  483.      *
  484.      * @param newFormats new formats
  485.      * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
  486.      */
  487.     @Override
  488.     public void setFormatsByArgumentIndex(final Format[] newFormats) {
  489.         throw new UnsupportedOperationException();
  490.     }

  491.     /**
  492.      * {@inheritDoc}
  493.      */
  494.     @Override
  495.     public String toPattern() {
  496.         return toPattern;
  497.     }
  498. }