Path: blob/master/src/java.base/share/classes/java/text/MessageFormat.java
41152 views
/*1* Copyright (c) 1996, 2020, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425/*26* (C) Copyright Taligent, Inc. 1996, 1997 - All Rights Reserved27* (C) Copyright IBM Corp. 1996 - 1998 - All Rights Reserved28*29* The original version of this source code and documentation is copyrighted30* and owned by Taligent, Inc., a wholly-owned subsidiary of IBM. These31* materials are provided under terms of a License Agreement between Taligent32* and Sun. This technology is protected by multiple US and International33* patents. This notice and attribution to Taligent may not be removed.34* Taligent is a registered trademark of Taligent, Inc.35*36*/3738package java.text;3940import java.io.InvalidObjectException;41import java.io.IOException;42import java.io.ObjectInputStream;43import java.text.DecimalFormat;44import java.util.ArrayList;45import java.util.Arrays;46import java.util.Date;47import java.util.List;48import java.util.Locale;495051/**52* {@code MessageFormat} provides a means to produce concatenated53* messages in a language-neutral way. Use this to construct messages54* displayed for end users.55*56* <p>57* {@code MessageFormat} takes a set of objects, formats them, then58* inserts the formatted strings into the pattern at the appropriate places.59*60* <p>61* <strong>Note:</strong>62* {@code MessageFormat} differs from the other {@code Format}63* classes in that you create a {@code MessageFormat} object with one64* of its constructors (not with a {@code getInstance} style factory65* method). The factory methods aren't necessary because {@code MessageFormat}66* itself doesn't implement locale specific behavior. Any locale specific67* behavior is defined by the pattern that you provide as well as the68* subformats used for inserted arguments.69*70* <h2><a id="patterns">Patterns and Their Interpretation</a></h2>71*72* {@code MessageFormat} uses patterns of the following form:73* <blockquote><pre>74* <i>MessageFormatPattern:</i>75* <i>String</i>76* <i>MessageFormatPattern</i> <i>FormatElement</i> <i>String</i>77*78* <i>FormatElement:</i>79* { <i>ArgumentIndex</i> }80* { <i>ArgumentIndex</i> , <i>FormatType</i> }81* { <i>ArgumentIndex</i> , <i>FormatType</i> , <i>FormatStyle</i> }82*83* <i>FormatType: one of </i>84* number date time choice85*86* <i>FormatStyle:</i>87* short88* medium89* long90* full91* integer92* currency93* percent94* <i>SubformatPattern</i>95* </pre></blockquote>96*97* <p>Within a <i>String</i>, a pair of single quotes can be used to98* quote any arbitrary characters except single quotes. For example,99* pattern string <code>"'{0}'"</code> represents string100* <code>"{0}"</code>, not a <i>FormatElement</i>. A single quote itself101* must be represented by doubled single quotes {@code ''} throughout a102* <i>String</i>. For example, pattern string <code>"'{''}'"</code> is103* interpreted as a sequence of <code>'{</code> (start of quoting and a104* left curly brace), {@code ''} (a single quote), and105* <code>}'</code> (a right curly brace and end of quoting),106* <em>not</em> <code>'{'</code> and <code>'}'</code> (quoted left and107* right curly braces): representing string <code>"{'}"</code>,108* <em>not</em> <code>"{}"</code>.109*110* <p>A <i>SubformatPattern</i> is interpreted by its corresponding111* subformat, and subformat-dependent pattern rules apply. For example,112* pattern string <code>"{1,number,<u>$'#',##</u>}"</code>113* (<i>SubformatPattern</i> with underline) will produce a number format114* with the pound-sign quoted, with a result such as: {@code115* "$#31,45"}. Refer to each {@code Format} subclass documentation for116* details.117*118* <p>Any unmatched quote is treated as closed at the end of the given119* pattern. For example, pattern string {@code "'{0}"} is treated as120* pattern {@code "'{0}'"}.121*122* <p>Any curly braces within an unquoted pattern must be balanced. For123* example, <code>"ab {0} de"</code> and <code>"ab '}' de"</code> are124* valid patterns, but <code>"ab {0'}' de"</code>, <code>"ab } de"</code>125* and <code>"''{''"</code> are not.126*127* <dl><dt><b>Warning:</b><dd>The rules for using quotes within message128* format patterns unfortunately have shown to be somewhat confusing.129* In particular, it isn't always obvious to localizers whether single130* quotes need to be doubled or not. Make sure to inform localizers about131* the rules, and tell them (for example, by using comments in resource132* bundle source files) which strings will be processed by {@code MessageFormat}.133* Note that localizers may need to use single quotes in translated134* strings where the original version doesn't have them.135* </dl>136* <p>137* The <i>ArgumentIndex</i> value is a non-negative integer written138* using the digits {@code '0'} through {@code '9'}, and represents an index into the139* {@code arguments} array passed to the {@code format} methods140* or the result array returned by the {@code parse} methods.141* <p>142* The <i>FormatType</i> and <i>FormatStyle</i> values are used to create143* a {@code Format} instance for the format element. The following144* table shows how the values map to {@code Format} instances. Combinations not145* shown in the table are illegal. A <i>SubformatPattern</i> must146* be a valid pattern string for the {@code Format} subclass used.147*148* <table class="plain">149* <caption style="display:none">Shows how FormatType and FormatStyle values map to Format instances</caption>150* <thead>151* <tr>152* <th scope="col" class="TableHeadingColor">FormatType153* <th scope="col" class="TableHeadingColor">FormatStyle154* <th scope="col" class="TableHeadingColor">Subformat Created155* </thead>156* <tbody>157* <tr>158* <th scope="row" style="text-weight: normal"><i>(none)</i>159* <th scope="row" style="text-weight: normal"><i>(none)</i>160* <td>{@code null}161* <tr>162* <th scope="row" style="text-weight: normal" rowspan=5>{@code number}163* <th scope="row" style="text-weight: normal"><i>(none)</i>164* <td>{@link NumberFormat#getInstance(Locale) NumberFormat.getInstance}{@code (getLocale())}165* <tr>166* <th scope="row" style="text-weight: normal">{@code integer}167* <td>{@link NumberFormat#getIntegerInstance(Locale) NumberFormat.getIntegerInstance}{@code (getLocale())}168* <tr>169* <th scope="row" style="text-weight: normal">{@code currency}170* <td>{@link NumberFormat#getCurrencyInstance(Locale) NumberFormat.getCurrencyInstance}{@code (getLocale())}171* <tr>172* <th scope="row" style="text-weight: normal">{@code percent}173* <td>{@link NumberFormat#getPercentInstance(Locale) NumberFormat.getPercentInstance}{@code (getLocale())}174* <tr>175* <th scope="row" style="text-weight: normal"><i>SubformatPattern</i>176* <td>{@code new} {@link DecimalFormat#DecimalFormat(String,DecimalFormatSymbols) DecimalFormat}{@code (subformatPattern,} {@link DecimalFormatSymbols#getInstance(Locale) DecimalFormatSymbols.getInstance}{@code (getLocale()))}177* <tr>178* <th scope="row" style="text-weight: normal" rowspan=6>{@code date}179* <th scope="row" style="text-weight: normal"><i>(none)</i>180* <td>{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())}181* <tr>182* <th scope="row" style="text-weight: normal">{@code short}183* <td>{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#SHORT}{@code , getLocale())}184* <tr>185* <th scope="row" style="text-weight: normal">{@code medium}186* <td>{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())}187* <tr>188* <th scope="row" style="text-weight: normal">{@code long}189* <td>{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#LONG}{@code , getLocale())}190* <tr>191* <th scope="row" style="text-weight: normal">{@code full}192* <td>{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#FULL}{@code , getLocale())}193* <tr>194* <th scope="row" style="text-weight: normal"><i>SubformatPattern</i>195* <td>{@code new} {@link SimpleDateFormat#SimpleDateFormat(String,Locale) SimpleDateFormat}{@code (subformatPattern, getLocale())}196* <tr>197* <th scope="row" style="text-weight: normal" rowspan=6>{@code time}198* <th scope="row" style="text-weight: normal"><i>(none)</i>199* <td>{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())}200* <tr>201* <th scope="row" style="text-weight: normal">{@code short}202* <td>{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#SHORT}{@code , getLocale())}203* <tr>204* <th scope="row" style="text-weight: normal">{@code medium}205* <td>{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())}206* <tr>207* <th scope="row" style="text-weight: normal">{@code long}208* <td>{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#LONG}{@code , getLocale())}209* <tr>210* <th scope="row" style="text-weight: normal">{@code full}211* <td>{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#FULL}{@code , getLocale())}212* <tr>213* <th scope="row" style="text-weight: normal"><i>SubformatPattern</i>214* <td>{@code new} {@link SimpleDateFormat#SimpleDateFormat(String,Locale) SimpleDateFormat}{@code (subformatPattern, getLocale())}215* <tr>216* <th scope="row" style="text-weight: normal">{@code choice}217* <th scope="row" style="text-weight: normal"><i>SubformatPattern</i>218* <td>{@code new} {@link ChoiceFormat#ChoiceFormat(String) ChoiceFormat}{@code (subformatPattern)}219* </tbody>220* </table>221*222* <h3>Usage Information</h3>223*224* <p>225* Here are some examples of usage.226* In real internationalized programs, the message format pattern and other227* static strings will, of course, be obtained from resource bundles.228* Other parameters will be dynamically determined at runtime.229* <p>230* The first example uses the static method {@code MessageFormat.format},231* which internally creates a {@code MessageFormat} for one-time use:232* <blockquote><pre>233* int planet = 7;234* String event = "a disturbance in the Force";235*236* String result = MessageFormat.format(237* "At {1,time} on {1,date}, there was {2} on planet {0,number,integer}.",238* planet, new Date(), event);239* </pre></blockquote>240* The output is:241* <blockquote><pre>242* At 12:30 PM on Jul 3, 2053, there was a disturbance in the Force on planet 7.243* </pre></blockquote>244*245* <p>246* The following example creates a {@code MessageFormat} instance that247* can be used repeatedly:248* <blockquote><pre>249* int fileCount = 1273;250* String diskName = "MyDisk";251* Object[] testArgs = {new Long(fileCount), diskName};252*253* MessageFormat form = new MessageFormat(254* "The disk \"{1}\" contains {0} file(s).");255*256* System.out.println(form.format(testArgs));257* </pre></blockquote>258* The output with different values for {@code fileCount}:259* <blockquote><pre>260* The disk "MyDisk" contains 0 file(s).261* The disk "MyDisk" contains 1 file(s).262* The disk "MyDisk" contains 1,273 file(s).263* </pre></blockquote>264*265* <p>266* For more sophisticated patterns, you can use a {@code ChoiceFormat}267* to produce correct forms for singular and plural:268* <blockquote><pre>269* MessageFormat form = new MessageFormat("The disk \"{1}\" contains {0}.");270* double[] filelimits = {0,1,2};271* String[] filepart = {"no files","one file","{0,number} files"};272* ChoiceFormat fileform = new ChoiceFormat(filelimits, filepart);273* form.setFormatByArgumentIndex(0, fileform);274*275* int fileCount = 1273;276* String diskName = "MyDisk";277* Object[] testArgs = {new Long(fileCount), diskName};278*279* System.out.println(form.format(testArgs));280* </pre></blockquote>281* The output with different values for {@code fileCount}:282* <blockquote><pre>283* The disk "MyDisk" contains no files.284* The disk "MyDisk" contains one file.285* The disk "MyDisk" contains 1,273 files.286* </pre></blockquote>287*288* <p>289* You can create the {@code ChoiceFormat} programmatically, as in the290* above example, or by using a pattern. See {@link ChoiceFormat}291* for more information.292* <blockquote><pre>{@code293* form.applyPattern(294* "There {0,choice,0#are no files|1#is one file|1<are {0,number,integer} files}.");295* }</pre></blockquote>296*297* <p>298* <strong>Note:</strong> As we see above, the string produced299* by a {@code ChoiceFormat} in {@code MessageFormat} is treated as special;300* occurrences of '{' are used to indicate subformats, and cause recursion.301* If you create both a {@code MessageFormat} and {@code ChoiceFormat}302* programmatically (instead of using the string patterns), then be careful not to303* produce a format that recurses on itself, which will cause an infinite loop.304* <p>305* When a single argument is parsed more than once in the string, the last match306* will be the final result of the parsing. For example,307* <blockquote><pre>308* MessageFormat mf = new MessageFormat("{0,number,#.##}, {0,number,#.#}");309* Object[] objs = {new Double(3.1415)};310* String result = mf.format( objs );311* // result now equals "3.14, 3.1"312* objs = null;313* objs = mf.parse(result, new ParsePosition(0));314* // objs now equals {new Double(3.1)}315* </pre></blockquote>316*317* <p>318* Likewise, parsing with a {@code MessageFormat} object using patterns containing319* multiple occurrences of the same argument would return the last match. For320* example,321* <blockquote><pre>322* MessageFormat mf = new MessageFormat("{0}, {0}, {0}");323* String forParsing = "x, y, z";324* Object[] objs = mf.parse(forParsing, new ParsePosition(0));325* // result now equals {new String("z")}326* </pre></blockquote>327*328* <h3><a id="synchronization">Synchronization</a></h3>329*330* <p>331* Message formats are not synchronized.332* It is recommended to create separate format instances for each thread.333* If multiple threads access a format concurrently, it must be synchronized334* externally.335*336* @see java.util.Locale337* @see Format338* @see NumberFormat339* @see DecimalFormat340* @see DecimalFormatSymbols341* @see ChoiceFormat342* @see DateFormat343* @see SimpleDateFormat344*345* @author Mark Davis346* @since 1.1347*/348349public class MessageFormat extends Format {350351@java.io.Serial352private static final long serialVersionUID = 6479157306784022952L;353354/**355* Constructs a MessageFormat for the default356* {@link java.util.Locale.Category#FORMAT FORMAT} locale and the357* specified pattern.358* The constructor first sets the locale, then parses the pattern and359* creates a list of subformats for the format elements contained in it.360* Patterns and their interpretation are specified in the361* <a href="#patterns">class description</a>.362*363* @param pattern the pattern for this message format364* @throws IllegalArgumentException if the pattern is invalid365* @throws NullPointerException if {@code pattern} is366* {@code null}367*/368public MessageFormat(String pattern) {369this.locale = Locale.getDefault(Locale.Category.FORMAT);370applyPattern(pattern);371}372373/**374* Constructs a MessageFormat for the specified locale and375* pattern.376* The constructor first sets the locale, then parses the pattern and377* creates a list of subformats for the format elements contained in it.378* Patterns and their interpretation are specified in the379* <a href="#patterns">class description</a>.380*381* @param pattern the pattern for this message format382* @param locale the locale for this message format383* @throws IllegalArgumentException if the pattern is invalid384* @throws NullPointerException if {@code pattern} is385* {@code null}386* @since 1.4387*/388public MessageFormat(String pattern, Locale locale) {389this.locale = locale;390applyPattern(pattern);391}392393/**394* Sets the locale to be used when creating or comparing subformats.395* This affects subsequent calls396* <ul>397* <li>to the {@link #applyPattern applyPattern}398* and {@link #toPattern toPattern} methods if format elements specify399* a format type and therefore have the subformats created in the400* {@code applyPattern} method, as well as401* <li>to the {@code format} and402* {@link #formatToCharacterIterator formatToCharacterIterator} methods403* if format elements do not specify a format type and therefore have404* the subformats created in the formatting methods.405* </ul>406* Subformats that have already been created are not affected.407*408* @param locale the locale to be used when creating or comparing subformats409*/410public void setLocale(Locale locale) {411this.locale = locale;412}413414/**415* Gets the locale that's used when creating or comparing subformats.416*417* @return the locale used when creating or comparing subformats418*/419public Locale getLocale() {420return locale;421}422423424/**425* Sets the pattern used by this message format.426* The method parses the pattern and creates a list of subformats427* for the format elements contained in it.428* Patterns and their interpretation are specified in the429* <a href="#patterns">class description</a>.430*431* @param pattern the pattern for this message format432* @throws IllegalArgumentException if the pattern is invalid433* @throws NullPointerException if {@code pattern} is434* {@code null}435*/436@SuppressWarnings("fallthrough") // fallthrough in switch is expected, suppress it437public void applyPattern(String pattern) {438StringBuilder[] segments = new StringBuilder[4];439// Allocate only segments[SEG_RAW] here. The rest are440// allocated on demand.441segments[SEG_RAW] = new StringBuilder();442443int part = SEG_RAW;444int formatNumber = 0;445boolean inQuote = false;446int braceStack = 0;447maxOffset = -1;448for (int i = 0; i < pattern.length(); ++i) {449char ch = pattern.charAt(i);450if (part == SEG_RAW) {451if (ch == '\'') {452if (i + 1 < pattern.length()453&& pattern.charAt(i+1) == '\'') {454segments[part].append(ch); // handle doubles455++i;456} else {457inQuote = !inQuote;458}459} else if (ch == '{' && !inQuote) {460part = SEG_INDEX;461if (segments[SEG_INDEX] == null) {462segments[SEG_INDEX] = new StringBuilder();463}464} else {465segments[part].append(ch);466}467} else {468if (inQuote) { // just copy quotes in parts469segments[part].append(ch);470if (ch == '\'') {471inQuote = false;472}473} else {474switch (ch) {475case ',':476if (part < SEG_MODIFIER) {477if (segments[++part] == null) {478segments[part] = new StringBuilder();479}480} else {481segments[part].append(ch);482}483break;484case '{':485++braceStack;486segments[part].append(ch);487break;488case '}':489if (braceStack == 0) {490part = SEG_RAW;491makeFormat(i, formatNumber, segments);492formatNumber++;493// throw away other segments494segments[SEG_INDEX] = null;495segments[SEG_TYPE] = null;496segments[SEG_MODIFIER] = null;497} else {498--braceStack;499segments[part].append(ch);500}501break;502case ' ':503// Skip any leading space chars for SEG_TYPE.504if (part != SEG_TYPE || segments[SEG_TYPE].length() > 0) {505segments[part].append(ch);506}507break;508case '\'':509inQuote = true;510// fall through, so we keep quotes in other parts511default:512segments[part].append(ch);513break;514}515}516}517}518if (braceStack == 0 && part != 0) {519maxOffset = -1;520throw new IllegalArgumentException("Unmatched braces in the pattern.");521}522this.pattern = segments[0].toString();523}524525526/**527* Returns a pattern representing the current state of the message format.528* The string is constructed from internal information and therefore529* does not necessarily equal the previously applied pattern.530*531* @return a pattern representing the current state of the message format532*/533public String toPattern() {534// later, make this more extensible535int lastOffset = 0;536StringBuilder result = new StringBuilder();537for (int i = 0; i <= maxOffset; ++i) {538copyAndFixQuotes(pattern, lastOffset, offsets[i], result);539lastOffset = offsets[i];540result.append('{').append(argumentNumbers[i]);541Format fmt = formats[i];542if (fmt == null) {543// do nothing, string format544} else if (fmt instanceof NumberFormat) {545if (fmt.equals(NumberFormat.getInstance(locale))) {546result.append(",number");547} else if (fmt.equals(NumberFormat.getCurrencyInstance(locale))) {548result.append(",number,currency");549} else if (fmt.equals(NumberFormat.getPercentInstance(locale))) {550result.append(",number,percent");551} else if (fmt.equals(NumberFormat.getIntegerInstance(locale))) {552result.append(",number,integer");553} else {554if (fmt instanceof DecimalFormat) {555result.append(",number,").append(((DecimalFormat)fmt).toPattern());556} else if (fmt instanceof ChoiceFormat) {557result.append(",choice,").append(((ChoiceFormat)fmt).toPattern());558} else {559// UNKNOWN560}561}562} else if (fmt instanceof DateFormat) {563int index;564for (index = MODIFIER_DEFAULT; index < DATE_TIME_MODIFIERS.length; index++) {565DateFormat df = DateFormat.getDateInstance(DATE_TIME_MODIFIERS[index],566locale);567if (fmt.equals(df)) {568result.append(",date");569break;570}571df = DateFormat.getTimeInstance(DATE_TIME_MODIFIERS[index],572locale);573if (fmt.equals(df)) {574result.append(",time");575break;576}577}578if (index >= DATE_TIME_MODIFIERS.length) {579if (fmt instanceof SimpleDateFormat) {580result.append(",date,").append(((SimpleDateFormat)fmt).toPattern());581} else {582// UNKNOWN583}584} else if (index != MODIFIER_DEFAULT) {585result.append(',').append(DATE_TIME_MODIFIER_KEYWORDS[index]);586}587} else {588//result.append(", unknown");589}590result.append('}');591}592copyAndFixQuotes(pattern, lastOffset, pattern.length(), result);593return result.toString();594}595596/**597* Sets the formats to use for the values passed into598* {@code format} methods or returned from {@code parse}599* methods. The indices of elements in {@code newFormats}600* correspond to the argument indices used in the previously set601* pattern string.602* The order of formats in {@code newFormats} thus corresponds to603* the order of elements in the {@code arguments} array passed604* to the {@code format} methods or the result array returned605* by the {@code parse} methods.606* <p>607* If an argument index is used for more than one format element608* in the pattern string, then the corresponding new format is used609* for all such format elements. If an argument index is not used610* for any format element in the pattern string, then the611* corresponding new format is ignored. If fewer formats are provided612* than needed, then only the formats for argument indices less613* than {@code newFormats.length} are replaced.614*615* @param newFormats the new formats to use616* @throws NullPointerException if {@code newFormats} is null617* @since 1.4618*/619public void setFormatsByArgumentIndex(Format[] newFormats) {620for (int i = 0; i <= maxOffset; i++) {621int j = argumentNumbers[i];622if (j < newFormats.length) {623formats[i] = newFormats[j];624}625}626}627628/**629* Sets the formats to use for the format elements in the630* previously set pattern string.631* The order of formats in {@code newFormats} corresponds to632* the order of format elements in the pattern string.633* <p>634* If more formats are provided than needed by the pattern string,635* the remaining ones are ignored. If fewer formats are provided636* than needed, then only the first {@code newFormats.length}637* formats are replaced.638* <p>639* Since the order of format elements in a pattern string often640* changes during localization, it is generally better to use the641* {@link #setFormatsByArgumentIndex setFormatsByArgumentIndex}642* method, which assumes an order of formats corresponding to the643* order of elements in the {@code arguments} array passed to644* the {@code format} methods or the result array returned by645* the {@code parse} methods.646*647* @param newFormats the new formats to use648* @throws NullPointerException if {@code newFormats} is null649*/650public void setFormats(Format[] newFormats) {651int runsToCopy = newFormats.length;652if (runsToCopy > maxOffset + 1) {653runsToCopy = maxOffset + 1;654}655for (int i = 0; i < runsToCopy; i++) {656formats[i] = newFormats[i];657}658}659660/**661* Sets the format to use for the format elements within the662* previously set pattern string that use the given argument663* index.664* The argument index is part of the format element definition and665* represents an index into the {@code arguments} array passed666* to the {@code format} methods or the result array returned667* by the {@code parse} methods.668* <p>669* If the argument index is used for more than one format element670* in the pattern string, then the new format is used for all such671* format elements. If the argument index is not used for any format672* element in the pattern string, then the new format is ignored.673*674* @param argumentIndex the argument index for which to use the new format675* @param newFormat the new format to use676* @since 1.4677*/678public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) {679for (int j = 0; j <= maxOffset; j++) {680if (argumentNumbers[j] == argumentIndex) {681formats[j] = newFormat;682}683}684}685686/**687* Sets the format to use for the format element with the given688* format element index within the previously set pattern string.689* The format element index is the zero-based number of the format690* element counting from the start of the pattern string.691* <p>692* Since the order of format elements in a pattern string often693* changes during localization, it is generally better to use the694* {@link #setFormatByArgumentIndex setFormatByArgumentIndex}695* method, which accesses format elements based on the argument696* index they specify.697*698* @param formatElementIndex the index of a format element within the pattern699* @param newFormat the format to use for the specified format element700* @throws ArrayIndexOutOfBoundsException if {@code formatElementIndex} is equal to or701* larger than the number of format elements in the pattern string702*/703public void setFormat(int formatElementIndex, Format newFormat) {704705if (formatElementIndex > maxOffset) {706throw new ArrayIndexOutOfBoundsException(formatElementIndex);707}708formats[formatElementIndex] = newFormat;709}710711/**712* Gets the formats used for the values passed into713* {@code format} methods or returned from {@code parse}714* methods. The indices of elements in the returned array715* correspond to the argument indices used in the previously set716* pattern string.717* The order of formats in the returned array thus corresponds to718* the order of elements in the {@code arguments} array passed719* to the {@code format} methods or the result array returned720* by the {@code parse} methods.721* <p>722* If an argument index is used for more than one format element723* in the pattern string, then the format used for the last such724* format element is returned in the array. If an argument index725* is not used for any format element in the pattern string, then726* null is returned in the array.727*728* @return the formats used for the arguments within the pattern729* @since 1.4730*/731public Format[] getFormatsByArgumentIndex() {732int maximumArgumentNumber = -1;733for (int i = 0; i <= maxOffset; i++) {734if (argumentNumbers[i] > maximumArgumentNumber) {735maximumArgumentNumber = argumentNumbers[i];736}737}738Format[] resultArray = new Format[maximumArgumentNumber + 1];739for (int i = 0; i <= maxOffset; i++) {740resultArray[argumentNumbers[i]] = formats[i];741}742return resultArray;743}744745/**746* Gets the formats used for the format elements in the747* previously set pattern string.748* The order of formats in the returned array corresponds to749* the order of format elements in the pattern string.750* <p>751* Since the order of format elements in a pattern string often752* changes during localization, it's generally better to use the753* {@link #getFormatsByArgumentIndex getFormatsByArgumentIndex}754* method, which assumes an order of formats corresponding to the755* order of elements in the {@code arguments} array passed to756* the {@code format} methods or the result array returned by757* the {@code parse} methods.758*759* @return the formats used for the format elements in the pattern760*/761public Format[] getFormats() {762Format[] resultArray = new Format[maxOffset + 1];763System.arraycopy(formats, 0, resultArray, 0, maxOffset + 1);764return resultArray;765}766767/**768* Formats an array of objects and appends the {@code MessageFormat}'s769* pattern, with format elements replaced by the formatted objects, to the770* provided {@code StringBuffer}.771* <p>772* The text substituted for the individual format elements is derived from773* the current subformat of the format element and the774* {@code arguments} element at the format element's argument index775* as indicated by the first matching line of the following table. An776* argument is <i>unavailable</i> if {@code arguments} is777* {@code null} or has fewer than argumentIndex+1 elements.778*779* <table class="plain">780* <caption style="display:none">Examples of subformat,argument,and formatted text</caption>781* <thead>782* <tr>783* <th scope="col">Subformat784* <th scope="col">Argument785* <th scope="col">Formatted Text786* </thead>787* <tbody>788* <tr>789* <th scope="row" style="text-weight-normal" rowspan=2><i>any</i>790* <th scope="row" style="text-weight-normal"><i>unavailable</i>791* <td><code>"{" + argumentIndex + "}"</code>792* <tr>793* <th scope="row" style="text-weight-normal">{@code null}794* <td>{@code "null"}795* <tr>796* <th scope="row" style="text-weight-normal">{@code instanceof ChoiceFormat}797* <th scope="row" style="text-weight-normal"><i>any</i>798* <td><code>subformat.format(argument).indexOf('{') >= 0 ?<br>799* (new MessageFormat(subformat.format(argument), getLocale())).format(argument) :800* subformat.format(argument)</code>801* <tr>802* <th scope="row" style="text-weight-normal">{@code != null}803* <th scope="row" style="text-weight-normal"><i>any</i>804* <td>{@code subformat.format(argument)}805* <tr>806* <th scope="row" style="text-weight-normal" rowspan=4>{@code null}807* <th scope="row" style="text-weight-normal">{@code instanceof Number}808* <td>{@code NumberFormat.getInstance(getLocale()).format(argument)}809* <tr>810* <th scope="row" style="text-weight-normal">{@code instanceof Date}811* <td>{@code DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, getLocale()).format(argument)}812* <tr>813* <th scope="row" style="text-weight-normal">{@code instanceof String}814* <td>{@code argument}815* <tr>816* <th scope="row" style="text-weight-normal"><i>any</i>817* <td>{@code argument.toString()}818* </tbody>819* </table>820* <p>821* If {@code pos} is non-null, and refers to822* {@code Field.ARGUMENT}, the location of the first formatted823* string will be returned.824*825* @param arguments an array of objects to be formatted and substituted.826* @param result where text is appended.827* @param pos keeps track on the position of the first replaced argument828* in the output string.829* @return the string buffer passed in as {@code result}, with formatted830* text appended831* @throws IllegalArgumentException if an argument in the832* {@code arguments} array is not of the type833* expected by the format element(s) that use it.834* @throws NullPointerException if {@code result} is {@code null}835*/836public final StringBuffer format(Object[] arguments, StringBuffer result,837FieldPosition pos)838{839return subformat(arguments, result, pos, null);840}841842/**843* Creates a MessageFormat with the given pattern and uses it844* to format the given arguments. This is equivalent to845* <blockquote>846* <code>(new {@link #MessageFormat(String) MessageFormat}(pattern)).{@link #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition) format}(arguments, new StringBuffer(), null).toString()</code>847* </blockquote>848*849* @param pattern the pattern string850* @param arguments object(s) to format851* @return the formatted string852* @throws IllegalArgumentException if the pattern is invalid,853* or if an argument in the {@code arguments} array854* is not of the type expected by the format element(s)855* that use it.856* @throws NullPointerException if {@code pattern} is {@code null}857*/858public static String format(String pattern, Object ... arguments) {859MessageFormat temp = new MessageFormat(pattern);860return temp.format(arguments);861}862863// Overrides864/**865* Formats an array of objects and appends the {@code MessageFormat}'s866* pattern, with format elements replaced by the formatted objects, to the867* provided {@code StringBuffer}.868* This is equivalent to869* <blockquote>870* <code>{@link #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition) format}((Object[]) arguments, result, pos)</code>871* </blockquote>872*873* @param arguments an array of objects to be formatted and substituted.874* @param result where text is appended.875* @param pos keeps track on the position of the first replaced argument876* in the output string.877* @throws IllegalArgumentException if an argument in the878* {@code arguments} array is not of the type879* expected by the format element(s) that use it.880* @throws NullPointerException if {@code result} is {@code null}881*/882public final StringBuffer format(Object arguments, StringBuffer result,883FieldPosition pos)884{885return subformat((Object[]) arguments, result, pos, null);886}887888/**889* Formats an array of objects and inserts them into the890* {@code MessageFormat}'s pattern, producing an891* {@code AttributedCharacterIterator}.892* You can use the returned {@code AttributedCharacterIterator}893* to build the resulting String, as well as to determine information894* about the resulting String.895* <p>896* The text of the returned {@code AttributedCharacterIterator} is897* the same that would be returned by898* <blockquote>899* <code>{@link #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition) format}(arguments, new StringBuffer(), null).toString()</code>900* </blockquote>901* <p>902* In addition, the {@code AttributedCharacterIterator} contains at903* least attributes indicating where text was generated from an904* argument in the {@code arguments} array. The keys of these attributes are of905* type {@code MessageFormat.Field}, their values are906* {@code Integer} objects indicating the index in the {@code arguments}907* array of the argument from which the text was generated.908* <p>909* The attributes/value from the underlying {@code Format}910* instances that {@code MessageFormat} uses will also be911* placed in the resulting {@code AttributedCharacterIterator}.912* This allows you to not only find where an argument is placed in the913* resulting String, but also which fields it contains in turn.914*915* @param arguments an array of objects to be formatted and substituted.916* @return AttributedCharacterIterator describing the formatted value.917* @throws NullPointerException if {@code arguments} is null.918* @throws IllegalArgumentException if an argument in the919* {@code arguments} array is not of the type920* expected by the format element(s) that use it.921* @since 1.4922*/923public AttributedCharacterIterator formatToCharacterIterator(Object arguments) {924StringBuffer result = new StringBuffer();925ArrayList<AttributedCharacterIterator> iterators = new ArrayList<>();926927if (arguments == null) {928throw new NullPointerException(929"formatToCharacterIterator must be passed non-null object");930}931subformat((Object[]) arguments, result, null, iterators);932if (iterators.size() == 0) {933return createAttributedCharacterIterator("");934}935return createAttributedCharacterIterator(936iterators.toArray(937new AttributedCharacterIterator[iterators.size()]));938}939940/**941* Parses the string.942*943* <p>Caveats: The parse may fail in a number of circumstances.944* For example:945* <ul>946* <li>If one of the arguments does not occur in the pattern.947* <li>If the format of an argument loses information, such as948* with a choice format where a large number formats to "many".949* <li>Does not yet handle recursion (where950* the substituted strings contain {n} references.)951* <li>Will not always find a match (or the correct match)952* if some part of the parse is ambiguous.953* For example, if the pattern "{1},{2}" is used with the954* string arguments {"a,b", "c"}, it will format as "a,b,c".955* When the result is parsed, it will return {"a", "b,c"}.956* <li>If a single argument is parsed more than once in the string,957* then the later parse wins.958* </ul>959* When the parse fails, use ParsePosition.getErrorIndex() to find out960* where in the string the parsing failed. The returned error961* index is the starting offset of the sub-patterns that the string962* is comparing with. For example, if the parsing string "AAA {0} BBB"963* is comparing against the pattern "AAD {0} BBB", the error index is964* 0. When an error occurs, the call to this method will return null.965* If the source is null, return an empty array.966*967* @param source the string to parse968* @param pos the parse position969* @return an array of parsed objects970* @throws NullPointerException if {@code pos} is {@code null}971* for a non-null {@code source} string.972*/973public Object[] parse(String source, ParsePosition pos) {974if (source == null) {975Object[] empty = {};976return empty;977}978979int maximumArgumentNumber = -1;980for (int i = 0; i <= maxOffset; i++) {981if (argumentNumbers[i] > maximumArgumentNumber) {982maximumArgumentNumber = argumentNumbers[i];983}984}985Object[] resultArray = new Object[maximumArgumentNumber + 1];986987int patternOffset = 0;988int sourceOffset = pos.index;989ParsePosition tempStatus = new ParsePosition(0);990for (int i = 0; i <= maxOffset; ++i) {991// match up to format992int len = offsets[i] - patternOffset;993if (len == 0 || pattern.regionMatches(patternOffset,994source, sourceOffset, len)) {995sourceOffset += len;996patternOffset += len;997} else {998pos.errorIndex = sourceOffset;999return null; // leave index as is to signal error1000}10011002// now use format1003if (formats[i] == null) { // string format1004// if at end, use longest possible match1005// otherwise uses first match to intervening string1006// does NOT recursively try all possibilities1007int tempLength = (i != maxOffset) ? offsets[i+1] : pattern.length();10081009int next;1010if (patternOffset >= tempLength) {1011next = source.length();1012}else{1013next = source.indexOf(pattern.substring(patternOffset, tempLength),1014sourceOffset);1015}10161017if (next < 0) {1018pos.errorIndex = sourceOffset;1019return null; // leave index as is to signal error1020} else {1021String strValue= source.substring(sourceOffset,next);1022if (!strValue.equals("{"+argumentNumbers[i]+"}"))1023resultArray[argumentNumbers[i]]1024= source.substring(sourceOffset,next);1025sourceOffset = next;1026}1027} else {1028tempStatus.index = sourceOffset;1029resultArray[argumentNumbers[i]]1030= formats[i].parseObject(source,tempStatus);1031if (tempStatus.index == sourceOffset) {1032pos.errorIndex = sourceOffset;1033return null; // leave index as is to signal error1034}1035sourceOffset = tempStatus.index; // update1036}1037}1038int len = pattern.length() - patternOffset;1039if (len == 0 || pattern.regionMatches(patternOffset,1040source, sourceOffset, len)) {1041pos.index = sourceOffset + len;1042} else {1043pos.errorIndex = sourceOffset;1044return null; // leave index as is to signal error1045}1046return resultArray;1047}10481049/**1050* Parses text from the beginning of the given string to produce an object1051* array.1052* The method may not use the entire text of the given string.1053* <p>1054* See the {@link #parse(String, ParsePosition)} method for more information1055* on message parsing.1056*1057* @param source A {@code String} whose beginning should be parsed.1058* @return An {@code Object} array parsed from the string.1059* @throws ParseException if the beginning of the specified string1060* cannot be parsed.1061*/1062public Object[] parse(String source) throws ParseException {1063ParsePosition pos = new ParsePosition(0);1064Object[] result = parse(source, pos);1065if (pos.index == 0) // unchanged, returned object is null1066throw new ParseException("MessageFormat parse error!", pos.errorIndex);10671068return result;1069}10701071/**1072* Parses text from a string to produce an object array.1073* <p>1074* The method attempts to parse text starting at the index given by1075* {@code pos}.1076* If parsing succeeds, then the index of {@code pos} is updated1077* to the index after the last character used (parsing does not necessarily1078* use all characters up to the end of the string), and the parsed1079* object array is returned. The updated {@code pos} can be used to1080* indicate the starting point for the next call to this method.1081* If an error occurs, then the index of {@code pos} is not1082* changed, the error index of {@code pos} is set to the index of1083* the character where the error occurred, and null is returned.1084* <p>1085* See the {@link #parse(String, ParsePosition)} method for more information1086* on message parsing.1087*1088* @param source A {@code String}, part of which should be parsed.1089* @param pos A {@code ParsePosition} object with index and error1090* index information as described above.1091* @return An {@code Object} array parsed from the string. In case of1092* error, returns null.1093* @throws NullPointerException if {@code pos} is null.1094*/1095public Object parseObject(String source, ParsePosition pos) {1096return parse(source, pos);1097}10981099/**1100* Creates and returns a copy of this object.1101*1102* @return a clone of this instance.1103*/1104public Object clone() {1105MessageFormat other = (MessageFormat) super.clone();11061107// clone arrays. Can't do with utility because of bug in Cloneable1108other.formats = formats.clone(); // shallow clone1109for (int i = 0; i < formats.length; ++i) {1110if (formats[i] != null)1111other.formats[i] = (Format)formats[i].clone();1112}1113// for primitives or immutables, shallow clone is enough1114other.offsets = offsets.clone();1115other.argumentNumbers = argumentNumbers.clone();11161117return other;1118}11191120/**1121* Equality comparison between two message format objects1122*/1123public boolean equals(Object obj) {1124if (this == obj) // quick check1125return true;1126if (obj == null || getClass() != obj.getClass())1127return false;1128MessageFormat other = (MessageFormat) obj;1129return (maxOffset == other.maxOffset1130&& pattern.equals(other.pattern)1131&& ((locale != null && locale.equals(other.locale))1132|| (locale == null && other.locale == null))1133&& Arrays.equals(offsets,other.offsets)1134&& Arrays.equals(argumentNumbers,other.argumentNumbers)1135&& Arrays.equals(formats,other.formats));1136}11371138/**1139* Generates a hash code for the message format object.1140*/1141public int hashCode() {1142return pattern.hashCode(); // enough for reasonable distribution1143}114411451146/**1147* Defines constants that are used as attribute keys in the1148* {@code AttributedCharacterIterator} returned1149* from {@code MessageFormat.formatToCharacterIterator}.1150*1151* @since 1.41152*/1153public static class Field extends Format.Field {11541155// Proclaim serial compatibility with 1.4 FCS1156@java.io.Serial1157private static final long serialVersionUID = 7899943957617360810L;11581159/**1160* Creates a Field with the specified name.1161*1162* @param name Name of the attribute1163*/1164protected Field(String name) {1165super(name);1166}11671168/**1169* Resolves instances being deserialized to the predefined constants.1170*1171* @throws InvalidObjectException if the constant could not be1172* resolved.1173* @return resolved MessageFormat.Field constant1174*/1175@java.io.Serial1176protected Object readResolve() throws InvalidObjectException {1177if (this.getClass() != MessageFormat.Field.class) {1178throw new InvalidObjectException("subclass didn't correctly implement readResolve");1179}11801181return ARGUMENT;1182}11831184//1185// The constants1186//11871188/**1189* Constant identifying a portion of a message that was generated1190* from an argument passed into {@code formatToCharacterIterator}.1191* The value associated with the key will be an {@code Integer}1192* indicating the index in the {@code arguments} array of the1193* argument from which the text was generated.1194*/1195public static final Field ARGUMENT =1196new Field("message argument field");1197}11981199// ===========================privates============================12001201/**1202* The locale to use for formatting numbers and dates.1203* @serial1204*/1205private Locale locale;12061207/**1208* The string that the formatted values are to be plugged into. In other words, this1209* is the pattern supplied on construction with all of the {} expressions taken out.1210* @serial1211*/1212private String pattern = "";12131214/** The initially expected number of subformats in the format */1215private static final int INITIAL_FORMATS = 10;12161217/**1218* An array of formatters, which are used to format the arguments.1219* @serial1220*/1221private Format[] formats = new Format[INITIAL_FORMATS];12221223/**1224* The positions where the results of formatting each argument are to be inserted1225* into the pattern.1226* @serial1227*/1228private int[] offsets = new int[INITIAL_FORMATS];12291230/**1231* The argument numbers corresponding to each formatter. (The formatters are stored1232* in the order they occur in the pattern, not in the order in which the arguments1233* are specified.)1234* @serial1235*/1236private int[] argumentNumbers = new int[INITIAL_FORMATS];12371238/**1239* One less than the number of entries in {@code offsets}. Can also be thought of1240* as the index of the highest-numbered element in {@code offsets} that is being used.1241* All of these arrays should have the same number of elements being used as {@code offsets}1242* does, and so this variable suffices to tell us how many entries are in all of them.1243* @serial1244*/1245private int maxOffset = -1;12461247/**1248* Internal routine used by format. If {@code characterIterators} is1249* {@code non-null}, AttributedCharacterIterator will be created from the1250* subformats as necessary. If {@code characterIterators} is {@code null}1251* and {@code fp} is {@code non-null} and identifies1252* {@code Field.ARGUMENT} as the field attribute, the location of1253* the first replaced argument will be set in it.1254*1255* @throws IllegalArgumentException if an argument in the1256* {@code arguments} array is not of the type1257* expected by the format element(s) that use it.1258*/1259private StringBuffer subformat(Object[] arguments, StringBuffer result,1260FieldPosition fp, List<AttributedCharacterIterator> characterIterators) {1261// note: this implementation assumes a fast substring & index.1262// if this is not true, would be better to append chars one by one.1263int lastOffset = 0;1264int last = result.length();1265for (int i = 0; i <= maxOffset; ++i) {1266result.append(pattern, lastOffset, offsets[i]);1267lastOffset = offsets[i];1268int argumentNumber = argumentNumbers[i];1269if (arguments == null || argumentNumber >= arguments.length) {1270result.append('{').append(argumentNumber).append('}');1271continue;1272}1273// int argRecursion = ((recursionProtection >> (argumentNumber*2)) & 0x3);1274if (false) { // if (argRecursion == 3){1275// prevent loop!!!1276result.append('\uFFFD');1277} else {1278Object obj = arguments[argumentNumber];1279String arg = null;1280Format subFormatter = null;1281if (obj == null) {1282arg = "null";1283} else if (formats[i] != null) {1284subFormatter = formats[i];1285if (subFormatter instanceof ChoiceFormat) {1286arg = formats[i].format(obj);1287if (arg.indexOf('{') >= 0) {1288subFormatter = new MessageFormat(arg, locale);1289obj = arguments;1290arg = null;1291}1292}1293} else if (obj instanceof Number) {1294// format number if can1295subFormatter = NumberFormat.getInstance(locale);1296} else if (obj instanceof Date) {1297// format a Date if can1298subFormatter = DateFormat.getDateTimeInstance(1299DateFormat.SHORT, DateFormat.SHORT, locale);//fix1300} else if (obj instanceof String) {1301arg = (String) obj;13021303} else {1304arg = obj.toString();1305if (arg == null) arg = "null";1306}13071308// At this point we are in two states, either subFormatter1309// is non-null indicating we should format obj using it,1310// or arg is non-null and we should use it as the value.13111312if (characterIterators != null) {1313// If characterIterators is non-null, it indicates we need1314// to get the CharacterIterator from the child formatter.1315if (last != result.length()) {1316characterIterators.add(1317createAttributedCharacterIterator(result.substring1318(last)));1319last = result.length();1320}1321if (subFormatter != null) {1322AttributedCharacterIterator subIterator =1323subFormatter.formatToCharacterIterator(obj);13241325append(result, subIterator);1326if (last != result.length()) {1327characterIterators.add(1328createAttributedCharacterIterator(1329subIterator, Field.ARGUMENT,1330Integer.valueOf(argumentNumber)));1331last = result.length();1332}1333arg = null;1334}1335if (arg != null && !arg.isEmpty()) {1336result.append(arg);1337characterIterators.add(1338createAttributedCharacterIterator(1339arg, Field.ARGUMENT,1340Integer.valueOf(argumentNumber)));1341last = result.length();1342}1343}1344else {1345if (subFormatter != null) {1346arg = subFormatter.format(obj);1347}1348last = result.length();1349result.append(arg);1350if (i == 0 && fp != null && Field.ARGUMENT.equals(1351fp.getFieldAttribute())) {1352fp.setBeginIndex(last);1353fp.setEndIndex(result.length());1354}1355last = result.length();1356}1357}1358}1359result.append(pattern, lastOffset, pattern.length());1360if (characterIterators != null && last != result.length()) {1361characterIterators.add(createAttributedCharacterIterator(1362result.substring(last)));1363}1364return result;1365}13661367/**1368* Convenience method to append all the characters in1369* {@code iterator} to the StringBuffer {@code result}.1370*/1371private void append(StringBuffer result, CharacterIterator iterator) {1372if (iterator.first() != CharacterIterator.DONE) {1373char aChar;13741375result.append(iterator.first());1376while ((aChar = iterator.next()) != CharacterIterator.DONE) {1377result.append(aChar);1378}1379}1380}13811382// Indices for segments1383private static final int SEG_RAW = 0;1384private static final int SEG_INDEX = 1;1385private static final int SEG_TYPE = 2;1386private static final int SEG_MODIFIER = 3; // modifier or subformat13871388// Indices for type keywords1389private static final int TYPE_NULL = 0;1390private static final int TYPE_NUMBER = 1;1391private static final int TYPE_DATE = 2;1392private static final int TYPE_TIME = 3;1393private static final int TYPE_CHOICE = 4;13941395private static final String[] TYPE_KEYWORDS = {1396"",1397"number",1398"date",1399"time",1400"choice"1401};14021403// Indices for number modifiers1404private static final int MODIFIER_DEFAULT = 0; // common in number and date-time1405private static final int MODIFIER_CURRENCY = 1;1406private static final int MODIFIER_PERCENT = 2;1407private static final int MODIFIER_INTEGER = 3;14081409private static final String[] NUMBER_MODIFIER_KEYWORDS = {1410"",1411"currency",1412"percent",1413"integer"1414};14151416// Indices for date-time modifiers1417private static final int MODIFIER_SHORT = 1;1418private static final int MODIFIER_MEDIUM = 2;1419private static final int MODIFIER_LONG = 3;1420private static final int MODIFIER_FULL = 4;14211422private static final String[] DATE_TIME_MODIFIER_KEYWORDS = {1423"",1424"short",1425"medium",1426"long",1427"full"1428};14291430// Date-time style values corresponding to the date-time modifiers.1431private static final int[] DATE_TIME_MODIFIERS = {1432DateFormat.DEFAULT,1433DateFormat.SHORT,1434DateFormat.MEDIUM,1435DateFormat.LONG,1436DateFormat.FULL,1437};14381439private void makeFormat(int position, int offsetNumber,1440StringBuilder[] textSegments)1441{1442String[] segments = new String[textSegments.length];1443for (int i = 0; i < textSegments.length; i++) {1444StringBuilder oneseg = textSegments[i];1445segments[i] = (oneseg != null) ? oneseg.toString() : "";1446}14471448// get the argument number1449int argumentNumber;1450try {1451argumentNumber = Integer.parseInt(segments[SEG_INDEX]); // always unlocalized!1452} catch (NumberFormatException e) {1453throw new IllegalArgumentException("can't parse argument number: "1454+ segments[SEG_INDEX], e);1455}1456if (argumentNumber < 0) {1457throw new IllegalArgumentException("negative argument number: "1458+ argumentNumber);1459}14601461// resize format information arrays if necessary1462if (offsetNumber >= formats.length) {1463int newLength = formats.length * 2;1464Format[] newFormats = new Format[newLength];1465int[] newOffsets = new int[newLength];1466int[] newArgumentNumbers = new int[newLength];1467System.arraycopy(formats, 0, newFormats, 0, maxOffset + 1);1468System.arraycopy(offsets, 0, newOffsets, 0, maxOffset + 1);1469System.arraycopy(argumentNumbers, 0, newArgumentNumbers, 0, maxOffset + 1);1470formats = newFormats;1471offsets = newOffsets;1472argumentNumbers = newArgumentNumbers;1473}1474int oldMaxOffset = maxOffset;1475maxOffset = offsetNumber;1476offsets[offsetNumber] = segments[SEG_RAW].length();1477argumentNumbers[offsetNumber] = argumentNumber;14781479// now get the format1480Format newFormat = null;1481if (!segments[SEG_TYPE].isEmpty()) {1482int type = findKeyword(segments[SEG_TYPE], TYPE_KEYWORDS);1483switch (type) {1484case TYPE_NULL:1485// Type "" is allowed. e.g., "{0,}", "{0,,}", and "{0,,#}"1486// are treated as "{0}".1487break;14881489case TYPE_NUMBER:1490switch (findKeyword(segments[SEG_MODIFIER], NUMBER_MODIFIER_KEYWORDS)) {1491case MODIFIER_DEFAULT:1492newFormat = NumberFormat.getInstance(locale);1493break;1494case MODIFIER_CURRENCY:1495newFormat = NumberFormat.getCurrencyInstance(locale);1496break;1497case MODIFIER_PERCENT:1498newFormat = NumberFormat.getPercentInstance(locale);1499break;1500case MODIFIER_INTEGER:1501newFormat = NumberFormat.getIntegerInstance(locale);1502break;1503default: // DecimalFormat pattern1504try {1505newFormat = new DecimalFormat(segments[SEG_MODIFIER],1506DecimalFormatSymbols.getInstance(locale));1507} catch (IllegalArgumentException e) {1508maxOffset = oldMaxOffset;1509throw e;1510}1511break;1512}1513break;15141515case TYPE_DATE:1516case TYPE_TIME:1517int mod = findKeyword(segments[SEG_MODIFIER], DATE_TIME_MODIFIER_KEYWORDS);1518if (mod >= 0 && mod < DATE_TIME_MODIFIER_KEYWORDS.length) {1519if (type == TYPE_DATE) {1520newFormat = DateFormat.getDateInstance(DATE_TIME_MODIFIERS[mod],1521locale);1522} else {1523newFormat = DateFormat.getTimeInstance(DATE_TIME_MODIFIERS[mod],1524locale);1525}1526} else {1527// SimpleDateFormat pattern1528try {1529newFormat = new SimpleDateFormat(segments[SEG_MODIFIER], locale);1530} catch (IllegalArgumentException e) {1531maxOffset = oldMaxOffset;1532throw e;1533}1534}1535break;15361537case TYPE_CHOICE:1538try {1539// ChoiceFormat pattern1540newFormat = new ChoiceFormat(segments[SEG_MODIFIER]);1541} catch (Exception e) {1542maxOffset = oldMaxOffset;1543throw new IllegalArgumentException("Choice Pattern incorrect: "1544+ segments[SEG_MODIFIER], e);1545}1546break;15471548default:1549maxOffset = oldMaxOffset;1550throw new IllegalArgumentException("unknown format type: " +1551segments[SEG_TYPE]);1552}1553}1554formats[offsetNumber] = newFormat;1555}15561557private static final int findKeyword(String s, String[] list) {1558for (int i = 0; i < list.length; ++i) {1559if (s.equals(list[i]))1560return i;1561}15621563// Try trimmed lowercase.1564String ls = s.trim().toLowerCase(Locale.ROOT);1565if (ls != s) {1566for (int i = 0; i < list.length; ++i) {1567if (ls.equals(list[i]))1568return i;1569}1570}1571return -1;1572}15731574private static final void copyAndFixQuotes(String source, int start, int end,1575StringBuilder target) {1576boolean quoted = false;15771578for (int i = start; i < end; ++i) {1579char ch = source.charAt(i);1580if (ch == '{') {1581if (!quoted) {1582target.append('\'');1583quoted = true;1584}1585target.append(ch);1586} else if (ch == '\'') {1587target.append("''");1588} else {1589if (quoted) {1590target.append('\'');1591quoted = false;1592}1593target.append(ch);1594}1595}1596if (quoted) {1597target.append('\'');1598}1599}16001601/**1602* After reading an object from the input stream, do a simple verification1603* to maintain class invariants.1604* @throws InvalidObjectException if the objects read from the stream is invalid.1605*/1606@java.io.Serial1607private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {1608in.defaultReadObject();1609boolean isValid = maxOffset >= -11610&& formats.length > maxOffset1611&& offsets.length > maxOffset1612&& argumentNumbers.length > maxOffset;1613if (isValid) {1614int lastOffset = pattern.length() + 1;1615for (int i = maxOffset; i >= 0; --i) {1616if ((offsets[i] < 0) || (offsets[i] > lastOffset)) {1617isValid = false;1618break;1619} else {1620lastOffset = offsets[i];1621}1622}1623}1624if (!isValid) {1625throw new InvalidObjectException("Could not reconstruct MessageFormat from corrupt stream.");1626}1627}1628}162916301631