diff options
Diffstat (limited to 'src/com/ibm/icu/simple/MessageFormat.java')
-rw-r--r-- | src/com/ibm/icu/simple/MessageFormat.java | 2516 |
1 files changed, 2516 insertions, 0 deletions
diff --git a/src/com/ibm/icu/simple/MessageFormat.java b/src/com/ibm/icu/simple/MessageFormat.java new file mode 100644 index 0000000..0a883dd --- /dev/null +++ b/src/com/ibm/icu/simple/MessageFormat.java @@ -0,0 +1,2516 @@ +/* +********************************************************************** +* Copyright (c) 2004-2014, International Business Machines +* Corporation and others. All Rights Reserved. +********************************************************************** +* Author: Alan Liu +* Created: April 6, 2004 +* Since: ICU 3.0 +********************************************************************** +*/ +package com.ibm.icu.simple; + +import java.io.IOException; +import java.io.InvalidObjectException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedCharacterIterator.Attribute; +import java.text.AttributedString; +import java.text.CharacterIterator; +import java.text.ChoiceFormat; +import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.FieldPosition; +import java.text.Format; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import com.ibm.icu.impl.PatternProps; +import com.ibm.icu.simple.PluralRules.PluralType; +import com.ibm.icu.text.MessagePattern; +import com.ibm.icu.text.MessagePattern.ArgType; +import com.ibm.icu.text.MessagePattern.Part; +import com.ibm.icu.text.SelectFormat; +import com.ibm.icu.util.ICUUncheckedIOException; + +/** + * {@icuenhanced java.text.MessageFormat}.{@icu _usage_} + * + * <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). + * + * <h4><a name="patterns">Patterns and Their Interpretation</a></h4> + * + * <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 apostrohpe 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. + * + * <p><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()) + * <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()) + * <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> + * <p> + * + * <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 + * @stable ICU 3.0 + */ +public class MessageFormat extends Format { + + // Incremented by 1 for ICU 4.8's new format. + static final long serialVersionUID = 7136212545847378652L; + + /** + * Formats a message pattern string with a variable number of name/value pair arguments. + * Creates an ICU MessageFormat for the locale and pattern, + * and formats with the arguments. + * + * @param locale Locale for number formatting and plural selection etc. + * @param msg an ICU-MessageFormat-syntax string + * @param nameValuePairs (argument name, argument value) pairs + */ + public static final String formatNamedArgs(Locale locale, String msg, Object... nameValuePairs) { + StringBuilder result = new StringBuilder(msg.length()); + new MessageFormat(msg, locale).format(0, null, null, null, nameValuePairs, + new AppendableWrapper(result), null); + return result.toString(); + } + + /** + * 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 + * @stable ICU 3.0 + */ + public MessageFormat(String pattern) { + locale_ = Locale.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) { + locale_ = locale; + applyPattern(pattern); + } + + /** + * 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 locale_; + } + + /** + * 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 + * @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 + * <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 + * @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 + * <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 + * @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 + * <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. + * @stable ICU 3.8 + */ + 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 + * @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 <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 + * @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. + * <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 + * @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. + * <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 + * @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 + * <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 + * @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<Format> list = new ArrayList<Format>(); + 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 + * @stable ICU 3.0 + */ + public Format[] getFormats() { + ArrayList<Format> list = new ArrayList<Format>(); + 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<String> getArgumentNames() { + Set<String> result = new HashSet<String>(); + 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 <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. + * <p> + * <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 + * @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 <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 corresopnding 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 + * @stable ICU 3.8 + */ + 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 + * @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. + * <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[]) + * @stable ICU 3.8 + */ + public static String format(String pattern, Map<String, Object> 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 <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 + * @stable ICU 3.0 + */ + 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. + * @stable ICU 3.8 + */ + 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 + * @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<String, Object> parseToMap(String source, ParsePosition pos) { + Map<String, Object> result = new HashMap<String, Object>(); + 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 + * @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<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 = 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. + * + * <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) + * @stable ICU 3.8 + */ + public Map<String, Object> parseToMap(String source) throws ParseException { + ParsePosition pos = new ParsePosition(0); + Map<String, Object> result = new HashMap<String, Object>(); + 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. + * @stable ICU 3.0 + */ + 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 boolean equals(Object obj) { + if (this == obj) // quick check + return true; + if (obj == null || getClass() != obj.getClass()) + return false; + MessageFormat other = (MessageFormat) obj; + return Utility.objectEquals(ulocale, other.ulocale) + && Utility.objectEquals(msgPattern, other.msgPattern) + && Utility.objectEquals(cachedFormatters, other.cachedFormatters) + && Utility.objectEquals(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 + * <code>AttributedCharacterIterator</code> returned + * from <code>MessageFormat.formatToCharacterIterator</code>. + * + * @stable ICU 3.8 + */ + 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 + * + * @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 + */ + 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. + * + * @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 Locale locale_; + + /** + * 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, locale_);//fix + } + return stockDateFormatter; + } + private NumberFormat getStockNumberFormatter() { + if (stockNumberFormatter == null) { + stockNumberFormatter = NumberFormat.getInstance(locale_); + } + 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, Object[] nameValuePairs, + 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<args.length) { + arg=args[argNumber]; + } else { + arg=null; + noArg=true; + } + } else if(nameValuePairs!=null) { + argId = argName; + for(int nvIndex=0;; nvIndex+=2) { + if(nvIndex<nameValuePairs.length) { + if(argName.equals(nameValuePairs[nvIndex].toString())) { + arg=nameValuePairs[nvIndex+1]; + break; + } + } else { + arg=null; + noArg=true; + break; + } + } + } else { + argId = argName; + if(argsMap!=null && argsMap.containsKey(argName)) { + arg=argsMap.get(argName); + } else { + arg=null; + noArg=true; + } + } + ++i; + int prevDestLength=dest.length; + Format formatter = null; + if (noArg) { + dest.append("{"+argName+"}"); + } 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. + { + 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, nameValuePairs, 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, nameValuePairs, dest); + } else if(argType==ArgType.SELECT) { + int subMsgStart=SelectFormat.findSubMessage(msgPattern, i, arg.toString()); + formatComplexSubMessage(subMsgStart, null, args, argsMap, nameValuePairs, 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, Object[] nameValuePairs, + AppendableWrapper dest) { + if (!msgPattern.jdkAposMode()) { + format(msgStart, pluralNumber, args, argsMap, nameValuePairs, dest, null); + return; + } + // JDK compatibility mode: (see JDK MessageFormat.format() API docs) + throw new UnsupportedOperationException("JDK apostrophe mode not supported"); + /* + // - 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; + } + public String select(Object ctx, double number) { + if(rules == null) { + rules = PluralRules.forLocale(msgFormat.locale_, 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); + /* TODO: Try to get FixedDecimal from formatted string. + if(context.formatter instanceof DecimalFormat) { + FixedDecimal 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, null, 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(locale_); + break; + case MODIFIER_CURRENCY: + newFormat = NumberFormat.getCurrencyInstance(locale_); + break; + case MODIFIER_PERCENT: + newFormat = NumberFormat.getPercentInstance(locale_); + break; + case MODIFIER_INTEGER: + newFormat = NumberFormat.getIntegerInstance(locale_); + break; + default: // pattern + newFormat = new DecimalFormat(style, + new DecimalFormatSymbols(locale_)); + break; + } + break; + case TYPE_DATE: + switch (findKeyword(style, dateModifierList)) { + case DATE_MODIFIER_EMPTY: + newFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, locale_); + break; + case DATE_MODIFIER_SHORT: + newFormat = DateFormat.getDateInstance(DateFormat.SHORT, locale_); + break; + case DATE_MODIFIER_MEDIUM: + newFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, locale_); + break; + case DATE_MODIFIER_LONG: + newFormat = DateFormat.getDateInstance(DateFormat.LONG, locale_); + break; + case DATE_MODIFIER_FULL: + newFormat = DateFormat.getDateInstance(DateFormat.FULL, locale_); + break; + default: + newFormat = new SimpleDateFormat(style, locale_); + break; + } + break; + case TYPE_TIME: + switch (findKeyword(style, dateModifierList)) { + case DATE_MODIFIER_EMPTY: + newFormat = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale_); + break; + case DATE_MODIFIER_SHORT: + newFormat = DateFormat.getTimeInstance(DateFormat.SHORT, locale_); + break; + case DATE_MODIFIER_MEDIUM: + newFormat = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale_); + break; + case DATE_MODIFIER_LONG: + newFormat = DateFormat.getTimeInstance(DateFormat.LONG, locale_); + break; + case DATE_MODIFIER_FULL: + newFormat = DateFormat.getTimeInstance(DateFormat.FULL, locale_); + break; + default: + newFormat = new SimpleDateFormat(style, locale_); + break; + } + break; + /* There is no java.text.RuleBasedNumberFormat -- + 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; + } + + 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<Integer, Format>(); + } + 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<Integer>(); + } + 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. + * <em>This is obsolete for ICU 4.8 and higher MessageFormat pattern strings.</em> + * It can still be useful together with the JDK MessageFormat. + * + * <p>See the class description for more about apostrophes and quoting, + * and differences between ICU and the JDK. + * + * <p>The JDK 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 + * @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<AttributeAndPosition>(); + } + + 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; + } +} |