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('{') >= 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 }