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