MessageFormat.java

/*
 * Copyright (c) 2001-2017, Zoltan Farkas All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * Additionally licensed with:
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.spf4j.text;
//CHECKSTYLE:OFF
/*
 * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

 /*
 * (C) Copyright Taligent, Inc. 1996, 1997 - All Rights Reserved
 * (C) Copyright IBM Corp. 1996 - 1998 - All Rights Reserved
 *
 *   The original version of this source code and documentation is copyrighted
 * and owned by Taligent, Inc., a wholly-owned subsidiary of IBM. These
 * materials are provided under terms of a License Agreement between Taligent
 * and Sun. This technology is protected by multiple US and International
 * patents. This notice and attribution to Taligent may not be removed.
 *   Taligent is a registered trademark of Taligent, Inc.
 *
 */
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.InvalidObjectException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.UncheckedIOException;
import java.text.AttributedCharacterIterator;
import java.text.CharacterIterator;
import java.text.ChoiceFormat;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.FieldPosition;
import java.text.Format;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import org.spf4j.base.CharSequences;

/**
 * Performance mutation of the JDK message formatter.
 * Lots things of things have been done:
 * 1) reduced the amount of garbage generated during formatting.
 * 2) made some method invocations static.
 * 3) made this more flexible and usable against StringBuilder not only StringBuffer...
 * 4) thrown exceptions provide more detail on what went wrong.
 * 5) cleaned up lots of static analisys reported issues.
 *
 * <code>MessageFormat</code> provides a means to produce concatenated messages in a language-neutral way. Use this to
 * construct messages displayed for end users.
 *
 * this implementation is based on java.text.MessageFormat with the goal to be a faster and more flexible implementation
 *
 * <p>
 * <code>MessageFormat</code> takes a set of objects, formats them, then inserts the formatted strings into the pattern
 * at the appropriate places.
 *
 * <p>
 * <strong>Note:</strong>
 * <code>MessageFormat</code> differs from the other <code>Format</code> classes in that you create a
 * <code>MessageFormat</code> object with one of its constructors (not with a <code>getInstance</code> style factory
 * method). The factory methods aren't necessary because <code>MessageFormat</code> itself doesn't implement locale
 * specific behavior. Any locale specific behavior is defined by the pattern that you provide as well as the sub-formats
 * used for inserted arguments.
 *
 * <h3><a name="patterns">Patterns and Their Interpretation</a></h3>
 *
 * <code>MessageFormat</code> uses patterns of the following form:
 * <blockquote><pre>
 * <i>MessageFormatPattern:</i>
 *         <i>String</i>
 *         <i>MessageFormatPattern</i> <i>FormatElement</i> <i>String</i>
 *
 * <i>FormatElement:</i>
 *         { <i>ArgumentIndex</i> }
 *         { <i>ArgumentIndex</i> , <i>FormatType</i> }
 *         { <i>ArgumentIndex</i> , <i>FormatType</i> , <i>FormatStyle</i> }
 *
 * <i>FormatType: one of </i>
 *         number date time choice
 *
 * <i>FormatStyle:</i>
 *         short
 *         medium
 *         long
 *         full
 *         integer
 *         currency
 *         percent
 *         <i>SubformatPattern</i>
 * </pre></blockquote>
 *
 * <p>
 * Within a <i>String</i>, a pair of single quotes can be used to quote any arbitrary characters except single quotes.
 * For example, pattern string <code>"'{0}'"</code> represents string <code>"{0}"</code>, not a <i>FormatElement</i>. A
 * single quote itself must be represented by doubled single quotes {@code ''} throughout a
 * <i>String</i>. For example, pattern string <code>"'{''}'"</code> is interpreted as a sequence of <code>'{</code>
 * (start of quoting and a left curly brace), <code>''</code> (a single quote), and <code>}'</code> (a right curly brace
 * and end of quoting),
 * <em>not</em> <code>'{'</code> and <code>'}'</code> (quoted left and right curly braces): representing string
 * <code>"{'}"</code>,
 * <em>not</em> <code>"{}"</code>.
 *
 * <p>
 * A <i>SubformatPattern</i> is interpreted by its corresponding sub-format, and sub-format-dependent pattern rules
 * apply. For example, pattern string <code>"{1,number,<u>$'#',##</u>}"</code> (<i>SubformatPattern</i> with underline)
 * will produce a number format with the pound-sign quoted, with a result such as: {@code
 * "$#31,45"}. Refer to each {@code Format} subclass documentation for details.
 *
 * <p>
 * Any unmatched quote is treated as closed at the end of the given pattern. For example, pattern string {@code "'{0}"}
 * is treated as pattern {@code "'{0}'"}.
 *
 * <p>
 * Any curly braces within an unquoted pattern must be balanced. For example, <code>"ab {0} de"</code> and
 * <code>"ab '}' de"</code> are valid patterns, but <code>"ab {0'}' de"</code>, <code>"ab } de"</code> and
 * <code>"''{''"</code> are not.
 *
 * <dl><dt><b>Warning:</b><dd>The rules for using quotes within message format patterns unfortunately have shown to be
 * somewhat confusing. In particular, it isn't always obvious to localizers whether single quotes need to be doubled or
 * not. Make sure to inform localizers about the rules, and tell them (for example, by using comments in resource bundle
 * source files) which strings will be processed by {@code MessageFormat}. Note that localizers may need to use single
 * quotes in translated strings where the original version doesn't have them.
 * </dl>
 * <p>
 * The <i>ArgumentIndex</i> value is a non-negative integer written using the digits {@code '0'} through {@code '9'},
 * and represents an index into the {@code arguments} array passed to the {@code format} methods or the result array
 * returned by the {@code parse} methods.
 * <p>
 * The <i>FormatType</i> and <i>FormatStyle</i> values are used to create a {@code Format} instance for the format
 * element. The following table shows how the values map to {@code Format} instances. Combinations not shown in the
 * table are illegal. A <i>SubformatPattern</i> must be a valid pattern string for the {@code Format} subclass used.
 *
 * <table border=1 summary="Shows how FormatType and FormatStyle values map to Format instances">
 * <tr>
 * <th id="ft" class="TableHeadingColor">FormatType
 * <th id="fs" class="TableHeadingColor">FormatStyle
 * <th id="sc" class="TableHeadingColor">Subformat Created
 * <tr>
 * <td headers="ft"><i>(none)</i>
 * <td headers="fs"><i>(none)</i>
 * <td headers="sc"><code>null</code>
 * <tr>
 * <td headers="ft" rowspan=5><code>number</code>
 * <td headers="fs"><i>(none)</i>
 * <td headers="sc">{@link NumberFormat#getInstance(Locale) NumberFormat.getInstance}{@code (getLocale())}
 * <tr>
 * <td headers="fs"><code>integer</code>
 * <td headers="sc">{@link NumberFormat#getIntegerInstance(Locale) NumberFormat.getIntegerInstance}{@code (getLocale())}
 * <tr>
 * <td headers="fs"><code>currency</code>
 * <td headers="sc">{@link NumberFormat#getCurrencyInstance(Locale) NumberFormat.getCurrencyInstance}
 * {@code (getLocale())}
 * <tr>
 * <td headers="fs"><code>percent</code>
 * <td headers="sc">{@link NumberFormat#getPercentInstance(Locale) NumberFormat.getPercentInstance}{@code (getLocale())}
 * <tr>
 * <td headers="fs"><i>SubformatPattern</i>
 * <td headers="sc">{@code new}
 * {@link DecimalFormat#DecimalFormat(String,DecimalFormatSymbols) DecimalFormat}{@code (subformatPattern,}
 * {@link DecimalFormatSymbols#getInstance(Locale) DecimalFormatSymbols.getInstance}{@code (getLocale()))}
 * <tr>
 * <td headers="ft" rowspan=6><code>date</code>
 * <td headers="fs"><i>(none)</i>
 * <td headers="sc">{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}
 * {@code (}{@link DateFormat#DEFAULT}{@code , getLocale())}
 * <tr>
 * <td headers="fs"><code>short</code>
 * <td headers="sc">{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}
 * {@code (}{@link DateFormat#SHORT}{@code , getLocale())}
 * <tr>
 * <td headers="fs"><code>medium</code>
 * <td headers="sc">{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}
 * {@code (}{@link DateFormat#DEFAULT}{@code , getLocale())}
 * <tr>
 * <td headers="fs"><code>long</code>
 * <td headers="sc">{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}
 * {@code (}{@link DateFormat#LONG}{@code , getLocale())}
 * <tr>
 * <td headers="fs"><code>full</code>
 * <td headers="sc">{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}
 * {@code (}{@link DateFormat#FULL}{@code , getLocale())}
 * <tr>
 * <td headers="fs"><i>SubformatPattern</i>
 * <td headers="sc">{@code new}
 * {@link SimpleDateFormat#SimpleDateFormat(String,Locale) SimpleDateFormat}{@code (subformatPattern, getLocale())}
 * <tr>
 * <td headers="ft" rowspan=6><code>time</code>
 * <td headers="fs"><i>(none)</i>
 * <td headers="sc">{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}
 * {@code (}{@link DateFormat#DEFAULT}{@code , getLocale())}
 * <tr>
 * <td headers="fs"><code>short</code>
 * <td headers="sc">{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}
 * {@code (}{@link DateFormat#SHORT}{@code , getLocale())}
 * <tr>
 * <td headers="fs"><code>medium</code>
 * <td headers="sc">{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}
 * {@code (}{@link DateFormat#DEFAULT}{@code , getLocale())}
 * <tr>
 * <td headers="fs"><code>long</code>
 * <td headers="sc">{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}
 * {@code (}{@link DateFormat#LONG}{@code , getLocale())}
 * <tr>
 * <td headers="fs"><code>full</code>
 * <td headers="sc">{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}
 * {@code (}{@link DateFormat#FULL}{@code , getLocale())}
 * <tr>
 * <td headers="fs"><i>SubformatPattern</i>
 * <td headers="sc">{@code new}
 * {@link SimpleDateFormat#SimpleDateFormat(String,Locale) SimpleDateFormat}{@code (subformatPattern, getLocale())}
 * <tr>
 * <td headers="ft"><code>choice</code>
 * <td headers="fs"><i>SubformatPattern</i>
 * <td headers="sc">{@code new} {@link ChoiceFormat#ChoiceFormat(String) ChoiceFormat}{@code (subformatPattern)}
 * </table>
 *
 * <h4>Usage Information</h4>
 *
 * <p>
 * Here are some examples of usage. In real internationalized programs, the message format pattern and other static
 * strings will, of course, be obtained from resource bundles. Other parameters will be dynamically determined at
 * runtime.
 * <p>
 * The first example uses the static method <code>MessageFormat.format</code>, which internally creates a
 * <code>MessageFormat</code> for one-time use:
 * <blockquote><pre>
 * int planet = 7;
 * String event = "a disturbance in the Force";
 *
 * String result = MessageFormat.format(
 *     "At {1,time} on {1,date}, there was {2} on planet {0,number,integer}.",
 *     planet, new Date(), event);
 * </pre></blockquote>
 * The output is:
 * <blockquote><pre>
 * At 12:30 PM on Jul 3, 2053, there was a disturbance in the Force on planet 7.
 * </pre></blockquote>
 *
 * <p>
 * The following example creates a <code>MessageFormat</code> instance that can be used repeatedly:
 * <blockquote><pre>
 * int fileCount = 1273;
 * String diskName = "MyDisk";
 * Object[] testArgs = {new Long(fileCount), diskName};
 *
 * MessageFormat form = new MessageFormat(
 *     "The disk \"{1}\" contains {0} file(s).");
 *
 * System.out.println(form.format(testArgs));
 * </pre></blockquote>
 * The output with different values for <code>fileCount</code>:
 * <blockquote><pre>
 * The disk "MyDisk" contains 0 file(s).
 * The disk "MyDisk" contains 1 file(s).
 * The disk "MyDisk" contains 1,273 file(s).
 * </pre></blockquote>
 *
 * <p>
 * For more sophisticated patterns, you can use a <code>ChoiceFormat</code> to produce correct forms for singular and
 * plural:
 * <blockquote><pre>
 * MessageFormat form = new MessageFormat("The disk \"{1}\" contains {0}.");
 * double[] filelimits = {0,1,2};
 * String[] filepart = {"no files","one file","{0,number} files"};
 * ChoiceFormat fileform = new ChoiceFormat(filelimits, filepart);
 * form.setFormatByArgumentIndex(0, fileform);
 *
 * int fileCount = 1273;
 * String diskName = "MyDisk";
 * Object[] testArgs = {new Long(fileCount), diskName};
 *
 * System.out.println(form.format(testArgs));
 * </pre></blockquote>
 * The output with different values for <code>fileCount</code>:
 * <blockquote><pre>
 * The disk "MyDisk" contains no files.
 * The disk "MyDisk" contains one file.
 * The disk "MyDisk" contains 1,273 files.
 * </pre></blockquote>
 *
 * <p>
 * You can create the <code>ChoiceFormat</code> programmatically, as in the above example, or by using a pattern. See
 * {@link ChoiceFormat} for more information.
 * <blockquote><pre>{@code
 * form.applyPattern(
 *    "There {0,choice,0#are no files|1#is one file|1<are {0,number,integer} files}.");
 * }</pre></blockquote>
 *
 * <p>
 * <strong>Note:</strong> As we see above, the string produced by a <code>ChoiceFormat</code> in
 * <code>MessageFormat</code> is treated as special; occurrences of '{' are used to indicate subformats, and cause
 * recursion. If you create both a <code>MessageFormat</code> and <code>ChoiceFormat</code> programmatically (instead of
 * using the string patterns), then be careful not to produce a format that recurses on itself, which will cause an
 * infinite loop.
 * <p>
 * When a single argument is parsed more than once in the string, the last match will be the final result of the
 * parsing. For example,
 * <blockquote><pre>
 * MessageFormat mf = new MessageFormat("{0,number,#.##}, {0,number,#.#}");
 * Object[] objs = {new Double(3.1415)};
 * String result = mf.format( objs );
 * // result now equals "3.14, 3.1"
 * objs = null;
 * objs = mf.parse(result, new ParsePosition(0));
 * // objs now equals {new Double(3.1)}
 * </pre></blockquote>
 *
 * <p>
 * Likewise, parsing with a {@code MessageFormat} object using patterns containing multiple occurrences of the same
 * argument would return the last match. For example,
 * <blockquote><pre>
 * MessageFormat mf = new MessageFormat("{0}, {0}, {0}");
 * String forParsing = "x, y, z";
 * Object[] objs = mf.parse(forParsing, new ParsePosition(0));
 * // result now equals {new String("z")}
 * </pre></blockquote>
 *
 * <h4><a name="synchronization">Synchronization</a></h4>
 *
 * <p>
 * Message formats are not synchronized. It is recommended to create separate format instances for each thread. If
 * multiple threads access a format concurrently, it must be synchronized externally.
 *
 * @see java.util.Locale
 * @see Format
 * @see NumberFormat
 * @see DecimalFormat
 * @see DecimalFormatSymbols
 * @see ChoiceFormat
 * @see DateFormat
 * @see SimpleDateFormat
 *
 * @author Mark Davis
 */
@SuppressFBWarnings("IMC_IMMATURE_CLASS_WRONG_FIELD_ORDER")
@NotThreadSafe
public final class MessageFormat extends Format {

  private static final long serialVersionUID = 1L;

  // Indices for segments
  private static final int SEG_RAW = 0;
  private static final int SEG_INDEX = 1;
  private static final int SEG_TYPE = 2;
  private static final int SEG_MODIFIER = 3; // modifier or subformat

  // Indices for type keywords
  private static final int TYPE_NULL = 0;
  private static final int TYPE_NUMBER = 1;
  private static final int TYPE_DATE = 2;
  private static final int TYPE_TIME = 3;
  private static final int TYPE_CHOICE = 4;

  private static final String[] TYPE_KEYWORDS = {
    "",
    "number",
    "date",
    "time",
    "choice"
  };

  // Indices for number modifiers
  private static final int MODIFIER_DEFAULT = 0; // common in number and date-time
  private static final int MODIFIER_CURRENCY = 1;
  private static final int MODIFIER_PERCENT = 2;
  private static final int MODIFIER_INTEGER = 3;

  private static final String[] NUMBER_MODIFIER_KEYWORDS = {
    "",
    "currency",
    "percent",
    "integer"
  };

  private static final String[] DATE_TIME_MODIFIER_KEYWORDS = {
    "",
    "short",
    "medium",
    "long",
    "full"
  };

  // Date-time style values corresponding to the date-time modifiers.
  private static final int[] DATE_TIME_MODIFIERS = {
    DateFormat.DEFAULT,
    DateFormat.SHORT,
    DateFormat.MEDIUM,
    DateFormat.LONG,
    DateFormat.FULL,};



  // ===========================privates============================
  /**
   * The locale to use for formatting numbers and dates.
   *
   * @serial
   */
  private Locale locale;

  /**
   * The string that the formatted values are to be plugged into. In other words, this is the pattern supplied on
   * construction with all of the {} expressions taken out.
   *
   * @serial
   */
  private transient CharSequence pattern;

  /**
   * An array of formatters, which are used to format the arguments.
   *
   * @serial
   */
  private FormatInfo[] formats = {};

  /**
   * One less than the number of entries in <code>offsets</code>. Can also be thought of as the index of the
   * highest-numbered element in <code>offsets</code> that is being used. All of these arrays should have the same
   * number of elements being used as <code>offsets</code> does, and so this variable suffices to tell us how many
   * entries are in all of them.
   *
   * @serial
   */
  private int maxOffset = -1;


  private int hash = 0;


  /**
   * Constructs a FastMessageFormat for the default {@link java.util.Locale.Category#FORMAT FORMAT} locale and the
   * specified pattern. The constructor first sets the locale, then parses the pattern and creates a list of subformats
   * for the format elements contained in it. Patterns and their interpretation are specified in the
   * <a href="#patterns">class description</a>.
   *
   * @param pattern the pattern for this message format
   * @exception IllegalArgumentException if the pattern is invalid
   */
  public MessageFormat(String pattern) {
    this.locale = Locale.getDefault(Locale.Category.FORMAT);
    applyPattern(pattern);
  }

  /**
   * Constructs a FastMessageFormat for the specified locale and pattern. The constructor first sets the locale, then
   * parses the pattern and creates a list of sub-formats for the format elements contained in it. Patterns and their
   * interpretation are specified in the
   * <a href="#patterns">class description</a>.
   *
   * @param pattern the pattern for this message format
   * @param locale the locale for this message format
   * @exception IllegalArgumentException if the pattern is invalid
   * @since 1.4
   */
  @SuppressFBWarnings("EI_EXPOSE_REP2")
  public MessageFormat(String pattern, Locale locale) {
    this.locale = locale;
    applyPattern(pattern);
  }

  /**
   * Sets the locale to be used when creating or comparing subformats. This affects subsequent calls
   * <ul>
   * <li>to the {@link #applyPattern applyPattern} and {@link #toPattern toPattern} methods if format elements specify a
   * format type and therefore have the subformats created in the <code>applyPattern</code> method, as well as
   * <li>to the <code>format</code> and {@link #formatToCharacterIterator formatToCharacterIterator} methods if format
   * elements do not specify a format type and therefore have the subformats created in the formatting methods.
   * </ul>
   * Subformats that have already been created are not affected.
   *
   * @param locale the locale to be used when creating or comparing subformats
   */
  @SuppressFBWarnings("EI_EXPOSE_REP2")
  public void setLocale(final Locale locale) {
    this.locale = locale;
  }

  /**
   * Gets the locale that's used when creating or comparing subformats.
   *
   * @return the locale used when creating or comparing subformats
   */
  @SuppressFBWarnings("EI_EXPOSE_REP")
  public Locale getLocale() {
    return locale;
  }

  /**
   * Sets the pattern used by this message format. The method parses the pattern and creates a list of subformats for
   * the format elements contained in it. Patterns and their interpretation are specified in the
   * <a href="#patterns">class description</a>.
   *
   * @param pattern the pattern for this message format
   * @exception IllegalArgumentException if the pattern is invalid
   */
  @SuppressWarnings("fallthrough") // fallthrough in switch is expected, suppress it
  @SuppressFBWarnings("CLI_CONSTANT_LIST_INDEX")
  public void applyPattern(final String pattern) {
    StringBuilder[] segments = new StringBuilder[4];
    // Allocate only segments[SEG_RAW] here. The rest are
    // allocated on demand.
    final int length = pattern.length();
    segments[SEG_RAW] = new StringBuilder(length);

    int part = SEG_RAW;
    int formatNumber = 0;
    boolean inQuote = false;
    int braceStack = 0;
    maxOffset = -1;
    for (int i = 0; i < length; ++i) {
      char ch = pattern.charAt(i);
      if (part == SEG_RAW) {
        if (ch == '\'') {
          int next = i + 1;
          if (next < length
                  && pattern.charAt(next) == '\'') {
            segments[part].append(ch);  // handle doubles
            i = next;
          } else {
            inQuote = !inQuote;
          }
        } else if (ch == '{' && !inQuote) {
          part = SEG_INDEX;
          if (segments[part] == null) {
            segments[part] = new StringBuilder();
          }
        } else {
          segments[part].append(ch);
        }
      } else if (inQuote) {              // just copy quotes in parts
        segments[part].append(ch);
        if (ch == '\'') {
          inQuote = false;
        }
      } else {
        switch (ch) {
          case ',':
            if (part < SEG_MODIFIER) {
              if (segments[++part] == null) {
                segments[part] = new StringBuilder();
              }
            } else {
              segments[part].append(ch);
            }
            break;
          case '{':
            ++braceStack;
            segments[part].append(ch);
            break;
          case '}':
            if (braceStack == 0) {
              part = SEG_RAW;
              makeFormat(formatNumber, segments);
              formatNumber++;
              // throw away other segments
              segments[SEG_INDEX] = null;
              segments[SEG_TYPE] = null;
              segments[SEG_MODIFIER] = null;
            } else {
              --braceStack;
              segments[part].append(ch);
            }
            break;
          case ' ':
            // Skip any leading space chars for SEG_TYPE.
            if (part != SEG_TYPE || segments[SEG_TYPE].length() > 0) {
              segments[part].append(ch);
            }
            break;
          case '\'':
            inQuote = true;
          // fall through, so we keep quotes in other parts
          default:
            segments[part].append(ch);
            break;
        }
      }
    }
    if (braceStack == 0 && part != 0) {
      maxOffset = -1;
      throw new IllegalArgumentException("Unmatched braces in the pattern: " + pattern);
    }
    this.pattern = segments[0];
  }

  /**
   * Returns a pattern representing the current state of the message format. The string is constructed from internal
   * information and therefore does not necessarily equal the previously applied pattern.
   *
   * @return a pattern representing the current state of the message format
   */
  @SuppressFBWarnings({"CLI_CONSTANT_LIST_INDEX", "ITC_INHERITANCE_TYPE_CHECKING"})
  public String toPattern() {
    // later, make this more extensible
    int lastOffset = 0;
    StringBuilder result = new StringBuilder();
    for (int i = 0; i <= maxOffset; ++i) {
      FormatInfo finfo = formats[i];
      int offset = finfo.getOffset();
      copyAndFixQuotes(pattern, lastOffset, offset, result);
      lastOffset = offset;
      result.append('{').append(finfo.getArgumentNumber());
      Format fmt = finfo.getFormat();
      if (fmt instanceof NumberFormat) {
        if (fmt.equals(NumberFormat.getInstance(locale))) {
          result.append(",number");
        } else if (fmt.equals(NumberFormat.getCurrencyInstance(locale))) {
          result.append(",number,currency");
        } else if (fmt.equals(NumberFormat.getPercentInstance(locale))) {
          result.append(",number,percent");
        } else if (fmt.equals(NumberFormat.getIntegerInstance(locale))) {
          result.append(",number,integer");
        } else if (fmt instanceof DecimalFormat) {
          result.append(",number,").append(((DecimalFormat) fmt).toPattern());
        } else if (fmt instanceof ChoiceFormat) {
          result.append(",choice,").append(((ChoiceFormat) fmt).toPattern());
        } else {
          throw new UnsupportedOperationException("Unsupported format " + fmt);
        }
      } else if (fmt instanceof DateFormat) {
        int index;
        for (index = MODIFIER_DEFAULT; index < DATE_TIME_MODIFIERS.length; index++) {
          DateFormat df = DateFormat.getDateInstance(DATE_TIME_MODIFIERS[index],
                  locale);
          if (fmt.equals(df)) {
            result.append(",date");
            break;
          }
          df = DateFormat.getTimeInstance(DATE_TIME_MODIFIERS[index],
                  locale);
          if (fmt.equals(df)) {
            result.append(",time");
            break;
          }
        }
        if (index >= DATE_TIME_MODIFIERS.length) {
          if (fmt instanceof SimpleDateFormat) {
            result.append(",date,").append(((SimpleDateFormat) fmt).toPattern());
          } else {
            throw new UnsupportedOperationException("Unsupported format " + fmt);
          }
        } else if (index != MODIFIER_DEFAULT) {
          result.append(',').append(DATE_TIME_MODIFIER_KEYWORDS[index]);
        }
      } else if (fmt != null) {
        throw new UnsupportedOperationException("Unsupported format " + fmt);
      }
      result.append('}');
    }
    copyAndFixQuotes(pattern, lastOffset, pattern.length(), result);
    return result.toString();
  }

  /**
   * Sets the formats to use for the values passed into <code>format</code> methods or returned from <code>parse</code>
   * methods. The indices of elements in <code>newFormats</code> correspond to the argument indices used in the
   * previously set pattern string. The order of formats in <code>newFormats</code> thus corresponds to the order of
   * elements in the <code>arguments</code> array passed to the <code>format</code> methods or the result array returned
   * by the <code>parse</code> methods.
   * <p>
   * If an argument index is used for more than one format element in the pattern string, then the corresponding new
   * format is used for all such format elements. If an argument index is not used for any format element in the pattern
   * string, then the corresponding new format is ignored. If fewer formats are provided than needed, then only the
   * formats for argument indices less than <code>newFormats.length</code> are replaced.
   *
   * @param newFormats the new formats to use
   * @exception NullPointerException if <code>newFormats</code> is null
   * @since 1.4
   */
  public void setFormatsByArgumentIndex(final Format[] newFormats) {
    for (int i = 0; i <= maxOffset; i++) {
      final FormatInfo finfo = formats[i];
      int j = finfo.getArgumentNumber();
      if (j < newFormats.length) {
        formats[i] = new FormatInfo(newFormats[j], finfo.getOffset(), j);
      }
    }
  }

  /**
   * Sets the formats to use for the format elements in the previously set pattern string. The order of formats in
   * <code>newFormats</code> corresponds to the order of format elements in the pattern string.
   * <p>
   * If more formats are provided than needed by the pattern string, the remaining ones are ignored. If fewer formats
   * are provided than needed, then only the first <code>newFormats.length</code> formats are replaced.
   * <p>
   * Since the order of format elements in a pattern string often changes during localization, it is generally better to
   * use the {@link #setFormatsByArgumentIndex setFormatsByArgumentIndex} method, which assumes an order of formats
   * corresponding to the order of elements in the <code>arguments</code> array passed to the <code>format</code>
   * methods or the result array returned by the <code>parse</code> methods.
   *
   * @param newFormats the new formats to use
   * @exception NullPointerException if <code>newFormats</code> is null
   */
  public void setFormats(final Format[] newFormats) {
    int runsToCopy = newFormats.length;
    if (runsToCopy > maxOffset + 1) {
      runsToCopy = maxOffset + 1;
    }
    for (int i = 0; i < runsToCopy; i++) {
      FormatInfo finfo = formats[i];
      formats[i] = new FormatInfo(newFormats[i], finfo.getOffset(), finfo.getArgumentNumber());
    }
  }

  /**
   * Sets the format to use for the format elements within the previously set pattern string that use the given argument
   * index. The argument index is part of the format element definition and represents an index into the
   * <code>arguments</code> array passed to the <code>format</code> methods or the result array returned by the
   * <code>parse</code> methods.
   * <p>
   * If the argument index is used for more than one format element in the pattern string, then the new format is used
   * for all such format elements. If the argument index is not used for any format element in the pattern string, then
   * the new format is ignored.
   *
   * @param argumentIndex the argument index for which to use the new format
   * @param newFormat the new format to use
   * @since 1.4
   */
  public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) {
    for (int j = 0; j <= maxOffset; j++) {
      FormatInfo finfo = formats[j];
      int argNr = finfo.getArgumentNumber();
      if (argNr == argumentIndex) {
        formats[j] = new FormatInfo(newFormat, finfo.getOffset(), argNr);
      }
    }
  }

  /**
   * Sets the format to use for the format element with the given format element index within the previously set pattern
   * string. The format element index is the zero-based number of the format element counting from the start of the
   * pattern string.
   * <p>
   * Since the order of format elements in a pattern string often changes during localization, it is generally better to
   * use the {@link #setFormatByArgumentIndex setFormatByArgumentIndex} method, which accesses format elements based on
   * the argument index they specify.
   *
   * @param formatElementIndex the index of a format element within the pattern
   * @param newFormat the format to use for the specified format element
   * @exception ArrayIndexOutOfBoundsException if {@code formatElementIndex} is equal to or larger than the number of
   * format elements in the pattern string
   */
  public void setFormat(final int formatElementIndex, final Format newFormat) {
    FormatInfo finfo = formats[formatElementIndex];
    formats[formatElementIndex] = new FormatInfo(newFormat, finfo.getOffset(), finfo.getArgumentNumber());
  }

  /**
   * Gets the formats used for the values passed into <code>format</code> methods or returned from <code>parse</code>
   * methods. The indices of elements in the returned array correspond to the argument indices used in the previously
   * set pattern string. The order of formats in the returned array thus corresponds to the order of elements in the
   * <code>arguments</code> array passed to the <code>format</code> methods or the result array returned by the
   * <code>parse</code> methods.
   * <p>
   * If an argument index is used for more than one format element in the pattern string, then the format used for the
   * last such format element is returned in the array. If an argument index is not used for any format element in the
   * pattern string, then null is returned in the array.
   *
   * @return the formats used for the arguments within the pattern
   * @since 1.4
   */
//    public Format[] getFormatsByArgumentIndex() {
//        int maximumArgumentNumber = -1;
//        for (int i = 0; i <= maxOffset; i++) {
//            if (argumentNumbers[i] > maximumArgumentNumber) {
//                maximumArgumentNumber = argumentNumbers[i];
//            }
//        }
//        Format[] resultArray = new Format[maximumArgumentNumber + 1];
//        for (int i = 0; i <= maxOffset; i++) {
//            resultArray[argumentNumbers[i]] = formats[i];
//        }
//        return resultArray;
//    }
  /**
   * Gets the formats used for the format elements in the previously set pattern string. The order of formats in the
   * returned array corresponds to the order of format elements in the pattern string.
   * <p>
   * Since the order of format elements in a pattern string often changes during localization, it's generally better to
   * use the {@link #getFormatsByArgumentIndex getFormatsByArgumentIndex} method, which assumes an order of formats
   * corresponding to the order of elements in the <code>arguments</code> array passed to the <code>format</code>
   * methods or the result array returned by the <code>parse</code> methods.
   *
   * @return the formats used for the format elements in the pattern
   */
  public Format[] getFormats() {
    Format[] resultArray = new Format[maxOffset + 1];
    System.arraycopy(formats, 0, resultArray, 0, maxOffset + 1);
    return resultArray;
  }

  /**
   * Formats an array of objects and appends the <code>MessageFormat</code>'s pattern, with format elements replaced by
   * the formatted objects, to the provided <code>StringBuffer</code>.
   * <p>
   * The text substituted for the individual format elements is derived from the current subformat of the format element
   * and the <code>arguments</code> element at the format element's argument index as indicated by the first matching
   * line of the following table. An argument is <i>unavailable</i> if <code>arguments</code> is <code>null</code> or
   * has fewer than argumentIndex+1 elements.
   *
   * <table border=1 summary="Examples of subformat,argument,and formatted text">
   * <tr>
   * <th>Subformat
   * <th>Argument
   * <th>Formatted Text
   * <tr>
   * <td><i>any</i>
   * <td><i>unavailable</i>
   * <td><code>"{" + argumentIndex + "}"</code>
   * <tr>
   * <td><i>any</i>
   * <td><code>null</code>
   * <td><code>"null"</code>
   * <tr>
   * <td><code>instanceof ChoiceFormat</code>
   * <td><i>any</i>
   * <td><code>subformat.format(argument).indexOf('{') &gt;= 0 ?<br>
   * (new MessageFormat(subformat.format(argument), getLocale())).format(argument) : subformat.format(argument)</code>
   * <tr>
   * <td><code>!= null</code>
   * <td><i>any</i>
   * <td><code>subformat.format(argument)</code>
   * <tr>
   * <td><code>null</code>
   * <td><code>instanceof Number</code>
   * <td><code>NumberFormat.getInstance(getLocale()).format(argument)</code>
   * <tr>
   * <td><code>null</code>
   * <td><code>instanceof Date</code>
   * <td><code>DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, getLocale()).format(argument)</code>
   * <tr>
   * <td><code>null</code>
   * <td><code>instanceof String</code>
   * <td><code>argument</code>
   * <tr>
   * <td><code>null</code>
   * <td><i>any</i>
   * <td><code>argument.toString()</code>
   * </table>
   * <p>
   * If <code>pos</code> is non-null, and refers to <code>Field.ARGUMENT</code>, the location of the first formatted
   * string will be returned.
   *
   * @param arguments an array of objects to be formatted and substituted.
   * @param result where text is appended.
   * @param pos On input: an alignment field, if desired. On output: the offsets of the alignment field.
   * @return the string buffer passed in as {@code result}, with formatted text appended
   * @exception IllegalArgumentException if an argument in the <code>arguments</code> array is not of the type expected
   * by the format element(s) that use it.
   */
  public final <T extends CharSequence & Appendable> boolean[] format(Object[] arguments, T result,
          @Nullable FieldPosition pos) throws IOException {
    return subformat(arguments, result, pos, null);
  }

  public final <T extends CharSequence & Appendable> boolean[] format(Object[] arguments, T result) throws IOException {
    return format(arguments, result, null);
  }

  /**
   * Creates a MessageFormat with the given pattern and uses it to format the given arguments. This is equivalent to
   * <blockquote>
   * <code>(new {@link #MessageFormat(String) MessageFormat}(pattern)).
   * {@link #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition) format}(arguments,
   * new StringBuffer(), null).toString()</code>
   * </blockquote>
   *
   * @param pattern the pattern string
   * @param arguments object(s) to format
   * @return the formatted string
   * @exception IllegalArgumentException if the pattern is invalid, or if an argument in the <code>arguments</code>
   * array is not of the type expected by the format element(s) that use it.
   */
  public static String format(final String pattern, final Object... arguments) {
    MessageFormat temp = new MessageFormat(pattern);
    return temp.format(arguments);
  }

  // Overrides
  /**
   * Formats an array of objects and appends the <code>MessageFormat</code>'s pattern, with format elements replaced by
   * the formatted objects, to the provided <code>StringBuffer</code>. This is equivalent to
   * <blockquote>
   * <code>{@link #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition) format}((Object[])
   * arguments, result, pos)</code>
   * </blockquote>
   *
   * @param arguments an array of objects to be formatted and substituted.
   * @param result where text is appended.
   * @param pos On input: an alignment field, if desired. On output: the offsets of the alignment field.
   * @exception IllegalArgumentException if an argument in the <code>arguments</code> array is not of the type expected
   * by the format element(s) that use it.
   */
  public final <T extends CharSequence & Appendable> boolean[] format(final Object arguments, final T result,
          final FieldPosition pos) throws IOException {
    if (arguments instanceof Object[]) {
      return subformat((Object[]) arguments, result, pos, null);
    } else {
      return subformat(new Object [] {arguments}, result, pos, null);
    }
  }

  /**
   * Formats an array of objects and inserts them into the <code>MessageFormat</code>'s pattern, producing an
   * <code>AttributedCharacterIterator</code>. You can use the returned <code>AttributedCharacterIterator</code> to
   * build the resulting String, as well as to determine information about the resulting String.
   * <p>
   * The text of the returned <code>AttributedCharacterIterator</code> is the same that would be returned by
   * <blockquote>
   * <code>{@link #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition) format}(arguments, new
   * StringBuffer(), null).toString()</code>
   * </blockquote>
   * <p>
   * In addition, the <code>AttributedCharacterIterator</code> contains at least attributes indicating where text was
   * generated from an argument in the <code>arguments</code> array. The keys of these attributes are of type
   * <code>MessageFormat.Field</code>, their values are <code>Integer</code> objects indicating the index in the
   * <code>arguments</code> array of the argument from which the text was generated.
   * <p>
   * The attributes/value from the underlying <code>Format</code> instances that <code>MessageFormat</code> uses will
   * also be placed in the resulting <code>AttributedCharacterIterator</code>. This allows you to not only find where an
   * argument is placed in the resulting String, but also which fields it contains in turn.
   *
   * @param arguments an array of objects to be formatted and substituted.
   * @return AttributedCharacterIterator describing the formatted value.
   * @exception NullPointerException if <code>arguments</code> is null.
   * @exception IllegalArgumentException if an argument in the <code>arguments</code> array is not of the type expected
   * by the format element(s) that use it.
   * @since 1.4
   */
  public AttributedCharacterIterator formatToCharacterIterator(@Nonnull Object arguments) {
    StringBuilder result = new StringBuilder();
    ArrayList<AttributedCharacterIterator> iterators = new ArrayList<>();
    try {
      subformat((Object[]) arguments, result, null, iterators);
    } catch (IOException ex) {
      throw new UncheckedIOException(ex);
    }
    if (iterators.isEmpty()) {
      return createAttributedCharacterIterator("");
    }
    return new AttributedString(
            iterators.toArray(
                    new AttributedCharacterIterator[iterators.size()])).getIterator();
  }

  /**
   * Parses the string.
   *
   * <p>
   * Caveats: The parse may fail in a number of circumstances. For example:
   * <ul>
   * <li>If one of the arguments does not occur in the pattern.
   * <li>If the format of an argument loses information, such as with a choice format where a large number formats to
   * "many".
   * <li>Does not yet handle recursion (where the substituted strings contain {n} references.)
   * <li>Will not always find a match (or the correct match) if some part of the parse is ambiguous. For example, if the
   * pattern "{1},{2}" is used with the string arguments {"a,b", "c"}, it will format as "a,b,c". When the result is
   * parsed, it will return {"a", "b,c"}.
   * <li>If a single argument is parsed more than once in the string, then the later parse wins.
   * </ul>
   * When the parse fails, use ParsePosition.getErrorIndex() to find out where in the string the parsing failed. The
   * returned error index is the starting offset of the sub-patterns that the string is comparing with. For example, if
   * the parsing string "AAA {0} BBB" is comparing against the pattern "AAD {0} BBB", the error index is 0. When an
   * error occurs, the call to this method will return null. If the source is null, return an empty array. (zoltan:
   * yuck)
   *
   *
   * @param source the string to parse
   * @param pos the parse position
   * @return an array of parsed objects
   */
  @Nullable
  @SuppressFBWarnings({"PZLA_PREFER_ZERO_LENGTH_ARRAYS", "STT_STRING_PARSING_A_FIELD"}) // maintaining JDK behavior
  public Object[] parse(@Nullable String source, @Nonnull ParsePosition pos) {

    if (source == null) {
      return org.spf4j.base.Arrays.EMPTY_OBJ_ARRAY;
    }

    int maximumArgumentNumber = -1;
    for (int i = 0; i <= maxOffset; i++) {
      FormatInfo finfo = formats[i];
      final int argumentNumber = finfo.getArgumentNumber();
      if (argumentNumber > maximumArgumentNumber) {
        maximumArgumentNumber = argumentNumber;
      }
    }
    Object[] resultArray = new Object[maximumArgumentNumber + 1];

    int patternOffset = 0;
    int sourceOffset = pos.getIndex();
    ParsePosition tempStatus = new ParsePosition(0);
    for (int i = 0; i <= maxOffset; ++i) {
      // match up to format
      FormatInfo finfo = formats[i];
      int len = finfo.getOffset() - patternOffset;
      if (len == 0 || CharSequences.regionMatches(pattern, patternOffset, source, sourceOffset, len)) {
        sourceOffset += len;
        patternOffset += len;
      } else {
        pos.setErrorIndex(sourceOffset);
        return null;
      }

      // now use format
      if (finfo.getFormat() == null) {   // string format
        // if at end, use longest possible match
        // otherwise uses first match to intervening string
        // does NOT recursively try all possibilities
        int tempLength = (i != maxOffset) ? formats[i + 1].getOffset() : pattern.length();

        int next;
        if (patternOffset >= tempLength) {
          next = source.length();
        } else {
          next = source.indexOf(pattern.subSequence(patternOffset, tempLength).toString(), sourceOffset);
        }

        if (next < 0) {
          pos.setErrorIndex(sourceOffset);
          return null;
        } else {
          String strValue = source.substring(sourceOffset, next);
          int argNr = finfo.getArgumentNumber();
          if (!strValue.equals("{" + argNr + "}")) {
            resultArray[argNr] = source.substring(sourceOffset, next);
          }
          sourceOffset = next;
        }
      } else {
        tempStatus.setIndex(sourceOffset);
        resultArray[finfo.getArgumentNumber()] = finfo.getFormat().parseObject(source, tempStatus);
        if (tempStatus.getIndex() == sourceOffset) {
          pos.setErrorIndex(sourceOffset);
          return null; // leave index as is to signal error
        }
        sourceOffset = tempStatus.getIndex(); // update
      }
    }
    int len = pattern.length() - patternOffset;
    if (len == 0 || CharSequences.regionMatches(pattern, patternOffset, source, sourceOffset, len)) {
      pos.setIndex(sourceOffset + len);
    } else {
      pos.setErrorIndex(sourceOffset);
      return null;
    }
    return resultArray;
  }

  /**
   * Parses text from the beginning of the given string to produce an object array. The method may not use the entire
   * text of the given string.
   * <p>
   * See the {@link #parse(String, ParsePosition)} method for more information on message parsing.
   *
   * @param source A <code>String</code> whose beginning should be parsed.
   * @return An <code>Object</code> array parsed from the string.
   * @exception ParseException if the beginning of the specified string cannot be parsed.
   */
  @Nullable
  public Object[] parse(String source) throws ParseException {
    ParsePosition pos = new ParsePosition(0);
    Object[] result = parse(source, pos);
    if (pos.getIndex() == 0) // unchanged, returned object is null
    {
      throw new ParseException("MessageFormat source = " + source + " parse error!", pos.getErrorIndex());
    }

    return result;
  }

  /**
   * Parses text from a string to produce an object array.
   * <p>
   * The method attempts to parse text starting at the index given by <code>pos</code>. If parsing succeeds, then the
   * index of <code>pos</code> is updated to the index after the last character used (parsing does not necessarily use
   * all characters up to the end of the string), and the parsed object array is returned. The updated <code>pos</code>
   * can be used to indicate the starting point for the next call to this method. If an error occurs, then the index of
   * <code>pos</code> is not changed, the error index of <code>pos</code> is set to the index of the character where the
   * error occurred, and null is returned.
   * <p>
   * See the {@link #parse(String, ParsePosition)} method for more information on message parsing.
   *
   * @param source A <code>String</code>, part of which should be parsed.
   * @param pos A <code>ParsePosition</code> object with index and error index information as described above.
   * @return An <code>Object</code> array parsed from the string. In case of error, returns null.
   * @exception NullPointerException if <code>pos</code> is null.
   */
  @Nullable
  public Object parseObject(String source, ParsePosition pos) {
    return parse(source, pos);
  }

  /**
   * Creates and returns a copy of this object.
   *
   * @return a clone of this instance.
   */
  public MessageFormat clone() {
    MessageFormat other = (MessageFormat) super.clone();

    // clone arrays. Can't do with utility because of bug in Cloneable
    other.formats = formats.clone(); // shallow clone
    for (int i = 0; i < formats.length; ++i) {
      FormatInfo finfo = formats[i];
      if (finfo != null) {
        other.formats[i] = finfo.clone();
      }
    }
    return other;
  }

  /**
   * Equality comparison between two message format objects
   */
  public boolean equals(Object obj) {
    if (this == obj) // quick check
    {
      return true;
    }
    if (obj == null || getClass() != obj.getClass()) {
      return false;
    }
    MessageFormat other = (MessageFormat) obj;
    return (maxOffset == other.maxOffset
            && CharSequences.equals(pattern, other.pattern)
            && ((locale != null && locale.equals(other.locale))
            || (locale == null && other.locale == null))
            && Arrays.equals(formats, other.formats));
  }

  /**
   * Generates a hash code for the message format object.
   */
  public int hashCode() {
    int h = hash;
    if (h == 0) {
      h = CharSequences.hashcode(pattern);
      hash = h;
    }
    return h;
  }

  @Override
  public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
    try {
      syntethicFormat(obj, toAppendTo, pos);
      return toAppendTo;
    } catch (IOException ex) {
      throw new UncheckedIOException(ex);
    }
  }

  private <T extends CharSequence & Appendable> boolean[] syntethicFormat(Object obj, T toAppendTo, FieldPosition pos)
          throws IOException {
    return format(obj, toAppendTo, pos);
  }


  /**
   * Internal routine used by format. If <code>characterIterators</code> is non-null, AttributedCharacterIterator will
   * be created from the sub-formats as necessary. If <code>characterIterators</code> is null and <code>fp</code> is
   * non-null and identifies <code>Field.MESSAGE_ARGUMENT</code>, the location of the first replaced argument will be
   * set in it.
   *
   * @exception IllegalArgumentException if an argument in the <code>arguments</code> array is not of the type expected
   * by the format element(s) that use it.
   */
  @SuppressFBWarnings({"PRMC_POSSIBLY_REDUNDANT_METHOD_CALLS"}) // Unfortunately I have no other way to write this
  // without code duplication to work for StringBuilder and StringBuffer....
  private <T extends Appendable & CharSequence> boolean[] subformat(@Nullable Object[] arguments, @Nonnull T result,
          @Nullable FieldPosition fp,
          @Nullable List<AttributedCharacterIterator> characterIterators)
          throws IOException {
    int lastOffset = 0;
    int last = result.length();
    boolean[] used;
    if (arguments == null)  {
      used = org.spf4j.base.Arrays.EMPTY_BOOLEAN_ARRAY;
    } else {
      used = new boolean[arguments.length];
    }
    for (int i = 0; i <= maxOffset; ++i) {
      FormatInfo finfo = formats[i];
      int offset = finfo.getOffset();
      result.append(pattern, lastOffset, offset);
      lastOffset = offset;
      int argumentNumber = finfo.getArgumentNumber();
      if (arguments == null || argumentNumber >= arguments.length) {
        result.append('{').append(Integer.toString(argumentNumber)).append('}');
        continue;
      }
      used[argumentNumber] = true;
      Object obj = arguments[argumentNumber];
      String arg = null;
      Format subFormatter = null;
      Format fmt = finfo.getFormat();
      if (obj == null) {
        arg = "null";
      } else if (fmt != null) {
        subFormatter = fmt;
        if (subFormatter instanceof ChoiceFormat) {
          arg = subFormatter.format(obj);
          if (arg.indexOf('{') >= 0) {
            subFormatter = new MessageFormat(arg, locale);
            obj = arguments;
            arg = null;
          }
        }
      } else if (obj instanceof Number) {
        // format number if can
        subFormatter = NumberFormat.getInstance(locale);
      } else if (obj instanceof Date) {
        // format a Date if can
        subFormatter = DateFormat.getDateTimeInstance(
                DateFormat.SHORT, DateFormat.SHORT, locale);//fix
      } else if (obj instanceof String) {
        arg = (String) obj;

      } else {
        arg = obj.toString();
      }

      // At this point we are in two states, either subFormatter
      // is non-null indicating we should format obj using it,
      // or arg is non-null and we should use it as the value.
      if (characterIterators != null) {
        // If characterIterators is non-null, it indicates we need
        // to get the CharacterIterator from the child formatter.
        int cl = result.length();
        if (last != cl) {
          characterIterators.add(
                  createAttributedCharacterIterator(result.subSequence(last, cl).toString()));
          last = cl;
        }
        if (subFormatter != null) {
          AttributedCharacterIterator subIterator
                  = subFormatter.formatToCharacterIterator(obj);

          append(result, subIterator);
          int cl2 = result.length();
          if (last != cl2) {
            characterIterators.add(
                    createAttributedCharacterIterator(
                            subIterator, java.text.MessageFormat.Field.ARGUMENT,
                            Integer.valueOf(argumentNumber)));
            last = cl2;
          }
          arg = null;
        }
        if (arg != null && arg.length() > 0) {
          result.append(arg);
          characterIterators.add(
                  createAttributedCharacterIterator(
                          arg, java.text.MessageFormat.Field.ARGUMENT,
                          Integer.valueOf(argumentNumber)));
          last = result.length();
        }
      } else {
        if (subFormatter != null) {
          arg = subFormatter.format(obj);
        }
        last = result.length();
        result.append(arg);
        if (i == 0 && fp != null && java.text.MessageFormat.Field.ARGUMENT.equals(
                fp.getFieldAttribute())) {
          fp.setBeginIndex(last);
          fp.setEndIndex(result.length());
        }
        last = result.length();
      }

    }
    result.append(pattern, lastOffset, pattern.length());
    if (characterIterators != null && last != result.length()) {
      characterIterators.add(createAttributedCharacterIterator(result.subSequence(last, result.length()).toString()));
    }
    return used;
  }

  /**
   * Convenience method to append all the characters in <code>iterator</code> to the StringBuffer <code>result</code>.
   */
  private static void append(Appendable result, CharacterIterator iterator) throws IOException {
    final char first = iterator.first();
    if (first != CharacterIterator.DONE) {
      result.append(first);
      char aChar;
      while ((aChar = iterator.next()) != CharacterIterator.DONE) {
        result.append(aChar);
      }
    }
  }


  @SuppressFBWarnings("CLI_CONSTANT_LIST_INDEX") //jdk inherited
  private void makeFormat(int offsetNumber, StringBuilder[] textSegments) {
    String[] segments = new String[textSegments.length];
    for (int i = 0; i < textSegments.length; i++) {
      StringBuilder oneseg = textSegments[i];
      segments[i] = (oneseg != null) ? oneseg.toString() : "";
    }

    // get the argument number
    int argumentNumber;
    try {
      argumentNumber = Integer.parseInt(segments[SEG_INDEX]);
    } catch (NumberFormatException e) {
      throw new IllegalArgumentException("can't parse argument number: " + segments[SEG_INDEX], e);
    }
    if (argumentNumber < 0) {
      throw new IllegalArgumentException("negative argument number: " + argumentNumber);
    }

    // resize format information arrays if necessary
    if (offsetNumber >= formats.length) {
      int newLength = Math.max(4, formats.length << 2);
      FormatInfo[] newFormats = new FormatInfo[newLength];
      System.arraycopy(formats, 0, newFormats, 0, maxOffset + 1);
      formats = newFormats;
    }
    int oldMaxOffset = maxOffset;
    maxOffset = offsetNumber;

    // now get the format
    Format newFormat = null;
    if (segments[SEG_TYPE].length() != 0) {
      int type = findKeyword(segments[SEG_TYPE], TYPE_KEYWORDS);
      switch (type) {
        case TYPE_NULL:
          // Type "" is allowed. e.g., "{0,}", "{0,,}", and "{0,,#}"
          // are treated as "{0}".
          break;

        case TYPE_NUMBER:
          switch (findKeyword(segments[SEG_MODIFIER], NUMBER_MODIFIER_KEYWORDS)) {
            case MODIFIER_DEFAULT:
              newFormat = NumberFormat.getInstance(locale);
              break;
            case MODIFIER_CURRENCY:
              newFormat = NumberFormat.getCurrencyInstance(locale);
              break;
            case MODIFIER_PERCENT:
              newFormat = NumberFormat.getPercentInstance(locale);
              break;
            case MODIFIER_INTEGER:
              newFormat = NumberFormat.getIntegerInstance(locale);
              break;
            default: // DecimalFormat pattern
              try {
                newFormat = new DecimalFormat(segments[SEG_MODIFIER],
                        DecimalFormatSymbols.getInstance(locale));
              } catch (IllegalArgumentException e) {
                maxOffset = oldMaxOffset;
                throw e;
              }
              break;
          }
          break;

        case TYPE_DATE:
        case TYPE_TIME:
          int mod = findKeyword(segments[SEG_MODIFIER], DATE_TIME_MODIFIER_KEYWORDS);
          if (mod >= 0 && mod < DATE_TIME_MODIFIER_KEYWORDS.length) {
            if (type == TYPE_DATE) {
              newFormat = DateFormat.getDateInstance(DATE_TIME_MODIFIERS[mod],
                      locale);
            } else {
              newFormat = DateFormat.getTimeInstance(DATE_TIME_MODIFIERS[mod],
                      locale);
            }
          } else {
            // SimpleDateFormat pattern
            try {
              newFormat = new SimpleDateFormat(segments[SEG_MODIFIER], locale);
            } catch (IllegalArgumentException e) {
              maxOffset = oldMaxOffset;
              throw e;
            }
          }
          break;

        case TYPE_CHOICE:
          try {
            // ChoiceFormat pattern
            newFormat = new ChoiceFormat(segments[SEG_MODIFIER]);
          } catch (Exception e) {
            maxOffset = oldMaxOffset;
            throw new IllegalArgumentException("Choice Pattern incorrect: "
                    + segments[SEG_MODIFIER], e);
          }
          break;

        default:
          maxOffset = oldMaxOffset;
          throw new IllegalArgumentException("unknown format type: " + segments[SEG_TYPE]);
      }
    }
    formats[offsetNumber] = new FormatInfo(newFormat, segments[SEG_RAW].length(), argumentNumber);
  }

  @SuppressFBWarnings(value = {"ES_COMPARING_STRINGS_WITH_EQ", "IMPROPER_UNICODE"},
          justification = "optimization")
  private static final int findKeyword(String s, String[] list) {
    int l = list.length;
    for (int i = 0; i < l; ++i) {
      if (s.equals(list[i])) {
        return i;
      }
    }

    // Try trimmed lowercase.
    String ls = s.trim();
    if (ls != s) {
      for (int i = 0; i < l; ++i) {
        if (ls.equalsIgnoreCase(list[i])) {
          return i;
        }
      }
    }
    return -1;
  }

  private static final void copyAndFixQuotes(CharSequence source, int start, int end,
          StringBuilder target) {
    boolean quoted = false;

    for (int i = start; i < end; ++i) {
      char ch = source.charAt(i);
      if (ch == '{') {
        if (!quoted) {
          target.append('\'');
          quoted = true;
        }
        target.append(ch);
      } else if (ch == '\'') {
        target.append("''");
      } else {
        if (quoted) {
          target.append('\'');
          quoted = false;
        }
        target.append(ch);
      }
    }
    if (quoted) {
      target.append('\'');
    }
  }

    private void writeObject(final java.io.ObjectOutputStream s)
        throws IOException {
        // Write out element count, and any hidden stuff
        s.defaultWriteObject();
        s.writeUTF(pattern.toString());
    }


  /**
   * After reading an object from the input stream, do a simple verification to maintain class invariants.
   *
   * @throws InvalidObjectException if the objects read from the stream is invalid.
   */
  @SuppressFBWarnings({"WEM_WEAK_EXCEPTION_MESSAGING", "DESERIALIZATION_GADGET"})
  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    pattern = in.readUTF();
    boolean isValid = maxOffset >= -1
            && formats.length > maxOffset;
    if (isValid) {
      int lastOffset = pattern.length() + 1;
      for (int i = maxOffset; i >= 0; --i) {
        FormatInfo finfo = formats[i];
        int offset = finfo.getOffset();
        if ((offset < 0) || (offset > lastOffset)) {
          isValid = false;
          break;
        } else {
          lastOffset = offset;
        }
      }
    }
    if (!isValid) {
      throw new InvalidObjectException("Could not reconstruct MessageFormat from corrupt stream");
    }
  }

  /**
   * Creates an <code>AttributedCharacterIterator</code> for the String <code>s</code>.
   *
   * @param s String to create AttributedCharacterIterator from
   * @return AttributedCharacterIterator wrapping s
   */
  static AttributedCharacterIterator createAttributedCharacterIterator(String s) {
    return new java.text.AttributedString(s).getIterator();
  }


  /**
   * Returns an AttributedCharacterIterator with the String <code>string</code> and additional key/value pair
   * <code>key</code>, <code>value</code>.
   *
   * @param string String to create AttributedCharacterIterator from
   * @param key Key for AttributedCharacterIterator
   * @param value Value associated with key in AttributedCharacterIterator
   * @return AttributedCharacterIterator wrapping args
   */
  static AttributedCharacterIterator createAttributedCharacterIterator(
          String string, AttributedCharacterIterator.Attribute key,
          Object value) {
    java.text.AttributedString as = new java.text.AttributedString(string);

    as.addAttribute(key, value);
    return as.getIterator();
  }

  /**
   * Creates an AttributedCharacterIterator with the contents of <code>iterator</code> and the additional attribute
   * <code>key</code> <code>value</code>.
   *
   * @param iterator Initial AttributedCharacterIterator to add arg to
   * @param key Key for AttributedCharacterIterator
   * @param value Value associated with key in AttributedCharacterIterator
   * @return AttributedCharacterIterator wrapping args
   */
  AttributedCharacterIterator createAttributedCharacterIterator(
          AttributedCharacterIterator iterator,
          AttributedCharacterIterator.Attribute key, Object value) {
    java.text.AttributedString as = new java.text.AttributedString(iterator);

    as.addAttribute(key, value);
    return as.getIterator();
  }

  @Override
  public String toString() {
    return "MessageFormat{" + "locale=" + locale + ", pattern=" + pattern + '}';
  }

}