2659 lines
111 KiB
Java
2659 lines
111 KiB
Java
/* GENERATED SOURCE. DO NOT MODIFY. */
|
|
// © 2016 and later: Unicode, Inc. and others.
|
|
// License & terms of use: http://www.unicode.org/copyright.html
|
|
/*
|
|
**********************************************************************
|
|
* Copyright (c) 2004-2016, International Business Machines
|
|
* Corporation and others. All Rights Reserved.
|
|
**********************************************************************
|
|
* Author: Alan Liu
|
|
* Created: April 6, 2004
|
|
* Since: ICU 3.0
|
|
**********************************************************************
|
|
*/
|
|
package android.icu.text;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InvalidObjectException;
|
|
import java.io.ObjectInputStream;
|
|
import java.text.AttributedCharacterIterator;
|
|
import java.text.AttributedCharacterIterator.Attribute;
|
|
import java.text.AttributedString;
|
|
import java.text.CharacterIterator;
|
|
import java.text.ChoiceFormat;
|
|
import java.text.FieldPosition;
|
|
import java.text.Format;
|
|
import java.text.ParseException;
|
|
import java.text.ParsePosition;
|
|
import java.util.ArrayList;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
|
|
import android.icu.impl.PatternProps;
|
|
import android.icu.number.NumberFormatter;
|
|
import android.icu.text.MessagePattern.ArgType;
|
|
import android.icu.text.MessagePattern.Part;
|
|
import android.icu.text.PluralRules.IFixedDecimal;
|
|
import android.icu.text.PluralRules.PluralType;
|
|
import android.icu.util.ICUUncheckedIOException;
|
|
import android.icu.util.ULocale;
|
|
import android.icu.util.ULocale.Category;
|
|
|
|
/**
|
|
* <strong>[icu enhancement]</strong> ICU's replacement for {@link java.text.MessageFormat}. Methods, fields, and other functionality specific to ICU are labeled '<strong>[icu]</strong>'.
|
|
*
|
|
* <p>MessageFormat prepares strings for display to users,
|
|
* with optional arguments (variables/placeholders).
|
|
* The arguments can occur in any order, which is necessary for translation
|
|
* into languages with different grammars.
|
|
*
|
|
* <p>A MessageFormat is constructed from a <em>pattern</em> string
|
|
* with arguments in {curly braces} which will be replaced by formatted values.
|
|
*
|
|
* <p><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). 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 and the
|
|
* subformats used for inserted arguments.
|
|
*
|
|
* <p>Arguments can be named (using identifiers) or numbered (using small ASCII-digit integers).
|
|
* Some of the API methods work only with argument numbers and throw an exception
|
|
* if the pattern has named arguments (see {@link #usesNamedArguments()}).
|
|
*
|
|
* <p>An argument might not specify any format type. In this case,
|
|
* a Number value is formatted with a default (for the locale) NumberFormat,
|
|
* a Date value is formatted with a default (for the locale) DateFormat,
|
|
* and for any other value its toString() value is used.
|
|
*
|
|
* <p>An argument might specify a "simple" type for which the specified
|
|
* Format object is created, cached and used.
|
|
*
|
|
* <p>An argument might have a "complex" type with nested MessageFormat sub-patterns.
|
|
* During formatting, one of these sub-messages is selected according to the argument value
|
|
* and recursively formatted.
|
|
*
|
|
* <p>After construction, a custom Format object can be set for
|
|
* a top-level argument, overriding the default formatting and parsing behavior
|
|
* for that argument.
|
|
* However, custom formatting can be achieved more simply by writing
|
|
* a typeless argument in the pattern string
|
|
* and supplying it with a preformatted string value.
|
|
*
|
|
* <p>When formatting, MessageFormat takes a collection of argument values
|
|
* and writes an output string.
|
|
* The argument values may be passed as an array
|
|
* (when the pattern contains only numbered arguments)
|
|
* or as a Map (which works for both named and numbered arguments).
|
|
*
|
|
* <p>Each argument is matched with one of the input values by array index or map key
|
|
* and formatted according to its pattern specification
|
|
* (or using a custom Format object if one was set).
|
|
* A numbered pattern argument is matched with a map key that contains that number
|
|
* as an ASCII-decimal-digit string (without leading zero).
|
|
*
|
|
* <h3><a name="patterns">Patterns and Their Interpretation</a></h3>
|
|
*
|
|
* <code>MessageFormat</code> uses patterns of the following form:
|
|
* <blockquote><pre>
|
|
* message = messageText (argument messageText)*
|
|
* argument = noneArg | simpleArg | complexArg
|
|
* complexArg = choiceArg | pluralArg | selectArg | selectordinalArg
|
|
*
|
|
* noneArg = '{' argNameOrNumber '}'
|
|
* simpleArg = '{' argNameOrNumber ',' argType [',' argStyle] '}'
|
|
* choiceArg = '{' argNameOrNumber ',' "choice" ',' choiceStyle '}'
|
|
* pluralArg = '{' argNameOrNumber ',' "plural" ',' pluralStyle '}'
|
|
* selectArg = '{' argNameOrNumber ',' "select" ',' selectStyle '}'
|
|
* selectordinalArg = '{' argNameOrNumber ',' "selectordinal" ',' pluralStyle '}'
|
|
*
|
|
* choiceStyle: see {@link ChoiceFormat}
|
|
* pluralStyle: see {@link PluralFormat}
|
|
* selectStyle: see {@link SelectFormat}
|
|
*
|
|
* argNameOrNumber = argName | argNumber
|
|
* argName = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+
|
|
* argNumber = '0' | ('1'..'9' ('0'..'9')*)
|
|
*
|
|
* argType = "number" | "date" | "time" | "spellout" | "ordinal" | "duration"
|
|
* argStyle = "short" | "medium" | "long" | "full" | "integer" | "currency" | "percent" | argStyleText
|
|
* </pre></blockquote>
|
|
*
|
|
* <ul>
|
|
* <li>messageText can contain quoted literal strings including syntax characters.
|
|
* A quoted literal string begins with an ASCII apostrophe and a syntax character
|
|
* (usually a {curly brace}) and continues until the next single apostrophe.
|
|
* A double ASCII apostrophe inside or outside of a quoted string represents
|
|
* one literal apostrophe.
|
|
* <li>Quotable syntax characters are the {curly braces} in all messageText parts,
|
|
* plus the '#' sign in a messageText immediately inside a pluralStyle,
|
|
* and the '|' symbol in a messageText immediately inside a choiceStyle.
|
|
* <li>See also {@link MessagePattern.ApostropheMode}
|
|
* <li>In argStyleText, every single ASCII apostrophe begins and ends quoted literal text,
|
|
* and unquoted {curly braces} must occur in matched pairs.
|
|
* </ul>
|
|
*
|
|
* <p>Recommendation: Use the real apostrophe (single quote) character \\u2019 for
|
|
* human-readable text, and use the ASCII apostrophe (\\u0027 ' )
|
|
* only in program syntax, like quoting in MessageFormat.
|
|
* See the annotations for U+0027 Apostrophe in The Unicode Standard.
|
|
*
|
|
* <p>The <code>choice</code> argument type is deprecated.
|
|
* Use <code>plural</code> arguments for proper plural selection,
|
|
* and <code>select</code> arguments for simple selection among a fixed set of choices.
|
|
*
|
|
* <p>The <code>argType</code> and <code>argStyle</code> values are used to create
|
|
* a <code>Format</code> instance for the format element. The following
|
|
* table shows how the values map to Format instances. Combinations not
|
|
* shown in the table are illegal. Any <code>argStyleText</code> must
|
|
* be a valid pattern string for the Format subclass used.
|
|
*
|
|
* <table border=1>
|
|
* <tr>
|
|
* <th>argType
|
|
* <th>argStyle
|
|
* <th>resulting Format object
|
|
* <tr>
|
|
* <td colspan=2><i>(none)</i>
|
|
* <td><code>null</code>
|
|
* <tr>
|
|
* <td rowspan=5><code>number</code>
|
|
* <td><i>(none)</i>
|
|
* <td><code>NumberFormat.getInstance(getLocale())</code>
|
|
* <tr>
|
|
* <td><code>integer</code>
|
|
* <td><code>NumberFormat.getIntegerInstance(getLocale())</code>
|
|
* <tr>
|
|
* <td><code>currency</code>
|
|
* <td><code>NumberFormat.getCurrencyInstance(getLocale())</code>
|
|
* <tr>
|
|
* <td><code>percent</code>
|
|
* <td><code>NumberFormat.getPercentInstance(getLocale())</code>
|
|
* <tr>
|
|
* <td><i>argStyleText</i>
|
|
* <td><code>new DecimalFormat(argStyleText, new DecimalFormatSymbols(getLocale()))</code>
|
|
* <tr>
|
|
* <td rowspan=6><code>date</code>
|
|
* <td><i>(none)</i>
|
|
* <td><code>DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale())</code>
|
|
* <tr>
|
|
* <td><code>short</code>
|
|
* <td><code>DateFormat.getDateInstance(DateFormat.SHORT, getLocale())</code>
|
|
* <tr>
|
|
* <td><code>medium</code>
|
|
* <td><code>DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale())</code>
|
|
* <tr>
|
|
* <td><code>long</code>
|
|
* <td><code>DateFormat.getDateInstance(DateFormat.LONG, getLocale())</code>
|
|
* <tr>
|
|
* <td><code>full</code>
|
|
* <td><code>DateFormat.getDateInstance(DateFormat.FULL, getLocale())</code>
|
|
* <tr>
|
|
* <td><i>argStyleText</i>
|
|
* <td><code>new SimpleDateFormat(argStyleText, getLocale())</code>
|
|
* <tr>
|
|
* <td rowspan=6><code>time</code>
|
|
* <td><i>(none)</i>
|
|
* <td><code>DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale())</code>
|
|
* <tr>
|
|
* <td><code>short</code>
|
|
* <td><code>DateFormat.getTimeInstance(DateFormat.SHORT, getLocale())</code>
|
|
* <tr>
|
|
* <td><code>medium</code>
|
|
* <td><code>DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale())</code>
|
|
* <tr>
|
|
* <td><code>long</code>
|
|
* <td><code>DateFormat.getTimeInstance(DateFormat.LONG, getLocale())</code>
|
|
* <tr>
|
|
* <td><code>full</code>
|
|
* <td><code>DateFormat.getTimeInstance(DateFormat.FULL, getLocale())</code>
|
|
* <tr>
|
|
* <td><i>argStyleText</i>
|
|
* <td><code>new SimpleDateFormat(argStyleText, getLocale())</code>
|
|
* <tr>
|
|
* <td><code>spellout</code>
|
|
* <td><i>argStyleText (optional)</i>
|
|
* <td><code>new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.SPELLOUT)
|
|
* <br> .setDefaultRuleset(argStyleText);</code>
|
|
* <tr>
|
|
* <td><code>ordinal</code>
|
|
* <td><i>argStyleText (optional)</i>
|
|
* <td><code>new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.ORDINAL)
|
|
* <br> .setDefaultRuleset(argStyleText);</code>
|
|
* <tr>
|
|
* <td><code>duration</code>
|
|
* <td><i>argStyleText (optional)</i>
|
|
* <td><code>new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.DURATION)
|
|
* <br> .setDefaultRuleset(argStyleText);</code>
|
|
* </table>
|
|
*
|
|
* <h4><a name="diffsjdk">Differences from java.text.MessageFormat</a></h4>
|
|
*
|
|
* <p>The ICU MessageFormat supports both named and numbered arguments,
|
|
* while the JDK MessageFormat only supports numbered arguments.
|
|
* Named arguments make patterns more readable.
|
|
*
|
|
* <p>ICU implements a more user-friendly apostrophe quoting syntax.
|
|
* In message text, an apostrophe only begins quoting literal text
|
|
* if it immediately precedes a syntax character (mostly {curly braces}).<br>
|
|
* In the JDK MessageFormat, an apostrophe always begins quoting,
|
|
* which requires common text like "don't" and "aujourd'hui"
|
|
* to be written with doubled apostrophes like "don''t" and "aujourd''hui".
|
|
* For more details see {@link MessagePattern.ApostropheMode}.
|
|
*
|
|
* <p>ICU does not create a ChoiceFormat object for a choiceArg, pluralArg or selectArg
|
|
* but rather handles such arguments itself.
|
|
* The JDK MessageFormat does create and use a ChoiceFormat object
|
|
* (<code>new ChoiceFormat(argStyleText)</code>).
|
|
* The JDK does not support plural and select arguments at all.
|
|
*
|
|
* <h4>Usage Information</h4>
|
|
*
|
|
* <p>Here are some examples of usage:
|
|
* <blockquote>
|
|
* <pre>
|
|
* Object[] arguments = {
|
|
* 7,
|
|
* new Date(System.currentTimeMillis()),
|
|
* "a disturbance in the Force"
|
|
* };
|
|
*
|
|
* String result = MessageFormat.format(
|
|
* "At {1,time} on {1,date}, there was {2} on planet {0,number,integer}.",
|
|
* arguments);
|
|
*
|
|
* <em>output</em>: At 12:30 PM on Jul 3, 2053, there was a disturbance
|
|
* in the Force on planet 7.
|
|
*
|
|
* </pre>
|
|
* </blockquote>
|
|
* Typically, the message format will come from resources, and the
|
|
* arguments will be dynamically set at runtime.
|
|
*
|
|
* <p>Example 2:
|
|
* <blockquote>
|
|
* <pre>
|
|
* Object[] testArgs = { 3, "MyDisk" };
|
|
*
|
|
* MessageFormat form = new MessageFormat(
|
|
* "The disk \"{1}\" contains {0} file(s).");
|
|
*
|
|
* System.out.println(form.format(testArgs));
|
|
*
|
|
* // output, with different testArgs
|
|
* <em>output</em>: The disk "MyDisk" contains 0 file(s).
|
|
* <em>output</em>: The disk "MyDisk" contains 1 file(s).
|
|
* <em>output</em>: The disk "MyDisk" contains 1,273 file(s).
|
|
* </pre>
|
|
* </blockquote>
|
|
*
|
|
* <p>For messages that include plural forms, you can use a plural argument:
|
|
* <pre>
|
|
* MessageFormat msgFmt = new MessageFormat(
|
|
* "{num_files, plural, " +
|
|
* "=0{There are no files on disk \"{disk_name}\".}" +
|
|
* "=1{There is one file on disk \"{disk_name}\".}" +
|
|
* "other{There are # files on disk \"{disk_name}\".}}",
|
|
* ULocale.ENGLISH);
|
|
* Map args = new HashMap();
|
|
* args.put("num_files", 0);
|
|
* args.put("disk_name", "MyDisk");
|
|
* System.out.println(msgFmt.format(args));
|
|
* args.put("num_files", 3);
|
|
* System.out.println(msgFmt.format(args));
|
|
*
|
|
* <em>output</em>:
|
|
* There are no files on disk "MyDisk".
|
|
* There are 3 files on "MyDisk".
|
|
* </pre>
|
|
* See {@link PluralFormat} and {@link PluralRules} for details.
|
|
*
|
|
* <h4><a name="synchronization">Synchronization</a></h4>
|
|
*
|
|
* <p>MessageFormats 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 ChoiceFormat
|
|
* @see PluralFormat
|
|
* @see SelectFormat
|
|
* @author Mark Davis
|
|
* @author Markus Scherer
|
|
*/
|
|
public class MessageFormat extends UFormat {
|
|
|
|
// Incremented by 1 for ICU 4.8's new format.
|
|
static final long serialVersionUID = 7136212545847378652L;
|
|
|
|
/**
|
|
* Constructs a MessageFormat for the default <code>FORMAT</code> locale and the
|
|
* specified pattern.
|
|
* Sets the locale and calls applyPattern(pattern).
|
|
*
|
|
* @param pattern the pattern for this message format
|
|
* @exception IllegalArgumentException if the pattern is invalid
|
|
* @see Category#FORMAT
|
|
*/
|
|
public MessageFormat(String pattern) {
|
|
this.ulocale = ULocale.getDefault(Category.FORMAT);
|
|
applyPattern(pattern);
|
|
}
|
|
|
|
/**
|
|
* Constructs a MessageFormat for the specified locale and
|
|
* pattern.
|
|
* Sets the locale and calls applyPattern(pattern).
|
|
*
|
|
* @param pattern the pattern for this message format
|
|
* @param locale the locale for this message format
|
|
* @exception IllegalArgumentException if the pattern is invalid
|
|
*/
|
|
public MessageFormat(String pattern, Locale locale) {
|
|
this(pattern, ULocale.forLocale(locale));
|
|
}
|
|
|
|
/**
|
|
* Constructs a MessageFormat for the specified locale and
|
|
* pattern.
|
|
* Sets the locale and calls applyPattern(pattern).
|
|
*
|
|
* @param pattern the pattern for this message format
|
|
* @param locale the locale for this message format
|
|
* @exception IllegalArgumentException if the pattern is invalid
|
|
*/
|
|
public MessageFormat(String pattern, ULocale locale) {
|
|
this.ulocale = locale;
|
|
applyPattern(pattern);
|
|
}
|
|
|
|
/**
|
|
* Sets the locale to be used for creating argument Format objects.
|
|
* This affects subsequent calls to the {@link #applyPattern applyPattern}
|
|
* method as well as to the <code>format</code> and
|
|
* {@link #formatToCharacterIterator formatToCharacterIterator} methods.
|
|
*
|
|
* @param locale the locale to be used when creating or comparing subformats
|
|
*/
|
|
public void setLocale(Locale locale) {
|
|
setLocale(ULocale.forLocale(locale));
|
|
}
|
|
|
|
/**
|
|
* Sets the locale to be used for creating argument Format objects.
|
|
* This affects subsequent calls to the {@link #applyPattern applyPattern}
|
|
* method as well as to the <code>format</code> and
|
|
* {@link #formatToCharacterIterator formatToCharacterIterator} methods.
|
|
*
|
|
* @param locale the locale to be used when creating or comparing subformats
|
|
*/
|
|
public void setLocale(ULocale locale) {
|
|
/* Save the pattern, and then reapply so that */
|
|
/* we pick up any changes in locale specific */
|
|
/* elements */
|
|
String existingPattern = toPattern(); /*ibm.3550*/
|
|
this.ulocale = locale;
|
|
// Invalidate all stock formatters. They are no longer valid since
|
|
// the locale has changed.
|
|
stockDateFormatter = null;
|
|
stockNumberFormatter = null;
|
|
pluralProvider = null;
|
|
ordinalProvider = null;
|
|
applyPattern(existingPattern); /*ibm.3550*/
|
|
}
|
|
|
|
/**
|
|
* Returns the locale that's used when creating or comparing subformats.
|
|
*
|
|
* @return the locale used when creating or comparing subformats
|
|
*/
|
|
public Locale getLocale() {
|
|
return ulocale.toLocale();
|
|
}
|
|
|
|
/**
|
|
* <strong>[icu]</strong> Returns the locale that's used when creating argument Format objects.
|
|
*
|
|
* @return the locale used when creating or comparing subformats
|
|
*/
|
|
public ULocale getULocale() {
|
|
return ulocale;
|
|
}
|
|
|
|
/**
|
|
* Sets the pattern used by this message format.
|
|
* Parses the pattern and caches Format objects for simple argument types.
|
|
* Patterns and their interpretation are specified in the
|
|
* <a href="#patterns">class description</a>.
|
|
*
|
|
* @param pttrn the pattern for this message format
|
|
* @throws IllegalArgumentException if the pattern is invalid
|
|
*/
|
|
public void applyPattern(String pttrn) {
|
|
try {
|
|
if (msgPattern == null) {
|
|
msgPattern = new MessagePattern(pttrn);
|
|
} else {
|
|
msgPattern.parse(pttrn);
|
|
}
|
|
// Cache the formats that are explicitly mentioned in the message pattern.
|
|
cacheExplicitFormats();
|
|
} catch(RuntimeException e) {
|
|
resetPattern();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* <strong>[icu]</strong> Sets the ApostropheMode and the pattern used by this message format.
|
|
* Parses the pattern and caches Format objects for simple argument types.
|
|
* Patterns and their interpretation are specified in the
|
|
* <a href="#patterns">class description</a>.
|
|
* <p>
|
|
* This method is best used only once on a given object to avoid confusion about the mode,
|
|
* and after constructing the object with an empty pattern string to minimize overhead.
|
|
*
|
|
* @param pattern the pattern for this message format
|
|
* @param aposMode the new ApostropheMode
|
|
* @throws IllegalArgumentException if the pattern is invalid
|
|
* @see MessagePattern.ApostropheMode
|
|
*/
|
|
public void applyPattern(String pattern, MessagePattern.ApostropheMode aposMode) {
|
|
if (msgPattern == null) {
|
|
msgPattern = new MessagePattern(aposMode);
|
|
} else if (aposMode != msgPattern.getApostropheMode()) {
|
|
msgPattern.clearPatternAndSetApostropheMode(aposMode);
|
|
}
|
|
applyPattern(pattern);
|
|
}
|
|
|
|
/**
|
|
* <strong>[icu]</strong>
|
|
* @return this instance's ApostropheMode.
|
|
*/
|
|
public MessagePattern.ApostropheMode getApostropheMode() {
|
|
if (msgPattern == null) {
|
|
msgPattern = new MessagePattern(); // Sets the default mode.
|
|
}
|
|
return msgPattern.getApostropheMode();
|
|
}
|
|
|
|
/**
|
|
* Returns the applied pattern string.
|
|
* @return the pattern string
|
|
* @throws IllegalStateException after custom Format objects have been set
|
|
* via setFormat() or similar APIs
|
|
*/
|
|
public String toPattern() {
|
|
// Return the original, applied pattern string, or else "".
|
|
// Note: This does not take into account
|
|
// - changes from setFormat() and similar methods, or
|
|
// - normalization of apostrophes and arguments, for example,
|
|
// whether some date/time/number formatter was created via a pattern
|
|
// but is equivalent to the "medium" default format.
|
|
if (customFormatArgStarts != null) {
|
|
throw new IllegalStateException(
|
|
"toPattern() is not supported after custom Format objects "+
|
|
"have been set via setFormat() or similar APIs");
|
|
}
|
|
if (msgPattern == null) {
|
|
return "";
|
|
}
|
|
String originalPattern = msgPattern.getPatternString();
|
|
return originalPattern == null ? "" : originalPattern;
|
|
}
|
|
|
|
/**
|
|
* Returns the part index of the next ARG_START after partIndex, or -1 if there is none more.
|
|
* @param partIndex Part index of the previous ARG_START (initially 0).
|
|
*/
|
|
private int nextTopLevelArgStart(int partIndex) {
|
|
if (partIndex != 0) {
|
|
partIndex = msgPattern.getLimitPartIndex(partIndex);
|
|
}
|
|
for (;;) {
|
|
MessagePattern.Part.Type type = msgPattern.getPartType(++partIndex);
|
|
if (type == MessagePattern.Part.Type.ARG_START) {
|
|
return partIndex;
|
|
}
|
|
if (type == MessagePattern.Part.Type.MSG_LIMIT) {
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean argNameMatches(int partIndex, String argName, int argNumber) {
|
|
Part part = msgPattern.getPart(partIndex);
|
|
return part.getType() == MessagePattern.Part.Type.ARG_NAME ?
|
|
msgPattern.partSubstringMatches(part, argName) :
|
|
part.getValue() == argNumber; // ARG_NUMBER
|
|
}
|
|
|
|
private String getArgName(int partIndex) {
|
|
Part part = msgPattern.getPart(partIndex);
|
|
if (part.getType() == MessagePattern.Part.Type.ARG_NAME) {
|
|
return msgPattern.getSubstring(part);
|
|
} else {
|
|
return Integer.toString(part.getValue());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the Format objects to use for the values passed into
|
|
* <code>format</code> methods or returned from <code>parse</code>
|
|
* methods. The indices of elements in <code>newFormats</code>
|
|
* correspond to the argument indices used in the previously set
|
|
* pattern string.
|
|
* The order of formats in <code>newFormats</code> thus corresponds to
|
|
* the order of elements in the <code>arguments</code> array passed
|
|
* to the <code>format</code> methods or the result array returned
|
|
* by the <code>parse</code> methods.
|
|
* <p>
|
|
* If an argument index is used for more than one format element
|
|
* in the pattern string, then the corresponding new format is used
|
|
* for all such format elements. If an argument index is not used
|
|
* for any format element in the pattern string, then the
|
|
* corresponding new format is ignored. If fewer formats are provided
|
|
* than needed, then only the formats for argument indices less
|
|
* than <code>newFormats.length</code> are replaced.
|
|
*
|
|
* This method is only supported if the format does not use
|
|
* named arguments, otherwise an IllegalArgumentException is thrown.
|
|
*
|
|
* @param newFormats the new formats to use
|
|
* @throws NullPointerException if <code>newFormats</code> is null
|
|
* @throws IllegalArgumentException if this formatter uses named arguments
|
|
*/
|
|
public void setFormatsByArgumentIndex(Format[] newFormats) {
|
|
if (msgPattern.hasNamedArguments()) {
|
|
throw new IllegalArgumentException(
|
|
"This method is not available in MessageFormat objects " +
|
|
"that use alphanumeric argument names.");
|
|
}
|
|
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
|
|
int argNumber = msgPattern.getPart(partIndex + 1).getValue();
|
|
if (argNumber < newFormats.length) {
|
|
setCustomArgStartFormat(partIndex, newFormats[argNumber]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* <strong>[icu]</strong> Sets the Format objects to use for the values passed into
|
|
* <code>format</code> methods or returned from <code>parse</code>
|
|
* methods. The keys in <code>newFormats</code> are the argument
|
|
* names in the previously set pattern string, and the values
|
|
* are the formats.
|
|
* <p>
|
|
* Only argument names from the pattern string are considered.
|
|
* Extra keys in <code>newFormats</code> that do not correspond
|
|
* to an argument name are ignored. Similarly, if there is no
|
|
* format in newFormats for an argument name, the formatter
|
|
* for that argument remains unchanged.
|
|
* <p>
|
|
* This may be called on formats that do not use named arguments.
|
|
* In this case the map will be queried for key Strings that
|
|
* represent argument indices, e.g. "0", "1", "2" etc.
|
|
*
|
|
* @param newFormats a map from String to Format providing new
|
|
* formats for named arguments.
|
|
*/
|
|
public void setFormatsByArgumentName(Map<String, Format> newFormats) {
|
|
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
|
|
String key = getArgName(partIndex + 1);
|
|
if (newFormats.containsKey(key)) {
|
|
setCustomArgStartFormat(partIndex, newFormats.get(key));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the Format objects to use for the format elements in the
|
|
* previously set pattern string.
|
|
* The order of formats in <code>newFormats</code> corresponds to
|
|
* the order of format elements in the pattern string.
|
|
* <p>
|
|
* If more formats are provided than needed by the pattern string,
|
|
* the remaining ones are ignored. If fewer formats are provided
|
|
* than needed, then only the first <code>newFormats.length</code>
|
|
* formats are replaced.
|
|
* <p>
|
|
* Since the order of format elements in a pattern string often
|
|
* changes during localization, it is generally better to use the
|
|
* {@link #setFormatsByArgumentIndex setFormatsByArgumentIndex}
|
|
* method, which assumes an order of formats corresponding to the
|
|
* order of elements in the <code>arguments</code> array passed to
|
|
* the <code>format</code> methods or the result array returned by
|
|
* the <code>parse</code> methods.
|
|
*
|
|
* @param newFormats the new formats to use
|
|
* @exception NullPointerException if <code>newFormats</code> is null
|
|
*/
|
|
public void setFormats(Format[] newFormats) {
|
|
int formatNumber = 0;
|
|
for (int partIndex = 0;
|
|
formatNumber < newFormats.length &&
|
|
(partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
|
|
setCustomArgStartFormat(partIndex, newFormats[formatNumber]);
|
|
++formatNumber;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the Format object to use for the format elements within the
|
|
* previously set pattern string that use the given argument
|
|
* index.
|
|
* The argument index is part of the format element definition and
|
|
* represents an index into the <code>arguments</code> array passed
|
|
* to the <code>format</code> methods or the result array returned
|
|
* by the <code>parse</code> methods.
|
|
* <p>
|
|
* If the argument index is used for more than one format element
|
|
* in the pattern string, then the new format is used for all such
|
|
* format elements. If the argument index is not used for any format
|
|
* element in the pattern string, then the new format is ignored.
|
|
*
|
|
* This method is only supported when exclusively numbers are used for
|
|
* argument names. Otherwise an IllegalArgumentException is thrown.
|
|
*
|
|
* @param argumentIndex the argument index for which to use the new format
|
|
* @param newFormat the new format to use
|
|
* @throws IllegalArgumentException if this format uses named arguments
|
|
*/
|
|
public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) {
|
|
if (msgPattern.hasNamedArguments()) {
|
|
throw new IllegalArgumentException(
|
|
"This method is not available in MessageFormat objects " +
|
|
"that use alphanumeric argument names.");
|
|
}
|
|
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
|
|
if (msgPattern.getPart(partIndex + 1).getValue() == argumentIndex) {
|
|
setCustomArgStartFormat(partIndex, newFormat);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* <strong>[icu]</strong> Sets the Format object to use for the format elements within the
|
|
* previously set pattern string that use the given argument
|
|
* name.
|
|
* <p>
|
|
* If the argument name is used for more than one format element
|
|
* in the pattern string, then the new format is used for all such
|
|
* format elements. If the argument name is not used for any format
|
|
* element in the pattern string, then the new format is ignored.
|
|
* <p>
|
|
* This API may be used on formats that do not use named arguments.
|
|
* In this case <code>argumentName</code> should be a String that names
|
|
* an argument index, e.g. "0", "1", "2"... etc. If it does not name
|
|
* a valid index, the format will be ignored. No error is thrown.
|
|
*
|
|
* @param argumentName the name of the argument to change
|
|
* @param newFormat the new format to use
|
|
*/
|
|
public void setFormatByArgumentName(String argumentName, Format newFormat) {
|
|
int argNumber = MessagePattern.validateArgumentName(argumentName);
|
|
if (argNumber < MessagePattern.ARG_NAME_NOT_NUMBER) {
|
|
return;
|
|
}
|
|
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
|
|
if (argNameMatches(partIndex + 1, argumentName, argNumber)) {
|
|
setCustomArgStartFormat(partIndex, newFormat);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the Format object to use for the format element with the given
|
|
* format element index within the previously set pattern string.
|
|
* The format element index is the zero-based number of the format
|
|
* element counting from the start of the pattern string.
|
|
* <p>
|
|
* Since the order of format elements in a pattern string often
|
|
* changes during localization, it is generally better to use the
|
|
* {@link #setFormatByArgumentIndex setFormatByArgumentIndex}
|
|
* method, which accesses format elements based on the argument
|
|
* index they specify.
|
|
*
|
|
* @param formatElementIndex the index of a format element within the pattern
|
|
* @param newFormat the format to use for the specified format element
|
|
* @exception ArrayIndexOutOfBoundsException if formatElementIndex is equal to or
|
|
* larger than the number of format elements in the pattern string
|
|
*/
|
|
public void setFormat(int formatElementIndex, Format newFormat) {
|
|
int formatNumber = 0;
|
|
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
|
|
if (formatNumber == formatElementIndex) {
|
|
setCustomArgStartFormat(partIndex, newFormat);
|
|
return;
|
|
}
|
|
++formatNumber;
|
|
}
|
|
throw new ArrayIndexOutOfBoundsException(formatElementIndex);
|
|
}
|
|
|
|
/**
|
|
* Returns the Format objects used for the values passed into
|
|
* <code>format</code> methods or returned from <code>parse</code>
|
|
* methods. The indices of elements in the returned array
|
|
* correspond to the argument indices used in the previously set
|
|
* pattern string.
|
|
* The order of formats in the returned array thus corresponds to
|
|
* the order of elements in the <code>arguments</code> array passed
|
|
* to the <code>format</code> methods or the result array returned
|
|
* by the <code>parse</code> methods.
|
|
* <p>
|
|
* If an argument index is used for more than one format element
|
|
* in the pattern string, then the format used for the last such
|
|
* format element is returned in the array. If an argument index
|
|
* is not used for any format element in the pattern string, then
|
|
* null is returned in the array.
|
|
*
|
|
* This method is only supported when exclusively numbers are used for
|
|
* argument names. Otherwise an IllegalArgumentException is thrown.
|
|
*
|
|
* @return the formats used for the arguments within the pattern
|
|
* @throws IllegalArgumentException if this format uses named arguments
|
|
*/
|
|
public Format[] getFormatsByArgumentIndex() {
|
|
if (msgPattern.hasNamedArguments()) {
|
|
throw new IllegalArgumentException(
|
|
"This method is not available in MessageFormat objects " +
|
|
"that use alphanumeric argument names.");
|
|
}
|
|
ArrayList<Format> list = new ArrayList<>();
|
|
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
|
|
int argNumber = msgPattern.getPart(partIndex + 1).getValue();
|
|
while (argNumber >= list.size()) {
|
|
list.add(null);
|
|
}
|
|
list.set(argNumber, cachedFormatters == null ? null : cachedFormatters.get(partIndex));
|
|
}
|
|
return list.toArray(new Format[list.size()]);
|
|
}
|
|
|
|
/**
|
|
* Returns the Format objects used for the format elements in the
|
|
* previously set pattern string.
|
|
* The order of formats in the returned array corresponds to
|
|
* the order of format elements in the pattern string.
|
|
* <p>
|
|
* Since the order of format elements in a pattern string often
|
|
* changes during localization, it's generally better to use the
|
|
* {@link #getFormatsByArgumentIndex()}
|
|
* method, which assumes an order of formats corresponding to the
|
|
* order of elements in the <code>arguments</code> array passed to
|
|
* the <code>format</code> methods or the result array returned by
|
|
* the <code>parse</code> methods.
|
|
*
|
|
* This method is only supported when exclusively numbers are used for
|
|
* argument names. Otherwise an IllegalArgumentException is thrown.
|
|
*
|
|
* @return the formats used for the format elements in the pattern
|
|
* @throws IllegalArgumentException if this format uses named arguments
|
|
*/
|
|
public Format[] getFormats() {
|
|
ArrayList<Format> list = new ArrayList<>();
|
|
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
|
|
list.add(cachedFormatters == null ? null : cachedFormatters.get(partIndex));
|
|
}
|
|
return list.toArray(new Format[list.size()]);
|
|
}
|
|
|
|
/**
|
|
* <strong>[icu]</strong> Returns the top-level argument names. For more details, see
|
|
* {@link #setFormatByArgumentName(String, Format)}.
|
|
* @return a Set of argument names
|
|
*/
|
|
public Set<String> getArgumentNames() {
|
|
Set<String> result = new HashSet<>();
|
|
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
|
|
result.add(getArgName(partIndex + 1));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* <strong>[icu]</strong> Returns the first top-level format associated with the given argument name.
|
|
* For more details, see {@link #setFormatByArgumentName(String, Format)}.
|
|
* @param argumentName The name of the desired argument.
|
|
* @return the Format associated with the name, or null if there isn't one.
|
|
*/
|
|
public Format getFormatByArgumentName(String argumentName) {
|
|
if (cachedFormatters == null) {
|
|
return null;
|
|
}
|
|
int argNumber = MessagePattern.validateArgumentName(argumentName);
|
|
if (argNumber < MessagePattern.ARG_NAME_NOT_NUMBER) {
|
|
return null;
|
|
}
|
|
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
|
|
if (argNameMatches(partIndex + 1, argumentName, argNumber)) {
|
|
return cachedFormatters.get(partIndex);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Formats an array of objects and appends the <code>MessageFormat</code>'s
|
|
* pattern, with arguments replaced by the formatted objects, to the
|
|
* provided <code>StringBuffer</code>.
|
|
* <p>
|
|
* The text substituted for the individual format elements is derived from
|
|
* the current subformat of the format element and the
|
|
* <code>arguments</code> element at the format element's argument index
|
|
* as indicated by the first matching line of the following table. An
|
|
* argument is <i>unavailable</i> if <code>arguments</code> is
|
|
* <code>null</code> or has fewer than argumentIndex+1 elements. When
|
|
* an argument is unavailable no substitution is performed.
|
|
*
|
|
* <table border=1>
|
|
* <tr>
|
|
* <th>argType or Format
|
|
* <th>value object
|
|
* <th>Formatted Text
|
|
* <tr>
|
|
* <td><i>any</i>
|
|
* <td><i>unavailable</i>
|
|
* <td><code>"{" + argNameOrNumber + "}"</code>
|
|
* <tr>
|
|
* <td><i>any</i>
|
|
* <td><code>null</code>
|
|
* <td><code>"null"</code>
|
|
* <tr>
|
|
* <td>custom Format <code>!= null</code>
|
|
* <td><i>any</i>
|
|
* <td><code>customFormat.format(argument)</code>
|
|
* <tr>
|
|
* <td>noneArg, or custom Format <code>== null</code>
|
|
* <td><code>instanceof Number</code>
|
|
* <td><code>NumberFormat.getInstance(getLocale()).format(argument)</code>
|
|
* <tr>
|
|
* <td>noneArg, or custom Format <code>== null</code>
|
|
* <td><code>instanceof Date</code>
|
|
* <td><code>DateFormat.getDateTimeInstance(DateFormat.SHORT,
|
|
* DateFormat.SHORT, getLocale()).format(argument)</code>
|
|
* <tr>
|
|
* <td>noneArg, or custom Format <code>== null</code>
|
|
* <td><code>instanceof String</code>
|
|
* <td><code>argument</code>
|
|
* <tr>
|
|
* <td>noneArg, or custom Format <code>== null</code>
|
|
* <td><i>any</i>
|
|
* <td><code>argument.toString()</code>
|
|
* <tr>
|
|
* <td>complexArg
|
|
* <td><i>any</i>
|
|
* <td>result of recursive formatting of a selected sub-message
|
|
* </table>
|
|
* <p>
|
|
* If <code>pos</code> is non-null, and refers to
|
|
* <code>Field.ARGUMENT</code>, the location of the first formatted
|
|
* string will be returned.
|
|
*
|
|
* This method is only supported when the format does not use named
|
|
* arguments, otherwise an IllegalArgumentException is thrown.
|
|
*
|
|
* @param arguments an array of objects to be formatted and substituted.
|
|
* @param result where text is appended.
|
|
* @param pos On input: an alignment field, if desired.
|
|
* On output: the offsets of the alignment field.
|
|
* @throws IllegalArgumentException if a value in the
|
|
* <code>arguments</code> array is not of the type
|
|
* expected by the corresponding argument or custom Format object.
|
|
* @throws IllegalArgumentException if this format uses named arguments
|
|
*/
|
|
public final StringBuffer format(Object[] arguments, StringBuffer result,
|
|
FieldPosition pos)
|
|
{
|
|
format(arguments, null, new AppendableWrapper(result), pos);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Formats a map of objects and appends the <code>MessageFormat</code>'s
|
|
* pattern, with arguments replaced by the formatted objects, to the
|
|
* provided <code>StringBuffer</code>.
|
|
* <p>
|
|
* The text substituted for the individual format elements is derived from
|
|
* the current subformat of the format element and the
|
|
* <code>arguments</code> value corresponding to the format element's
|
|
* argument name.
|
|
* <p>
|
|
* A numbered pattern argument is matched with a map key that contains that number
|
|
* as an ASCII-decimal-digit string (without leading zero).
|
|
* <p>
|
|
* An argument is <i>unavailable</i> if <code>arguments</code> is
|
|
* <code>null</code> or does not have a value corresponding to an argument
|
|
* name in the pattern. When an argument is unavailable no substitution
|
|
* is performed.
|
|
*
|
|
* @param arguments a map of objects to be formatted and substituted.
|
|
* @param result where text is appended.
|
|
* @param pos On input: an alignment field, if desired.
|
|
* On output: the offsets of the alignment field.
|
|
* @throws IllegalArgumentException if a value in the
|
|
* <code>arguments</code> array is not of the type
|
|
* expected by the corresponding argument or custom Format object.
|
|
* @return the passed-in StringBuffer
|
|
*/
|
|
public final StringBuffer format(Map<String, Object> arguments, StringBuffer result,
|
|
FieldPosition pos) {
|
|
format(null, arguments, new AppendableWrapper(result), pos);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Creates a MessageFormat with the given pattern and uses it
|
|
* to format the given arguments. This is equivalent to
|
|
* <blockquote>
|
|
* <code>(new {@link #MessageFormat(String) MessageFormat}(pattern)).{@link
|
|
* #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition)
|
|
* format}(arguments, new StringBuffer(), null).toString()</code>
|
|
* </blockquote>
|
|
*
|
|
* @throws IllegalArgumentException if the pattern is invalid
|
|
* @throws IllegalArgumentException if a value in the
|
|
* <code>arguments</code> array is not of the type
|
|
* expected by the corresponding argument or custom Format object.
|
|
* @throws IllegalArgumentException if this format uses named arguments
|
|
*/
|
|
public static String format(String pattern, Object... arguments) {
|
|
MessageFormat temp = new MessageFormat(pattern);
|
|
return temp.format(arguments);
|
|
}
|
|
|
|
/**
|
|
* Creates a MessageFormat with the given pattern and uses it to
|
|
* format the given arguments. The pattern must identifyarguments
|
|
* by name instead of by number.
|
|
* <p>
|
|
* @throws IllegalArgumentException if the pattern is invalid
|
|
* @throws IllegalArgumentException if a value in the
|
|
* <code>arguments</code> array is not of the type
|
|
* expected by the corresponding argument or custom Format object.
|
|
* @see #format(Map, StringBuffer, FieldPosition)
|
|
* @see #format(String, Object[])
|
|
*/
|
|
public static String format(String pattern, Map<String, Object> arguments) {
|
|
MessageFormat temp = new MessageFormat(pattern);
|
|
return temp.format(arguments);
|
|
}
|
|
|
|
/**
|
|
* <strong>[icu]</strong> Returns true if this MessageFormat uses named arguments,
|
|
* and false otherwise. See class description.
|
|
*
|
|
* @return true if named arguments are used.
|
|
*/
|
|
public boolean usesNamedArguments() {
|
|
return msgPattern.hasNamedArguments();
|
|
}
|
|
|
|
// Overrides
|
|
/**
|
|
* Formats a map or array of objects and appends the <code>MessageFormat</code>'s
|
|
* pattern, with format elements replaced by the formatted objects, to the
|
|
* provided <code>StringBuffer</code>.
|
|
* This is equivalent to either of
|
|
* <blockquote>
|
|
* <code>{@link #format(java.lang.Object[], java.lang.StringBuffer,
|
|
* java.text.FieldPosition) format}((Object[]) arguments, result, pos)</code>
|
|
* <code>{@link #format(java.util.Map, java.lang.StringBuffer,
|
|
* java.text.FieldPosition) format}((Map) arguments, result, pos)</code>
|
|
* </blockquote>
|
|
* A map must be provided if this format uses named arguments, otherwise
|
|
* an IllegalArgumentException will be thrown.
|
|
* @param arguments a map or array of objects to be formatted
|
|
* @param result where text is appended
|
|
* @param pos On input: an alignment field, if desired
|
|
* On output: the offsets of the alignment field
|
|
* @throws IllegalArgumentException if an argument in
|
|
* <code>arguments</code> is not of the type
|
|
* expected by the format element(s) that use it
|
|
* @throws IllegalArgumentException if <code>arguments</code> is
|
|
* an array of Object and this format uses named arguments
|
|
*/
|
|
@Override
|
|
public final StringBuffer format(Object arguments, StringBuffer result,
|
|
FieldPosition pos)
|
|
{
|
|
format(arguments, new AppendableWrapper(result), pos);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Formats an array of objects and inserts them into the
|
|
* <code>MessageFormat</code>'s pattern, producing an
|
|
* <code>AttributedCharacterIterator</code>.
|
|
* You can use the returned <code>AttributedCharacterIterator</code>
|
|
* to build the resulting String, as well as to determine information
|
|
* about the resulting String.
|
|
* <p>
|
|
* The text of the returned <code>AttributedCharacterIterator</code> is
|
|
* the same that would be returned by
|
|
* <blockquote>
|
|
* <code>{@link #format(java.lang.Object[], java.lang.StringBuffer,
|
|
* java.text.FieldPosition) format}(arguments, new StringBuffer(), null).toString()</code>
|
|
* </blockquote>
|
|
* <p>
|
|
* In addition, the <code>AttributedCharacterIterator</code> contains at
|
|
* least attributes indicating where text was generated from an
|
|
* argument in the <code>arguments</code> array. The keys of these attributes are of
|
|
* type <code>MessageFormat.Field</code>, their values are
|
|
* <code>Integer</code> objects indicating the index in the <code>arguments</code>
|
|
* array of the argument from which the text was generated.
|
|
* <p>
|
|
* The attributes/value from the underlying <code>Format</code>
|
|
* instances that <code>MessageFormat</code> uses will also be
|
|
* placed in the resulting <code>AttributedCharacterIterator</code>.
|
|
* This allows you to not only find where an argument is placed in the
|
|
* resulting String, but also which fields it contains in turn.
|
|
*
|
|
* @param arguments an array of objects to be formatted and substituted.
|
|
* @return AttributedCharacterIterator describing the formatted value.
|
|
* @exception NullPointerException if <code>arguments</code> is null.
|
|
* @throws IllegalArgumentException if a value in the
|
|
* <code>arguments</code> array is not of the type
|
|
* expected by the corresponding argument or custom Format object.
|
|
*/
|
|
@Override
|
|
public AttributedCharacterIterator formatToCharacterIterator(Object arguments) {
|
|
if (arguments == null) {
|
|
throw new NullPointerException(
|
|
"formatToCharacterIterator must be passed non-null object");
|
|
}
|
|
StringBuilder result = new StringBuilder();
|
|
AppendableWrapper wrapper = new AppendableWrapper(result);
|
|
wrapper.useAttributes();
|
|
format(arguments, wrapper, null);
|
|
AttributedString as = new AttributedString(result.toString());
|
|
for (AttributeAndPosition a : wrapper.attributes) {
|
|
as.addAttribute(a.key, a.value, a.start, a.limit);
|
|
}
|
|
return as.getIterator();
|
|
}
|
|
|
|
/**
|
|
* Parses the string.
|
|
*
|
|
* <p>Caveats: The parse may fail in a number of circumstances.
|
|
* For example:
|
|
* <ul>
|
|
* <li>If one of the arguments does not occur in the pattern.
|
|
* <li>If the format of an argument loses information, such as
|
|
* with a choice format where a large number formats to "many".
|
|
* <li>Does not yet handle recursion (where
|
|
* the substituted strings contain {n} references.)
|
|
* <li>Will not always find a match (or the correct match)
|
|
* if some part of the parse is ambiguous.
|
|
* For example, if the pattern "{1},{2}" is used with the
|
|
* string arguments {"a,b", "c"}, it will format as "a,b,c".
|
|
* When the result is parsed, it will return {"a", "b,c"}.
|
|
* <li>If a single argument is parsed more than once in the string,
|
|
* then the later parse wins.
|
|
* </ul>
|
|
* When the parse fails, use ParsePosition.getErrorIndex() to find out
|
|
* where in the string did the parsing failed. The returned error
|
|
* index is the starting offset of the sub-patterns that the string
|
|
* is comparing with. For example, if the parsing string "AAA {0} BBB"
|
|
* is comparing against the pattern "AAD {0} BBB", the error index is
|
|
* 0. When an error occurs, the call to this method will return null.
|
|
* If the source is null, return an empty array.
|
|
*
|
|
* @throws IllegalArgumentException if this format uses named arguments
|
|
*/
|
|
public Object[] parse(String source, ParsePosition pos) {
|
|
if (msgPattern.hasNamedArguments()) {
|
|
throw new IllegalArgumentException(
|
|
"This method is not available in MessageFormat objects " +
|
|
"that use named argument.");
|
|
}
|
|
|
|
// Count how many slots we need in the array.
|
|
int maxArgId = -1;
|
|
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
|
|
int argNumber=msgPattern.getPart(partIndex + 1).getValue();
|
|
if (argNumber > maxArgId) {
|
|
maxArgId = argNumber;
|
|
}
|
|
}
|
|
Object[] resultArray = new Object[maxArgId + 1];
|
|
|
|
int backupStartPos = pos.getIndex();
|
|
parse(0, source, pos, resultArray, null);
|
|
if (pos.getIndex() == backupStartPos) { // unchanged, returned object is null
|
|
return null;
|
|
}
|
|
|
|
return resultArray;
|
|
}
|
|
|
|
/**
|
|
* <strong>[icu]</strong> Parses the string, returning the results in a Map.
|
|
* This is similar to the version that returns an array
|
|
* of Object. This supports both named and numbered
|
|
* arguments-- if numbered, the keys in the map are the
|
|
* corresponding ASCII-decimal-digit strings (e.g. "0", "1", "2"...).
|
|
*
|
|
* @param source the text to parse
|
|
* @param pos the position at which to start parsing. on return,
|
|
* contains the result of the parse.
|
|
* @return a Map containing key/value pairs for each parsed argument.
|
|
*/
|
|
public Map<String, Object> parseToMap(String source, ParsePosition pos) {
|
|
Map<String, Object> result = new HashMap<>();
|
|
int backupStartPos = pos.getIndex();
|
|
parse(0, source, pos, null, result);
|
|
if (pos.getIndex() == backupStartPos) {
|
|
return null;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Parses text from the beginning of the given string to produce an object
|
|
* array.
|
|
* The method may not use the entire text of the given string.
|
|
* <p>
|
|
* See the {@link #parse(String, ParsePosition)} method for more information
|
|
* on message parsing.
|
|
*
|
|
* @param source A <code>String</code> whose beginning should be parsed.
|
|
* @return An <code>Object</code> array parsed from the string.
|
|
* @exception ParseException if the beginning of the specified string cannot be parsed.
|
|
* @exception IllegalArgumentException if this format uses named arguments
|
|
*/
|
|
public Object[] parse(String source) throws ParseException {
|
|
ParsePosition pos = new ParsePosition(0);
|
|
Object[] result = parse(source, pos);
|
|
if (pos.getIndex() == 0) // unchanged, returned object is null
|
|
throw new ParseException("MessageFormat parse error!",
|
|
pos.getErrorIndex());
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Parses the string, filling either the Map or the Array.
|
|
* This is a private method that all the public parsing methods call.
|
|
* This supports both named and numbered
|
|
* arguments-- if numbered, the keys in the map are the
|
|
* corresponding ASCII-decimal-digit strings (e.g. "0", "1", "2"...).
|
|
*
|
|
* @param msgStart index in the message pattern to start from.
|
|
* @param source the text to parse
|
|
* @param pos the position at which to start parsing. on return,
|
|
* contains the result of the parse.
|
|
* @param args if not null, the parse results will be filled here (The pattern
|
|
* has to have numbered arguments in order for this to not be null).
|
|
* @param argsMap if not null, the parse results will be filled here.
|
|
*/
|
|
private void parse(int msgStart, String source, ParsePosition pos,
|
|
Object[] args, Map<String, Object> argsMap) {
|
|
if (source == null) {
|
|
return;
|
|
}
|
|
String msgString=msgPattern.getPatternString();
|
|
int prevIndex=msgPattern.getPart(msgStart).getLimit();
|
|
int sourceOffset = pos.getIndex();
|
|
ParsePosition tempStatus = new ParsePosition(0);
|
|
|
|
for(int i=msgStart+1; ; ++i) {
|
|
Part part=msgPattern.getPart(i);
|
|
Part.Type type=part.getType();
|
|
int index=part.getIndex();
|
|
// Make sure the literal string matches.
|
|
int len = index - prevIndex;
|
|
if (len == 0 || msgString.regionMatches(prevIndex, source, sourceOffset, len)) {
|
|
sourceOffset += len;
|
|
prevIndex += len;
|
|
} else {
|
|
pos.setErrorIndex(sourceOffset);
|
|
return; // leave index as is to signal error
|
|
}
|
|
if(type==Part.Type.MSG_LIMIT) {
|
|
// Things went well! Done.
|
|
pos.setIndex(sourceOffset);
|
|
return;
|
|
}
|
|
if(type==Part.Type.SKIP_SYNTAX || type==Part.Type.INSERT_CHAR) {
|
|
prevIndex=part.getLimit();
|
|
continue;
|
|
}
|
|
// We do not support parsing Plural formats. (No REPLACE_NUMBER here.)
|
|
assert type==Part.Type.ARG_START : "Unexpected Part "+part+" in parsed message.";
|
|
int argLimit=msgPattern.getLimitPartIndex(i);
|
|
|
|
ArgType argType=part.getArgType();
|
|
part=msgPattern.getPart(++i);
|
|
// Compute the argId, so we can use it as a key.
|
|
Object argId=null;
|
|
int argNumber = 0;
|
|
String key = null;
|
|
if(args!=null) {
|
|
argNumber=part.getValue(); // ARG_NUMBER
|
|
argId = argNumber;
|
|
} else {
|
|
if(part.getType()==MessagePattern.Part.Type.ARG_NAME) {
|
|
key=msgPattern.getSubstring(part);
|
|
} else /* ARG_NUMBER */ {
|
|
key=Integer.toString(part.getValue());
|
|
}
|
|
argId = key;
|
|
}
|
|
|
|
++i;
|
|
Format formatter = null;
|
|
boolean haveArgResult = false;
|
|
Object argResult = null;
|
|
if(cachedFormatters!=null && (formatter=cachedFormatters.get(i - 2))!=null) {
|
|
// Just parse using the formatter.
|
|
tempStatus.setIndex(sourceOffset);
|
|
argResult = formatter.parseObject(source, tempStatus);
|
|
if (tempStatus.getIndex() == sourceOffset) {
|
|
pos.setErrorIndex(sourceOffset);
|
|
return; // leave index as is to signal error
|
|
}
|
|
haveArgResult = true;
|
|
sourceOffset = tempStatus.getIndex();
|
|
} else if(
|
|
argType==ArgType.NONE ||
|
|
(cachedFormatters!=null && cachedFormatters.containsKey(i - 2))) {
|
|
// Match as a string.
|
|
// if at end, use longest possible match
|
|
// otherwise uses first match to intervening string
|
|
// does NOT recursively try all possibilities
|
|
String stringAfterArgument = getLiteralStringUntilNextArgument(argLimit);
|
|
int next;
|
|
if (stringAfterArgument.length() != 0) {
|
|
next = source.indexOf(stringAfterArgument, sourceOffset);
|
|
} else {
|
|
next = source.length();
|
|
}
|
|
if (next < 0) {
|
|
pos.setErrorIndex(sourceOffset);
|
|
return; // leave index as is to signal error
|
|
} else {
|
|
String strValue = source.substring(sourceOffset, next);
|
|
if (!strValue.equals("{" + argId.toString() + "}")) {
|
|
haveArgResult = true;
|
|
argResult = strValue;
|
|
}
|
|
sourceOffset = next;
|
|
}
|
|
} else if(argType==ArgType.CHOICE) {
|
|
tempStatus.setIndex(sourceOffset);
|
|
double choiceResult = parseChoiceArgument(msgPattern, i, source, tempStatus);
|
|
if (tempStatus.getIndex() == sourceOffset) {
|
|
pos.setErrorIndex(sourceOffset);
|
|
return; // leave index as is to signal error
|
|
}
|
|
argResult = choiceResult;
|
|
haveArgResult = true;
|
|
sourceOffset = tempStatus.getIndex();
|
|
} else if(argType.hasPluralStyle() || argType==ArgType.SELECT) {
|
|
// No can do!
|
|
throw new UnsupportedOperationException(
|
|
"Parsing of plural/select/selectordinal argument is not supported.");
|
|
} else {
|
|
// This should never happen.
|
|
throw new IllegalStateException("unexpected argType "+argType);
|
|
}
|
|
if (haveArgResult) {
|
|
if (args != null) {
|
|
args[argNumber] = argResult;
|
|
} else if (argsMap != null) {
|
|
argsMap.put(key, argResult);
|
|
}
|
|
}
|
|
prevIndex=msgPattern.getPart(argLimit).getLimit();
|
|
i=argLimit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* <strong>[icu]</strong> Parses text from the beginning of the given string to produce a map from
|
|
* argument to values. The method may not use the entire text of the given string.
|
|
*
|
|
* <p>See the {@link #parse(String, ParsePosition)} method for more information on
|
|
* message parsing.
|
|
*
|
|
* @param source A <code>String</code> whose beginning should be parsed.
|
|
* @return A <code>Map</code> parsed from the string.
|
|
* @throws ParseException if the beginning of the specified string cannot
|
|
* be parsed.
|
|
* @see #parseToMap(String, ParsePosition)
|
|
*/
|
|
public Map<String, Object> parseToMap(String source) throws ParseException {
|
|
ParsePosition pos = new ParsePosition(0);
|
|
Map<String, Object> result = new HashMap<>();
|
|
parse(0, source, pos, null, result);
|
|
if (pos.getIndex() == 0) // unchanged, returned object is null
|
|
throw new ParseException("MessageFormat parse error!",
|
|
pos.getErrorIndex());
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Parses text from a string to produce an object array or Map.
|
|
* <p>
|
|
* The method attempts to parse text starting at the index given by
|
|
* <code>pos</code>.
|
|
* If parsing succeeds, then the index of <code>pos</code> is updated
|
|
* to the index after the last character used (parsing does not necessarily
|
|
* use all characters up to the end of the string), and the parsed
|
|
* object array is returned. The updated <code>pos</code> can be used to
|
|
* indicate the starting point for the next call to this method.
|
|
* If an error occurs, then the index of <code>pos</code> is not
|
|
* changed, the error index of <code>pos</code> is set to the index of
|
|
* the character where the error occurred, and null is returned.
|
|
* <p>
|
|
* See the {@link #parse(String, ParsePosition)} method for more information
|
|
* on message parsing.
|
|
*
|
|
* @param source A <code>String</code>, part of which should be parsed.
|
|
* @param pos A <code>ParsePosition</code> object with index and error
|
|
* index information as described above.
|
|
* @return An <code>Object</code> parsed from the string, either an
|
|
* array of Object, or a Map, depending on whether named
|
|
* arguments are used. This can be queried using <code>usesNamedArguments</code>.
|
|
* In case of error, returns null.
|
|
* @throws NullPointerException if <code>pos</code> is null.
|
|
*/
|
|
@Override
|
|
public Object parseObject(String source, ParsePosition pos) {
|
|
if (!msgPattern.hasNamedArguments()) {
|
|
return parse(source, pos);
|
|
} else {
|
|
return parseToMap(source, pos);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public Object clone() {
|
|
MessageFormat other = (MessageFormat) super.clone();
|
|
|
|
if (customFormatArgStarts != null) {
|
|
other.customFormatArgStarts = new HashSet<>();
|
|
for (Integer key : customFormatArgStarts) {
|
|
other.customFormatArgStarts.add(key);
|
|
}
|
|
} else {
|
|
other.customFormatArgStarts = null;
|
|
}
|
|
|
|
if (cachedFormatters != null) {
|
|
other.cachedFormatters = new HashMap<>();
|
|
Iterator<Map.Entry<Integer, Format>> it = cachedFormatters.entrySet().iterator();
|
|
while (it.hasNext()){
|
|
Map.Entry<Integer, Format> entry = it.next();
|
|
other.cachedFormatters.put(entry.getKey(), entry.getValue());
|
|
}
|
|
} else {
|
|
other.cachedFormatters = null;
|
|
}
|
|
|
|
other.msgPattern = msgPattern == null ? null : (MessagePattern)msgPattern.clone();
|
|
other.stockDateFormatter =
|
|
stockDateFormatter == null ? null : (DateFormat) stockDateFormatter.clone();
|
|
other.stockNumberFormatter =
|
|
stockNumberFormatter == null ? null : (NumberFormat) stockNumberFormatter.clone();
|
|
|
|
other.pluralProvider = null;
|
|
other.ordinalProvider = null;
|
|
return other;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
if (this == obj) // quick check
|
|
return true;
|
|
if (obj == null || getClass() != obj.getClass())
|
|
return false;
|
|
MessageFormat other = (MessageFormat) obj;
|
|
return Objects.equals(ulocale, other.ulocale)
|
|
&& Objects.equals(msgPattern, other.msgPattern)
|
|
&& Objects.equals(cachedFormatters, other.cachedFormatters)
|
|
&& Objects.equals(customFormatArgStarts, other.customFormatArgStarts);
|
|
// Note: It might suffice to only compare custom formatters
|
|
// rather than all formatters.
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int hashCode() {
|
|
return msgPattern.getPatternString().hashCode(); // enough for reasonable distribution
|
|
}
|
|
|
|
/**
|
|
* Defines constants that are used as attribute keys in the
|
|
* <code>AttributedCharacterIterator</code> returned
|
|
* from <code>MessageFormat.formatToCharacterIterator</code>.
|
|
*/
|
|
public static class Field extends Format.Field {
|
|
|
|
private static final long serialVersionUID = 7510380454602616157L;
|
|
|
|
/**
|
|
* Create a <code>Field</code> with the specified name.
|
|
*
|
|
* @param name The name of the attribute
|
|
*/
|
|
protected Field(String name) {
|
|
super(name);
|
|
}
|
|
|
|
/**
|
|
* Resolves instances being deserialized to the predefined constants.
|
|
*
|
|
* @return resolved MessageFormat.Field constant
|
|
* @throws InvalidObjectException if the constant could not be resolved.
|
|
*/
|
|
@Override
|
|
protected Object readResolve() throws InvalidObjectException {
|
|
if (this.getClass() != MessageFormat.Field.class) {
|
|
throw new InvalidObjectException(
|
|
"A subclass of MessageFormat.Field must implement readResolve.");
|
|
}
|
|
if (this.getName().equals(ARGUMENT.getName())) {
|
|
return ARGUMENT;
|
|
} else {
|
|
throw new InvalidObjectException("Unknown attribute name.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constant identifying a portion of a message that was generated
|
|
* from an argument passed into <code>formatToCharacterIterator</code>.
|
|
* The value associated with the key will be an <code>Integer</code>
|
|
* indicating the index in the <code>arguments</code> array of the
|
|
* argument from which the text was generated.
|
|
*/
|
|
public static final Field ARGUMENT = new Field("message argument field");
|
|
}
|
|
|
|
// ===========================privates============================
|
|
|
|
// *Important*: All fields must be declared *transient* so that we can fully
|
|
// control serialization!
|
|
// See for example Joshua Bloch's "Effective Java", chapter 10 Serialization.
|
|
|
|
/**
|
|
* The locale to use for formatting numbers and dates.
|
|
*/
|
|
private transient ULocale ulocale;
|
|
|
|
/**
|
|
* The MessagePattern which contains the parsed structure of the pattern string.
|
|
*/
|
|
private transient MessagePattern msgPattern;
|
|
/**
|
|
* Cached formatters so we can just use them whenever needed instead of creating
|
|
* them from scratch every time.
|
|
*/
|
|
private transient Map<Integer, Format> cachedFormatters;
|
|
/**
|
|
* Set of ARG_START part indexes where custom, user-provided Format objects
|
|
* have been set via setFormat() or similar API.
|
|
*/
|
|
private transient Set<Integer> customFormatArgStarts;
|
|
|
|
/**
|
|
* Stock formatters. Those are used when a format is not explicitly mentioned in
|
|
* the message. The format is inferred from the argument.
|
|
*/
|
|
private transient DateFormat stockDateFormatter;
|
|
private transient NumberFormat stockNumberFormatter;
|
|
|
|
private transient PluralSelectorProvider pluralProvider;
|
|
private transient PluralSelectorProvider ordinalProvider;
|
|
|
|
private DateFormat getStockDateFormatter() {
|
|
if (stockDateFormatter == null) {
|
|
stockDateFormatter = DateFormat.getDateTimeInstance(
|
|
DateFormat.SHORT, DateFormat.SHORT, ulocale);//fix
|
|
}
|
|
return stockDateFormatter;
|
|
}
|
|
private NumberFormat getStockNumberFormatter() {
|
|
if (stockNumberFormatter == null) {
|
|
stockNumberFormatter = NumberFormat.getInstance(ulocale);
|
|
}
|
|
return stockNumberFormatter;
|
|
}
|
|
|
|
// *Important*: All fields must be declared *transient*.
|
|
// See the longer comment above ulocale.
|
|
|
|
/**
|
|
* Formats the arguments and writes the result into the
|
|
* AppendableWrapper, updates the field position.
|
|
*
|
|
* <p>Exactly one of args and argsMap must be null, the other non-null.
|
|
*
|
|
* @param msgStart Index to msgPattern part to start formatting from.
|
|
* @param pluralNumber null except when formatting a plural argument sub-message
|
|
* where a '#' is replaced by the format string for this number.
|
|
* @param args The formattable objects array. Non-null iff numbered values are used.
|
|
* @param argsMap The key-value map of formattable objects. Non-null iff named values are used.
|
|
* @param dest Output parameter to receive the result.
|
|
* The result (string & attributes) is appended to existing contents.
|
|
* @param fp Field position status.
|
|
*/
|
|
private void format(int msgStart, PluralSelectorContext pluralNumber,
|
|
Object[] args, Map<String, Object> argsMap,
|
|
AppendableWrapper dest, FieldPosition fp) {
|
|
String msgString=msgPattern.getPatternString();
|
|
int prevIndex=msgPattern.getPart(msgStart).getLimit();
|
|
for(int i=msgStart+1;; ++i) {
|
|
Part part=msgPattern.getPart(i);
|
|
Part.Type type=part.getType();
|
|
int index=part.getIndex();
|
|
dest.append(msgString, prevIndex, index);
|
|
if(type==Part.Type.MSG_LIMIT) {
|
|
return;
|
|
}
|
|
prevIndex=part.getLimit();
|
|
if(type==Part.Type.REPLACE_NUMBER) {
|
|
if(pluralNumber.forReplaceNumber) {
|
|
// number-offset was already formatted.
|
|
dest.formatAndAppend(pluralNumber.formatter,
|
|
pluralNumber.number, pluralNumber.numberString);
|
|
} else {
|
|
dest.formatAndAppend(getStockNumberFormatter(), pluralNumber.number);
|
|
}
|
|
continue;
|
|
}
|
|
if(type!=Part.Type.ARG_START) {
|
|
continue;
|
|
}
|
|
int argLimit=msgPattern.getLimitPartIndex(i);
|
|
ArgType argType=part.getArgType();
|
|
part=msgPattern.getPart(++i);
|
|
Object arg;
|
|
boolean noArg=false;
|
|
Object argId=null;
|
|
String argName=msgPattern.getSubstring(part);
|
|
if(args!=null) {
|
|
int argNumber=part.getValue(); // ARG_NUMBER
|
|
if (dest.attributes != null) {
|
|
// We only need argId if we add it into the attributes.
|
|
argId = argNumber;
|
|
}
|
|
if(0<=argNumber && argNumber<args.length) {
|
|
arg=args[argNumber];
|
|
} else {
|
|
arg=null;
|
|
noArg=true;
|
|
}
|
|
} else {
|
|
argId = argName;
|
|
if(argsMap!=null) {
|
|
arg=argsMap.get(argName);
|
|
if (arg==null) {
|
|
noArg=!argsMap.containsKey(argName);
|
|
}
|
|
} else {
|
|
arg=null;
|
|
noArg=true;
|
|
}
|
|
}
|
|
++i;
|
|
int prevDestLength=dest.length;
|
|
Format formatter = null;
|
|
if (noArg) {
|
|
dest.append("{");
|
|
dest.append(argName);
|
|
dest.append("}");
|
|
} else if (arg == null) {
|
|
dest.append("null");
|
|
} else if(pluralNumber!=null && pluralNumber.numberArgIndex==(i-2)) {
|
|
if(pluralNumber.offset == 0) {
|
|
// The number was already formatted with this formatter.
|
|
dest.formatAndAppend(pluralNumber.formatter, pluralNumber.number, pluralNumber.numberString);
|
|
} else {
|
|
// Do not use the formatted (number-offset) string for a named argument
|
|
// that formats the number without subtracting the offset.
|
|
dest.formatAndAppend(pluralNumber.formatter, arg);
|
|
}
|
|
} else if(cachedFormatters!=null && (formatter=cachedFormatters.get(i - 2))!=null) {
|
|
// Handles all ArgType.SIMPLE, and formatters from setFormat() and its siblings.
|
|
if ( formatter instanceof ChoiceFormat ||
|
|
formatter instanceof PluralFormat ||
|
|
formatter instanceof SelectFormat) {
|
|
// We only handle nested formats here if they were provided via setFormat() or its siblings.
|
|
// Otherwise they are not cached and instead handled below according to argType.
|
|
String subMsgString = formatter.format(arg);
|
|
if (subMsgString.indexOf('{') >= 0 ||
|
|
(subMsgString.indexOf('\'') >= 0 && !msgPattern.jdkAposMode())) {
|
|
MessageFormat subMsgFormat = new MessageFormat(subMsgString, ulocale);
|
|
subMsgFormat.format(0, null, args, argsMap, dest, null);
|
|
} else if (dest.attributes == null) {
|
|
dest.append(subMsgString);
|
|
} else {
|
|
// This formats the argument twice, once above to get the subMsgString
|
|
// and then once more here.
|
|
// It only happens in formatToCharacterIterator()
|
|
// on a complex Format set via setFormat(),
|
|
// and only when the selected subMsgString does not need further formatting.
|
|
// This imitates ICU 4.6 behavior.
|
|
dest.formatAndAppend(formatter, arg);
|
|
}
|
|
} else {
|
|
dest.formatAndAppend(formatter, arg);
|
|
}
|
|
} else if(
|
|
argType==ArgType.NONE ||
|
|
(cachedFormatters!=null && cachedFormatters.containsKey(i - 2))) {
|
|
// ArgType.NONE, or
|
|
// any argument which got reset to null via setFormat() or its siblings.
|
|
if (arg instanceof Number) {
|
|
// format number if can
|
|
dest.formatAndAppend(getStockNumberFormatter(), arg);
|
|
} else if (arg instanceof Date) {
|
|
// format a Date if can
|
|
dest.formatAndAppend(getStockDateFormatter(), arg);
|
|
} else {
|
|
dest.append(arg.toString());
|
|
}
|
|
} else if(argType==ArgType.CHOICE) {
|
|
if (!(arg instanceof Number)) {
|
|
throw new IllegalArgumentException("'" + arg + "' is not a Number");
|
|
}
|
|
double number = ((Number)arg).doubleValue();
|
|
int subMsgStart=findChoiceSubMessage(msgPattern, i, number);
|
|
formatComplexSubMessage(subMsgStart, null, args, argsMap, dest);
|
|
} else if(argType.hasPluralStyle()) {
|
|
if (!(arg instanceof Number)) {
|
|
throw new IllegalArgumentException("'" + arg + "' is not a Number");
|
|
}
|
|
PluralSelectorProvider selector;
|
|
if(argType == ArgType.PLURAL) {
|
|
if (pluralProvider == null) {
|
|
pluralProvider = new PluralSelectorProvider(this, PluralType.CARDINAL);
|
|
}
|
|
selector = pluralProvider;
|
|
} else {
|
|
if (ordinalProvider == null) {
|
|
ordinalProvider = new PluralSelectorProvider(this, PluralType.ORDINAL);
|
|
}
|
|
selector = ordinalProvider;
|
|
}
|
|
Number number = (Number)arg;
|
|
double offset=msgPattern.getPluralOffset(i);
|
|
PluralSelectorContext context =
|
|
new PluralSelectorContext(i, argName, number, offset);
|
|
int subMsgStart=PluralFormat.findSubMessage(
|
|
msgPattern, i, selector, context, number.doubleValue());
|
|
formatComplexSubMessage(subMsgStart, context, args, argsMap, dest);
|
|
} else if(argType==ArgType.SELECT) {
|
|
int subMsgStart=SelectFormat.findSubMessage(msgPattern, i, arg.toString());
|
|
formatComplexSubMessage(subMsgStart, null, args, argsMap, dest);
|
|
} else {
|
|
// This should never happen.
|
|
throw new IllegalStateException("unexpected argType "+argType);
|
|
}
|
|
fp = updateMetaData(dest, prevDestLength, fp, argId);
|
|
prevIndex=msgPattern.getPart(argLimit).getLimit();
|
|
i=argLimit;
|
|
}
|
|
}
|
|
|
|
private void formatComplexSubMessage(
|
|
int msgStart, PluralSelectorContext pluralNumber,
|
|
Object[] args, Map<String, Object> argsMap,
|
|
AppendableWrapper dest) {
|
|
if (!msgPattern.jdkAposMode()) {
|
|
format(msgStart, pluralNumber, args, argsMap, dest, null);
|
|
return;
|
|
}
|
|
// JDK compatibility mode: (see JDK MessageFormat.format() API docs)
|
|
// - remove SKIP_SYNTAX; that is, remove half of the apostrophes
|
|
// - if the result string contains an open curly brace '{' then
|
|
// instantiate a temporary MessageFormat object and format again;
|
|
// otherwise just append the result string
|
|
String msgString = msgPattern.getPatternString();
|
|
String subMsgString;
|
|
StringBuilder sb = null;
|
|
int prevIndex = msgPattern.getPart(msgStart).getLimit();
|
|
for (int i = msgStart;;) {
|
|
Part part = msgPattern.getPart(++i);
|
|
Part.Type type = part.getType();
|
|
int index = part.getIndex();
|
|
if (type == Part.Type.MSG_LIMIT) {
|
|
if (sb == null) {
|
|
subMsgString = msgString.substring(prevIndex, index);
|
|
} else {
|
|
subMsgString = sb.append(msgString, prevIndex, index).toString();
|
|
}
|
|
break;
|
|
} else if (type == Part.Type.REPLACE_NUMBER || type == Part.Type.SKIP_SYNTAX) {
|
|
if (sb == null) {
|
|
sb = new StringBuilder();
|
|
}
|
|
sb.append(msgString, prevIndex, index);
|
|
if (type == Part.Type.REPLACE_NUMBER) {
|
|
if(pluralNumber.forReplaceNumber) {
|
|
// number-offset was already formatted.
|
|
sb.append(pluralNumber.numberString);
|
|
} else {
|
|
sb.append(getStockNumberFormatter().format(pluralNumber.number));
|
|
}
|
|
}
|
|
prevIndex = part.getLimit();
|
|
} else if (type == Part.Type.ARG_START) {
|
|
if (sb == null) {
|
|
sb = new StringBuilder();
|
|
}
|
|
sb.append(msgString, prevIndex, index);
|
|
prevIndex = index;
|
|
i = msgPattern.getLimitPartIndex(i);
|
|
index = msgPattern.getPart(i).getLimit();
|
|
MessagePattern.appendReducedApostrophes(msgString, prevIndex, index, sb);
|
|
prevIndex = index;
|
|
}
|
|
}
|
|
if (subMsgString.indexOf('{') >= 0) {
|
|
MessageFormat subMsgFormat = new MessageFormat("", ulocale);
|
|
subMsgFormat.applyPattern(subMsgString, MessagePattern.ApostropheMode.DOUBLE_REQUIRED);
|
|
subMsgFormat.format(0, null, args, argsMap, dest, null);
|
|
} else {
|
|
dest.append(subMsgString);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read as much literal string from the pattern string as possible. This stops
|
|
* as soon as it finds an argument, or it reaches the end of the string.
|
|
* @param from Index in the pattern string to start from.
|
|
* @return A substring from the pattern string representing the longest possible
|
|
* substring with no arguments.
|
|
*/
|
|
private String getLiteralStringUntilNextArgument(int from) {
|
|
StringBuilder b = new StringBuilder();
|
|
String msgString=msgPattern.getPatternString();
|
|
int prevIndex=msgPattern.getPart(from).getLimit();
|
|
for(int i=from+1;; ++i) {
|
|
Part part=msgPattern.getPart(i);
|
|
Part.Type type=part.getType();
|
|
int index=part.getIndex();
|
|
b.append(msgString, prevIndex, index);
|
|
if(type==Part.Type.ARG_START || type==Part.Type.MSG_LIMIT) {
|
|
return b.toString();
|
|
}
|
|
assert type==Part.Type.SKIP_SYNTAX || type==Part.Type.INSERT_CHAR :
|
|
"Unexpected Part "+part+" in parsed message.";
|
|
prevIndex=part.getLimit();
|
|
}
|
|
}
|
|
|
|
private FieldPosition updateMetaData(AppendableWrapper dest, int prevLength,
|
|
FieldPosition fp, Object argId) {
|
|
if (dest.attributes != null && prevLength < dest.length) {
|
|
dest.attributes.add(new AttributeAndPosition(argId, prevLength, dest.length));
|
|
}
|
|
if (fp != null && Field.ARGUMENT.equals(fp.getFieldAttribute())) {
|
|
fp.setBeginIndex(prevLength);
|
|
fp.setEndIndex(dest.length);
|
|
return null;
|
|
}
|
|
return fp;
|
|
}
|
|
|
|
// This lives here because ICU4J does not have its own ChoiceFormat class.
|
|
/**
|
|
* Finds the ChoiceFormat sub-message for the given number.
|
|
* @param pattern A MessagePattern.
|
|
* @param partIndex the index of the first ChoiceFormat argument style part.
|
|
* @param number a number to be mapped to one of the ChoiceFormat argument's intervals
|
|
* @return the sub-message start part index.
|
|
*/
|
|
private static int findChoiceSubMessage(MessagePattern pattern, int partIndex, double number) {
|
|
int count=pattern.countParts();
|
|
int msgStart;
|
|
// Iterate over (ARG_INT|DOUBLE, ARG_SELECTOR, message) tuples
|
|
// until ARG_LIMIT or end of choice-only pattern.
|
|
// Ignore the first number and selector and start the loop on the first message.
|
|
partIndex+=2;
|
|
for(;;) {
|
|
// Skip but remember the current sub-message.
|
|
msgStart=partIndex;
|
|
partIndex=pattern.getLimitPartIndex(partIndex);
|
|
if(++partIndex>=count) {
|
|
// Reached the end of the choice-only pattern.
|
|
// Return with the last sub-message.
|
|
break;
|
|
}
|
|
Part part=pattern.getPart(partIndex++);
|
|
Part.Type type=part.getType();
|
|
if(type==Part.Type.ARG_LIMIT) {
|
|
// Reached the end of the ChoiceFormat style.
|
|
// Return with the last sub-message.
|
|
break;
|
|
}
|
|
// part is an ARG_INT or ARG_DOUBLE
|
|
assert type.hasNumericValue();
|
|
double boundary=pattern.getNumericValue(part);
|
|
// Fetch the ARG_SELECTOR character.
|
|
int selectorIndex=pattern.getPatternIndex(partIndex++);
|
|
char boundaryChar=pattern.getPatternString().charAt(selectorIndex);
|
|
if(boundaryChar=='<' ? !(number>boundary) : !(number>=boundary)) {
|
|
// The number is in the interval between the previous boundary and the current one.
|
|
// Return with the sub-message between them.
|
|
// The !(a>b) and !(a>=b) comparisons are equivalent to
|
|
// (a<=b) and (a<b) except they "catch" NaN.
|
|
break;
|
|
}
|
|
}
|
|
return msgStart;
|
|
}
|
|
|
|
// Ported from C++ ChoiceFormat::parse().
|
|
private static double parseChoiceArgument(
|
|
MessagePattern pattern, int partIndex,
|
|
String source, ParsePosition pos) {
|
|
// find the best number (defined as the one with the longest parse)
|
|
int start = pos.getIndex();
|
|
int furthest = start;
|
|
double bestNumber = Double.NaN;
|
|
double tempNumber = 0.0;
|
|
while (pattern.getPartType(partIndex) != Part.Type.ARG_LIMIT) {
|
|
tempNumber = pattern.getNumericValue(pattern.getPart(partIndex));
|
|
partIndex += 2; // skip the numeric part and ignore the ARG_SELECTOR
|
|
int msgLimit = pattern.getLimitPartIndex(partIndex);
|
|
int len = matchStringUntilLimitPart(pattern, partIndex, msgLimit, source, start);
|
|
if (len >= 0) {
|
|
int newIndex = start + len;
|
|
if (newIndex > furthest) {
|
|
furthest = newIndex;
|
|
bestNumber = tempNumber;
|
|
if (furthest == source.length()) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
partIndex = msgLimit + 1;
|
|
}
|
|
if (furthest == start) {
|
|
pos.setErrorIndex(start);
|
|
} else {
|
|
pos.setIndex(furthest);
|
|
}
|
|
return bestNumber;
|
|
}
|
|
|
|
/**
|
|
* Matches the pattern string from the end of the partIndex to
|
|
* the beginning of the limitPartIndex,
|
|
* including all syntax except SKIP_SYNTAX,
|
|
* against the source string starting at sourceOffset.
|
|
* If they match, returns the length of the source string match.
|
|
* Otherwise returns -1.
|
|
*/
|
|
private static int matchStringUntilLimitPart(
|
|
MessagePattern pattern, int partIndex, int limitPartIndex,
|
|
String source, int sourceOffset) {
|
|
int matchingSourceLength = 0;
|
|
String msgString = pattern.getPatternString();
|
|
int prevIndex = pattern.getPart(partIndex).getLimit();
|
|
for (;;) {
|
|
Part part = pattern.getPart(++partIndex);
|
|
if (partIndex == limitPartIndex || part.getType() == Part.Type.SKIP_SYNTAX) {
|
|
int index = part.getIndex();
|
|
int length = index - prevIndex;
|
|
if (length != 0 && !source.regionMatches(sourceOffset, msgString, prevIndex, length)) {
|
|
return -1; // mismatch
|
|
}
|
|
matchingSourceLength += length;
|
|
if (partIndex == limitPartIndex) {
|
|
return matchingSourceLength;
|
|
}
|
|
prevIndex = part.getLimit(); // SKIP_SYNTAX
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds the "other" sub-message.
|
|
* @param partIndex the index of the first PluralFormat argument style part.
|
|
* @return the "other" sub-message start part index.
|
|
*/
|
|
private int findOtherSubMessage(int partIndex) {
|
|
int count=msgPattern.countParts();
|
|
MessagePattern.Part part=msgPattern.getPart(partIndex);
|
|
if(part.getType().hasNumericValue()) {
|
|
++partIndex;
|
|
}
|
|
// Iterate over (ARG_SELECTOR [ARG_INT|ARG_DOUBLE] message) tuples
|
|
// until ARG_LIMIT or end of plural-only pattern.
|
|
do {
|
|
part=msgPattern.getPart(partIndex++);
|
|
MessagePattern.Part.Type type=part.getType();
|
|
if(type==MessagePattern.Part.Type.ARG_LIMIT) {
|
|
break;
|
|
}
|
|
assert type==MessagePattern.Part.Type.ARG_SELECTOR;
|
|
// part is an ARG_SELECTOR followed by an optional explicit value, and then a message
|
|
if(msgPattern.partSubstringMatches(part, "other")) {
|
|
return partIndex;
|
|
}
|
|
if(msgPattern.getPartType(partIndex).hasNumericValue()) {
|
|
++partIndex; // skip the numeric-value part of "=1" etc.
|
|
}
|
|
partIndex=msgPattern.getLimitPartIndex(partIndex);
|
|
} while(++partIndex<count);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Returns the ARG_START index of the first occurrence of the plural number in a sub-message.
|
|
* Returns -1 if it is a REPLACE_NUMBER.
|
|
* Returns 0 if there is neither.
|
|
*/
|
|
private int findFirstPluralNumberArg(int msgStart, String argName) {
|
|
for(int i=msgStart+1;; ++i) {
|
|
Part part=msgPattern.getPart(i);
|
|
Part.Type type=part.getType();
|
|
if(type==Part.Type.MSG_LIMIT) {
|
|
return 0;
|
|
}
|
|
if(type==Part.Type.REPLACE_NUMBER) {
|
|
return -1;
|
|
}
|
|
if(type==Part.Type.ARG_START) {
|
|
ArgType argType=part.getArgType();
|
|
if(argName.length()!=0 && (argType==ArgType.NONE || argType==ArgType.SIMPLE)) {
|
|
part=msgPattern.getPart(i+1); // ARG_NUMBER or ARG_NAME
|
|
if(msgPattern.partSubstringMatches(part, argName)) {
|
|
return i;
|
|
}
|
|
}
|
|
i=msgPattern.getLimitPartIndex(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mutable input/output values for the PluralSelectorProvider.
|
|
* Separate so that it is possible to make MessageFormat Freezable.
|
|
*/
|
|
private static final class PluralSelectorContext {
|
|
private PluralSelectorContext(int start, String name, Number num, double off) {
|
|
startIndex = start;
|
|
argName = name;
|
|
// number needs to be set even when select() is not called.
|
|
// Keep it as a Number/Formattable:
|
|
// For format() methods, and to preserve information (e.g., BigDecimal).
|
|
if(off == 0) {
|
|
number = num;
|
|
} else {
|
|
number = num.doubleValue() - off;
|
|
}
|
|
offset = off;
|
|
}
|
|
@Override
|
|
public String toString() {
|
|
throw new AssertionError("PluralSelectorContext being formatted, rather than its number");
|
|
}
|
|
|
|
// Input values for plural selection with decimals.
|
|
int startIndex;
|
|
String argName;
|
|
/** argument number - plural offset */
|
|
Number number;
|
|
double offset;
|
|
// Output values for plural selection with decimals.
|
|
/** -1 if REPLACE_NUMBER, 0 arg not found, >0 ARG_START index */
|
|
int numberArgIndex;
|
|
Format formatter;
|
|
/** formatted argument number - plural offset */
|
|
String numberString;
|
|
/** true if number-offset was formatted with the stock number formatter */
|
|
boolean forReplaceNumber;
|
|
}
|
|
|
|
/**
|
|
* This provider helps defer instantiation of a PluralRules object
|
|
* until we actually need to select a keyword.
|
|
* For example, if the number matches an explicit-value selector like "=1"
|
|
* we do not need any PluralRules.
|
|
*/
|
|
private static final class PluralSelectorProvider implements PluralFormat.PluralSelector {
|
|
public PluralSelectorProvider(MessageFormat mf, PluralType type) {
|
|
msgFormat = mf;
|
|
this.type = type;
|
|
}
|
|
@Override
|
|
public String select(Object ctx, double number) {
|
|
if(rules == null) {
|
|
rules = PluralRules.forLocale(msgFormat.ulocale, type);
|
|
}
|
|
// Select a sub-message according to how the number is formatted,
|
|
// which is specified in the selected sub-message.
|
|
// We avoid this circle by looking at how
|
|
// the number is formatted in the "other" sub-message
|
|
// which must always be present and usually contains the number.
|
|
// Message authors should be consistent across sub-messages.
|
|
PluralSelectorContext context = (PluralSelectorContext)ctx;
|
|
int otherIndex = msgFormat.findOtherSubMessage(context.startIndex);
|
|
context.numberArgIndex = msgFormat.findFirstPluralNumberArg(otherIndex, context.argName);
|
|
if(context.numberArgIndex > 0 && msgFormat.cachedFormatters != null) {
|
|
context.formatter = msgFormat.cachedFormatters.get(context.numberArgIndex);
|
|
}
|
|
if(context.formatter == null) {
|
|
context.formatter = msgFormat.getStockNumberFormatter();
|
|
context.forReplaceNumber = true;
|
|
}
|
|
assert context.number.doubleValue() == number; // argument number minus the offset
|
|
context.numberString = context.formatter.format(context.number);
|
|
if(context.formatter instanceof DecimalFormat) {
|
|
IFixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number);
|
|
return rules.select(dec);
|
|
} else {
|
|
return rules.select(number);
|
|
}
|
|
}
|
|
private MessageFormat msgFormat;
|
|
private PluralRules rules;
|
|
private PluralType type;
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
private void format(Object arguments, AppendableWrapper result, FieldPosition fp) {
|
|
if ((arguments == null || arguments instanceof Map)) {
|
|
format(null, (Map<String, Object>)arguments, result, fp);
|
|
} else {
|
|
format((Object[])arguments, null, result, fp);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal routine used by format.
|
|
*
|
|
* @throws IllegalArgumentException if an argument in the
|
|
* <code>arguments</code> map is not of the type
|
|
* expected by the format element(s) that use it.
|
|
*/
|
|
private void format(Object[] arguments, Map<String, Object> argsMap,
|
|
AppendableWrapper dest, FieldPosition fp) {
|
|
if (arguments != null && msgPattern.hasNamedArguments()) {
|
|
throw new IllegalArgumentException(
|
|
"This method is not available in MessageFormat objects " +
|
|
"that use alphanumeric argument names.");
|
|
}
|
|
format(0, null, arguments, argsMap, dest, fp);
|
|
}
|
|
|
|
private void resetPattern() {
|
|
if (msgPattern != null) {
|
|
msgPattern.clear();
|
|
}
|
|
if (cachedFormatters != null) {
|
|
cachedFormatters.clear();
|
|
}
|
|
customFormatArgStarts = null;
|
|
}
|
|
|
|
private static final String[] typeList =
|
|
{ "number", "date", "time", "spellout", "ordinal", "duration" };
|
|
private static final int
|
|
TYPE_NUMBER = 0,
|
|
TYPE_DATE = 1,
|
|
TYPE_TIME = 2,
|
|
TYPE_SPELLOUT = 3,
|
|
TYPE_ORDINAL = 4,
|
|
TYPE_DURATION = 5;
|
|
|
|
private static final String[] modifierList =
|
|
{"", "currency", "percent", "integer"};
|
|
|
|
private static final int
|
|
MODIFIER_EMPTY = 0,
|
|
MODIFIER_CURRENCY = 1,
|
|
MODIFIER_PERCENT = 2,
|
|
MODIFIER_INTEGER = 3;
|
|
|
|
private static final String[] dateModifierList =
|
|
{"", "short", "medium", "long", "full"};
|
|
|
|
private static final int
|
|
DATE_MODIFIER_EMPTY = 0,
|
|
DATE_MODIFIER_SHORT = 1,
|
|
DATE_MODIFIER_MEDIUM = 2,
|
|
DATE_MODIFIER_LONG = 3,
|
|
DATE_MODIFIER_FULL = 4;
|
|
|
|
Format dateTimeFormatForPatternOrSkeleton(String style) {
|
|
// Ignore leading whitespace when looking for "::", the skeleton signal sequence
|
|
int i = PatternProps.skipWhiteSpace(style, 0);
|
|
if (style.regionMatches(i, "::", 0, 2)) { // Skeleton
|
|
return DateFormat.getInstanceForSkeleton(style.substring(i + 2), ulocale);
|
|
} else { // Pattern
|
|
return new SimpleDateFormat(style, ulocale);
|
|
}
|
|
}
|
|
|
|
// Creates an appropriate Format object for the type and style passed.
|
|
// Both arguments cannot be null.
|
|
private Format createAppropriateFormat(String type, String style) {
|
|
Format newFormat = null;
|
|
int subformatType = findKeyword(type, typeList);
|
|
switch (subformatType){
|
|
case TYPE_NUMBER:
|
|
switch (findKeyword(style, modifierList)) {
|
|
case MODIFIER_EMPTY:
|
|
newFormat = NumberFormat.getInstance(ulocale);
|
|
break;
|
|
case MODIFIER_CURRENCY:
|
|
newFormat = NumberFormat.getCurrencyInstance(ulocale);
|
|
break;
|
|
case MODIFIER_PERCENT:
|
|
newFormat = NumberFormat.getPercentInstance(ulocale);
|
|
break;
|
|
case MODIFIER_INTEGER:
|
|
newFormat = NumberFormat.getIntegerInstance(ulocale);
|
|
break;
|
|
default: // pattern or skeleton
|
|
// Ignore leading whitespace when looking for "::", the skeleton signal sequence
|
|
int i = PatternProps.skipWhiteSpace(style, 0);
|
|
if (style.regionMatches(i, "::", 0, 2)) {
|
|
// Skeleton
|
|
newFormat = NumberFormatter.forSkeleton(style.substring(i + 2)).locale(ulocale).toFormat();
|
|
} else {
|
|
// Pattern
|
|
newFormat = new DecimalFormat(style, new DecimalFormatSymbols(ulocale));
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case TYPE_DATE:
|
|
switch (findKeyword(style, dateModifierList)) {
|
|
case DATE_MODIFIER_EMPTY:
|
|
newFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, ulocale);
|
|
break;
|
|
case DATE_MODIFIER_SHORT:
|
|
newFormat = DateFormat.getDateInstance(DateFormat.SHORT, ulocale);
|
|
break;
|
|
case DATE_MODIFIER_MEDIUM:
|
|
newFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, ulocale);
|
|
break;
|
|
case DATE_MODIFIER_LONG:
|
|
newFormat = DateFormat.getDateInstance(DateFormat.LONG, ulocale);
|
|
break;
|
|
case DATE_MODIFIER_FULL:
|
|
newFormat = DateFormat.getDateInstance(DateFormat.FULL, ulocale);
|
|
break;
|
|
default: // pattern or skeleton
|
|
newFormat = dateTimeFormatForPatternOrSkeleton(style);
|
|
break;
|
|
}
|
|
break;
|
|
case TYPE_TIME:
|
|
switch (findKeyword(style, dateModifierList)) {
|
|
case DATE_MODIFIER_EMPTY:
|
|
newFormat = DateFormat.getTimeInstance(DateFormat.DEFAULT, ulocale);
|
|
break;
|
|
case DATE_MODIFIER_SHORT:
|
|
newFormat = DateFormat.getTimeInstance(DateFormat.SHORT, ulocale);
|
|
break;
|
|
case DATE_MODIFIER_MEDIUM:
|
|
newFormat = DateFormat.getTimeInstance(DateFormat.DEFAULT, ulocale);
|
|
break;
|
|
case DATE_MODIFIER_LONG:
|
|
newFormat = DateFormat.getTimeInstance(DateFormat.LONG, ulocale);
|
|
break;
|
|
case DATE_MODIFIER_FULL:
|
|
newFormat = DateFormat.getTimeInstance(DateFormat.FULL, ulocale);
|
|
break;
|
|
default: // pattern or skeleton
|
|
newFormat = dateTimeFormatForPatternOrSkeleton(style);
|
|
break;
|
|
}
|
|
break;
|
|
case TYPE_SPELLOUT:
|
|
{
|
|
RuleBasedNumberFormat rbnf = new RuleBasedNumberFormat(ulocale,
|
|
RuleBasedNumberFormat.SPELLOUT);
|
|
String ruleset = style.trim();
|
|
if (ruleset.length() != 0) {
|
|
try {
|
|
rbnf.setDefaultRuleSet(ruleset);
|
|
}
|
|
catch (Exception e) {
|
|
// warn invalid ruleset
|
|
}
|
|
}
|
|
newFormat = rbnf;
|
|
}
|
|
break;
|
|
case TYPE_ORDINAL:
|
|
{
|
|
RuleBasedNumberFormat rbnf = new RuleBasedNumberFormat(ulocale,
|
|
RuleBasedNumberFormat.ORDINAL);
|
|
String ruleset = style.trim();
|
|
if (ruleset.length() != 0) {
|
|
try {
|
|
rbnf.setDefaultRuleSet(ruleset);
|
|
}
|
|
catch (Exception e) {
|
|
// warn invalid ruleset
|
|
}
|
|
}
|
|
newFormat = rbnf;
|
|
}
|
|
break;
|
|
case TYPE_DURATION:
|
|
{
|
|
RuleBasedNumberFormat rbnf = new RuleBasedNumberFormat(ulocale,
|
|
RuleBasedNumberFormat.DURATION);
|
|
String ruleset = style.trim();
|
|
if (ruleset.length() != 0) {
|
|
try {
|
|
rbnf.setDefaultRuleSet(ruleset);
|
|
}
|
|
catch (Exception e) {
|
|
// warn invalid ruleset
|
|
}
|
|
}
|
|
newFormat = rbnf;
|
|
}
|
|
break;
|
|
default:
|
|
throw new IllegalArgumentException("Unknown format type \"" + type + "\"");
|
|
}
|
|
return newFormat;
|
|
}
|
|
|
|
private static final Locale rootLocale = new Locale(""); // Locale.ROOT only @since 1.6
|
|
|
|
private static final int findKeyword(String s, String[] list) {
|
|
s = PatternProps.trimWhiteSpace(s).toLowerCase(rootLocale);
|
|
for (int i = 0; i < list.length; ++i) {
|
|
if (s.equals(list[i]))
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Custom serialization, new in ICU 4.8.
|
|
* We do not want to use default serialization because we only have a small
|
|
* amount of persistent state which is better expressed explicitly
|
|
* rather than via writing field objects.
|
|
* @param out The output stream.
|
|
* @serialData Writes the locale as a BCP 47 language tag string,
|
|
* the MessagePattern.ApostropheMode as an object,
|
|
* and the pattern string (null if none was applied).
|
|
* Followed by an int with the number of (int formatIndex, Object formatter) pairs,
|
|
* and that many such pairs, corresponding to previous setFormat() calls for custom formats.
|
|
* Followed by an int with the number of (int, Object) pairs,
|
|
* and that many such pairs, for future (post-ICU 4.8) extension of the serialization format.
|
|
*/
|
|
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
|
|
out.defaultWriteObject();
|
|
// ICU 4.8 custom serialization.
|
|
// locale as a BCP 47 language tag
|
|
out.writeObject(ulocale.toLanguageTag());
|
|
// ApostropheMode
|
|
if (msgPattern == null) {
|
|
msgPattern = new MessagePattern();
|
|
}
|
|
out.writeObject(msgPattern.getApostropheMode());
|
|
// message pattern string
|
|
out.writeObject(msgPattern.getPatternString());
|
|
// custom formatters
|
|
if (customFormatArgStarts == null || customFormatArgStarts.isEmpty()) {
|
|
out.writeInt(0);
|
|
} else {
|
|
out.writeInt(customFormatArgStarts.size());
|
|
int formatIndex = 0;
|
|
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
|
|
if (customFormatArgStarts.contains(partIndex)) {
|
|
out.writeInt(formatIndex);
|
|
out.writeObject(cachedFormatters.get(partIndex));
|
|
}
|
|
++formatIndex;
|
|
}
|
|
}
|
|
// number of future (int, Object) pairs
|
|
out.writeInt(0);
|
|
}
|
|
|
|
/**
|
|
* Custom deserialization, new in ICU 4.8. See comments on writeObject().
|
|
* @throws InvalidObjectException if the objects read from the stream is invalid.
|
|
*/
|
|
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
|
|
in.defaultReadObject();
|
|
// ICU 4.8 custom deserialization.
|
|
String languageTag = (String)in.readObject();
|
|
ulocale = ULocale.forLanguageTag(languageTag);
|
|
MessagePattern.ApostropheMode aposMode = (MessagePattern.ApostropheMode)in.readObject();
|
|
if (msgPattern == null || aposMode != msgPattern.getApostropheMode()) {
|
|
msgPattern = new MessagePattern(aposMode);
|
|
}
|
|
String msg = (String)in.readObject();
|
|
if (msg != null) {
|
|
applyPattern(msg);
|
|
}
|
|
// custom formatters
|
|
for (int numFormatters = in.readInt(); numFormatters > 0; --numFormatters) {
|
|
int formatIndex = in.readInt();
|
|
Format formatter = (Format)in.readObject();
|
|
setFormat(formatIndex, formatter);
|
|
}
|
|
// skip future (int, Object) pairs
|
|
for (int numPairs = in.readInt(); numPairs > 0; --numPairs) {
|
|
in.readInt();
|
|
in.readObject();
|
|
}
|
|
}
|
|
|
|
private void cacheExplicitFormats() {
|
|
if (cachedFormatters != null) {
|
|
cachedFormatters.clear();
|
|
}
|
|
customFormatArgStarts = null;
|
|
// The last two "parts" can at most be ARG_LIMIT and MSG_LIMIT
|
|
// which we need not examine.
|
|
int limit = msgPattern.countParts() - 2;
|
|
// This loop starts at part index 1 because we do need to examine
|
|
// ARG_START parts. (But we can ignore the MSG_START.)
|
|
for(int i=1; i < limit; ++i) {
|
|
Part part = msgPattern.getPart(i);
|
|
if(part.getType()!=Part.Type.ARG_START) {
|
|
continue;
|
|
}
|
|
ArgType argType=part.getArgType();
|
|
if(argType != ArgType.SIMPLE) {
|
|
continue;
|
|
}
|
|
int index = i;
|
|
i += 2;
|
|
String explicitType = msgPattern.getSubstring(msgPattern.getPart(i++));
|
|
String style = "";
|
|
if ((part = msgPattern.getPart(i)).getType() == MessagePattern.Part.Type.ARG_STYLE) {
|
|
style = msgPattern.getSubstring(part);
|
|
++i;
|
|
}
|
|
Format formatter = createAppropriateFormat(explicitType, style);
|
|
setArgStartFormat(index, formatter);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a formatter for a MessagePattern ARG_START part index.
|
|
*/
|
|
private void setArgStartFormat(int argStart, Format formatter) {
|
|
if (cachedFormatters == null) {
|
|
cachedFormatters = new HashMap<>();
|
|
}
|
|
cachedFormatters.put(argStart, formatter);
|
|
}
|
|
|
|
/**
|
|
* Sets a custom formatter for a MessagePattern ARG_START part index.
|
|
* "Custom" formatters are provided by the user via setFormat() or similar APIs.
|
|
*/
|
|
private void setCustomArgStartFormat(int argStart, Format formatter) {
|
|
setArgStartFormat(argStart, formatter);
|
|
if (customFormatArgStarts == null) {
|
|
customFormatArgStarts = new HashSet<>();
|
|
}
|
|
customFormatArgStarts.add(argStart);
|
|
}
|
|
|
|
private static final char SINGLE_QUOTE = '\'';
|
|
private static final char CURLY_BRACE_LEFT = '{';
|
|
private static final char CURLY_BRACE_RIGHT = '}';
|
|
|
|
private static final int STATE_INITIAL = 0;
|
|
private static final int STATE_SINGLE_QUOTE = 1;
|
|
private static final int STATE_IN_QUOTE = 2;
|
|
private static final int STATE_MSG_ELEMENT = 3;
|
|
|
|
/**
|
|
* <strong>[icu]</strong> Converts an 'apostrophe-friendly' pattern into a standard
|
|
* pattern.
|
|
* <em>This is obsolete for ICU 4.8 and higher MessageFormat pattern strings.</em>
|
|
* It can still be useful together with {@link java.text.MessageFormat}.
|
|
*
|
|
* <p>See the class description for more about apostrophes and quoting,
|
|
* and differences between ICU and {@link java.text.MessageFormat}.
|
|
*
|
|
* <p>{@link java.text.MessageFormat} and ICU 4.6 and earlier MessageFormat
|
|
* treat all ASCII apostrophes as
|
|
* quotes, which is problematic in some languages, e.g.
|
|
* French, where apostrophe is commonly used. This utility
|
|
* assumes that only an unpaired apostrophe immediately before
|
|
* a brace is a true quote. Other unpaired apostrophes are paired,
|
|
* and the resulting standard pattern string is returned.
|
|
*
|
|
* <p><b>Note</b>: It is not guaranteed that the returned pattern
|
|
* is indeed a valid pattern. The only effect is to convert
|
|
* between patterns having different quoting semantics.
|
|
*
|
|
* <p><b>Note</b>: This method only works on top-level messageText,
|
|
* not messageText nested inside a complexArg.
|
|
*
|
|
* @param pattern the 'apostrophe-friendly' pattern to convert
|
|
* @return the standard equivalent of the original pattern
|
|
*/
|
|
public static String autoQuoteApostrophe(String pattern) {
|
|
StringBuilder buf = new StringBuilder(pattern.length() * 2);
|
|
int state = STATE_INITIAL;
|
|
int braceCount = 0;
|
|
for (int i = 0, j = pattern.length(); i < j; ++i) {
|
|
char c = pattern.charAt(i);
|
|
switch (state) {
|
|
case STATE_INITIAL:
|
|
switch (c) {
|
|
case SINGLE_QUOTE:
|
|
state = STATE_SINGLE_QUOTE;
|
|
break;
|
|
case CURLY_BRACE_LEFT:
|
|
state = STATE_MSG_ELEMENT;
|
|
++braceCount;
|
|
break;
|
|
}
|
|
break;
|
|
case STATE_SINGLE_QUOTE:
|
|
switch (c) {
|
|
case SINGLE_QUOTE:
|
|
state = STATE_INITIAL;
|
|
break;
|
|
case CURLY_BRACE_LEFT:
|
|
case CURLY_BRACE_RIGHT:
|
|
state = STATE_IN_QUOTE;
|
|
break;
|
|
default:
|
|
buf.append(SINGLE_QUOTE);
|
|
state = STATE_INITIAL;
|
|
break;
|
|
}
|
|
break;
|
|
case STATE_IN_QUOTE:
|
|
switch (c) {
|
|
case SINGLE_QUOTE:
|
|
state = STATE_INITIAL;
|
|
break;
|
|
}
|
|
break;
|
|
case STATE_MSG_ELEMENT:
|
|
switch (c) {
|
|
case CURLY_BRACE_LEFT:
|
|
++braceCount;
|
|
break;
|
|
case CURLY_BRACE_RIGHT:
|
|
if (--braceCount == 0) {
|
|
state = STATE_INITIAL;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
///CLOVER:OFF
|
|
default: // Never happens.
|
|
break;
|
|
///CLOVER:ON
|
|
}
|
|
buf.append(c);
|
|
}
|
|
// End of scan
|
|
if (state == STATE_SINGLE_QUOTE || state == STATE_IN_QUOTE) {
|
|
buf.append(SINGLE_QUOTE);
|
|
}
|
|
return new String(buf);
|
|
}
|
|
|
|
/**
|
|
* Convenience wrapper for Appendable, tracks the result string length.
|
|
* Also, Appendable throws IOException, and we turn that into a RuntimeException
|
|
* so that we need no throws clauses.
|
|
*/
|
|
private static final class AppendableWrapper {
|
|
public AppendableWrapper(StringBuilder sb) {
|
|
app = sb;
|
|
length = sb.length();
|
|
attributes = null;
|
|
}
|
|
|
|
public AppendableWrapper(StringBuffer sb) {
|
|
app = sb;
|
|
length = sb.length();
|
|
attributes = null;
|
|
}
|
|
|
|
public void useAttributes() {
|
|
attributes = new ArrayList<>();
|
|
}
|
|
|
|
public void append(CharSequence s) {
|
|
try {
|
|
app.append(s);
|
|
length += s.length();
|
|
} catch(IOException e) {
|
|
throw new ICUUncheckedIOException(e);
|
|
}
|
|
}
|
|
|
|
public void append(CharSequence s, int start, int limit) {
|
|
try {
|
|
app.append(s, start, limit);
|
|
length += limit - start;
|
|
} catch(IOException e) {
|
|
throw new ICUUncheckedIOException(e);
|
|
}
|
|
}
|
|
|
|
public void append(CharacterIterator iterator) {
|
|
length += append(app, iterator);
|
|
}
|
|
|
|
public static int append(Appendable result, CharacterIterator iterator) {
|
|
try {
|
|
int start = iterator.getBeginIndex();
|
|
int limit = iterator.getEndIndex();
|
|
int length = limit - start;
|
|
if (start < limit) {
|
|
result.append(iterator.first());
|
|
while (++start < limit) {
|
|
result.append(iterator.next());
|
|
}
|
|
}
|
|
return length;
|
|
} catch(IOException e) {
|
|
throw new ICUUncheckedIOException(e);
|
|
}
|
|
}
|
|
|
|
public void formatAndAppend(Format formatter, Object arg) {
|
|
if (attributes == null) {
|
|
append(formatter.format(arg));
|
|
} else {
|
|
AttributedCharacterIterator formattedArg = formatter.formatToCharacterIterator(arg);
|
|
int prevLength = length;
|
|
append(formattedArg);
|
|
// Copy all of the attributes from formattedArg to our attributes list.
|
|
formattedArg.first();
|
|
int start = formattedArg.getIndex(); // Should be 0 but might not be.
|
|
int limit = formattedArg.getEndIndex(); // == start + length - prevLength
|
|
int offset = prevLength - start; // Adjust attribute indexes for the result string.
|
|
while (start < limit) {
|
|
Map<Attribute, Object> map = formattedArg.getAttributes();
|
|
int runLimit = formattedArg.getRunLimit();
|
|
if (map.size() != 0) {
|
|
for (Map.Entry<Attribute, Object> entry : map.entrySet()) {
|
|
attributes.add(
|
|
new AttributeAndPosition(
|
|
entry.getKey(), entry.getValue(),
|
|
offset + start, offset + runLimit));
|
|
}
|
|
}
|
|
start = runLimit;
|
|
formattedArg.setIndex(start);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void formatAndAppend(Format formatter, Object arg, String argString) {
|
|
if (attributes == null && argString != null) {
|
|
append(argString);
|
|
} else {
|
|
formatAndAppend(formatter, arg);
|
|
}
|
|
}
|
|
|
|
private Appendable app;
|
|
private int length;
|
|
private List<AttributeAndPosition> attributes;
|
|
}
|
|
|
|
private static final class AttributeAndPosition {
|
|
/**
|
|
* Defaults the field to Field.ARGUMENT.
|
|
*/
|
|
public AttributeAndPosition(Object fieldValue, int startIndex, int limitIndex) {
|
|
init(Field.ARGUMENT, fieldValue, startIndex, limitIndex);
|
|
}
|
|
|
|
public AttributeAndPosition(Attribute field, Object fieldValue, int startIndex, int limitIndex) {
|
|
init(field, fieldValue, startIndex, limitIndex);
|
|
}
|
|
|
|
public void init(Attribute field, Object fieldValue, int startIndex, int limitIndex) {
|
|
key = field;
|
|
value = fieldValue;
|
|
start = startIndex;
|
|
limit = limitIndex;
|
|
}
|
|
|
|
private Attribute key;
|
|
private Object value;
|
|
private int start;
|
|
private int limit;
|
|
}
|
|
}
|