|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
|
|
/* |
|
* (C) Copyright Taligent, Inc. 1996, 1997 - All Rights Reserved |
|
* (C) Copyright IBM Corp. 1996 - 1998 - All Rights Reserved |
|
* |
|
* The original version of this source code and documentation is copyrighted |
|
* and owned by Taligent, Inc., a wholly-owned subsidiary of IBM. These |
|
* materials are provided under terms of a License Agreement between Taligent |
|
* and Sun. This technology is protected by multiple US and International |
|
* patents. This notice and attribution to Taligent may not be removed. |
|
* Taligent is a registered trademark of Taligent, Inc. |
|
* |
|
*/ |
|
|
|
package java.text; |
|
|
|
import java.io.InvalidObjectException; |
|
import java.io.IOException; |
|
import java.io.ObjectInputStream; |
|
import java.text.DecimalFormat; |
|
import java.util.ArrayList; |
|
import java.util.Arrays; |
|
import java.util.Date; |
|
import java.util.List; |
|
import java.util.Locale; |
|
|
|
|
|
/** |
|
* <code>MessageFormat</code> provides a means to produce concatenated |
|
* messages in a language-neutral way. Use this to construct messages |
|
* displayed for end users. |
|
* |
|
* <p> |
|
* <code>MessageFormat</code> takes a set of objects, formats them, then |
|
* inserts the formatted strings into the pattern at the appropriate places. |
|
* |
|
* <p> |
|
* <strong>Note:</strong> |
|
* <code>MessageFormat</code> differs from the other <code>Format</code> |
|
* classes in that you create a <code>MessageFormat</code> object with one |
|
* of its constructors (not with a <code>getInstance</code> style factory |
|
* method). The factory methods aren't necessary because <code>MessageFormat</code> |
|
* itself doesn't implement locale specific behavior. Any locale specific |
|
* behavior is defined by the pattern that you provide as well as the |
|
* subformats used for inserted arguments. |
|
* |
|
* <h3><a id="patterns">Patterns and Their Interpretation</a></h3> |
|
* |
|
* <code>MessageFormat</code> uses patterns of the following form: |
|
* <blockquote><pre> |
|
* <i>MessageFormatPattern:</i> |
|
* <i>String</i> |
|
* <i>MessageFormatPattern</i> <i>FormatElement</i> <i>String</i> |
|
* |
|
* <i>FormatElement:</i> |
|
* { <i>ArgumentIndex</i> } |
|
* { <i>ArgumentIndex</i> , <i>FormatType</i> } |
|
* { <i>ArgumentIndex</i> , <i>FormatType</i> , <i>FormatStyle</i> } |
|
* |
|
* <i>FormatType: one of </i> |
|
* number date time choice |
|
* |
|
* <i>FormatStyle:</i> |
|
* short |
|
* medium |
|
* long |
|
* full |
|
* integer |
|
* currency |
|
* percent |
|
* <i>SubformatPattern</i> |
|
* </pre></blockquote> |
|
* |
|
* <p>Within a <i>String</i>, a pair of single quotes can be used to |
|
* quote any arbitrary characters except single quotes. For example, |
|
* pattern string <code>"'{0}'"</code> represents string |
|
* <code>"{0}"</code>, not a <i>FormatElement</i>. A single quote itself |
|
* must be represented by doubled single quotes {@code ''} throughout a |
|
* <i>String</i>. For example, pattern string <code>"'{''}'"</code> is |
|
* interpreted as a sequence of <code>'{</code> (start of quoting and a |
|
* left curly brace), <code>''</code> (a single quote), and |
|
* <code>}'</code> (a right curly brace and end of quoting), |
|
* <em>not</em> <code>'{'</code> and <code>'}'</code> (quoted left and |
|
* right curly braces): representing string <code>"{'}"</code>, |
|
* <em>not</em> <code>"{}"</code>. |
|
* |
|
* <p>A <i>SubformatPattern</i> is interpreted by its corresponding |
|
* subformat, and subformat-dependent pattern rules apply. For example, |
|
* pattern string <code>"{1,number,<u>$'#',##</u>}"</code> |
|
* (<i>SubformatPattern</i> with underline) will produce a number format |
|
* with the pound-sign quoted, with a result such as: {@code |
|
* "$#31,45"}. Refer to each {@code Format} subclass documentation for |
|
* details. |
|
* |
|
* <p>Any unmatched quote is treated as closed at the end of the given |
|
* pattern. For example, pattern string {@code "'{0}"} is treated as |
|
* pattern {@code "'{0}'"}. |
|
* |
|
* <p>Any curly braces within an unquoted pattern must be balanced. For |
|
* example, <code>"ab {0} de"</code> and <code>"ab '}' de"</code> are |
|
* valid patterns, but <code>"ab {0'}' de"</code>, <code>"ab } de"</code> |
|
* and <code>"''{''"</code> are not. |
|
* |
|
* <dl><dt><b>Warning:</b><dd>The rules for using quotes within message |
|
* format patterns unfortunately have shown to be somewhat confusing. |
|
* In particular, it isn't always obvious to localizers whether single |
|
* quotes need to be doubled or not. Make sure to inform localizers about |
|
* the rules, and tell them (for example, by using comments in resource |
|
* bundle source files) which strings will be processed by {@code MessageFormat}. |
|
* Note that localizers may need to use single quotes in translated |
|
* strings where the original version doesn't have them. |
|
* </dl> |
|
* <p> |
|
* The <i>ArgumentIndex</i> value is a non-negative integer written |
|
* using the digits {@code '0'} through {@code '9'}, and represents an index into the |
|
* {@code arguments} array passed to the {@code format} methods |
|
* or the result array returned by the {@code parse} methods. |
|
* <p> |
|
* The <i>FormatType</i> and <i>FormatStyle</i> values are used to create |
|
* a {@code Format} instance for the format element. The following |
|
* table shows how the values map to {@code Format} instances. Combinations not |
|
* shown in the table are illegal. A <i>SubformatPattern</i> must |
|
* be a valid pattern string for the {@code Format} subclass used. |
|
* |
|
* <table class="plain"> |
|
* <caption style="display:none">Shows how FormatType and FormatStyle values map to Format instances</caption> |
|
* <thead> |
|
* <tr> |
|
* <th scope="col" class="TableHeadingColor">FormatType |
|
* <th scope="col" class="TableHeadingColor">FormatStyle |
|
* <th scope="col" class="TableHeadingColor">Subformat Created |
|
* </thead> |
|
* <tbody> |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal"><i>(none)</i> |
|
* <th scope="row" style="text-weight: normal"><i>(none)</i> |
|
* <td>{@code null} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal" rowspan=5>{@code number} |
|
* <th scope="row" style="text-weight: normal"><i>(none)</i> |
|
* <td>{@link NumberFormat#getInstance(Locale) NumberFormat.getInstance}{@code (getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal">{@code integer} |
|
* <td>{@link NumberFormat#getIntegerInstance(Locale) NumberFormat.getIntegerInstance}{@code (getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal">{@code currency} |
|
* <td>{@link NumberFormat#getCurrencyInstance(Locale) NumberFormat.getCurrencyInstance}{@code (getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal">{@code percent} |
|
* <td>{@link NumberFormat#getPercentInstance(Locale) NumberFormat.getPercentInstance}{@code (getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal"><i>SubformatPattern</i> |
|
* <td>{@code new} {@link DecimalFormat#DecimalFormat(String,DecimalFormatSymbols) DecimalFormat}{@code (subformatPattern,} {@link DecimalFormatSymbols#getInstance(Locale) DecimalFormatSymbols.getInstance}{@code (getLocale()))} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal" rowspan=6>{@code date} |
|
* <th scope="row" style="text-weight: normal"><i>(none)</i> |
|
* <td>{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal">{@code short} |
|
* <td>{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#SHORT}{@code , getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal">{@code medium} |
|
* <td>{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal">{@code long} |
|
* <td>{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#LONG}{@code , getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal">{@code full} |
|
* <td>{@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#FULL}{@code , getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal"><i>SubformatPattern</i> |
|
* <td>{@code new} {@link SimpleDateFormat#SimpleDateFormat(String,Locale) SimpleDateFormat}{@code (subformatPattern, getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal" rowspan=6>{@code time} |
|
* <th scope="row" style="text-weight: normal"><i>(none)</i> |
|
* <td>{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal">{@code short} |
|
* <td>{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#SHORT}{@code , getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal">{@code medium} |
|
* <td>{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal">{@code long} |
|
* <td>{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#LONG}{@code , getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal">{@code full} |
|
* <td>{@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#FULL}{@code , getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal"><i>SubformatPattern</i> |
|
* <td>{@code new} {@link SimpleDateFormat#SimpleDateFormat(String,Locale) SimpleDateFormat}{@code (subformatPattern, getLocale())} |
|
* <tr> |
|
* <th scope="row" style="text-weight: normal">{@code choice} |
|
* <th scope="row" style="text-weight: normal"><i>SubformatPattern</i> |
|
* <td>{@code new} {@link ChoiceFormat#ChoiceFormat(String) ChoiceFormat}{@code (subformatPattern)} |
|
* </tbody> |
|
* </table> |
|
* |
|
* <h4>Usage Information</h4> |
|
* |
|
* <p> |
|
* Here are some examples of usage. |
|
* In real internationalized programs, the message format pattern and other |
|
* static strings will, of course, be obtained from resource bundles. |
|
* Other parameters will be dynamically determined at runtime. |
|
* <p> |
|
* The first example uses the static method <code>MessageFormat.format</code>, |
|
* which internally creates a <code>MessageFormat</code> for one-time use: |
|
* <blockquote><pre> |
|
* int planet = 7; |
|
* String event = "a disturbance in the Force"; |
|
* |
|
* String result = MessageFormat.format( |
|
* "At {1,time} on {1,date}, there was {2} on planet {0,number,integer}.", |
|
* planet, new Date(), event); |
|
* </pre></blockquote> |
|
* The output is: |
|
* <blockquote><pre> |
|
* At 12:30 PM on Jul 3, 2053, there was a disturbance in the Force on planet 7. |
|
* </pre></blockquote> |
|
* |
|
* <p> |
|
* The following example creates a <code>MessageFormat</code> instance that |
|
* can be used repeatedly: |
|
* <blockquote><pre> |
|
* int fileCount = 1273; |
|
* String diskName = "MyDisk"; |
|
* Object[] testArgs = {new Long(fileCount), diskName}; |
|
* |
|
* MessageFormat form = new MessageFormat( |
|
* "The disk \"{1}\" contains {0} file(s)."); |
|
* |
|
* System.out.println(form.format(testArgs)); |
|
* </pre></blockquote> |
|
* The output with different values for <code>fileCount</code>: |
|
* <blockquote><pre> |
|
* The disk "MyDisk" contains 0 file(s). |
|
* The disk "MyDisk" contains 1 file(s). |
|
* The disk "MyDisk" contains 1,273 file(s). |
|
* </pre></blockquote> |
|
* |
|
* <p> |
|
* For more sophisticated patterns, you can use a <code>ChoiceFormat</code> |
|
* to produce correct forms for singular and plural: |
|
* <blockquote><pre> |
|
* MessageFormat form = new MessageFormat("The disk \"{1}\" contains {0}."); |
|
* double[] filelimits = {0,1,2}; |
|
* String[] filepart = {"no files","one file","{0,number} files"}; |
|
* ChoiceFormat fileform = new ChoiceFormat(filelimits, filepart); |
|
* form.setFormatByArgumentIndex(0, fileform); |
|
* |
|
* int fileCount = 1273; |
|
* String diskName = "MyDisk"; |
|
* Object[] testArgs = {new Long(fileCount), diskName}; |
|
* |
|
* System.out.println(form.format(testArgs)); |
|
* </pre></blockquote> |
|
* The output with different values for <code>fileCount</code>: |
|
* <blockquote><pre> |
|
* The disk "MyDisk" contains no files. |
|
* The disk "MyDisk" contains one file. |
|
* The disk "MyDisk" contains 1,273 files. |
|
* </pre></blockquote> |
|
* |
|
* <p> |
|
* You can create the <code>ChoiceFormat</code> programmatically, as in the |
|
* above example, or by using a pattern. See {@link ChoiceFormat} |
|
* for more information. |
|
* <blockquote><pre>{@code |
|
* form.applyPattern( |
|
* "There {0,choice,0#are no files|1#is one file|1<are {0,number,integer} files}."); |
|
* }</pre></blockquote> |
|
* |
|
* <p> |
|
* <strong>Note:</strong> As we see above, the string produced |
|
* by a <code>ChoiceFormat</code> in <code>MessageFormat</code> is treated as special; |
|
* occurrences of '{' are used to indicate subformats, and cause recursion. |
|
* If you create both a <code>MessageFormat</code> and <code>ChoiceFormat</code> |
|
* programmatically (instead of using the string patterns), then be careful not to |
|
* produce a format that recurses on itself, which will cause an infinite loop. |
|
* <p> |
|
* When a single argument is parsed more than once in the string, the last match |
|
* will be the final result of the parsing. For example, |
|
* <blockquote><pre> |
|
* MessageFormat mf = new MessageFormat("{0,number,#.##}, {0,number,#.#}"); |
|
* Object[] objs = {new Double(3.1415)}; |
|
* String result = mf.format( objs ); |
|
* // result now equals "3.14, 3.1" |
|
* objs = null; |
|
* objs = mf.parse(result, new ParsePosition(0)); |
|
* // objs now equals {new Double(3.1)} |
|
* </pre></blockquote> |
|
* |
|
* <p> |
|
* Likewise, parsing with a {@code MessageFormat} object using patterns containing |
|
* multiple occurrences of the same argument would return the last match. For |
|
* example, |
|
* <blockquote><pre> |
|
* MessageFormat mf = new MessageFormat("{0}, {0}, {0}"); |
|
* String forParsing = "x, y, z"; |
|
* Object[] objs = mf.parse(forParsing, new ParsePosition(0)); |
|
* // result now equals {new String("z")} |
|
* </pre></blockquote> |
|
* |
|
* <h4><a id="synchronization">Synchronization</a></h4> |
|
* |
|
* <p> |
|
* Message formats are not synchronized. |
|
* It is recommended to create separate format instances for each thread. |
|
* If multiple threads access a format concurrently, it must be synchronized |
|
* externally. |
|
* |
|
* @see java.util.Locale |
|
* @see Format |
|
* @see NumberFormat |
|
* @see DecimalFormat |
|
* @see DecimalFormatSymbols |
|
* @see ChoiceFormat |
|
* @see DateFormat |
|
* @see SimpleDateFormat |
|
* |
|
* @author Mark Davis |
|
* @since 1.1 |
|
*/ |
|
|
|
public class MessageFormat extends Format { |
|
|
|
private static final long serialVersionUID = 6479157306784022952L; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public MessageFormat(String pattern) { |
|
this.locale = Locale.getDefault(Locale.Category.FORMAT); |
|
applyPattern(pattern); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public MessageFormat(String pattern, Locale locale) { |
|
this.locale = locale; |
|
applyPattern(pattern); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public void setLocale(Locale locale) { |
|
this.locale = locale; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public Locale getLocale() { |
|
return locale; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
@SuppressWarnings("fallthrough") |
|
public void applyPattern(String pattern) { |
|
StringBuilder[] segments = new StringBuilder[4]; |
|
// Allocate only segments[SEG_RAW] here. The rest are |
|
|
|
segments[SEG_RAW] = new StringBuilder(); |
|
|
|
int part = SEG_RAW; |
|
int formatNumber = 0; |
|
boolean inQuote = false; |
|
int braceStack = 0; |
|
maxOffset = -1; |
|
for (int i = 0; i < pattern.length(); ++i) { |
|
char ch = pattern.charAt(i); |
|
if (part == SEG_RAW) { |
|
if (ch == '\'') { |
|
if (i + 1 < pattern.length() |
|
&& pattern.charAt(i+1) == '\'') { |
|
segments[part].append(ch); |
|
++i; |
|
} else { |
|
inQuote = !inQuote; |
|
} |
|
} else if (ch == '{' && !inQuote) { |
|
part = SEG_INDEX; |
|
if (segments[SEG_INDEX] == null) { |
|
segments[SEG_INDEX] = new StringBuilder(); |
|
} |
|
} else { |
|
segments[part].append(ch); |
|
} |
|
} else { |
|
if (inQuote) { |
|
segments[part].append(ch); |
|
if (ch == '\'') { |
|
inQuote = false; |
|
} |
|
} else { |
|
switch (ch) { |
|
case ',': |
|
if (part < SEG_MODIFIER) { |
|
if (segments[++part] == null) { |
|
segments[part] = new StringBuilder(); |
|
} |
|
} else { |
|
segments[part].append(ch); |
|
} |
|
break; |
|
case '{': |
|
++braceStack; |
|
segments[part].append(ch); |
|
break; |
|
case '}': |
|
if (braceStack == 0) { |
|
part = SEG_RAW; |
|
makeFormat(i, formatNumber, segments); |
|
formatNumber++; |
|
|
|
segments[SEG_INDEX] = null; |
|
segments[SEG_TYPE] = null; |
|
segments[SEG_MODIFIER] = null; |
|
} else { |
|
--braceStack; |
|
segments[part].append(ch); |
|
} |
|
break; |
|
case ' ': |
|
|
|
if (part != SEG_TYPE || segments[SEG_TYPE].length() > 0) { |
|
segments[part].append(ch); |
|
} |
|
break; |
|
case '\'': |
|
inQuote = true; |
|
|
|
default: |
|
segments[part].append(ch); |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
if (braceStack == 0 && part != 0) { |
|
maxOffset = -1; |
|
throw new IllegalArgumentException("Unmatched braces in the pattern."); |
|
} |
|
this.pattern = segments[0].toString(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public String toPattern() { |
|
|
|
int lastOffset = 0; |
|
StringBuilder result = new StringBuilder(); |
|
for (int i = 0; i <= maxOffset; ++i) { |
|
copyAndFixQuotes(pattern, lastOffset, offsets[i], result); |
|
lastOffset = offsets[i]; |
|
result.append('{').append(argumentNumbers[i]); |
|
Format fmt = formats[i]; |
|
if (fmt == null) { |
|
// do nothing, string format |
|
} else if (fmt instanceof NumberFormat) { |
|
if (fmt.equals(NumberFormat.getInstance(locale))) { |
|
result.append(",number"); |
|
} else if (fmt.equals(NumberFormat.getCurrencyInstance(locale))) { |
|
result.append(",number,currency"); |
|
} else if (fmt.equals(NumberFormat.getPercentInstance(locale))) { |
|
result.append(",number,percent"); |
|
} else if (fmt.equals(NumberFormat.getIntegerInstance(locale))) { |
|
result.append(",number,integer"); |
|
} else { |
|
if (fmt instanceof DecimalFormat) { |
|
result.append(",number,").append(((DecimalFormat)fmt).toPattern()); |
|
} else if (fmt instanceof ChoiceFormat) { |
|
result.append(",choice,").append(((ChoiceFormat)fmt).toPattern()); |
|
} else { |
|
// UNKNOWN |
|
} |
|
} |
|
} else if (fmt instanceof DateFormat) { |
|
int index; |
|
for (index = MODIFIER_DEFAULT; index < DATE_TIME_MODIFIERS.length; index++) { |
|
DateFormat df = DateFormat.getDateInstance(DATE_TIME_MODIFIERS[index], |
|
locale); |
|
if (fmt.equals(df)) { |
|
result.append(",date"); |
|
break; |
|
} |
|
df = DateFormat.getTimeInstance(DATE_TIME_MODIFIERS[index], |
|
locale); |
|
if (fmt.equals(df)) { |
|
result.append(",time"); |
|
break; |
|
} |
|
} |
|
if (index >= DATE_TIME_MODIFIERS.length) { |
|
if (fmt instanceof SimpleDateFormat) { |
|
result.append(",date,").append(((SimpleDateFormat)fmt).toPattern()); |
|
} else { |
|
// UNKNOWN |
|
} |
|
} else if (index != MODIFIER_DEFAULT) { |
|
result.append(',').append(DATE_TIME_MODIFIER_KEYWORDS[index]); |
|
} |
|
} else { |
|
//result.append(", unknown"); |
|
} |
|
result.append('}'); |
|
} |
|
copyAndFixQuotes(pattern, lastOffset, pattern.length(), result); |
|
return result.toString(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public void setFormatsByArgumentIndex(Format[] newFormats) { |
|
for (int i = 0; i <= maxOffset; i++) { |
|
int j = argumentNumbers[i]; |
|
if (j < newFormats.length) { |
|
formats[i] = newFormats[j]; |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public void setFormats(Format[] newFormats) { |
|
int runsToCopy = newFormats.length; |
|
if (runsToCopy > maxOffset + 1) { |
|
runsToCopy = maxOffset + 1; |
|
} |
|
for (int i = 0; i < runsToCopy; i++) { |
|
formats[i] = newFormats[i]; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) { |
|
for (int j = 0; j <= maxOffset; j++) { |
|
if (argumentNumbers[j] == argumentIndex) { |
|
formats[j] = newFormat; |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public void setFormat(int formatElementIndex, Format newFormat) { |
|
|
|
if (formatElementIndex > maxOffset) { |
|
throw new ArrayIndexOutOfBoundsException(formatElementIndex); |
|
} |
|
formats[formatElementIndex] = newFormat; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public Format[] getFormatsByArgumentIndex() { |
|
int maximumArgumentNumber = -1; |
|
for (int i = 0; i <= maxOffset; i++) { |
|
if (argumentNumbers[i] > maximumArgumentNumber) { |
|
maximumArgumentNumber = argumentNumbers[i]; |
|
} |
|
} |
|
Format[] resultArray = new Format[maximumArgumentNumber + 1]; |
|
for (int i = 0; i <= maxOffset; i++) { |
|
resultArray[argumentNumbers[i]] = formats[i]; |
|
} |
|
return resultArray; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public Format[] getFormats() { |
|
Format[] resultArray = new Format[maxOffset + 1]; |
|
System.arraycopy(formats, 0, resultArray, 0, maxOffset + 1); |
|
return resultArray; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public final StringBuffer format(Object[] arguments, StringBuffer result, |
|
FieldPosition pos) |
|
{ |
|
return subformat(arguments, result, pos, null); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public static String format(String pattern, Object ... arguments) { |
|
MessageFormat temp = new MessageFormat(pattern); |
|
return temp.format(arguments); |
|
} |
|
|
|
// Overrides |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public final StringBuffer format(Object arguments, StringBuffer result, |
|
FieldPosition pos) |
|
{ |
|
return subformat((Object[]) arguments, result, pos, null); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public AttributedCharacterIterator formatToCharacterIterator(Object arguments) { |
|
StringBuffer result = new StringBuffer(); |
|
ArrayList<AttributedCharacterIterator> iterators = new ArrayList<>(); |
|
|
|
if (arguments == null) { |
|
throw new NullPointerException( |
|
"formatToCharacterIterator must be passed non-null object"); |
|
} |
|
subformat((Object[]) arguments, result, null, iterators); |
|
if (iterators.size() == 0) { |
|
return createAttributedCharacterIterator(""); |
|
} |
|
return createAttributedCharacterIterator( |
|
iterators.toArray( |
|
new AttributedCharacterIterator[iterators.size()])); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public Object[] parse(String source, ParsePosition pos) { |
|
if (source == null) { |
|
Object[] empty = {}; |
|
return empty; |
|
} |
|
|
|
int maximumArgumentNumber = -1; |
|
for (int i = 0; i <= maxOffset; i++) { |
|
if (argumentNumbers[i] > maximumArgumentNumber) { |
|
maximumArgumentNumber = argumentNumbers[i]; |
|
} |
|
} |
|
Object[] resultArray = new Object[maximumArgumentNumber + 1]; |
|
|
|
int patternOffset = 0; |
|
int sourceOffset = pos.index; |
|
ParsePosition tempStatus = new ParsePosition(0); |
|
for (int i = 0; i <= maxOffset; ++i) { |
|
|
|
int len = offsets[i] - patternOffset; |
|
if (len == 0 || pattern.regionMatches(patternOffset, |
|
source, sourceOffset, len)) { |
|
sourceOffset += len; |
|
patternOffset += len; |
|
} else { |
|
pos.errorIndex = sourceOffset; |
|
return null; |
|
} |
|
|
|
|
|
if (formats[i] == null) { // string format |
|
// if at end, use longest possible match |
|
// otherwise uses first match to intervening string |
|
|
|
int tempLength = (i != maxOffset) ? offsets[i+1] : pattern.length(); |
|
|
|
int next; |
|
if (patternOffset >= tempLength) { |
|
next = source.length(); |
|
}else{ |
|
next = source.indexOf(pattern.substring(patternOffset, tempLength), |
|
sourceOffset); |
|
} |
|
|
|
if (next < 0) { |
|
pos.errorIndex = sourceOffset; |
|
return null; |
|
} else { |
|
String strValue= source.substring(sourceOffset,next); |
|
if (!strValue.equals("{"+argumentNumbers[i]+"}")) |
|
resultArray[argumentNumbers[i]] |
|
= source.substring(sourceOffset,next); |
|
sourceOffset = next; |
|
} |
|
} else { |
|
tempStatus.index = sourceOffset; |
|
resultArray[argumentNumbers[i]] |
|
= formats[i].parseObject(source,tempStatus); |
|
if (tempStatus.index == sourceOffset) { |
|
pos.errorIndex = sourceOffset; |
|
return null; |
|
} |
|
sourceOffset = tempStatus.index; |
|
} |
|
} |
|
int len = pattern.length() - patternOffset; |
|
if (len == 0 || pattern.regionMatches(patternOffset, |
|
source, sourceOffset, len)) { |
|
pos.index = sourceOffset + len; |
|
} else { |
|
pos.errorIndex = sourceOffset; |
|
return null; |
|
} |
|
return resultArray; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public Object[] parse(String source) throws ParseException { |
|
ParsePosition pos = new ParsePosition(0); |
|
Object[] result = parse(source, pos); |
|
if (pos.index == 0) |
|
throw new ParseException("MessageFormat parse error!", pos.errorIndex); |
|
|
|
return result; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public Object parseObject(String source, ParsePosition pos) { |
|
return parse(source, pos); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public Object clone() { |
|
MessageFormat other = (MessageFormat) super.clone(); |
|
|
|
// clone arrays. Can't do with utility because of bug in Cloneable |
|
other.formats = formats.clone(); |
|
for (int i = 0; i < formats.length; ++i) { |
|
if (formats[i] != null) |
|
other.formats[i] = (Format)formats[i].clone(); |
|
} |
|
|
|
other.offsets = offsets.clone(); |
|
other.argumentNumbers = argumentNumbers.clone(); |
|
|
|
return other; |
|
} |
|
|
|
|
|
|
|
*/ |
|
public boolean equals(Object obj) { |
|
if (this == obj) |
|
return true; |
|
if (obj == null || getClass() != obj.getClass()) |
|
return false; |
|
MessageFormat other = (MessageFormat) obj; |
|
return (maxOffset == other.maxOffset |
|
&& pattern.equals(other.pattern) |
|
&& ((locale != null && locale.equals(other.locale)) |
|
|| (locale == null && other.locale == null)) |
|
&& Arrays.equals(offsets,other.offsets) |
|
&& Arrays.equals(argumentNumbers,other.argumentNumbers) |
|
&& Arrays.equals(formats,other.formats)); |
|
} |
|
|
|
|
|
|
|
*/ |
|
public int hashCode() { |
|
return pattern.hashCode(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public static class Field extends Format.Field { |
|
|
|
|
|
private static final long serialVersionUID = 7899943957617360810L; |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
protected Field(String name) { |
|
super(name); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
protected Object readResolve() throws InvalidObjectException { |
|
if (this.getClass() != MessageFormat.Field.class) { |
|
throw new InvalidObjectException("subclass didn't correctly implement readResolve"); |
|
} |
|
|
|
return ARGUMENT; |
|
} |
|
|
|
// |
|
// The constants |
|
// |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public static final Field ARGUMENT = |
|
new Field("message argument field"); |
|
} |
|
|
|
// ===========================privates============================ |
|
|
|
|
|
|
|
|
|
*/ |
|
private Locale locale; |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private String pattern = ""; |
|
|
|
|
|
private static final int INITIAL_FORMATS = 10; |
|
|
|
|
|
|
|
|
|
*/ |
|
private Format[] formats = new Format[INITIAL_FORMATS]; |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private int[] offsets = new int[INITIAL_FORMATS]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private int[] argumentNumbers = new int[INITIAL_FORMATS]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private int maxOffset = -1; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private StringBuffer subformat(Object[] arguments, StringBuffer result, |
|
FieldPosition fp, List<AttributedCharacterIterator> characterIterators) { |
|
// note: this implementation assumes a fast substring & index. |
|
|
|
int lastOffset = 0; |
|
int last = result.length(); |
|
for (int i = 0; i <= maxOffset; ++i) { |
|
result.append(pattern, lastOffset, offsets[i]); |
|
lastOffset = offsets[i]; |
|
int argumentNumber = argumentNumbers[i]; |
|
if (arguments == null || argumentNumber >= arguments.length) { |
|
result.append('{').append(argumentNumber).append('}'); |
|
continue; |
|
} |
|
|
|
if (false) { // if (argRecursion == 3){ |
|
|
|
result.append('\uFFFD'); |
|
} else { |
|
Object obj = arguments[argumentNumber]; |
|
String arg = null; |
|
Format subFormatter = null; |
|
if (obj == null) { |
|
arg = "null"; |
|
} else if (formats[i] != null) { |
|
subFormatter = formats[i]; |
|
if (subFormatter instanceof ChoiceFormat) { |
|
arg = formats[i].format(obj); |
|
if (arg.indexOf('{') >= 0) { |
|
subFormatter = new MessageFormat(arg, locale); |
|
obj = arguments; |
|
arg = null; |
|
} |
|
} |
|
} else if (obj instanceof Number) { |
|
|
|
subFormatter = NumberFormat.getInstance(locale); |
|
} else if (obj instanceof Date) { |
|
|
|
subFormatter = DateFormat.getDateTimeInstance( |
|
DateFormat.SHORT, DateFormat.SHORT, locale); |
|
} else if (obj instanceof String) { |
|
arg = (String) obj; |
|
|
|
} else { |
|
arg = obj.toString(); |
|
if (arg == null) arg = "null"; |
|
} |
|
|
|
// At this point we are in two states, either subFormatter |
|
// is non-null indicating we should format obj using it, |
|
// or arg is non-null and we should use it as the value. |
|
|
|
if (characterIterators != null) { |
|
// If characterIterators is non-null, it indicates we need |
|
|
|
if (last != result.length()) { |
|
characterIterators.add( |
|
createAttributedCharacterIterator(result.substring |
|
(last))); |
|
last = result.length(); |
|
} |
|
if (subFormatter != null) { |
|
AttributedCharacterIterator subIterator = |
|
subFormatter.formatToCharacterIterator(obj); |
|
|
|
append(result, subIterator); |
|
if (last != result.length()) { |
|
characterIterators.add( |
|
createAttributedCharacterIterator( |
|
subIterator, Field.ARGUMENT, |
|
Integer.valueOf(argumentNumber))); |
|
last = result.length(); |
|
} |
|
arg = null; |
|
} |
|
if (arg != null && arg.length() > 0) { |
|
result.append(arg); |
|
characterIterators.add( |
|
createAttributedCharacterIterator( |
|
arg, Field.ARGUMENT, |
|
Integer.valueOf(argumentNumber))); |
|
last = result.length(); |
|
} |
|
} |
|
else { |
|
if (subFormatter != null) { |
|
arg = subFormatter.format(obj); |
|
} |
|
last = result.length(); |
|
result.append(arg); |
|
if (i == 0 && fp != null && Field.ARGUMENT.equals( |
|
fp.getFieldAttribute())) { |
|
fp.setBeginIndex(last); |
|
fp.setEndIndex(result.length()); |
|
} |
|
last = result.length(); |
|
} |
|
} |
|
} |
|
result.append(pattern, lastOffset, pattern.length()); |
|
if (characterIterators != null && last != result.length()) { |
|
characterIterators.add(createAttributedCharacterIterator( |
|
result.substring(last))); |
|
} |
|
return result; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private void append(StringBuffer result, CharacterIterator iterator) { |
|
if (iterator.first() != CharacterIterator.DONE) { |
|
char aChar; |
|
|
|
result.append(iterator.first()); |
|
while ((aChar = iterator.next()) != CharacterIterator.DONE) { |
|
result.append(aChar); |
|
} |
|
} |
|
} |
|
|
|
|
|
private static final int SEG_RAW = 0; |
|
private static final int SEG_INDEX = 1; |
|
private static final int SEG_TYPE = 2; |
|
private static final int SEG_MODIFIER = 3; |
|
|
|
|
|
private static final int TYPE_NULL = 0; |
|
private static final int TYPE_NUMBER = 1; |
|
private static final int TYPE_DATE = 2; |
|
private static final int TYPE_TIME = 3; |
|
private static final int TYPE_CHOICE = 4; |
|
|
|
private static final String[] TYPE_KEYWORDS = { |
|
"", |
|
"number", |
|
"date", |
|
"time", |
|
"choice" |
|
}; |
|
|
|
// Indices for number modifiers |
|
private static final int MODIFIER_DEFAULT = 0; |
|
private static final int MODIFIER_CURRENCY = 1; |
|
private static final int MODIFIER_PERCENT = 2; |
|
private static final int MODIFIER_INTEGER = 3; |
|
|
|
private static final String[] NUMBER_MODIFIER_KEYWORDS = { |
|
"", |
|
"currency", |
|
"percent", |
|
"integer" |
|
}; |
|
|
|
|
|
private static final int MODIFIER_SHORT = 1; |
|
private static final int MODIFIER_MEDIUM = 2; |
|
private static final int MODIFIER_LONG = 3; |
|
private static final int MODIFIER_FULL = 4; |
|
|
|
private static final String[] DATE_TIME_MODIFIER_KEYWORDS = { |
|
"", |
|
"short", |
|
"medium", |
|
"long", |
|
"full" |
|
}; |
|
|
|
|
|
private static final int[] DATE_TIME_MODIFIERS = { |
|
DateFormat.DEFAULT, |
|
DateFormat.SHORT, |
|
DateFormat.MEDIUM, |
|
DateFormat.LONG, |
|
DateFormat.FULL, |
|
}; |
|
|
|
private void makeFormat(int position, int offsetNumber, |
|
StringBuilder[] textSegments) |
|
{ |
|
String[] segments = new String[textSegments.length]; |
|
for (int i = 0; i < textSegments.length; i++) { |
|
StringBuilder oneseg = textSegments[i]; |
|
segments[i] = (oneseg != null) ? oneseg.toString() : ""; |
|
} |
|
|
|
|
|
int argumentNumber; |
|
try { |
|
argumentNumber = Integer.parseInt(segments[SEG_INDEX]); |
|
} catch (NumberFormatException e) { |
|
throw new IllegalArgumentException("can't parse argument number: " |
|
+ segments[SEG_INDEX], e); |
|
} |
|
if (argumentNumber < 0) { |
|
throw new IllegalArgumentException("negative argument number: " |
|
+ argumentNumber); |
|
} |
|
|
|
|
|
if (offsetNumber >= formats.length) { |
|
int newLength = formats.length * 2; |
|
Format[] newFormats = new Format[newLength]; |
|
int[] newOffsets = new int[newLength]; |
|
int[] newArgumentNumbers = new int[newLength]; |
|
System.arraycopy(formats, 0, newFormats, 0, maxOffset + 1); |
|
System.arraycopy(offsets, 0, newOffsets, 0, maxOffset + 1); |
|
System.arraycopy(argumentNumbers, 0, newArgumentNumbers, 0, maxOffset + 1); |
|
formats = newFormats; |
|
offsets = newOffsets; |
|
argumentNumbers = newArgumentNumbers; |
|
} |
|
int oldMaxOffset = maxOffset; |
|
maxOffset = offsetNumber; |
|
offsets[offsetNumber] = segments[SEG_RAW].length(); |
|
argumentNumbers[offsetNumber] = argumentNumber; |
|
|
|
|
|
Format newFormat = null; |
|
if (segments[SEG_TYPE].length() != 0) { |
|
int type = findKeyword(segments[SEG_TYPE], TYPE_KEYWORDS); |
|
switch (type) { |
|
case TYPE_NULL: |
|
// Type "" is allowed. e.g., "{0,}", "{0,,}", and "{0,,#}" |
|
|
|
break; |
|
|
|
case TYPE_NUMBER: |
|
switch (findKeyword(segments[SEG_MODIFIER], NUMBER_MODIFIER_KEYWORDS)) { |
|
case MODIFIER_DEFAULT: |
|
newFormat = NumberFormat.getInstance(locale); |
|
break; |
|
case MODIFIER_CURRENCY: |
|
newFormat = NumberFormat.getCurrencyInstance(locale); |
|
break; |
|
case MODIFIER_PERCENT: |
|
newFormat = NumberFormat.getPercentInstance(locale); |
|
break; |
|
case MODIFIER_INTEGER: |
|
newFormat = NumberFormat.getIntegerInstance(locale); |
|
break; |
|
default: |
|
try { |
|
newFormat = new DecimalFormat(segments[SEG_MODIFIER], |
|
DecimalFormatSymbols.getInstance(locale)); |
|
} catch (IllegalArgumentException e) { |
|
maxOffset = oldMaxOffset; |
|
throw e; |
|
} |
|
break; |
|
} |
|
break; |
|
|
|
case TYPE_DATE: |
|
case TYPE_TIME: |
|
int mod = findKeyword(segments[SEG_MODIFIER], DATE_TIME_MODIFIER_KEYWORDS); |
|
if (mod >= 0 && mod < DATE_TIME_MODIFIER_KEYWORDS.length) { |
|
if (type == TYPE_DATE) { |
|
newFormat = DateFormat.getDateInstance(DATE_TIME_MODIFIERS[mod], |
|
locale); |
|
} else { |
|
newFormat = DateFormat.getTimeInstance(DATE_TIME_MODIFIERS[mod], |
|
locale); |
|
} |
|
} else { |
|
|
|
try { |
|
newFormat = new SimpleDateFormat(segments[SEG_MODIFIER], locale); |
|
} catch (IllegalArgumentException e) { |
|
maxOffset = oldMaxOffset; |
|
throw e; |
|
} |
|
} |
|
break; |
|
|
|
case TYPE_CHOICE: |
|
try { |
|
|
|
newFormat = new ChoiceFormat(segments[SEG_MODIFIER]); |
|
} catch (Exception e) { |
|
maxOffset = oldMaxOffset; |
|
throw new IllegalArgumentException("Choice Pattern incorrect: " |
|
+ segments[SEG_MODIFIER], e); |
|
} |
|
break; |
|
|
|
default: |
|
maxOffset = oldMaxOffset; |
|
throw new IllegalArgumentException("unknown format type: " + |
|
segments[SEG_TYPE]); |
|
} |
|
} |
|
formats[offsetNumber] = newFormat; |
|
} |
|
|
|
private static final int findKeyword(String s, String[] list) { |
|
for (int i = 0; i < list.length; ++i) { |
|
if (s.equals(list[i])) |
|
return i; |
|
} |
|
|
|
|
|
String ls = s.trim().toLowerCase(Locale.ROOT); |
|
if (ls != s) { |
|
for (int i = 0; i < list.length; ++i) { |
|
if (ls.equals(list[i])) |
|
return i; |
|
} |
|
} |
|
return -1; |
|
} |
|
|
|
private static final void copyAndFixQuotes(String source, int start, int end, |
|
StringBuilder target) { |
|
boolean quoted = false; |
|
|
|
for (int i = start; i < end; ++i) { |
|
char ch = source.charAt(i); |
|
if (ch == '{') { |
|
if (!quoted) { |
|
target.append('\''); |
|
quoted = true; |
|
} |
|
target.append(ch); |
|
} else if (ch == '\'') { |
|
target.append("''"); |
|
} else { |
|
if (quoted) { |
|
target.append('\''); |
|
quoted = false; |
|
} |
|
target.append(ch); |
|
} |
|
} |
|
if (quoted) { |
|
target.append('\''); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { |
|
in.defaultReadObject(); |
|
boolean isValid = maxOffset >= -1 |
|
&& formats.length > maxOffset |
|
&& offsets.length > maxOffset |
|
&& argumentNumbers.length > maxOffset; |
|
if (isValid) { |
|
int lastOffset = pattern.length() + 1; |
|
for (int i = maxOffset; i >= 0; --i) { |
|
if ((offsets[i] < 0) || (offsets[i] > lastOffset)) { |
|
isValid = false; |
|
break; |
|
} else { |
|
lastOffset = offsets[i]; |
|
} |
|
} |
|
} |
|
if (!isValid) { |
|
throw new InvalidObjectException("Could not reconstruct MessageFormat from corrupt stream."); |
|
} |
|
} |
|
} |