com.ibm.icu.text.MessageFormat Maven / Gradle / Ivy
Show all versions of icu4j Show documentation
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
/*
**********************************************************************
* 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 com.ibm.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 com.ibm.icu.impl.PatternProps;
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.text.MessagePattern.ArgType;
import com.ibm.icu.text.MessagePattern.Part;
import com.ibm.icu.text.PluralRules.IFixedDecimal;
import com.ibm.icu.text.PluralRules.PluralType;
import com.ibm.icu.util.ICUUncheckedIOException;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.ULocale.Category;
/**
* {@icuenhanced java.text.MessageFormat}.{@icu _usage_}
*
* 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.
*
*
A MessageFormat is constructed from a pattern string
* with arguments in {curly braces} which will be replaced by formatted values.
*
*
MessageFormat
differs from the other Format
* classes in that you create a MessageFormat
object with one
* of its constructors (not with a getInstance
style factory
* method). Factory methods aren't necessary because MessageFormat
* 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.
*
*
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()}).
*
*
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.
*
*
An argument might specify a "simple" type for which the specified
* Format object is created, cached and used.
*
*
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.
*
*
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.
*
*
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).
*
*
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).
*
*
Patterns and Their Interpretation
*
* MessageFormat
uses patterns of the following form:
*
* 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 | "::" argSkeletonText
*
*
*
* - 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 apostrohpe inside or outside of a quoted string represents
* one literal apostrophe.
*
- 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.
*
- See also {@link MessagePattern.ApostropheMode}
*
- In argStyleText, every single ASCII apostrophe begins and ends quoted literal text,
* and unquoted {curly braces} must occur in matched pairs.
*
*
* 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.
*
*
The choice
argument type is deprecated.
* Use plural
arguments for proper plural selection,
* and select
arguments for simple selection among a fixed set of choices.
*
*
The argType
and argStyle
values are used to create
* a Format
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 argStyleText
must
* be a valid pattern string for the Format subclass used.
*
*
*
* argType
* argStyle
* resulting Format object
*
* (none)
* null
*
* number
* (none)
* NumberFormat.getInstance(getLocale())
*
* integer
* NumberFormat.getIntegerInstance(getLocale())
*
* currency
* NumberFormat.getCurrencyInstance(getLocale())
*
* percent
* NumberFormat.getPercentInstance(getLocale())
*
* argStyleText
* new DecimalFormat(argStyleText, new DecimalFormatSymbols(getLocale()))
*
* argSkeletonText
* NumberFormatter.forSkeleton(argSkeletonText).locale(getLocale()).toFormat()
*
* date
* (none)
* DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale())
*
* short
* DateFormat.getDateInstance(DateFormat.SHORT, getLocale())
*
* medium
* DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale())
*
* long
* DateFormat.getDateInstance(DateFormat.LONG, getLocale())
*
* full
* DateFormat.getDateInstance(DateFormat.FULL, getLocale())
*
* argStyleText
* new SimpleDateFormat(argStyleText, getLocale())
*
* time
* (none)
* DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale())
*
* short
* DateFormat.getTimeInstance(DateFormat.SHORT, getLocale())
*
* medium
* DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale())
*
* long
* DateFormat.getTimeInstance(DateFormat.LONG, getLocale())
*
* full
* DateFormat.getTimeInstance(DateFormat.FULL, getLocale())
*
* argStyleText
* new SimpleDateFormat(argStyleText, getLocale())
*
* spellout
* argStyleText (optional)
* new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.SPELLOUT)
*
.setDefaultRuleset(argStyleText);
*
* ordinal
* argStyleText (optional)
* new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.ORDINAL)
*
.setDefaultRuleset(argStyleText);
*
* duration
* argStyleText (optional)
* new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.DURATION)
*
.setDefaultRuleset(argStyleText);
*
*
* Differences from java.text.MessageFormat
*
* The ICU MessageFormat supports both named and numbered arguments,
* while the JDK MessageFormat only supports numbered arguments.
* Named arguments make patterns more readable.
*
*
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}).
* 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}.
*
*
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
* (new ChoiceFormat(argStyleText)
).
* The JDK does not support plural and select arguments at all.
*
*
Usage Information
*
* Here are some examples of usage:
*
*
* 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);
*
* output: At 12:30 PM on Jul 3, 2053, there was a disturbance
* in the Force on planet 7.
*
*
*
* Typically, the message format will come from resources, and the
* arguments will be dynamically set at runtime.
*
* Example 2:
*
*
* 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
* output: The disk "MyDisk" contains 0 file(s).
* output: The disk "MyDisk" contains 1 file(s).
* output: The disk "MyDisk" contains 1,273 file(s).
*
*
*
* For messages that include plural forms, you can use a plural argument:
*
* 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));
*
* output:
* There are no files on disk "MyDisk".
* There are 3 files on "MyDisk".
*
* See {@link PluralFormat} and {@link PluralRules} for details.
*
* Synchronization
*
* 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
* @stable ICU 3.0
*/
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 FORMAT
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
* @stable ICU 3.0
*/
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
* @stable ICU 3.0
*/
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
* @stable ICU 3.2
*/
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 format
and
* {@link #formatToCharacterIterator formatToCharacterIterator} methods.
*
* @param locale the locale to be used when creating or comparing subformats
* @stable ICU 3.0
*/
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 format
and
* {@link #formatToCharacterIterator formatToCharacterIterator} methods.
*
* @param locale the locale to be used when creating or comparing subformats
* @stable ICU 3.2
*/
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
* @stable ICU 3.0
*/
public Locale getLocale() {
return ulocale.toLocale();
}
/**
* {@icu} Returns the locale that's used when creating argument Format objects.
*
* @return the locale used when creating or comparing subformats
* @stable ICU 3.2
*/
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
* class description.
*
* @param pttrn the pattern for this message format
* @throws IllegalArgumentException if the pattern is invalid
* @stable ICU 3.0
*/
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;
}
}
/**
* {@icu} 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
* class description.
*
* 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
* @stable ICU 4.8
*/
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);
}
/**
* {@icu}
* @return this instance's ApostropheMode.
* @stable ICU 4.8
*/
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
* @stable ICU 3.0
*/
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
* format
methods or returned from parse
* methods. The indices of elements in newFormats
* correspond to the argument indices used in the previously set
* pattern string.
* The order of formats in newFormats
thus corresponds to
* the order of elements in the arguments
array passed
* to the format
methods or the result array returned
* by the parse
methods.
*
* 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 newFormats.length
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 newFormats
is null
* @throws IllegalArgumentException if this formatter uses named arguments
* @stable ICU 3.0
*/
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]);
}
}
}
/**
* {@icu} Sets the Format objects to use for the values passed into
* format
methods or returned from parse
* methods. The keys in newFormats
are the argument
* names in the previously set pattern string, and the values
* are the formats.
*
* Only argument names from the pattern string are considered.
* Extra keys in newFormats
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.
*
* 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.
* @stable ICU 3.8
*/
public void setFormatsByArgumentName(Map 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 newFormats
corresponds to
* the order of format elements in the pattern string.
*
* 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 newFormats.length
* formats are replaced.
*
* 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 arguments
array passed to
* the format
methods or the result array returned by
* the parse
methods.
*
* @param newFormats the new formats to use
* @exception NullPointerException if newFormats
is null
* @stable ICU 3.0
*/
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 arguments
array passed
* to the format
methods or the result array returned
* by the parse
methods.
*
* 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
* @stable ICU 3.0
*/
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);
}
}
}
/**
* {@icu} Sets the Format object to use for the format elements within the
* previously set pattern string that use the given argument
* name.
*
* 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.
*
* This API may be used on formats that do not use named arguments.
* In this case argumentName
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
* @stable ICU 3.8
*/
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.
*
* 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
* @stable ICU 3.0
*/
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
* format
methods or returned from parse
* 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 arguments
array passed
* to the format
methods or the result array returned
* by the parse
methods.
*
* 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
* @stable ICU 3.0
*/
public Format[] getFormatsByArgumentIndex() {
if (msgPattern.hasNamedArguments()) {
throw new IllegalArgumentException(
"This method is not available in MessageFormat objects " +
"that use alphanumeric argument names.");
}
ArrayList 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.
*
* 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 arguments
array passed to
* the format
methods or the result array returned by
* the parse
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
* @stable ICU 3.0
*/
public Format[] getFormats() {
ArrayList 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()]);
}
/**
* {@icu} Returns the top-level argument names. For more details, see
* {@link #setFormatByArgumentName(String, Format)}.
* @return a Set of argument names
* @stable ICU 4.8
*/
public Set getArgumentNames() {
Set result = new HashSet<>();
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) {
result.add(getArgName(partIndex + 1));
}
return result;
}
/**
* {@icu} 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.
* @stable ICU 4.8
*/
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 MessageFormat
's
* pattern, with arguments replaced by the formatted objects, to the
* provided StringBuffer
.
*
* The text substituted for the individual format elements is derived from
* the current subformat of the format element and the
* arguments
element at the format element's argument index
* as indicated by the first matching line of the following table. An
* argument is unavailable if arguments
is
* null
or has fewer than argumentIndex+1 elements. When
* an argument is unavailable no substitution is performed.
*
*
*
* argType or Format
* value object
* Formatted Text
*
* any
* unavailable
* "{" + argNameOrNumber + "}"
*
* any
* null
* "null"
*
* custom Format != null
* any
* customFormat.format(argument)
*
* noneArg, or custom Format == null
* instanceof Number
* NumberFormat.getInstance(getLocale()).format(argument)
*
* noneArg, or custom Format == null
* instanceof Date
* DateFormat.getDateTimeInstance(DateFormat.SHORT,
* DateFormat.SHORT, getLocale()).format(argument)
*
* noneArg, or custom Format == null
* instanceof String
* argument
*
* noneArg, or custom Format == null
* any
* argument.toString()
*
* complexArg
* any
* result of recursive formatting of a selected sub-message
*
*
* If pos
is non-null, and refers to
* Field.ARGUMENT
, 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
* arguments
array is not of the type
* expected by the corresponding argument or custom Format object.
* @throws IllegalArgumentException if this format uses named arguments
* @stable ICU 3.0
*/
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 MessageFormat
's
* pattern, with arguments replaced by the formatted objects, to the
* provided StringBuffer
.
*
* The text substituted for the individual format elements is derived from
* the current subformat of the format element and the
* arguments
value corresopnding to the format element's
* argument name.
*
* A numbered pattern argument is matched with a map key that contains that number
* as an ASCII-decimal-digit string (without leading zero).
*
* An argument is unavailable if arguments
is
* null
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
* arguments
array is not of the type
* expected by the corresponding argument or custom Format object.
* @return the passed-in StringBuffer
* @stable ICU 3.8
*/
public final StringBuffer format(Map 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
*
* (new {@link #MessageFormat(String) MessageFormat}(pattern)).{@link
* #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition)
* format}(arguments, new StringBuffer(), null).toString()
*
*
* @throws IllegalArgumentException if the pattern is invalid
* @throws IllegalArgumentException if a value in the
* arguments
array is not of the type
* expected by the corresponding argument or custom Format object.
* @throws IllegalArgumentException if this format uses named arguments
* @stable ICU 3.0
*/
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.
*
* @throws IllegalArgumentException if the pattern is invalid
* @throws IllegalArgumentException if a value in the
* arguments
array is not of the type
* expected by the corresponding argument or custom Format object.
* @see #format(Map, StringBuffer, FieldPosition)
* @see #format(String, Object[])
* @stable ICU 3.8
*/
public static String format(String pattern, Map arguments) {
MessageFormat temp = new MessageFormat(pattern);
return temp.format(arguments);
}
/**
* {@icu} Returns true if this MessageFormat uses named arguments,
* and false otherwise. See class description.
*
* @return true if named arguments are used.
* @stable ICU 3.8
*/
public boolean usesNamedArguments() {
return msgPattern.hasNamedArguments();
}
// Overrides
/**
* Formats a map or array of objects and appends the MessageFormat
's
* pattern, with format elements replaced by the formatted objects, to the
* provided StringBuffer
.
* This is equivalent to either of
*
* {@link #format(java.lang.Object[], java.lang.StringBuffer,
* java.text.FieldPosition) format}((Object[]) arguments, result, pos)
* {@link #format(java.util.Map, java.lang.StringBuffer,
* java.text.FieldPosition) format}((Map) arguments, result, pos)
*
* 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
* arguments
is not of the type
* expected by the format element(s) that use it
* @throws IllegalArgumentException if arguments
is
* an array of Object and this format uses named arguments
* @stable ICU 3.0
*/
@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
* MessageFormat
's pattern, producing an
* AttributedCharacterIterator
.
* You can use the returned AttributedCharacterIterator
* to build the resulting String, as well as to determine information
* about the resulting String.
*
* The text of the returned AttributedCharacterIterator
is
* the same that would be returned by
*
* {@link #format(java.lang.Object[], java.lang.StringBuffer,
* java.text.FieldPosition) format}(arguments, new StringBuffer(), null).toString()
*
*
* In addition, the AttributedCharacterIterator
contains at
* least attributes indicating where text was generated from an
* argument in the arguments
array. The keys of these attributes are of
* type MessageFormat.Field
, their values are
* Integer
objects indicating the index in the arguments
* array of the argument from which the text was generated.
*
* The attributes/value from the underlying Format
* instances that MessageFormat
uses will also be
* placed in the resulting AttributedCharacterIterator
.
* 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 arguments
is null.
* @throws IllegalArgumentException if a value in the
* arguments
array is not of the type
* expected by the corresponding argument or custom Format object.
* @stable ICU 3.8
*/
@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.
*
*
Caveats: The parse may fail in a number of circumstances.
* For example:
*
* - If one of the arguments does not occur in the pattern.
*
- If the format of an argument loses information, such as
* with a choice format where a large number formats to "many".
*
- Does not yet handle recursion (where
* the substituted strings contain {n} references.)
*
- 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"}.
*
- If a single argument is parsed more than once in the string,
* then the later parse wins.
*
* 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
* @stable ICU 3.0
*/
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;
}
/**
* {@icu} 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.
* @stable ICU 3.8
*/
public Map parseToMap(String source, ParsePosition pos) {
Map 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.
*
* See the {@link #parse(String, ParsePosition)} method for more information
* on message parsing.
*
* @param source A String
whose beginning should be parsed.
* @return An Object
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
* @stable ICU 3.0
*/
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 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 = Integer.valueOf(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;
}
}
/**
* {@icu} 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.
*
* See the {@link #parse(String, ParsePosition)} method for more information on
* message parsing.
*
* @param source A String
whose beginning should be parsed.
* @return A Map
parsed from the string.
* @throws ParseException if the beginning of the specified string cannot
* be parsed.
* @see #parseToMap(String, ParsePosition)
* @stable ICU 3.8
*/
public Map parseToMap(String source) throws ParseException {
ParsePosition pos = new ParsePosition(0);
Map 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.
*
* The method attempts to parse text starting at the index given by
* pos
.
* If parsing succeeds, then the index of pos
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 pos
can be used to
* indicate the starting point for the next call to this method.
* If an error occurs, then the index of pos
is not
* changed, the error index of pos
is set to the index of
* the character where the error occurred, and null is returned.
*
* See the {@link #parse(String, ParsePosition)} method for more information
* on message parsing.
*
* @param source A String
, part of which should be parsed.
* @param pos A ParsePosition
object with index and error
* index information as described above.
* @return An Object
parsed from the string, either an
* array of Object, or a Map, depending on whether named
* arguments are used. This can be queried using usesNamedArguments
.
* In case of error, returns null.
* @throws NullPointerException if pos
is null.
* @stable ICU 3.0
*/
@Override
public Object parseObject(String source, ParsePosition pos) {
if (!msgPattern.hasNamedArguments()) {
return parse(source, pos);
} else {
return parseToMap(source, pos);
}
}
/**
* {@inheritDoc}
* @stable ICU 3.0
*/
@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> it = cachedFormatters.entrySet().iterator();
while (it.hasNext()){
Map.Entry 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}
* @stable ICU 3.0
*/
@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}
* @stable ICU 3.0
*/
@Override
public int hashCode() {
return msgPattern.getPatternString().hashCode(); // enough for reasonable distribution
}
/**
* Defines constants that are used as attribute keys in the
* AttributedCharacterIterator
returned
* from MessageFormat.formatToCharacterIterator
.
*
* @stable ICU 3.8
*/
public static class Field extends Format.Field {
private static final long serialVersionUID = 7510380454602616157L;
/**
* Create a Field
with the specified name.
*
* @param name The name of the attribute
*
* @stable ICU 3.8
*/
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.
*
* @stable ICU 3.8
*/
@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 formatToCharacterIterator
.
* The value associated with the key will be an Integer
* indicating the index in the arguments
array of the
* argument from which the text was generated.
*
* @stable ICU 3.8
*/
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 cachedFormatters;
/**
* Set of ARG_START part indexes where custom, user-provided Format objects
* have been set via setFormat() or similar API.
*/
private transient Set 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.
*
* 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 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 = Integer.valueOf(argNumber);
}
if(0<=argNumber && argNumber= 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 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= 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(++partIndex0 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)arguments, result, fp);
} else {
format((Object[])arguments, null, result, fp);
}
}
/**
* Internal routine used by format.
*
* @throws IllegalArgumentException if an argument in the
* arguments
map is not of the type
* expected by the format element(s) that use it.
*/
private void format(Object[] arguments, Map 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;
// 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 = 0;
for (; PatternProps.isWhiteSpace(style.charAt(i)); i++);
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:
newFormat = new SimpleDateFormat(style, ulocale);
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:
newFormat = new SimpleDateFormat(style, ulocale);
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;
/**
* {@icu} Converts an 'apostrophe-friendly' pattern into a standard
* pattern.
* This is obsolete for ICU 4.8 and higher MessageFormat pattern strings.
* It can still be useful together with {@link java.text.MessageFormat}.
*
* See the class description for more about apostrophes and quoting,
* and differences between ICU and {@link java.text.MessageFormat}.
*
*
{@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.
*
*
Note: 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.
*
*
Note: 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
* @stable ICU 3.4
*/
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 map = formattedArg.getAttributes();
int runLimit = formattedArg.getRunLimit();
if (map.size() != 0) {
for (Map.Entry 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 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;
}
}