View Javadoc
1   /*
2    * Copyright (c) 2001-2017, Zoltan Farkas All Rights Reserved.
3    *
4    * This library is free software; you can redistribute it and/or
5    * modify it under the terms of the GNU Lesser General Public
6    * License as published by the Free Software Foundation; either
7    * version 2.1 of the License, or (at your option) any later version.
8    *
9    * This library is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this program; if not, write to the Free Software
16   * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
17   *
18   * Additionally licensed with:
19   *
20   * Licensed under the Apache License, Version 2.0 (the "License");
21   * you may not use this file except in compliance with the License.
22   * You may obtain a copy of the License at
23   *
24   *      http://www.apache.org/licenses/LICENSE-2.0
25   *
26   * Unless required by applicable law or agreed to in writing, software
27   * distributed under the License is distributed on an "AS IS" BASIS,
28   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29   * See the License for the specific language governing permissions and
30   * limitations under the License.
31   */
32  package org.spf4j.text;
33  //CHECKSTYLE:OFF
34  /*
35   * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved.
36   * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
37   */
38  
39   /*
40   * (C) Copyright Taligent, Inc. 1996, 1997 - All Rights Reserved
41   * (C) Copyright IBM Corp. 1996 - 1998 - All Rights Reserved
42   *
43   *   The original version of this source code and documentation is copyrighted
44   * and owned by Taligent, Inc., a wholly-owned subsidiary of IBM. These
45   * materials are provided under terms of a License Agreement between Taligent
46   * and Sun. This technology is protected by multiple US and International
47   * patents. This notice and attribution to Taligent may not be removed.
48   *   Taligent is a registered trademark of Taligent, Inc.
49   *
50   */
51  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
52  import java.io.InvalidObjectException;
53  import java.io.IOException;
54  import java.io.ObjectInputStream;
55  import java.io.UncheckedIOException;
56  import java.text.AttributedCharacterIterator;
57  import java.text.CharacterIterator;
58  import java.text.ChoiceFormat;
59  import java.text.DateFormat;
60  import java.text.DecimalFormat;
61  import java.text.DecimalFormatSymbols;
62  import java.text.FieldPosition;
63  import java.text.Format;
64  import java.text.NumberFormat;
65  import java.text.ParseException;
66  import java.text.ParsePosition;
67  import java.text.SimpleDateFormat;
68  import java.util.ArrayList;
69  import java.util.Arrays;
70  import java.util.Date;
71  import java.util.List;
72  import java.util.Locale;
73  import javax.annotation.Nonnull;
74  import javax.annotation.Nullable;
75  import javax.annotation.concurrent.NotThreadSafe;
76  import org.spf4j.base.CharSequences;
77  
78  /**
79   * Performance mutation of the JDK message formatter.
80   * Lots things of things have been done:
81   * 1) reduced the amount of garbage generated during formatting.
82   * 2) made some method invocations static.
83   * 3) made this more flexible and usable against StringBuilder not only StringBuffer...
84   * 4) thrown exceptions provide more detail on what went wrong.
85   * 5) cleaned up lots of static analisys reported issues.
86   *
87   * <code>MessageFormat</code> provides a means to produce concatenated messages in a language-neutral way. Use this to
88   * construct messages displayed for end users.
89   *
90   * this implementation is based on java.text.MessageFormat with the goal to be a faster and more flexible implementation
91   *
92   * <p>
93   * <code>MessageFormat</code> takes a set of objects, formats them, then inserts the formatted strings into the pattern
94   * at the appropriate places.
95   *
96   * <p>
97   * <strong>Note:</strong>
98   * <code>MessageFormat</code> differs from the other <code>Format</code> classes in that you create a
99   * <code>MessageFormat</code> object with one of its constructors (not with a <code>getInstance</code> style factory
100  * method). The factory methods aren't necessary because <code>MessageFormat</code> itself doesn't implement locale
101  * specific behavior. Any locale specific behavior is defined by the pattern that you provide as well as the sub-formats
102  * used for inserted arguments.
103  *
104  * <h3><a name="patterns">Patterns and Their Interpretation</a></h3>
105  *
106  * <code>MessageFormat</code> uses patterns of the following form:
107  * <blockquote><pre>
108  * <i>MessageFormatPattern:</i>
109  *         <i>String</i>
110  *         <i>MessageFormatPattern</i> <i>FormatElement</i> <i>String</i>
111  *
112  * <i>FormatElement:</i>
113  *         { <i>ArgumentIndex</i> }
114  *         { <i>ArgumentIndex</i> , <i>FormatType</i> }
115  *         { <i>ArgumentIndex</i> , <i>FormatType</i> , <i>FormatStyle</i> }
116  *
117  * <i>FormatType: one of </i>
118  *         number date time choice
119  *
120  * <i>FormatStyle:</i>
121  *         short
122  *         medium
123  *         long
124  *         full
125  *         integer
126  *         currency
127  *         percent
128  *         <i>SubformatPattern</i>
129  * </pre></blockquote>
130  *
131  * <p>
132  * Within a <i>String</i>, a pair of single quotes can be used to quote any arbitrary characters except single quotes.
133  * For example, pattern string <code>"'{0}'"</code> represents string <code>"{0}"</code>, not a <i>FormatElement</i>. A
134  * single quote itself must be represented by doubled single quotes {@code ''} throughout a
135  * <i>String</i>. For example, pattern string <code>"'{''}'"</code> is interpreted as a sequence of <code>'{</code>
136  * (start of quoting and a left curly brace), <code>''</code> (a single quote), and <code>}'</code> (a right curly brace
137  * and end of quoting),
138  * <em>not</em> <code>'{'</code> and <code>'}'</code> (quoted left and right curly braces): representing string
139  * <code>"{'}"</code>,
140  * <em>not</em> <code>"{}"</code>.
141  *
142  * <p>
143  * A <i>SubformatPattern</i> is interpreted by its corresponding sub-format, and sub-format-dependent pattern rules
144  * apply. For example, pattern string <code>"{1,number,<u>$'#',##</u>}"</code> (<i>SubformatPattern</i> with underline)
145  * will produce a number format with the pound-sign quoted, with a result such as: {@code
146  * "$#31,45"}. Refer to each {@code Format} subclass documentation for details.
147  *
148  * <p>
149  * Any unmatched quote is treated as closed at the end of the given pattern. For example, pattern string {@code "'{0}"}
150  * is treated as pattern {@code "'{0}'"}.
151  *
152  * <p>
153  * Any curly braces within an unquoted pattern must be balanced. For example, <code>"ab {0} de"</code> and
154  * <code>"ab '}' de"</code> are valid patterns, but <code>"ab {0'}' de"</code>, <code>"ab } de"</code> and
155  * <code>"''{''"</code> are not.
156  *
157  * <dl><dt><b>Warning:</b><dd>The rules for using quotes within message format patterns unfortunately have shown to be
158  * somewhat confusing. In particular, it isn't always obvious to localizers whether single quotes need to be doubled or
159  * not. Make sure to inform localizers about the rules, and tell them (for example, by using comments in resource bundle
160  * source files) which strings will be processed by {@code MessageFormat}. Note that localizers may need to use single
161  * quotes in translated strings where the original version doesn't have them.
162  * </dl>
163  * <p>
164  * The <i>ArgumentIndex</i> value is a non-negative integer written using the digits {@code '0'} through {@code '9'},
165  * and represents an index into the {@code arguments} array passed to the {@code format} methods or the result array
166  * returned by the {@code parse} methods.
167  * <p>
168  * The <i>FormatType</i> and <i>FormatStyle</i> values are used to create a {@code Format} instance for the format
169  * element. The following table shows how the values map to {@code Format} instances. Combinations not shown in the
170  * table are illegal. A <i>SubformatPattern</i> must be a valid pattern string for the {@code Format} subclass used.
171  *
172  * <table border=1 summary="Shows how FormatType and FormatStyle values map to Format instances">
173  * <tr>
174  * <th id="ft" class="TableHeadingColor">FormatType
175  * <th id="fs" class="TableHeadingColor">FormatStyle
176  * <th id="sc" class="TableHeadingColor">Subformat Created
177  * <tr>
178  * <td headers="ft"><i>(none)</i>
179  * <td headers="fs"><i>(none)</i>
180  * <td headers="sc"><code>null</code>
181  * <tr>
182  * <td headers="ft" rowspan=5><code>number</code>
183  * <td headers="fs"><i>(none)</i>
184  * <td headers="sc">{@link NumberFormat#getInstance(Locale) NumberFormat.getInstance}{@code (getLocale())}
185  * <tr>
186  * <td headers="fs"><code>integer</code>
187  * <td headers="sc">{@link NumberFormat#getIntegerInstance(Locale) NumberFormat.getIntegerInstance}{@code (getLocale())}
188  * <tr>
189  * <td headers="fs"><code>currency</code>
190  * <td headers="sc">{@link NumberFormat#getCurrencyInstance(Locale) NumberFormat.getCurrencyInstance}
191  * {@code (getLocale())}
192  * <tr>
193  * <td headers="fs"><code>percent</code>
194  * <td headers="sc">{@link NumberFormat#getPercentInstance(Locale) NumberFormat.getPercentInstance}{@code (getLocale())}
195  * <tr>
196  * <td headers="fs"><i>SubformatPattern</i>
197  * <td headers="sc">{@code new}
198  * {@link DecimalFormat#DecimalFormat(String,DecimalFormatSymbols) DecimalFormat}{@code (subformatPattern,}
199  * {@link DecimalFormatSymbols#getInstance(Locale) DecimalFormatSymbols.getInstance}{@code (getLocale()))}
200  * <tr>
201  * <td headers="ft" rowspan=6><code>date</code>
202  * <td headers="fs"><i>(none)</i>
203  * <td headers="sc">{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}
204  * {@code (}{@link DateFormat#DEFAULT}{@code , getLocale())}
205  * <tr>
206  * <td headers="fs"><code>short</code>
207  * <td headers="sc">{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}
208  * {@code (}{@link DateFormat#SHORT}{@code , getLocale())}
209  * <tr>
210  * <td headers="fs"><code>medium</code>
211  * <td headers="sc">{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}
212  * {@code (}{@link DateFormat#DEFAULT}{@code , getLocale())}
213  * <tr>
214  * <td headers="fs"><code>long</code>
215  * <td headers="sc">{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}
216  * {@code (}{@link DateFormat#LONG}{@code , getLocale())}
217  * <tr>
218  * <td headers="fs"><code>full</code>
219  * <td headers="sc">{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}
220  * {@code (}{@link DateFormat#FULL}{@code , getLocale())}
221  * <tr>
222  * <td headers="fs"><i>SubformatPattern</i>
223  * <td headers="sc">{@code new}
224  * {@link SimpleDateFormat#SimpleDateFormat(String,Locale) SimpleDateFormat}{@code (subformatPattern, getLocale())}
225  * <tr>
226  * <td headers="ft" rowspan=6><code>time</code>
227  * <td headers="fs"><i>(none)</i>
228  * <td headers="sc">{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}
229  * {@code (}{@link DateFormat#DEFAULT}{@code , getLocale())}
230  * <tr>
231  * <td headers="fs"><code>short</code>
232  * <td headers="sc">{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}
233  * {@code (}{@link DateFormat#SHORT}{@code , getLocale())}
234  * <tr>
235  * <td headers="fs"><code>medium</code>
236  * <td headers="sc">{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}
237  * {@code (}{@link DateFormat#DEFAULT}{@code , getLocale())}
238  * <tr>
239  * <td headers="fs"><code>long</code>
240  * <td headers="sc">{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}
241  * {@code (}{@link DateFormat#LONG}{@code , getLocale())}
242  * <tr>
243  * <td headers="fs"><code>full</code>
244  * <td headers="sc">{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}
245  * {@code (}{@link DateFormat#FULL}{@code , getLocale())}
246  * <tr>
247  * <td headers="fs"><i>SubformatPattern</i>
248  * <td headers="sc">{@code new}
249  * {@link SimpleDateFormat#SimpleDateFormat(String,Locale) SimpleDateFormat}{@code (subformatPattern, getLocale())}
250  * <tr>
251  * <td headers="ft"><code>choice</code>
252  * <td headers="fs"><i>SubformatPattern</i>
253  * <td headers="sc">{@code new} {@link ChoiceFormat#ChoiceFormat(String) ChoiceFormat}{@code (subformatPattern)}
254  * </table>
255  *
256  * <h4>Usage Information</h4>
257  *
258  * <p>
259  * Here are some examples of usage. In real internationalized programs, the message format pattern and other static
260  * strings will, of course, be obtained from resource bundles. Other parameters will be dynamically determined at
261  * runtime.
262  * <p>
263  * The first example uses the static method <code>MessageFormat.format</code>, which internally creates a
264  * <code>MessageFormat</code> for one-time use:
265  * <blockquote><pre>
266  * int planet = 7;
267  * String event = "a disturbance in the Force";
268  *
269  * String result = MessageFormat.format(
270  *     "At {1,time} on {1,date}, there was {2} on planet {0,number,integer}.",
271  *     planet, new Date(), event);
272  * </pre></blockquote>
273  * The output is:
274  * <blockquote><pre>
275  * At 12:30 PM on Jul 3, 2053, there was a disturbance in the Force on planet 7.
276  * </pre></blockquote>
277  *
278  * <p>
279  * The following example creates a <code>MessageFormat</code> instance that can be used repeatedly:
280  * <blockquote><pre>
281  * int fileCount = 1273;
282  * String diskName = "MyDisk";
283  * Object[] testArgs = {new Long(fileCount), diskName};
284  *
285  * MessageFormat form = new MessageFormat(
286  *     "The disk \"{1}\" contains {0} file(s).");
287  *
288  * System.out.println(form.format(testArgs));
289  * </pre></blockquote>
290  * The output with different values for <code>fileCount</code>:
291  * <blockquote><pre>
292  * The disk "MyDisk" contains 0 file(s).
293  * The disk "MyDisk" contains 1 file(s).
294  * The disk "MyDisk" contains 1,273 file(s).
295  * </pre></blockquote>
296  *
297  * <p>
298  * For more sophisticated patterns, you can use a <code>ChoiceFormat</code> to produce correct forms for singular and
299  * plural:
300  * <blockquote><pre>
301  * MessageFormat form = new MessageFormat("The disk \"{1}\" contains {0}.");
302  * double[] filelimits = {0,1,2};
303  * String[] filepart = {"no files","one file","{0,number} files"};
304  * ChoiceFormat fileform = new ChoiceFormat(filelimits, filepart);
305  * form.setFormatByArgumentIndex(0, fileform);
306  *
307  * int fileCount = 1273;
308  * String diskName = "MyDisk";
309  * Object[] testArgs = {new Long(fileCount), diskName};
310  *
311  * System.out.println(form.format(testArgs));
312  * </pre></blockquote>
313  * The output with different values for <code>fileCount</code>:
314  * <blockquote><pre>
315  * The disk "MyDisk" contains no files.
316  * The disk "MyDisk" contains one file.
317  * The disk "MyDisk" contains 1,273 files.
318  * </pre></blockquote>
319  *
320  * <p>
321  * You can create the <code>ChoiceFormat</code> programmatically, as in the above example, or by using a pattern. See
322  * {@link ChoiceFormat} for more information.
323  * <blockquote><pre>{@code
324  * form.applyPattern(
325  *    "There {0,choice,0#are no files|1#is one file|1<are {0,number,integer} files}.");
326  * }</pre></blockquote>
327  *
328  * <p>
329  * <strong>Note:</strong> As we see above, the string produced by a <code>ChoiceFormat</code> in
330  * <code>MessageFormat</code> is treated as special; occurrences of '{' are used to indicate subformats, and cause
331  * recursion. If you create both a <code>MessageFormat</code> and <code>ChoiceFormat</code> programmatically (instead of
332  * using the string patterns), then be careful not to produce a format that recurses on itself, which will cause an
333  * infinite loop.
334  * <p>
335  * When a single argument is parsed more than once in the string, the last match will be the final result of the
336  * parsing. For example,
337  * <blockquote><pre>
338  * MessageFormat mf = new MessageFormat("{0,number,#.##}, {0,number,#.#}");
339  * Object[] objs = {new Double(3.1415)};
340  * String result = mf.format( objs );
341  * // result now equals "3.14, 3.1"
342  * objs = null;
343  * objs = mf.parse(result, new ParsePosition(0));
344  * // objs now equals {new Double(3.1)}
345  * </pre></blockquote>
346  *
347  * <p>
348  * Likewise, parsing with a {@code MessageFormat} object using patterns containing multiple occurrences of the same
349  * argument would return the last match. For example,
350  * <blockquote><pre>
351  * MessageFormat mf = new MessageFormat("{0}, {0}, {0}");
352  * String forParsing = "x, y, z";
353  * Object[] objs = mf.parse(forParsing, new ParsePosition(0));
354  * // result now equals {new String("z")}
355  * </pre></blockquote>
356  *
357  * <h4><a name="synchronization">Synchronization</a></h4>
358  *
359  * <p>
360  * Message formats are not synchronized. It is recommended to create separate format instances for each thread. If
361  * multiple threads access a format concurrently, it must be synchronized externally.
362  *
363  * @see java.util.Locale
364  * @see Format
365  * @see NumberFormat
366  * @see DecimalFormat
367  * @see DecimalFormatSymbols
368  * @see ChoiceFormat
369  * @see DateFormat
370  * @see SimpleDateFormat
371  *
372  * @author Mark Davis
373  */
374 @SuppressFBWarnings("IMC_IMMATURE_CLASS_WRONG_FIELD_ORDER")
375 @NotThreadSafe
376 public final class MessageFormat extends Format {
377 
378   private static final long serialVersionUID = 1L;
379 
380   // Indices for segments
381   private static final int SEG_RAW = 0;
382   private static final int SEG_INDEX = 1;
383   private static final int SEG_TYPE = 2;
384   private static final int SEG_MODIFIER = 3; // modifier or subformat
385 
386   // Indices for type keywords
387   private static final int TYPE_NULL = 0;
388   private static final int TYPE_NUMBER = 1;
389   private static final int TYPE_DATE = 2;
390   private static final int TYPE_TIME = 3;
391   private static final int TYPE_CHOICE = 4;
392 
393   private static final String[] TYPE_KEYWORDS = {
394     "",
395     "number",
396     "date",
397     "time",
398     "choice"
399   };
400 
401   // Indices for number modifiers
402   private static final int MODIFIER_DEFAULT = 0; // common in number and date-time
403   private static final int MODIFIER_CURRENCY = 1;
404   private static final int MODIFIER_PERCENT = 2;
405   private static final int MODIFIER_INTEGER = 3;
406 
407   private static final String[] NUMBER_MODIFIER_KEYWORDS = {
408     "",
409     "currency",
410     "percent",
411     "integer"
412   };
413 
414   private static final String[] DATE_TIME_MODIFIER_KEYWORDS = {
415     "",
416     "short",
417     "medium",
418     "long",
419     "full"
420   };
421 
422   // Date-time style values corresponding to the date-time modifiers.
423   private static final int[] DATE_TIME_MODIFIERS = {
424     DateFormat.DEFAULT,
425     DateFormat.SHORT,
426     DateFormat.MEDIUM,
427     DateFormat.LONG,
428     DateFormat.FULL,};
429 
430 
431 
432   // ===========================privates============================
433   /**
434    * The locale to use for formatting numbers and dates.
435    *
436    * @serial
437    */
438   private Locale locale;
439 
440   /**
441    * The string that the formatted values are to be plugged into. In other words, this is the pattern supplied on
442    * construction with all of the {} expressions taken out.
443    *
444    * @serial
445    */
446   private transient CharSequence pattern;
447 
448   /**
449    * An array of formatters, which are used to format the arguments.
450    *
451    * @serial
452    */
453   private FormatInfo[] formats = {};
454 
455   /**
456    * One less than the number of entries in <code>offsets</code>. Can also be thought of as the index of the
457    * highest-numbered element in <code>offsets</code> that is being used. All of these arrays should have the same
458    * number of elements being used as <code>offsets</code> does, and so this variable suffices to tell us how many
459    * entries are in all of them.
460    *
461    * @serial
462    */
463   private int maxOffset = -1;
464 
465 
466   private int hash = 0;
467 
468 
469   /**
470    * Constructs a FastMessageFormat for the default {@link java.util.Locale.Category#FORMAT FORMAT} locale and the
471    * specified pattern. The constructor first sets the locale, then parses the pattern and creates a list of subformats
472    * for the format elements contained in it. Patterns and their interpretation are specified in the
473    * <a href="#patterns">class description</a>.
474    *
475    * @param pattern the pattern for this message format
476    * @exception IllegalArgumentException if the pattern is invalid
477    */
478   public MessageFormat(String pattern) {
479     this.locale = Locale.getDefault(Locale.Category.FORMAT);
480     applyPattern(pattern);
481   }
482 
483   /**
484    * Constructs a FastMessageFormat for the specified locale and pattern. The constructor first sets the locale, then
485    * parses the pattern and creates a list of sub-formats for the format elements contained in it. Patterns and their
486    * interpretation are specified in the
487    * <a href="#patterns">class description</a>.
488    *
489    * @param pattern the pattern for this message format
490    * @param locale the locale for this message format
491    * @exception IllegalArgumentException if the pattern is invalid
492    * @since 1.4
493    */
494   @SuppressFBWarnings("EI_EXPOSE_REP2")
495   public MessageFormat(String pattern, Locale locale) {
496     this.locale = locale;
497     applyPattern(pattern);
498   }
499 
500   /**
501    * Sets the locale to be used when creating or comparing subformats. This affects subsequent calls
502    * <ul>
503    * <li>to the {@link #applyPattern applyPattern} and {@link #toPattern toPattern} methods if format elements specify a
504    * format type and therefore have the subformats created in the <code>applyPattern</code> method, as well as
505    * <li>to the <code>format</code> and {@link #formatToCharacterIterator formatToCharacterIterator} methods if format
506    * elements do not specify a format type and therefore have the subformats created in the formatting methods.
507    * </ul>
508    * Subformats that have already been created are not affected.
509    *
510    * @param locale the locale to be used when creating or comparing subformats
511    */
512   @SuppressFBWarnings("EI_EXPOSE_REP2")
513   public void setLocale(final Locale locale) {
514     this.locale = locale;
515   }
516 
517   /**
518    * Gets the locale that's used when creating or comparing subformats.
519    *
520    * @return the locale used when creating or comparing subformats
521    */
522   @SuppressFBWarnings("EI_EXPOSE_REP")
523   public Locale getLocale() {
524     return locale;
525   }
526 
527   /**
528    * Sets the pattern used by this message format. The method parses the pattern and creates a list of subformats for
529    * the format elements contained in it. Patterns and their interpretation are specified in the
530    * <a href="#patterns">class description</a>.
531    *
532    * @param pattern the pattern for this message format
533    * @exception IllegalArgumentException if the pattern is invalid
534    */
535   @SuppressWarnings("fallthrough") // fallthrough in switch is expected, suppress it
536   @SuppressFBWarnings("CLI_CONSTANT_LIST_INDEX")
537   public void applyPattern(final String pattern) {
538     StringBuilder[] segments = new StringBuilder[4];
539     // Allocate only segments[SEG_RAW] here. The rest are
540     // allocated on demand.
541     final int length = pattern.length();
542     segments[SEG_RAW] = new StringBuilder(length);
543 
544     int part = SEG_RAW;
545     int formatNumber = 0;
546     boolean inQuote = false;
547     int braceStack = 0;
548     maxOffset = -1;
549     for (int i = 0; i < length; ++i) {
550       char ch = pattern.charAt(i);
551       if (part == SEG_RAW) {
552         if (ch == '\'') {
553           int next = i + 1;
554           if (next < length
555                   && pattern.charAt(next) == '\'') {
556             segments[part].append(ch);  // handle doubles
557             i = next;
558           } else {
559             inQuote = !inQuote;
560           }
561         } else if (ch == '{' && !inQuote) {
562           part = SEG_INDEX;
563           if (segments[part] == null) {
564             segments[part] = new StringBuilder();
565           }
566         } else {
567           segments[part].append(ch);
568         }
569       } else if (inQuote) {              // just copy quotes in parts
570         segments[part].append(ch);
571         if (ch == '\'') {
572           inQuote = false;
573         }
574       } else {
575         switch (ch) {
576           case ',':
577             if (part < SEG_MODIFIER) {
578               if (segments[++part] == null) {
579                 segments[part] = new StringBuilder();
580               }
581             } else {
582               segments[part].append(ch);
583             }
584             break;
585           case '{':
586             ++braceStack;
587             segments[part].append(ch);
588             break;
589           case '}':
590             if (braceStack == 0) {
591               part = SEG_RAW;
592               makeFormat(formatNumber, segments);
593               formatNumber++;
594               // throw away other segments
595               segments[SEG_INDEX] = null;
596               segments[SEG_TYPE] = null;
597               segments[SEG_MODIFIER] = null;
598             } else {
599               --braceStack;
600               segments[part].append(ch);
601             }
602             break;
603           case ' ':
604             // Skip any leading space chars for SEG_TYPE.
605             if (part != SEG_TYPE || segments[SEG_TYPE].length() > 0) {
606               segments[part].append(ch);
607             }
608             break;
609           case '\'':
610             inQuote = true;
611           // fall through, so we keep quotes in other parts
612           default:
613             segments[part].append(ch);
614             break;
615         }
616       }
617     }
618     if (braceStack == 0 && part != 0) {
619       maxOffset = -1;
620       throw new IllegalArgumentException("Unmatched braces in the pattern: " + pattern);
621     }
622     this.pattern = segments[0];
623   }
624 
625   /**
626    * Returns a pattern representing the current state of the message format. The string is constructed from internal
627    * information and therefore does not necessarily equal the previously applied pattern.
628    *
629    * @return a pattern representing the current state of the message format
630    */
631   @SuppressFBWarnings({"CLI_CONSTANT_LIST_INDEX", "ITC_INHERITANCE_TYPE_CHECKING"})
632   public String toPattern() {
633     // later, make this more extensible
634     int lastOffset = 0;
635     StringBuilder result = new StringBuilder();
636     for (int i = 0; i <= maxOffset; ++i) {
637       FormatInfo finfo = formats[i];
638       int offset = finfo.getOffset();
639       copyAndFixQuotes(pattern, lastOffset, offset, result);
640       lastOffset = offset;
641       result.append('{').append(finfo.getArgumentNumber());
642       Format fmt = finfo.getFormat();
643       if (fmt instanceof NumberFormat) {
644         if (fmt.equals(NumberFormat.getInstance(locale))) {
645           result.append(",number");
646         } else if (fmt.equals(NumberFormat.getCurrencyInstance(locale))) {
647           result.append(",number,currency");
648         } else if (fmt.equals(NumberFormat.getPercentInstance(locale))) {
649           result.append(",number,percent");
650         } else if (fmt.equals(NumberFormat.getIntegerInstance(locale))) {
651           result.append(",number,integer");
652         } else if (fmt instanceof DecimalFormat) {
653           result.append(",number,").append(((DecimalFormat) fmt).toPattern());
654         } else if (fmt instanceof ChoiceFormat) {
655           result.append(",choice,").append(((ChoiceFormat) fmt).toPattern());
656         } else {
657           throw new UnsupportedOperationException("Unsupported format " + fmt);
658         }
659       } else if (fmt instanceof DateFormat) {
660         int index;
661         for (index = MODIFIER_DEFAULT; index < DATE_TIME_MODIFIERS.length; index++) {
662           DateFormat df = DateFormat.getDateInstance(DATE_TIME_MODIFIERS[index],
663                   locale);
664           if (fmt.equals(df)) {
665             result.append(",date");
666             break;
667           }
668           df = DateFormat.getTimeInstance(DATE_TIME_MODIFIERS[index],
669                   locale);
670           if (fmt.equals(df)) {
671             result.append(",time");
672             break;
673           }
674         }
675         if (index >= DATE_TIME_MODIFIERS.length) {
676           if (fmt instanceof SimpleDateFormat) {
677             result.append(",date,").append(((SimpleDateFormat) fmt).toPattern());
678           } else {
679             throw new UnsupportedOperationException("Unsupported format " + fmt);
680           }
681         } else if (index != MODIFIER_DEFAULT) {
682           result.append(',').append(DATE_TIME_MODIFIER_KEYWORDS[index]);
683         }
684       } else if (fmt != null) {
685         throw new UnsupportedOperationException("Unsupported format " + fmt);
686       }
687       result.append('}');
688     }
689     copyAndFixQuotes(pattern, lastOffset, pattern.length(), result);
690     return result.toString();
691   }
692 
693   /**
694    * Sets the formats to use for the values passed into <code>format</code> methods or returned from <code>parse</code>
695    * methods. The indices of elements in <code>newFormats</code> correspond to the argument indices used in the
696    * previously set pattern string. The order of formats in <code>newFormats</code> thus corresponds to the order of
697    * elements in the <code>arguments</code> array passed to the <code>format</code> methods or the result array returned
698    * by the <code>parse</code> methods.
699    * <p>
700    * If an argument index is used for more than one format element in the pattern string, then the corresponding new
701    * format is used for all such format elements. If an argument index is not used for any format element in the pattern
702    * string, then the corresponding new format is ignored. If fewer formats are provided than needed, then only the
703    * formats for argument indices less than <code>newFormats.length</code> are replaced.
704    *
705    * @param newFormats the new formats to use
706    * @exception NullPointerException if <code>newFormats</code> is null
707    * @since 1.4
708    */
709   public void setFormatsByArgumentIndex(final Format[] newFormats) {
710     for (int i = 0; i <= maxOffset; i++) {
711       final FormatInfo finfo = formats[i];
712       int j = finfo.getArgumentNumber();
713       if (j < newFormats.length) {
714         formats[i] = new FormatInfo(newFormats[j], finfo.getOffset(), j);
715       }
716     }
717   }
718 
719   /**
720    * Sets the formats to use for the format elements in the previously set pattern string. The order of formats in
721    * <code>newFormats</code> corresponds to the order of format elements in the pattern string.
722    * <p>
723    * If more formats are provided than needed by the pattern string, the remaining ones are ignored. If fewer formats
724    * are provided than needed, then only the first <code>newFormats.length</code> formats are replaced.
725    * <p>
726    * Since the order of format elements in a pattern string often changes during localization, it is generally better to
727    * use the {@link #setFormatsByArgumentIndex setFormatsByArgumentIndex} method, which assumes an order of formats
728    * corresponding to the order of elements in the <code>arguments</code> array passed to the <code>format</code>
729    * methods or the result array returned by the <code>parse</code> methods.
730    *
731    * @param newFormats the new formats to use
732    * @exception NullPointerException if <code>newFormats</code> is null
733    */
734   public void setFormats(final Format[] newFormats) {
735     int runsToCopy = newFormats.length;
736     if (runsToCopy > maxOffset + 1) {
737       runsToCopy = maxOffset + 1;
738     }
739     for (int i = 0; i < runsToCopy; i++) {
740       FormatInfo finfo = formats[i];
741       formats[i] = new FormatInfo(newFormats[i], finfo.getOffset(), finfo.getArgumentNumber());
742     }
743   }
744 
745   /**
746    * Sets the format to use for the format elements within the previously set pattern string that use the given argument
747    * index. The argument index is part of the format element definition and represents an index into the
748    * <code>arguments</code> array passed to the <code>format</code> methods or the result array returned by the
749    * <code>parse</code> methods.
750    * <p>
751    * If the argument index is used for more than one format element in the pattern string, then the new format is used
752    * for all such format elements. If the argument index is not used for any format element in the pattern string, then
753    * the new format is ignored.
754    *
755    * @param argumentIndex the argument index for which to use the new format
756    * @param newFormat the new format to use
757    * @since 1.4
758    */
759   public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) {
760     for (int j = 0; j <= maxOffset; j++) {
761       FormatInfo finfo = formats[j];
762       int argNr = finfo.getArgumentNumber();
763       if (argNr == argumentIndex) {
764         formats[j] = new FormatInfo(newFormat, finfo.getOffset(), argNr);
765       }
766     }
767   }
768 
769   /**
770    * Sets the format to use for the format element with the given format element index within the previously set pattern
771    * string. The format element index is the zero-based number of the format element counting from the start of the
772    * pattern string.
773    * <p>
774    * Since the order of format elements in a pattern string often changes during localization, it is generally better to
775    * use the {@link #setFormatByArgumentIndex setFormatByArgumentIndex} method, which accesses format elements based on
776    * the argument index they specify.
777    *
778    * @param formatElementIndex the index of a format element within the pattern
779    * @param newFormat the format to use for the specified format element
780    * @exception ArrayIndexOutOfBoundsException if {@code formatElementIndex} is equal to or larger than the number of
781    * format elements in the pattern string
782    */
783   public void setFormat(final int formatElementIndex, final Format newFormat) {
784     FormatInfo finfo = formats[formatElementIndex];
785     formats[formatElementIndex] = new FormatInfo(newFormat, finfo.getOffset(), finfo.getArgumentNumber());
786   }
787 
788   /**
789    * Gets the formats used for the values passed into <code>format</code> methods or returned from <code>parse</code>
790    * methods. The indices of elements in the returned array correspond to the argument indices used in the previously
791    * set pattern string. The order of formats in the returned array thus corresponds to the order of elements in the
792    * <code>arguments</code> array passed to the <code>format</code> methods or the result array returned by the
793    * <code>parse</code> methods.
794    * <p>
795    * If an argument index is used for more than one format element in the pattern string, then the format used for the
796    * last such format element is returned in the array. If an argument index is not used for any format element in the
797    * pattern string, then null is returned in the array.
798    *
799    * @return the formats used for the arguments within the pattern
800    * @since 1.4
801    */
802 //    public Format[] getFormatsByArgumentIndex() {
803 //        int maximumArgumentNumber = -1;
804 //        for (int i = 0; i <= maxOffset; i++) {
805 //            if (argumentNumbers[i] > maximumArgumentNumber) {
806 //                maximumArgumentNumber = argumentNumbers[i];
807 //            }
808 //        }
809 //        Format[] resultArray = new Format[maximumArgumentNumber + 1];
810 //        for (int i = 0; i <= maxOffset; i++) {
811 //            resultArray[argumentNumbers[i]] = formats[i];
812 //        }
813 //        return resultArray;
814 //    }
815   /**
816    * Gets the formats used for the format elements in the previously set pattern string. The order of formats in the
817    * returned array corresponds to the order of format elements in the pattern string.
818    * <p>
819    * Since the order of format elements in a pattern string often changes during localization, it's generally better to
820    * use the {@link #getFormatsByArgumentIndex getFormatsByArgumentIndex} method, which assumes an order of formats
821    * corresponding to the order of elements in the <code>arguments</code> array passed to the <code>format</code>
822    * methods or the result array returned by the <code>parse</code> methods.
823    *
824    * @return the formats used for the format elements in the pattern
825    */
826   public Format[] getFormats() {
827     Format[] resultArray = new Format[maxOffset + 1];
828     System.arraycopy(formats, 0, resultArray, 0, maxOffset + 1);
829     return resultArray;
830   }
831 
832   /**
833    * Formats an array of objects and appends the <code>MessageFormat</code>'s pattern, with format elements replaced by
834    * the formatted objects, to the provided <code>StringBuffer</code>.
835    * <p>
836    * The text substituted for the individual format elements is derived from the current subformat of the format element
837    * and the <code>arguments</code> element at the format element's argument index as indicated by the first matching
838    * line of the following table. An argument is <i>unavailable</i> if <code>arguments</code> is <code>null</code> or
839    * has fewer than argumentIndex+1 elements.
840    *
841    * <table border=1 summary="Examples of subformat,argument,and formatted text">
842    * <tr>
843    * <th>Subformat
844    * <th>Argument
845    * <th>Formatted Text
846    * <tr>
847    * <td><i>any</i>
848    * <td><i>unavailable</i>
849    * <td><code>"{" + argumentIndex + "}"</code>
850    * <tr>
851    * <td><i>any</i>
852    * <td><code>null</code>
853    * <td><code>"null"</code>
854    * <tr>
855    * <td><code>instanceof ChoiceFormat</code>
856    * <td><i>any</i>
857    * <td><code>subformat.format(argument).indexOf('{') &gt;= 0 ?<br>
858    * (new MessageFormat(subformat.format(argument), getLocale())).format(argument) : subformat.format(argument)</code>
859    * <tr>
860    * <td><code>!= null</code>
861    * <td><i>any</i>
862    * <td><code>subformat.format(argument)</code>
863    * <tr>
864    * <td><code>null</code>
865    * <td><code>instanceof Number</code>
866    * <td><code>NumberFormat.getInstance(getLocale()).format(argument)</code>
867    * <tr>
868    * <td><code>null</code>
869    * <td><code>instanceof Date</code>
870    * <td><code>DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, getLocale()).format(argument)</code>
871    * <tr>
872    * <td><code>null</code>
873    * <td><code>instanceof String</code>
874    * <td><code>argument</code>
875    * <tr>
876    * <td><code>null</code>
877    * <td><i>any</i>
878    * <td><code>argument.toString()</code>
879    * </table>
880    * <p>
881    * If <code>pos</code> is non-null, and refers to <code>Field.ARGUMENT</code>, the location of the first formatted
882    * string will be returned.
883    *
884    * @param arguments an array of objects to be formatted and substituted.
885    * @param result where text is appended.
886    * @param pos On input: an alignment field, if desired. On output: the offsets of the alignment field.
887    * @return the string buffer passed in as {@code result}, with formatted text appended
888    * @exception IllegalArgumentException if an argument in the <code>arguments</code> array is not of the type expected
889    * by the format element(s) that use it.
890    */
891   public final <T extends CharSequence & Appendable> boolean[] format(Object[] arguments, T result,
892           @Nullable FieldPosition pos) throws IOException {
893     return subformat(arguments, result, pos, null);
894   }
895 
896   public final <T extends CharSequence & Appendable> boolean[] format(Object[] arguments, T result) throws IOException {
897     return format(arguments, result, null);
898   }
899 
900   /**
901    * Creates a MessageFormat with the given pattern and uses it to format the given arguments. This is equivalent to
902    * <blockquote>
903    * <code>(new {@link #MessageFormat(String) MessageFormat}(pattern)).
904    * {@link #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition) format}(arguments,
905    * new StringBuffer(), null).toString()</code>
906    * </blockquote>
907    *
908    * @param pattern the pattern string
909    * @param arguments object(s) to format
910    * @return the formatted string
911    * @exception IllegalArgumentException if the pattern is invalid, or if an argument in the <code>arguments</code>
912    * array is not of the type expected by the format element(s) that use it.
913    */
914   public static String format(final String pattern, final Object... arguments) {
915     MessageFormat temp = new MessageFormat(pattern);
916     return temp.format(arguments);
917   }
918 
919   // Overrides
920   /**
921    * Formats an array of objects and appends the <code>MessageFormat</code>'s pattern, with format elements replaced by
922    * the formatted objects, to the provided <code>StringBuffer</code>. This is equivalent to
923    * <blockquote>
924    * <code>{@link #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition) format}((Object[])
925    * arguments, result, pos)</code>
926    * </blockquote>
927    *
928    * @param arguments an array of objects to be formatted and substituted.
929    * @param result where text is appended.
930    * @param pos On input: an alignment field, if desired. On output: the offsets of the alignment field.
931    * @exception IllegalArgumentException if an argument in the <code>arguments</code> array is not of the type expected
932    * by the format element(s) that use it.
933    */
934   public final <T extends CharSequence & Appendable> boolean[] format(final Object arguments, final T result,
935           final FieldPosition pos) throws IOException {
936     if (arguments instanceof Object[]) {
937       return subformat((Object[]) arguments, result, pos, null);
938     } else {
939       return subformat(new Object [] {arguments}, result, pos, null);
940     }
941   }
942 
943   /**
944    * Formats an array of objects and inserts them into the <code>MessageFormat</code>'s pattern, producing an
945    * <code>AttributedCharacterIterator</code>. You can use the returned <code>AttributedCharacterIterator</code> to
946    * build the resulting String, as well as to determine information about the resulting String.
947    * <p>
948    * The text of the returned <code>AttributedCharacterIterator</code> is the same that would be returned by
949    * <blockquote>
950    * <code>{@link #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition) format}(arguments, new
951    * StringBuffer(), null).toString()</code>
952    * </blockquote>
953    * <p>
954    * In addition, the <code>AttributedCharacterIterator</code> contains at least attributes indicating where text was
955    * generated from an argument in the <code>arguments</code> array. The keys of these attributes are of type
956    * <code>MessageFormat.Field</code>, their values are <code>Integer</code> objects indicating the index in the
957    * <code>arguments</code> array of the argument from which the text was generated.
958    * <p>
959    * The attributes/value from the underlying <code>Format</code> instances that <code>MessageFormat</code> uses will
960    * also be placed in the resulting <code>AttributedCharacterIterator</code>. This allows you to not only find where an
961    * argument is placed in the resulting String, but also which fields it contains in turn.
962    *
963    * @param arguments an array of objects to be formatted and substituted.
964    * @return AttributedCharacterIterator describing the formatted value.
965    * @exception NullPointerException if <code>arguments</code> is null.
966    * @exception IllegalArgumentException if an argument in the <code>arguments</code> array is not of the type expected
967    * by the format element(s) that use it.
968    * @since 1.4
969    */
970   public AttributedCharacterIterator formatToCharacterIterator(@Nonnull Object arguments) {
971     StringBuilder result = new StringBuilder();
972     ArrayList<AttributedCharacterIterator> iterators = new ArrayList<>();
973     try {
974       subformat((Object[]) arguments, result, null, iterators);
975     } catch (IOException ex) {
976       throw new UncheckedIOException(ex);
977     }
978     if (iterators.isEmpty()) {
979       return createAttributedCharacterIterator("");
980     }
981     return new AttributedString(
982             iterators.toArray(
983                     new AttributedCharacterIterator[iterators.size()])).getIterator();
984   }
985 
986   /**
987    * Parses the string.
988    *
989    * <p>
990    * Caveats: The parse may fail in a number of circumstances. For example:
991    * <ul>
992    * <li>If one of the arguments does not occur in the pattern.
993    * <li>If the format of an argument loses information, such as with a choice format where a large number formats to
994    * "many".
995    * <li>Does not yet handle recursion (where the substituted strings contain {n} references.)
996    * <li>Will not always find a match (or the correct match) if some part of the parse is ambiguous. For example, if the
997    * pattern "{1},{2}" is used with the string arguments {"a,b", "c"}, it will format as "a,b,c". When the result is
998    * parsed, it will return {"a", "b,c"}.
999    * <li>If a single argument is parsed more than once in the string, then the later parse wins.
1000    * </ul>
1001    * When the parse fails, use ParsePosition.getErrorIndex() to find out where in the string the parsing failed. The
1002    * returned error index is the starting offset of the sub-patterns that the string is comparing with. For example, if
1003    * the parsing string "AAA {0} BBB" is comparing against the pattern "AAD {0} BBB", the error index is 0. When an
1004    * error occurs, the call to this method will return null. If the source is null, return an empty array. (zoltan:
1005    * yuck)
1006    *
1007    *
1008    * @param source the string to parse
1009    * @param pos the parse position
1010    * @return an array of parsed objects
1011    */
1012   @Nullable
1013   @SuppressFBWarnings({"PZLA_PREFER_ZERO_LENGTH_ARRAYS", "STT_STRING_PARSING_A_FIELD"}) // maintaining JDK behavior
1014   public Object[] parse(@Nullable String source, @Nonnull ParsePosition pos) {
1015 
1016     if (source == null) {
1017       return org.spf4j.base.Arrays.EMPTY_OBJ_ARRAY;
1018     }
1019 
1020     int maximumArgumentNumber = -1;
1021     for (int i = 0; i <= maxOffset; i++) {
1022       FormatInfo finfo = formats[i];
1023       final int argumentNumber = finfo.getArgumentNumber();
1024       if (argumentNumber > maximumArgumentNumber) {
1025         maximumArgumentNumber = argumentNumber;
1026       }
1027     }
1028     Object[] resultArray = new Object[maximumArgumentNumber + 1];
1029 
1030     int patternOffset = 0;
1031     int sourceOffset = pos.getIndex();
1032     ParsePosition tempStatus = new ParsePosition(0);
1033     for (int i = 0; i <= maxOffset; ++i) {
1034       // match up to format
1035       FormatInfo finfo = formats[i];
1036       int len = finfo.getOffset() - patternOffset;
1037       if (len == 0 || CharSequences.regionMatches(pattern, patternOffset, source, sourceOffset, len)) {
1038         sourceOffset += len;
1039         patternOffset += len;
1040       } else {
1041         pos.setErrorIndex(sourceOffset);
1042         return null;
1043       }
1044 
1045       // now use format
1046       if (finfo.getFormat() == null) {   // string format
1047         // if at end, use longest possible match
1048         // otherwise uses first match to intervening string
1049         // does NOT recursively try all possibilities
1050         int tempLength = (i != maxOffset) ? formats[i + 1].getOffset() : pattern.length();
1051 
1052         int next;
1053         if (patternOffset >= tempLength) {
1054           next = source.length();
1055         } else {
1056           next = source.indexOf(pattern.subSequence(patternOffset, tempLength).toString(), sourceOffset);
1057         }
1058 
1059         if (next < 0) {
1060           pos.setErrorIndex(sourceOffset);
1061           return null;
1062         } else {
1063           String strValue = source.substring(sourceOffset, next);
1064           int argNr = finfo.getArgumentNumber();
1065           if (!strValue.equals("{" + argNr + "}")) {
1066             resultArray[argNr] = source.substring(sourceOffset, next);
1067           }
1068           sourceOffset = next;
1069         }
1070       } else {
1071         tempStatus.setIndex(sourceOffset);
1072         resultArray[finfo.getArgumentNumber()] = finfo.getFormat().parseObject(source, tempStatus);
1073         if (tempStatus.getIndex() == sourceOffset) {
1074           pos.setErrorIndex(sourceOffset);
1075           return null; // leave index as is to signal error
1076         }
1077         sourceOffset = tempStatus.getIndex(); // update
1078       }
1079     }
1080     int len = pattern.length() - patternOffset;
1081     if (len == 0 || CharSequences.regionMatches(pattern, patternOffset, source, sourceOffset, len)) {
1082       pos.setIndex(sourceOffset + len);
1083     } else {
1084       pos.setErrorIndex(sourceOffset);
1085       return null;
1086     }
1087     return resultArray;
1088   }
1089 
1090   /**
1091    * Parses text from the beginning of the given string to produce an object array. The method may not use the entire
1092    * text of the given string.
1093    * <p>
1094    * See the {@link #parse(String, ParsePosition)} method for more information on message parsing.
1095    *
1096    * @param source A <code>String</code> whose beginning should be parsed.
1097    * @return An <code>Object</code> array parsed from the string.
1098    * @exception ParseException if the beginning of the specified string cannot be parsed.
1099    */
1100   @Nullable
1101   public Object[] parse(String source) throws ParseException {
1102     ParsePosition pos = new ParsePosition(0);
1103     Object[] result = parse(source, pos);
1104     if (pos.getIndex() == 0) // unchanged, returned object is null
1105     {
1106       throw new ParseException("MessageFormat source = " + source + " parse error!", pos.getErrorIndex());
1107     }
1108 
1109     return result;
1110   }
1111 
1112   /**
1113    * Parses text from a string to produce an object array.
1114    * <p>
1115    * The method attempts to parse text starting at the index given by <code>pos</code>. If parsing succeeds, then the
1116    * index of <code>pos</code> is updated to the index after the last character used (parsing does not necessarily use
1117    * all characters up to the end of the string), and the parsed object array is returned. The updated <code>pos</code>
1118    * can be used to indicate the starting point for the next call to this method. If an error occurs, then the index of
1119    * <code>pos</code> is not changed, the error index of <code>pos</code> is set to the index of the character where the
1120    * error occurred, and null is returned.
1121    * <p>
1122    * See the {@link #parse(String, ParsePosition)} method for more information on message parsing.
1123    *
1124    * @param source A <code>String</code>, part of which should be parsed.
1125    * @param pos A <code>ParsePosition</code> object with index and error index information as described above.
1126    * @return An <code>Object</code> array parsed from the string. In case of error, returns null.
1127    * @exception NullPointerException if <code>pos</code> is null.
1128    */
1129   @Nullable
1130   public Object parseObject(String source, ParsePosition pos) {
1131     return parse(source, pos);
1132   }
1133 
1134   /**
1135    * Creates and returns a copy of this object.
1136    *
1137    * @return a clone of this instance.
1138    */
1139   public MessageFormat clone() {
1140     MessageFormat other = (MessageFormat) super.clone();
1141 
1142     // clone arrays. Can't do with utility because of bug in Cloneable
1143     other.formats = formats.clone(); // shallow clone
1144     for (int i = 0; i < formats.length; ++i) {
1145       FormatInfo finfo = formats[i];
1146       if (finfo != null) {
1147         other.formats[i] = finfo.clone();
1148       }
1149     }
1150     return other;
1151   }
1152 
1153   /**
1154    * Equality comparison between two message format objects
1155    */
1156   public boolean equals(Object obj) {
1157     if (this == obj) // quick check
1158     {
1159       return true;
1160     }
1161     if (obj == null || getClass() != obj.getClass()) {
1162       return false;
1163     }
1164     MessageFormat other = (MessageFormat) obj;
1165     return (maxOffset == other.maxOffset
1166             && CharSequences.equals(pattern, other.pattern)
1167             && ((locale != null && locale.equals(other.locale))
1168             || (locale == null && other.locale == null))
1169             && Arrays.equals(formats, other.formats));
1170   }
1171 
1172   /**
1173    * Generates a hash code for the message format object.
1174    */
1175   public int hashCode() {
1176     int h = hash;
1177     if (h == 0) {
1178       h = CharSequences.hashcode(pattern);
1179       hash = h;
1180     }
1181     return h;
1182   }
1183 
1184   @Override
1185   public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
1186     try {
1187       syntethicFormat(obj, toAppendTo, pos);
1188       return toAppendTo;
1189     } catch (IOException ex) {
1190       throw new UncheckedIOException(ex);
1191     }
1192   }
1193 
1194   private <T extends CharSequence & Appendable> boolean[] syntethicFormat(Object obj, T toAppendTo, FieldPosition pos)
1195           throws IOException {
1196     return format(obj, toAppendTo, pos);
1197   }
1198 
1199 
1200   /**
1201    * Internal routine used by format. If <code>characterIterators</code> is non-null, AttributedCharacterIterator will
1202    * be created from the sub-formats as necessary. If <code>characterIterators</code> is null and <code>fp</code> is
1203    * non-null and identifies <code>Field.MESSAGE_ARGUMENT</code>, the location of the first replaced argument will be
1204    * set in it.
1205    *
1206    * @exception IllegalArgumentException if an argument in the <code>arguments</code> array is not of the type expected
1207    * by the format element(s) that use it.
1208    */
1209   @SuppressFBWarnings({"PRMC_POSSIBLY_REDUNDANT_METHOD_CALLS"}) // Unfortunately I have no other way to write this
1210   // without code duplication to work for StringBuilder and StringBuffer....
1211   private <T extends Appendable & CharSequence> boolean[] subformat(@Nullable Object[] arguments, @Nonnull T result,
1212           @Nullable FieldPosition fp,
1213           @Nullable List<AttributedCharacterIterator> characterIterators)
1214           throws IOException {
1215     int lastOffset = 0;
1216     int last = result.length();
1217     boolean[] used;
1218     if (arguments == null)  {
1219       used = org.spf4j.base.Arrays.EMPTY_BOOLEAN_ARRAY;
1220     } else {
1221       used = new boolean[arguments.length];
1222     }
1223     for (int i = 0; i <= maxOffset; ++i) {
1224       FormatInfo finfo = formats[i];
1225       int offset = finfo.getOffset();
1226       result.append(pattern, lastOffset, offset);
1227       lastOffset = offset;
1228       int argumentNumber = finfo.getArgumentNumber();
1229       if (arguments == null || argumentNumber >= arguments.length) {
1230         result.append('{').append(Integer.toString(argumentNumber)).append('}');
1231         continue;
1232       }
1233       used[argumentNumber] = true;
1234       Object obj = arguments[argumentNumber];
1235       String arg = null;
1236       Format subFormatter = null;
1237       Format fmt = finfo.getFormat();
1238       if (obj == null) {
1239         arg = "null";
1240       } else if (fmt != null) {
1241         subFormatter = fmt;
1242         if (subFormatter instanceof ChoiceFormat) {
1243           arg = subFormatter.format(obj);
1244           if (arg.indexOf('{') >= 0) {
1245             subFormatter = new MessageFormat(arg, locale);
1246             obj = arguments;
1247             arg = null;
1248           }
1249         }
1250       } else if (obj instanceof Number) {
1251         // format number if can
1252         subFormatter = NumberFormat.getInstance(locale);
1253       } else if (obj instanceof Date) {
1254         // format a Date if can
1255         subFormatter = DateFormat.getDateTimeInstance(
1256                 DateFormat.SHORT, DateFormat.SHORT, locale);//fix
1257       } else if (obj instanceof String) {
1258         arg = (String) obj;
1259 
1260       } else {
1261         arg = obj.toString();
1262       }
1263 
1264       // At this point we are in two states, either subFormatter
1265       // is non-null indicating we should format obj using it,
1266       // or arg is non-null and we should use it as the value.
1267       if (characterIterators != null) {
1268         // If characterIterators is non-null, it indicates we need
1269         // to get the CharacterIterator from the child formatter.
1270         int cl = result.length();
1271         if (last != cl) {
1272           characterIterators.add(
1273                   createAttributedCharacterIterator(result.subSequence(last, cl).toString()));
1274           last = cl;
1275         }
1276         if (subFormatter != null) {
1277           AttributedCharacterIterator subIterator
1278                   = subFormatter.formatToCharacterIterator(obj);
1279 
1280           append(result, subIterator);
1281           int cl2 = result.length();
1282           if (last != cl2) {
1283             characterIterators.add(
1284                     createAttributedCharacterIterator(
1285                             subIterator, java.text.MessageFormat.Field.ARGUMENT,
1286                             Integer.valueOf(argumentNumber)));
1287             last = cl2;
1288           }
1289           arg = null;
1290         }
1291         if (arg != null && arg.length() > 0) {
1292           result.append(arg);
1293           characterIterators.add(
1294                   createAttributedCharacterIterator(
1295                           arg, java.text.MessageFormat.Field.ARGUMENT,
1296                           Integer.valueOf(argumentNumber)));
1297           last = result.length();
1298         }
1299       } else {
1300         if (subFormatter != null) {
1301           arg = subFormatter.format(obj);
1302         }
1303         last = result.length();
1304         result.append(arg);
1305         if (i == 0 && fp != null && java.text.MessageFormat.Field.ARGUMENT.equals(
1306                 fp.getFieldAttribute())) {
1307           fp.setBeginIndex(last);
1308           fp.setEndIndex(result.length());
1309         }
1310         last = result.length();
1311       }
1312 
1313     }
1314     result.append(pattern, lastOffset, pattern.length());
1315     if (characterIterators != null && last != result.length()) {
1316       characterIterators.add(createAttributedCharacterIterator(result.subSequence(last, result.length()).toString()));
1317     }
1318     return used;
1319   }
1320 
1321   /**
1322    * Convenience method to append all the characters in <code>iterator</code> to the StringBuffer <code>result</code>.
1323    */
1324   private static void append(Appendable result, CharacterIterator iterator) throws IOException {
1325     final char first = iterator.first();
1326     if (first != CharacterIterator.DONE) {
1327       result.append(first);
1328       char aChar;
1329       while ((aChar = iterator.next()) != CharacterIterator.DONE) {
1330         result.append(aChar);
1331       }
1332     }
1333   }
1334 
1335 
1336   @SuppressFBWarnings("CLI_CONSTANT_LIST_INDEX") //jdk inherited
1337   private void makeFormat(int offsetNumber, StringBuilder[] textSegments) {
1338     String[] segments = new String[textSegments.length];
1339     for (int i = 0; i < textSegments.length; i++) {
1340       StringBuilder oneseg = textSegments[i];
1341       segments[i] = (oneseg != null) ? oneseg.toString() : "";
1342     }
1343 
1344     // get the argument number
1345     int argumentNumber;
1346     try {
1347       argumentNumber = Integer.parseInt(segments[SEG_INDEX]);
1348     } catch (NumberFormatException e) {
1349       throw new IllegalArgumentException("can't parse argument number: " + segments[SEG_INDEX], e);
1350     }
1351     if (argumentNumber < 0) {
1352       throw new IllegalArgumentException("negative argument number: " + argumentNumber);
1353     }
1354 
1355     // resize format information arrays if necessary
1356     if (offsetNumber >= formats.length) {
1357       int newLength = Math.max(4, formats.length << 2);
1358       FormatInfo[] newFormats = new FormatInfo[newLength];
1359       System.arraycopy(formats, 0, newFormats, 0, maxOffset + 1);
1360       formats = newFormats;
1361     }
1362     int oldMaxOffset = maxOffset;
1363     maxOffset = offsetNumber;
1364 
1365     // now get the format
1366     Format newFormat = null;
1367     if (segments[SEG_TYPE].length() != 0) {
1368       int type = findKeyword(segments[SEG_TYPE], TYPE_KEYWORDS);
1369       switch (type) {
1370         case TYPE_NULL:
1371           // Type "" is allowed. e.g., "{0,}", "{0,,}", and "{0,,#}"
1372           // are treated as "{0}".
1373           break;
1374 
1375         case TYPE_NUMBER:
1376           switch (findKeyword(segments[SEG_MODIFIER], NUMBER_MODIFIER_KEYWORDS)) {
1377             case MODIFIER_DEFAULT:
1378               newFormat = NumberFormat.getInstance(locale);
1379               break;
1380             case MODIFIER_CURRENCY:
1381               newFormat = NumberFormat.getCurrencyInstance(locale);
1382               break;
1383             case MODIFIER_PERCENT:
1384               newFormat = NumberFormat.getPercentInstance(locale);
1385               break;
1386             case MODIFIER_INTEGER:
1387               newFormat = NumberFormat.getIntegerInstance(locale);
1388               break;
1389             default: // DecimalFormat pattern
1390               try {
1391                 newFormat = new DecimalFormat(segments[SEG_MODIFIER],
1392                         DecimalFormatSymbols.getInstance(locale));
1393               } catch (IllegalArgumentException e) {
1394                 maxOffset = oldMaxOffset;
1395                 throw e;
1396               }
1397               break;
1398           }
1399           break;
1400 
1401         case TYPE_DATE:
1402         case TYPE_TIME:
1403           int mod = findKeyword(segments[SEG_MODIFIER], DATE_TIME_MODIFIER_KEYWORDS);
1404           if (mod >= 0 && mod < DATE_TIME_MODIFIER_KEYWORDS.length) {
1405             if (type == TYPE_DATE) {
1406               newFormat = DateFormat.getDateInstance(DATE_TIME_MODIFIERS[mod],
1407                       locale);
1408             } else {
1409               newFormat = DateFormat.getTimeInstance(DATE_TIME_MODIFIERS[mod],
1410                       locale);
1411             }
1412           } else {
1413             // SimpleDateFormat pattern
1414             try {
1415               newFormat = new SimpleDateFormat(segments[SEG_MODIFIER], locale);
1416             } catch (IllegalArgumentException e) {
1417               maxOffset = oldMaxOffset;
1418               throw e;
1419             }
1420           }
1421           break;
1422 
1423         case TYPE_CHOICE:
1424           try {
1425             // ChoiceFormat pattern
1426             newFormat = new ChoiceFormat(segments[SEG_MODIFIER]);
1427           } catch (Exception e) {
1428             maxOffset = oldMaxOffset;
1429             throw new IllegalArgumentException("Choice Pattern incorrect: "
1430                     + segments[SEG_MODIFIER], e);
1431           }
1432           break;
1433 
1434         default:
1435           maxOffset = oldMaxOffset;
1436           throw new IllegalArgumentException("unknown format type: " + segments[SEG_TYPE]);
1437       }
1438     }
1439     formats[offsetNumber] = new FormatInfo(newFormat, segments[SEG_RAW].length(), argumentNumber);
1440   }
1441 
1442   @SuppressFBWarnings(value = {"ES_COMPARING_STRINGS_WITH_EQ", "IMPROPER_UNICODE"},
1443           justification = "optimization")
1444   private static final int findKeyword(String s, String[] list) {
1445     int l = list.length;
1446     for (int i = 0; i < l; ++i) {
1447       if (s.equals(list[i])) {
1448         return i;
1449       }
1450     }
1451 
1452     // Try trimmed lowercase.
1453     String ls = s.trim();
1454     if (ls != s) {
1455       for (int i = 0; i < l; ++i) {
1456         if (ls.equalsIgnoreCase(list[i])) {
1457           return i;
1458         }
1459       }
1460     }
1461     return -1;
1462   }
1463 
1464   private static final void copyAndFixQuotes(CharSequence source, int start, int end,
1465           StringBuilder target) {
1466     boolean quoted = false;
1467 
1468     for (int i = start; i < end; ++i) {
1469       char ch = source.charAt(i);
1470       if (ch == '{') {
1471         if (!quoted) {
1472           target.append('\'');
1473           quoted = true;
1474         }
1475         target.append(ch);
1476       } else if (ch == '\'') {
1477         target.append("''");
1478       } else {
1479         if (quoted) {
1480           target.append('\'');
1481           quoted = false;
1482         }
1483         target.append(ch);
1484       }
1485     }
1486     if (quoted) {
1487       target.append('\'');
1488     }
1489   }
1490 
1491     private void writeObject(final java.io.ObjectOutputStream s)
1492         throws IOException {
1493         // Write out element count, and any hidden stuff
1494         s.defaultWriteObject();
1495         s.writeUTF(pattern.toString());
1496     }
1497 
1498 
1499   /**
1500    * After reading an object from the input stream, do a simple verification to maintain class invariants.
1501    *
1502    * @throws InvalidObjectException if the objects read from the stream is invalid.
1503    */
1504   @SuppressFBWarnings({"WEM_WEAK_EXCEPTION_MESSAGING", "DESERIALIZATION_GADGET"})
1505   private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
1506     in.defaultReadObject();
1507     pattern = in.readUTF();
1508     boolean isValid = maxOffset >= -1
1509             && formats.length > maxOffset;
1510     if (isValid) {
1511       int lastOffset = pattern.length() + 1;
1512       for (int i = maxOffset; i >= 0; --i) {
1513         FormatInfo finfo = formats[i];
1514         int offset = finfo.getOffset();
1515         if ((offset < 0) || (offset > lastOffset)) {
1516           isValid = false;
1517           break;
1518         } else {
1519           lastOffset = offset;
1520         }
1521       }
1522     }
1523     if (!isValid) {
1524       throw new InvalidObjectException("Could not reconstruct MessageFormat from corrupt stream");
1525     }
1526   }
1527 
1528   /**
1529    * Creates an <code>AttributedCharacterIterator</code> for the String <code>s</code>.
1530    *
1531    * @param s String to create AttributedCharacterIterator from
1532    * @return AttributedCharacterIterator wrapping s
1533    */
1534   static AttributedCharacterIterator createAttributedCharacterIterator(String s) {
1535     return new java.text.AttributedString(s).getIterator();
1536   }
1537 
1538 
1539   /**
1540    * Returns an AttributedCharacterIterator with the String <code>string</code> and additional key/value pair
1541    * <code>key</code>, <code>value</code>.
1542    *
1543    * @param string String to create AttributedCharacterIterator from
1544    * @param key Key for AttributedCharacterIterator
1545    * @param value Value associated with key in AttributedCharacterIterator
1546    * @return AttributedCharacterIterator wrapping args
1547    */
1548   static AttributedCharacterIterator createAttributedCharacterIterator(
1549           String string, AttributedCharacterIterator.Attribute key,
1550           Object value) {
1551     java.text.AttributedString as = new java.text.AttributedString(string);
1552 
1553     as.addAttribute(key, value);
1554     return as.getIterator();
1555   }
1556 
1557   /**
1558    * Creates an AttributedCharacterIterator with the contents of <code>iterator</code> and the additional attribute
1559    * <code>key</code> <code>value</code>.
1560    *
1561    * @param iterator Initial AttributedCharacterIterator to add arg to
1562    * @param key Key for AttributedCharacterIterator
1563    * @param value Value associated with key in AttributedCharacterIterator
1564    * @return AttributedCharacterIterator wrapping args
1565    */
1566   AttributedCharacterIterator createAttributedCharacterIterator(
1567           AttributedCharacterIterator iterator,
1568           AttributedCharacterIterator.Attribute key, Object value) {
1569     java.text.AttributedString as = new java.text.AttributedString(iterator);
1570 
1571     as.addAttribute(key, value);
1572     return as.getIterator();
1573   }
1574 
1575   @Override
1576   public String toString() {
1577     return "MessageFormat{" + "locale=" + locale + ", pattern=" + pattern + '}';
1578   }
1579 
1580 }