All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.icu.text.MessageFormat Maven / Gradle / Ivy

Go to download

International Component for Unicode for Java (ICU4J) is a mature, widely used Java library providing Unicode and Globalization support

The newest version!
// © 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 com.ibm.icu.text;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.text.AttributedCharacterIterator;
import java.text.AttributedCharacterIterator.Attribute;
import java.text.AttributedString;
import java.text.CharacterIterator;
import java.text.ChoiceFormat;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParseException;
import java.text.ParsePosition;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import com.ibm.icu.impl.PatternProps;
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.text.MessagePattern.ArgType;
import com.ibm.icu.text.MessagePattern.Part;
import com.ibm.icu.text.PluralRules.IFixedDecimal;
import com.ibm.icu.text.PluralRules.PluralType;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.ICUUncheckedIOException;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.ULocale.Category;

/**
 * {@icuenhanced java.text.MessageFormat}.{@icu _usage_}
 *
 * 

MessageFormat prepares strings for display to users, * with optional arguments (variables/placeholders). * The arguments can occur in any order, which is necessary for translation * into languages with different grammars. * *

A MessageFormat is constructed from a pattern string * with arguments in {curly braces} which will be replaced by formatted values. * *

MessageFormat differs from the other Format * classes in that you create a MessageFormat object with one * of its constructors (not with a getInstance style factory * method). Factory methods aren't necessary because MessageFormat * itself doesn't implement locale-specific behavior. Any locale-specific * behavior is defined by the pattern that you provide and the * subformats used for inserted arguments. * *

Arguments can be named (using identifiers) or numbered (using small ASCII-digit integers). * Some of the API methods work only with argument numbers and throw an exception * if the pattern has named arguments (see {@link #usesNamedArguments()}). * *

An argument might not specify any format type. In this case, * a Number value is formatted with a default (for the locale) NumberFormat, * a Date value is formatted with a default (for the locale) DateFormat, * and for any other value its toString() value is used. * *

An argument might specify a "simple" type for which the specified * Format object is created, cached and used. * *

An argument might have a "complex" type with nested MessageFormat sub-patterns. * During formatting, one of these sub-messages is selected according to the argument value * and recursively formatted. * *

After construction, a custom Format object can be set for * a top-level argument, overriding the default formatting and parsing behavior * for that argument. * However, custom formatting can be achieved more simply by writing * a typeless argument in the pattern string * and supplying it with a preformatted string value. * *

When formatting, MessageFormat takes a collection of argument values * and writes an output string. * The argument values may be passed as an array * (when the pattern contains only numbered arguments) * or as a Map (which works for both named and numbered arguments). * *

Each argument is matched with one of the input values by array index or map key * and formatted according to its pattern specification * (or using a custom Format object if one was set). * A numbered pattern argument is matched with a map key that contains that number * as an ASCII-decimal-digit string (without leading zero). * *

Patterns and Their Interpretation

* * MessageFormat uses patterns of the following form: *
 * message = messageText (argument messageText)*
 * argument = noneArg | simpleArg | complexArg
 * complexArg = choiceArg | pluralArg | selectArg | selectordinalArg
 *
 * noneArg = '{' argNameOrNumber '}'
 * simpleArg = '{' argNameOrNumber ',' argType [',' argStyle] '}'
 * choiceArg = '{' argNameOrNumber ',' "choice" ',' choiceStyle '}'
 * pluralArg = '{' argNameOrNumber ',' "plural" ',' pluralStyle '}'
 * selectArg = '{' argNameOrNumber ',' "select" ',' selectStyle '}'
 * selectordinalArg = '{' argNameOrNumber ',' "selectordinal" ',' pluralStyle '}'
 *
 * choiceStyle: see {@link ChoiceFormat}
 * pluralStyle: see {@link PluralFormat}
 * selectStyle: see {@link SelectFormat}
 *
 * argNameOrNumber = argName | argNumber
 * argName = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+
 * argNumber = '0' | ('1'..'9' ('0'..'9')*)
 *
 * argType = "number" | "date" | "time" | "spellout" | "ordinal" | "duration"
 * argStyle = "short" | "medium" | "long" | "full" | "integer" | "currency" | "percent" | argStyleText | "::" argSkeletonText
 * 
* *
    *
  • messageText can contain quoted literal strings including syntax characters. * A quoted literal string begins with an ASCII apostrophe and a syntax character * (usually a {curly brace}) and continues until the next single apostrophe. * A double ASCII apostrophe inside or outside of a quoted string represents * one literal apostrophe. *
  • Quotable syntax characters are the {curly braces} in all messageText parts, * plus the '#' sign in a messageText immediately inside a pluralStyle, * and the '|' symbol in a messageText immediately inside a choiceStyle. *
  • See also {@link MessagePattern.ApostropheMode} *
  • In argStyleText, every single ASCII apostrophe begins and ends quoted literal text, * and unquoted {curly braces} must occur in matched pairs. *
* *

Recommendation: Use the real apostrophe (single quote) character \\u2019 for * human-readable text, and use the ASCII apostrophe (\\u0027 ' ) * only in program syntax, like quoting in MessageFormat. * See the annotations for U+0027 Apostrophe in The Unicode Standard. * *

The choice argument type is deprecated. * Use plural arguments for proper plural selection, * and select arguments for simple selection among a fixed set of choices. * *

The argType and argStyle values are used to create * a Format instance for the format element. The following * table shows how the values map to Format instances. Combinations not * shown in the table are illegal. Any argStyleText must * be a valid pattern string for the Format subclass used. * *

* * * * * * * * * * * * * * * * * * * * * * * * *
argType * argStyle * resulting Format object *
(none) * null *
number * (none) * NumberFormat.getInstance(getLocale()) *
integer * NumberFormat.getIntegerInstance(getLocale()) *
currency * NumberFormat.getCurrencyInstance(getLocale()) *
percent * NumberFormat.getPercentInstance(getLocale()) *
argStyleText * new DecimalFormat(argStyleText, new DecimalFormatSymbols(getLocale())) *
argSkeletonText * NumberFormatter.forSkeleton(argSkeletonText).locale(getLocale()).toFormat() *
date * (none) * DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale()) *
short * DateFormat.getDateInstance(DateFormat.SHORT, getLocale()) *
medium * DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale()) *
long * DateFormat.getDateInstance(DateFormat.LONG, getLocale()) *
full * DateFormat.getDateInstance(DateFormat.FULL, getLocale()) *
argStyleText * new SimpleDateFormat(argStyleText, getLocale()) *
argSkeletonText * DateFormat.getInstanceForSkeleton(argSkeletonText, getLocale()) *
time * (none) * DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale()) *
short * DateFormat.getTimeInstance(DateFormat.SHORT, getLocale()) *
medium * DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale()) *
long * DateFormat.getTimeInstance(DateFormat.LONG, getLocale()) *
full * DateFormat.getTimeInstance(DateFormat.FULL, getLocale()) *
argStyleText * new SimpleDateFormat(argStyleText, getLocale()) *
spellout * argStyleText (optional) * new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.SPELLOUT) *
    .setDefaultRuleset(argStyleText);
*
ordinal * argStyleText (optional) * new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.ORDINAL) *
    .setDefaultRuleset(argStyleText);
*
duration * argStyleText (optional) * new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.DURATION) *
    .setDefaultRuleset(argStyleText);
*
* *

Differences from java.text.MessageFormat

* *

The ICU MessageFormat supports both named and numbered arguments, * while the JDK MessageFormat only supports numbered arguments. * Named arguments make patterns more readable. * *

ICU implements a more user-friendly apostrophe quoting syntax. * In message text, an apostrophe only begins quoting literal text * if it immediately precedes a syntax character (mostly {curly braces}).
* In the JDK MessageFormat, an apostrophe always begins quoting, * which requires common text like "don't" and "aujourd'hui" * to be written with doubled apostrophes like "don''t" and "aujourd''hui". * For more details see {@link MessagePattern.ApostropheMode}. * *

ICU does not create a ChoiceFormat object for a choiceArg, pluralArg or selectArg * but rather handles such arguments itself. * The JDK MessageFormat does create and use a ChoiceFormat object * (new ChoiceFormat(argStyleText)). * The JDK does not support plural and select arguments at all. *

Both the ICU and the JDK MessageFormat can control the argument * formats by using argStyle. But the JDK MessageFormat only * supports predefined formats and number / date / time pattern strings (which would need * to be localized).
* ICU supports everything the JDK does, and also number / date / time skeletons using the * :: prefix (which automatically yield output appropriate for the * MessageFormat locale).

* *

Argument formatting

* *

Arguments are formatted according to their type, using the default * ICU formatters for those types, unless otherwise specified. * For unknown types, MessageFormat will call toString().

* *

There are also several ways to control the formatting.

* *

We recommend you use default styles, predefined style values, skeletons, * or preformatted values, but not pattern strings or custom format objects.

* *

For more details, see the * ICU User Guide.

* *

Usage Information

* *

Here are some examples of usage: *

*
 * Object[] arguments = {
 *     7,
 *     new Date(System.currentTimeMillis()),
 *     "a disturbance in the Force"
 * };
 *
 * String result = MessageFormat.format(
 *     "At {1,time,::jmm} on {1,date,::dMMMM}, there was {2} on planet {0,number,integer}.",
 *     arguments);
 *
 * output: At 4:34 PM on March 23, there was a disturbance
 *           in the Force on planet 7.
 *
 * 
*
* Typically, the message format will come from resources, and the * arguments will be dynamically set at runtime. * *

Example 2: *

*
 * Object[] testArgs = { 3, "MyDisk" };
 *
 * MessageFormat form = new MessageFormat(
 *     "The disk \"{1}\" contains {0} file(s).");
 *
 * System.out.println(form.format(testArgs));
 *
 * // output, with different testArgs
 * output: The disk "MyDisk" contains 0 file(s).
 * output: The disk "MyDisk" contains 1 file(s).
 * output: The disk "MyDisk" contains 1,273 file(s).
 * 
*
* *

For messages that include plural forms, you can use a plural argument: *

 * MessageFormat msgFmt = new MessageFormat(
 *     "{num_files, plural, " +
 *     "=0{There are no files on disk \"{disk_name}\".}" +
 *     "=1{There is one file on disk \"{disk_name}\".}" +
 *     "other{There are # files on disk \"{disk_name}\".}}",
 *     ULocale.ENGLISH);
 * Map args = new HashMap();
 * args.put("num_files", 0);
 * args.put("disk_name", "MyDisk");
 * System.out.println(msgFmt.format(args));
 * args.put("num_files", 3);
 * System.out.println(msgFmt.format(args));
 *
 * output:
 * There are no files on disk "MyDisk".
 * There are 3 files on "MyDisk".
 * 
* See {@link PluralFormat} and {@link PluralRules} for details. * *

Synchronization

* *

MessageFormats are not synchronized. * It is recommended to create separate format instances for each thread. * If multiple threads access a format concurrently, it must be synchronized * externally. * * @see java.util.Locale * @see Format * @see NumberFormat * @see DecimalFormat * @see ChoiceFormat * @see PluralFormat * @see SelectFormat * @author Mark Davis * @author Markus Scherer * @stable ICU 3.0 */ public class MessageFormat extends UFormat { // Incremented by 1 for ICU 4.8's new format. static final long serialVersionUID = 7136212545847378652L; /** * Constructs a MessageFormat for the default FORMAT locale and the * specified pattern. * Sets the locale and calls applyPattern(pattern). * * @param pattern the pattern for this message format * @exception IllegalArgumentException if the pattern is invalid * @see Category#FORMAT * @stable ICU 3.0 */ public MessageFormat(String pattern) { this.ulocale = ULocale.getDefault(Category.FORMAT); applyPattern(pattern); } /** * Constructs a MessageFormat for the specified locale and * pattern. * Sets the locale and calls applyPattern(pattern). * * @param pattern the pattern for this message format * @param locale the locale for this message format * @exception IllegalArgumentException if the pattern is invalid * @stable ICU 3.0 */ public MessageFormat(String pattern, Locale locale) { this(pattern, ULocale.forLocale(locale)); } /** * Constructs a MessageFormat for the specified locale and * pattern. * Sets the locale and calls applyPattern(pattern). * * @param pattern the pattern for this message format * @param locale the locale for this message format * @exception IllegalArgumentException if the pattern is invalid * @stable ICU 3.2 */ public MessageFormat(String pattern, ULocale locale) { this.ulocale = locale; applyPattern(pattern); } /** * Sets the locale to be used for creating argument Format objects. * This affects subsequent calls to the {@link #applyPattern applyPattern} * method as well as to the format and * {@link #formatToCharacterIterator formatToCharacterIterator} methods. * * @param locale the locale to be used when creating or comparing subformats * @stable ICU 3.0 */ public void setLocale(Locale locale) { setLocale(ULocale.forLocale(locale)); } /** * Sets the locale to be used for creating argument Format objects. * This affects subsequent calls to the {@link #applyPattern applyPattern} * method as well as to the format and * {@link #formatToCharacterIterator formatToCharacterIterator} methods. * * @param locale the locale to be used when creating or comparing subformats * @stable ICU 3.2 */ public void setLocale(ULocale locale) { /* Save the pattern, and then reapply so that */ /* we pick up any changes in locale specific */ /* elements */ String existingPattern = toPattern(); /*ibm.3550*/ this.ulocale = locale; // Invalidate all stock formatters. They are no longer valid since // the locale has changed. stockDateFormatter = null; stockNumberFormatter = null; pluralProvider = null; ordinalProvider = null; applyPattern(existingPattern); /*ibm.3550*/ } /** * Returns the locale that's used when creating or comparing subformats. * * @return the locale used when creating or comparing subformats * @stable ICU 3.0 */ public Locale getLocale() { return ulocale.toLocale(); } /** * {@icu} Returns the locale that's used when creating argument Format objects. * * @return the locale used when creating or comparing subformats * @stable ICU 3.2 */ public ULocale getULocale() { return ulocale; } /** * Sets the pattern used by this message format. * Parses the pattern and caches Format objects for simple argument types. * Patterns and their interpretation are specified in the * class description. * * @param pttrn the pattern for this message format * @throws IllegalArgumentException if the pattern is invalid * @stable ICU 3.0 */ public void applyPattern(String pttrn) { try { if (msgPattern == null) { msgPattern = new MessagePattern(pttrn); } else { msgPattern.parse(pttrn); } // Cache the formats that are explicitly mentioned in the message pattern. cacheExplicitFormats(); } catch(RuntimeException e) { resetPattern(); throw e; } } /** * {@icu} Sets the ApostropheMode and the pattern used by this message format. * Parses the pattern and caches Format objects for simple argument types. * Patterns and their interpretation are specified in the * class description. *

* This method is best used only once on a given object to avoid confusion about the mode, * and after constructing the object with an empty pattern string to minimize overhead. * * @param pattern the pattern for this message format * @param aposMode the new ApostropheMode * @throws IllegalArgumentException if the pattern is invalid * @see MessagePattern.ApostropheMode * @stable ICU 4.8 */ public void applyPattern(String pattern, MessagePattern.ApostropheMode aposMode) { if (msgPattern == null) { msgPattern = new MessagePattern(aposMode); } else if (aposMode != msgPattern.getApostropheMode()) { msgPattern.clearPatternAndSetApostropheMode(aposMode); } applyPattern(pattern); } /** * {@icu} * @return this instance's ApostropheMode. * @stable ICU 4.8 */ public MessagePattern.ApostropheMode getApostropheMode() { if (msgPattern == null) { msgPattern = new MessagePattern(); // Sets the default mode. } return msgPattern.getApostropheMode(); } /** * Returns the applied pattern string. * @return the pattern string * @throws IllegalStateException after custom Format objects have been set * via setFormat() or similar APIs * @stable ICU 3.0 */ public String toPattern() { // Return the original, applied pattern string, or else "". // Note: This does not take into account // - changes from setFormat() and similar methods, or // - normalization of apostrophes and arguments, for example, // whether some date/time/number formatter was created via a pattern // but is equivalent to the "medium" default format. if (customFormatArgStarts != null) { throw new IllegalStateException( "toPattern() is not supported after custom Format objects "+ "have been set via setFormat() or similar APIs"); } if (msgPattern == null) { return ""; } String originalPattern = msgPattern.getPatternString(); return originalPattern == null ? "" : originalPattern; } /** * Returns the part index of the next ARG_START after partIndex, or -1 if there is none more. * @param partIndex Part index of the previous ARG_START (initially 0). */ private int nextTopLevelArgStart(int partIndex) { if (partIndex != 0) { partIndex = msgPattern.getLimitPartIndex(partIndex); } for (;;) { MessagePattern.Part.Type type = msgPattern.getPartType(++partIndex); if (type == MessagePattern.Part.Type.ARG_START) { return partIndex; } if (type == MessagePattern.Part.Type.MSG_LIMIT) { return -1; } } } private boolean argNameMatches(int partIndex, String argName, int argNumber) { Part part = msgPattern.getPart(partIndex); return part.getType() == MessagePattern.Part.Type.ARG_NAME ? msgPattern.partSubstringMatches(part, argName) : part.getValue() == argNumber; // ARG_NUMBER } private String getArgName(int partIndex) { Part part = msgPattern.getPart(partIndex); if (part.getType() == MessagePattern.Part.Type.ARG_NAME) { return msgPattern.getSubstring(part); } else { return Integer.toString(part.getValue()); } } /** * Sets the Format objects to use for the values passed into * format methods or returned from parse * methods. The indices of elements in newFormats * correspond to the argument indices used in the previously set * pattern string. * The order of formats in newFormats thus corresponds to * the order of elements in the arguments array passed * to the format methods or the result array returned * by the parse methods. *

* If an argument index is used for more than one format element * in the pattern string, then the corresponding new format is used * for all such format elements. If an argument index is not used * for any format element in the pattern string, then the * corresponding new format is ignored. If fewer formats are provided * than needed, then only the formats for argument indices less * than newFormats.length are replaced. * * This method is only supported if the format does not use * named arguments, otherwise an IllegalArgumentException is thrown. * * @param newFormats the new formats to use * @throws NullPointerException if newFormats is null * @throws IllegalArgumentException if this formatter uses named arguments * @stable ICU 3.0 */ public void setFormatsByArgumentIndex(Format[] newFormats) { if (msgPattern.hasNamedArguments()) { throw new IllegalArgumentException( "This method is not available in MessageFormat objects " + "that use alphanumeric argument names."); } for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) { int argNumber = msgPattern.getPart(partIndex + 1).getValue(); if (argNumber < newFormats.length) { setCustomArgStartFormat(partIndex, newFormats[argNumber]); } } } /** * {@icu} Sets the Format objects to use for the values passed into * format methods or returned from parse * methods. The keys in newFormats are the argument * names in the previously set pattern string, and the values * are the formats. *

* Only argument names from the pattern string are considered. * Extra keys in newFormats that do not correspond * to an argument name are ignored. Similarly, if there is no * format in newFormats for an argument name, the formatter * for that argument remains unchanged. *

* This may be called on formats that do not use named arguments. * In this case the map will be queried for key Strings that * represent argument indices, e.g. "0", "1", "2" etc. * * @param newFormats a map from String to Format providing new * formats for named arguments. * @stable ICU 3.8 */ public void setFormatsByArgumentName(Map newFormats) { for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) { String key = getArgName(partIndex + 1); if (newFormats.containsKey(key)) { setCustomArgStartFormat(partIndex, newFormats.get(key)); } } } /** * Sets the Format objects to use for the format elements in the * previously set pattern string. * The order of formats in newFormats corresponds to * the order of format elements in the pattern string. *

* If more formats are provided than needed by the pattern string, * the remaining ones are ignored. If fewer formats are provided * than needed, then only the first newFormats.length * formats are replaced. *

* Since the order of format elements in a pattern string often * changes during localization, it is generally better to use the * {@link #setFormatsByArgumentIndex setFormatsByArgumentIndex} * method, which assumes an order of formats corresponding to the * order of elements in the arguments array passed to * the format methods or the result array returned by * the parse methods. * * @param newFormats the new formats to use * @exception NullPointerException if newFormats is null * @stable ICU 3.0 */ public void setFormats(Format[] newFormats) { int formatNumber = 0; for (int partIndex = 0; formatNumber < newFormats.length && (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) { setCustomArgStartFormat(partIndex, newFormats[formatNumber]); ++formatNumber; } } /** * Sets the Format object to use for the format elements within the * previously set pattern string that use the given argument * index. * The argument index is part of the format element definition and * represents an index into the arguments array passed * to the format methods or the result array returned * by the parse methods. *

* If the argument index is used for more than one format element * in the pattern string, then the new format is used for all such * format elements. If the argument index is not used for any format * element in the pattern string, then the new format is ignored. * * This method is only supported when exclusively numbers are used for * argument names. Otherwise an IllegalArgumentException is thrown. * * @param argumentIndex the argument index for which to use the new format * @param newFormat the new format to use * @throws IllegalArgumentException if this format uses named arguments * @stable ICU 3.0 */ public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) { if (msgPattern.hasNamedArguments()) { throw new IllegalArgumentException( "This method is not available in MessageFormat objects " + "that use alphanumeric argument names."); } for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) { if (msgPattern.getPart(partIndex + 1).getValue() == argumentIndex) { setCustomArgStartFormat(partIndex, newFormat); } } } /** * {@icu} Sets the Format object to use for the format elements within the * previously set pattern string that use the given argument * name. *

* If the argument name is used for more than one format element * in the pattern string, then the new format is used for all such * format elements. If the argument name is not used for any format * element in the pattern string, then the new format is ignored. *

* This API may be used on formats that do not use named arguments. * In this case argumentName should be a String that names * an argument index, e.g. "0", "1", "2"... etc. If it does not name * a valid index, the format will be ignored. No error is thrown. * * @param argumentName the name of the argument to change * @param newFormat the new format to use * @stable ICU 3.8 */ public void setFormatByArgumentName(String argumentName, Format newFormat) { int argNumber = MessagePattern.validateArgumentName(argumentName); if (argNumber < MessagePattern.ARG_NAME_NOT_NUMBER) { return; } for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) { if (argNameMatches(partIndex + 1, argumentName, argNumber)) { setCustomArgStartFormat(partIndex, newFormat); } } } /** * Sets the Format object to use for the format element with the given * format element index within the previously set pattern string. * The format element index is the zero-based number of the format * element counting from the start of the pattern string. *

* Since the order of format elements in a pattern string often * changes during localization, it is generally better to use the * {@link #setFormatByArgumentIndex setFormatByArgumentIndex} * method, which accesses format elements based on the argument * index they specify. * * @param formatElementIndex the index of a format element within the pattern * @param newFormat the format to use for the specified format element * @exception ArrayIndexOutOfBoundsException if formatElementIndex is equal to or * larger than the number of format elements in the pattern string * @stable ICU 3.0 */ public void setFormat(int formatElementIndex, Format newFormat) { int formatNumber = 0; for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) { if (formatNumber == formatElementIndex) { setCustomArgStartFormat(partIndex, newFormat); return; } ++formatNumber; } throw new ArrayIndexOutOfBoundsException(formatElementIndex); } /** * Returns the Format objects used for the values passed into * format methods or returned from parse * methods. The indices of elements in the returned array * correspond to the argument indices used in the previously set * pattern string. * The order of formats in the returned array thus corresponds to * the order of elements in the arguments array passed * to the format methods or the result array returned * by the parse methods. *

* If an argument index is used for more than one format element * in the pattern string, then the format used for the last such * format element is returned in the array. If an argument index * is not used for any format element in the pattern string, then * null is returned in the array. * * This method is only supported when exclusively numbers are used for * argument names. Otherwise an IllegalArgumentException is thrown. * * @return the formats used for the arguments within the pattern * @throws IllegalArgumentException if this format uses named arguments * @stable ICU 3.0 */ public Format[] getFormatsByArgumentIndex() { if (msgPattern.hasNamedArguments()) { throw new IllegalArgumentException( "This method is not available in MessageFormat objects " + "that use alphanumeric argument names."); } ArrayList list = new ArrayList<>(); for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) { int argNumber = msgPattern.getPart(partIndex + 1).getValue(); while (argNumber >= list.size()) { list.add(null); } list.set(argNumber, cachedFormatters == null ? null : cachedFormatters.get(partIndex)); } return list.toArray(new Format[list.size()]); } /** * Returns the Format objects used for the format elements in the * previously set pattern string. * The order of formats in the returned array corresponds to * the order of format elements in the pattern string. *

* Since the order of format elements in a pattern string often * changes during localization, it's generally better to use the * {@link #getFormatsByArgumentIndex()} * method, which assumes an order of formats corresponding to the * order of elements in the arguments array passed to * the format methods or the result array returned by * the parse methods. * * This method is only supported when exclusively numbers are used for * argument names. Otherwise an IllegalArgumentException is thrown. * * @return the formats used for the format elements in the pattern * @throws IllegalArgumentException if this format uses named arguments * @stable ICU 3.0 */ public Format[] getFormats() { ArrayList list = new ArrayList<>(); for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) { list.add(cachedFormatters == null ? null : cachedFormatters.get(partIndex)); } return list.toArray(new Format[list.size()]); } /** * {@icu} Returns the top-level argument names. For more details, see * {@link #setFormatByArgumentName(String, Format)}. * @return a Set of argument names * @stable ICU 4.8 */ public Set getArgumentNames() { Set result = new HashSet<>(); for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) { result.add(getArgName(partIndex + 1)); } return result; } /** * {@icu} Returns the first top-level format associated with the given argument name. * For more details, see {@link #setFormatByArgumentName(String, Format)}. * @param argumentName The name of the desired argument. * @return the Format associated with the name, or null if there isn't one. * @stable ICU 4.8 */ public Format getFormatByArgumentName(String argumentName) { if (cachedFormatters == null) { return null; } int argNumber = MessagePattern.validateArgumentName(argumentName); if (argNumber < MessagePattern.ARG_NAME_NOT_NUMBER) { return null; } for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) { if (argNameMatches(partIndex + 1, argumentName, argNumber)) { return cachedFormatters.get(partIndex); } } return null; } /** * Formats an array of objects and appends the MessageFormat's * pattern, with arguments replaced by the formatted objects, to the * provided StringBuffer. *

* The text substituted for the individual format elements is derived from * the current subformat of the format element and the * arguments element at the format element's argument index * as indicated by the first matching line of the following table. An * argument is unavailable if arguments is * null or has fewer than argumentIndex+1 elements. When * an argument is unavailable no substitution is performed. * *

* * * * * * * * * *
argType or Format * value object * Formatted Text *
any * unavailable * "{" + argNameOrNumber + "}" *
any * null * "null" *
custom Format != null * any * customFormat.format(argument) *
noneArg, or custom Format == null * instanceof Number * NumberFormat.getInstance(getLocale()).format(argument) *
noneArg, or custom Format == null * instanceof Date * DateFormat.getDateTimeInstance(DateFormat.SHORT, * DateFormat.SHORT, getLocale()).format(argument) *
noneArg, or custom Format == null * instanceof String * argument *
noneArg, or custom Format == null * any * argument.toString() *
complexArg * any * result of recursive formatting of a selected sub-message *
*

* If pos is non-null, and refers to * Field.ARGUMENT, the location of the first formatted * string will be returned. * * This method is only supported when the format does not use named * arguments, otherwise an IllegalArgumentException is thrown. * * @param arguments an array of objects to be formatted and substituted. * @param result where text is appended. * @param pos On input: an alignment field, if desired. * On output: the offsets of the alignment field. * @throws IllegalArgumentException if a value in the * arguments array is not of the type * expected by the corresponding argument or custom Format object. * @throws IllegalArgumentException if this format uses named arguments * @stable ICU 3.0 */ public final StringBuffer format(Object[] arguments, StringBuffer result, FieldPosition pos) { format(arguments, null, new AppendableWrapper(result), pos); return result; } /** * Formats a map of objects and appends the MessageFormat's * pattern, with arguments replaced by the formatted objects, to the * provided StringBuffer. *

* The text substituted for the individual format elements is derived from * the current subformat of the format element and the * arguments value corresponding to the format element's * argument name. *

* A numbered pattern argument is matched with a map key that contains that number * as an ASCII-decimal-digit string (without leading zero). *

* An argument is unavailable if arguments is * null or does not have a value corresponding to an argument * name in the pattern. When an argument is unavailable no substitution * is performed. * * @param arguments a map of objects to be formatted and substituted. * @param result where text is appended. * @param pos On input: an alignment field, if desired. * On output: the offsets of the alignment field. * @throws IllegalArgumentException if a value in the * arguments array is not of the type * expected by the corresponding argument or custom Format object. * @return the passed-in StringBuffer * @stable ICU 3.8 */ public final StringBuffer format(Map arguments, StringBuffer result, FieldPosition pos) { format(null, arguments, new AppendableWrapper(result), pos); return result; } /** * Creates a MessageFormat with the given pattern and uses it * to format the given arguments. This is equivalent to *

* (new {@link #MessageFormat(String) MessageFormat}(pattern)).{@link * #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition) * format}(arguments, new StringBuffer(), null).toString() *
* * @throws IllegalArgumentException if the pattern is invalid * @throws IllegalArgumentException if a value in the * arguments array is not of the type * expected by the corresponding argument or custom Format object. * @throws IllegalArgumentException if this format uses named arguments * @stable ICU 3.0 */ public static String format(String pattern, Object... arguments) { MessageFormat temp = new MessageFormat(pattern); return temp.format(arguments); } /** * Creates a MessageFormat with the given pattern and uses it to * format the given arguments. The pattern must identifyarguments * by name instead of by number. *

* @throws IllegalArgumentException if the pattern is invalid * @throws IllegalArgumentException if a value in the * arguments array is not of the type * expected by the corresponding argument or custom Format object. * @see #format(Map, StringBuffer, FieldPosition) * @see #format(String, Object[]) * @stable ICU 3.8 */ public static String format(String pattern, Map arguments) { MessageFormat temp = new MessageFormat(pattern); return temp.format(arguments); } /** * {@icu} Returns true if this MessageFormat uses named arguments, * and false otherwise. See class description. * * @return true if named arguments are used. * @stable ICU 3.8 */ public boolean usesNamedArguments() { return msgPattern.hasNamedArguments(); } // Overrides /** * Formats a map or array of objects and appends the MessageFormat's * pattern, with format elements replaced by the formatted objects, to the * provided StringBuffer. * This is equivalent to either of *

* {@link #format(java.lang.Object[], java.lang.StringBuffer, * java.text.FieldPosition) format}((Object[]) arguments, result, pos) * {@link #format(java.util.Map, java.lang.StringBuffer, * java.text.FieldPosition) format}((Map) arguments, result, pos) *
* A map must be provided if this format uses named arguments, otherwise * an IllegalArgumentException will be thrown. * @param arguments a map or array of objects to be formatted * @param result where text is appended * @param pos On input: an alignment field, if desired * On output: the offsets of the alignment field * @throws IllegalArgumentException if an argument in * arguments is not of the type * expected by the format element(s) that use it * @throws IllegalArgumentException if arguments is * an array of Object and this format uses named arguments * @stable ICU 3.0 */ @Override public final StringBuffer format(Object arguments, StringBuffer result, FieldPosition pos) { format(arguments, new AppendableWrapper(result), pos); return result; } /** * Formats an array of objects and inserts them into the * MessageFormat's pattern, producing an * AttributedCharacterIterator. * You can use the returned AttributedCharacterIterator * to build the resulting String, as well as to determine information * about the resulting String. *

* The text of the returned AttributedCharacterIterator is * the same that would be returned by *

* {@link #format(java.lang.Object[], java.lang.StringBuffer, * java.text.FieldPosition) format}(arguments, new StringBuffer(), null).toString() *
*

* In addition, the AttributedCharacterIterator contains at * least attributes indicating where text was generated from an * argument in the arguments array. The keys of these attributes are of * type MessageFormat.Field, their values are * Integer objects indicating the index in the arguments * array of the argument from which the text was generated. *

* The attributes/value from the underlying Format * instances that MessageFormat uses will also be * placed in the resulting AttributedCharacterIterator. * This allows you to not only find where an argument is placed in the * resulting String, but also which fields it contains in turn. * * @param arguments an array of objects to be formatted and substituted. * @return AttributedCharacterIterator describing the formatted value. * @exception NullPointerException if arguments is null. * @throws IllegalArgumentException if a value in the * arguments array is not of the type * expected by the corresponding argument or custom Format object. * @stable ICU 3.8 */ @Override public AttributedCharacterIterator formatToCharacterIterator(Object arguments) { if (arguments == null) { throw new NullPointerException( "formatToCharacterIterator must be passed non-null object"); } StringBuilder result = new StringBuilder(); AppendableWrapper wrapper = new AppendableWrapper(result); wrapper.useAttributes(); format(arguments, wrapper, null); AttributedString as = new AttributedString(result.toString()); for (AttributeAndPosition a : wrapper.attributes) { as.addAttribute(a.key, a.value, a.start, a.limit); } return as.getIterator(); } /** * Parses the string. * *

Caveats: The parse may fail in a number of circumstances. * For example: *

    *
  • If one of the arguments does not occur in the pattern. *
  • If the format of an argument loses information, such as * with a choice format where a large number formats to "many". *
  • Does not yet handle recursion (where * the substituted strings contain {n} references.) *
  • Will not always find a match (or the correct match) * if some part of the parse is ambiguous. * For example, if the pattern "{1},{2}" is used with the * string arguments {"a,b", "c"}, it will format as "a,b,c". * When the result is parsed, it will return {"a", "b,c"}. *
  • If a single argument is parsed more than once in the string, * then the later parse wins. *
* When the parse fails, use ParsePosition.getErrorIndex() to find out * where in the string did the parsing failed. The returned error * index is the starting offset of the sub-patterns that the string * is comparing with. For example, if the parsing string "AAA {0} BBB" * is comparing against the pattern "AAD {0} BBB", the error index is * 0. When an error occurs, the call to this method will return null. * If the source is null, return an empty array. * * @throws IllegalArgumentException if this format uses named arguments * @stable ICU 3.0 */ public Object[] parse(String source, ParsePosition pos) { if (msgPattern.hasNamedArguments()) { throw new IllegalArgumentException( "This method is not available in MessageFormat objects " + "that use named argument."); } // Count how many slots we need in the array. int maxArgId = -1; for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0;) { int argNumber=msgPattern.getPart(partIndex + 1).getValue(); if (argNumber > maxArgId) { maxArgId = argNumber; } } Object[] resultArray = new Object[maxArgId + 1]; int backupStartPos = pos.getIndex(); parse(0, source, pos, resultArray, null); if (pos.getIndex() == backupStartPos) { // unchanged, returned object is null return null; } return resultArray; } /** * {@icu} Parses the string, returning the results in a Map. * This is similar to the version that returns an array * of Object. This supports both named and numbered * arguments-- if numbered, the keys in the map are the * corresponding ASCII-decimal-digit strings (e.g. "0", "1", "2"...). * * @param source the text to parse * @param pos the position at which to start parsing. on return, * contains the result of the parse. * @return a Map containing key/value pairs for each parsed argument. * @stable ICU 3.8 */ public Map parseToMap(String source, ParsePosition pos) { Map result = new HashMap<>(); int backupStartPos = pos.getIndex(); parse(0, source, pos, null, result); if (pos.getIndex() == backupStartPos) { return null; } return result; } /** * Parses text from the beginning of the given string to produce an object * array. * The method may not use the entire text of the given string. *

* See the {@link #parse(String, ParsePosition)} method for more information * on message parsing. * * @param source A String whose beginning should be parsed. * @return An Object array parsed from the string. * @exception ParseException if the beginning of the specified string cannot be parsed. * @exception IllegalArgumentException if this format uses named arguments * @stable ICU 3.0 */ public Object[] parse(String source) throws ParseException { ParsePosition pos = new ParsePosition(0); Object[] result = parse(source, pos); if (pos.getIndex() == 0) // unchanged, returned object is null throw new ParseException("MessageFormat parse error!", pos.getErrorIndex()); return result; } /** * Parses the string, filling either the Map or the Array. * This is a private method that all the public parsing methods call. * This supports both named and numbered * arguments-- if numbered, the keys in the map are the * corresponding ASCII-decimal-digit strings (e.g. "0", "1", "2"...). * * @param msgStart index in the message pattern to start from. * @param source the text to parse * @param pos the position at which to start parsing. on return, * contains the result of the parse. * @param args if not null, the parse results will be filled here (The pattern * has to have numbered arguments in order for this to not be null). * @param argsMap if not null, the parse results will be filled here. */ private void parse(int msgStart, String source, ParsePosition pos, Object[] args, Map argsMap) { if (source == null) { return; } String msgString=msgPattern.getPatternString(); int prevIndex=msgPattern.getPart(msgStart).getLimit(); int sourceOffset = pos.getIndex(); ParsePosition tempStatus = new ParsePosition(0); for(int i=msgStart+1; ; ++i) { Part part=msgPattern.getPart(i); Part.Type type=part.getType(); int index=part.getIndex(); // Make sure the literal string matches. int len = index - prevIndex; if (len == 0 || msgString.regionMatches(prevIndex, source, sourceOffset, len)) { sourceOffset += len; prevIndex += len; } else { pos.setErrorIndex(sourceOffset); return; // leave index as is to signal error } if(type==Part.Type.MSG_LIMIT) { // Things went well! Done. pos.setIndex(sourceOffset); return; } if(type==Part.Type.SKIP_SYNTAX || type==Part.Type.INSERT_CHAR) { prevIndex=part.getLimit(); continue; } // We do not support parsing Plural formats. (No REPLACE_NUMBER here.) assert type==Part.Type.ARG_START : "Unexpected Part "+part+" in parsed message."; int argLimit=msgPattern.getLimitPartIndex(i); ArgType argType=part.getArgType(); part=msgPattern.getPart(++i); // Compute the argId, so we can use it as a key. Object argId=null; int argNumber = 0; String key = null; if(args!=null) { argNumber=part.getValue(); // ARG_NUMBER argId = argNumber; } else { if(part.getType()==MessagePattern.Part.Type.ARG_NAME) { key=msgPattern.getSubstring(part); } else /* ARG_NUMBER */ { key=Integer.toString(part.getValue()); } argId = key; } ++i; Format formatter = null; boolean haveArgResult = false; Object argResult = null; if(cachedFormatters!=null && (formatter=cachedFormatters.get(i - 2))!=null) { // Just parse using the formatter. tempStatus.setIndex(sourceOffset); argResult = formatter.parseObject(source, tempStatus); if (tempStatus.getIndex() == sourceOffset) { pos.setErrorIndex(sourceOffset); return; // leave index as is to signal error } haveArgResult = true; sourceOffset = tempStatus.getIndex(); } else if( argType==ArgType.NONE || (cachedFormatters!=null && cachedFormatters.containsKey(i - 2))) { // Match as a string. // if at end, use longest possible match // otherwise uses first match to intervening string // does NOT recursively try all possibilities String stringAfterArgument = getLiteralStringUntilNextArgument(argLimit); int next; if (stringAfterArgument.length() != 0) { next = source.indexOf(stringAfterArgument, sourceOffset); } else { next = source.length(); } if (next < 0) { pos.setErrorIndex(sourceOffset); return; // leave index as is to signal error } else { String strValue = source.substring(sourceOffset, next); if (!strValue.equals("{" + argId.toString() + "}")) { haveArgResult = true; argResult = strValue; } sourceOffset = next; } } else if(argType==ArgType.CHOICE) { tempStatus.setIndex(sourceOffset); double choiceResult = parseChoiceArgument(msgPattern, i, source, tempStatus); if (tempStatus.getIndex() == sourceOffset) { pos.setErrorIndex(sourceOffset); return; // leave index as is to signal error } argResult = choiceResult; haveArgResult = true; sourceOffset = tempStatus.getIndex(); } else if(argType.hasPluralStyle() || argType==ArgType.SELECT) { // No can do! throw new UnsupportedOperationException( "Parsing of plural/select/selectordinal argument is not supported."); } else { // This should never happen. throw new IllegalStateException("unexpected argType "+argType); } if (haveArgResult) { if (args != null) { args[argNumber] = argResult; } else if (argsMap != null) { argsMap.put(key, argResult); } } prevIndex=msgPattern.getPart(argLimit).getLimit(); i=argLimit; } } /** * {@icu} Parses text from the beginning of the given string to produce a map from * argument to values. The method may not use the entire text of the given string. * *

See the {@link #parse(String, ParsePosition)} method for more information on * message parsing. * * @param source A String whose beginning should be parsed. * @return A Map parsed from the string. * @throws ParseException if the beginning of the specified string cannot * be parsed. * @see #parseToMap(String, ParsePosition) * @stable ICU 3.8 */ public Map parseToMap(String source) throws ParseException { ParsePosition pos = new ParsePosition(0); Map result = new HashMap<>(); parse(0, source, pos, null, result); if (pos.getIndex() == 0) // unchanged, returned object is null throw new ParseException("MessageFormat parse error!", pos.getErrorIndex()); return result; } /** * Parses text from a string to produce an object array or Map. *

* The method attempts to parse text starting at the index given by * pos. * If parsing succeeds, then the index of pos is updated * to the index after the last character used (parsing does not necessarily * use all characters up to the end of the string), and the parsed * object array is returned. The updated pos can be used to * indicate the starting point for the next call to this method. * If an error occurs, then the index of pos is not * changed, the error index of pos is set to the index of * the character where the error occurred, and null is returned. *

* See the {@link #parse(String, ParsePosition)} method for more information * on message parsing. * * @param source A String, part of which should be parsed. * @param pos A ParsePosition object with index and error * index information as described above. * @return An Object parsed from the string, either an * array of Object, or a Map, depending on whether named * arguments are used. This can be queried using usesNamedArguments. * In case of error, returns null. * @throws NullPointerException if pos is null. * @stable ICU 3.0 */ @Override public Object parseObject(String source, ParsePosition pos) { if (!msgPattern.hasNamedArguments()) { return parse(source, pos); } else { return parseToMap(source, pos); } } /** * {@inheritDoc} * @stable ICU 3.0 */ @Override public Object clone() { MessageFormat other = (MessageFormat) super.clone(); if (customFormatArgStarts != null) { other.customFormatArgStarts = new HashSet<>(); for (Integer key : customFormatArgStarts) { other.customFormatArgStarts.add(key); } } else { other.customFormatArgStarts = null; } if (cachedFormatters != null) { other.cachedFormatters = new HashMap<>(); Iterator> it = cachedFormatters.entrySet().iterator(); while (it.hasNext()){ Map.Entry entry = it.next(); other.cachedFormatters.put(entry.getKey(), entry.getValue()); } } else { other.cachedFormatters = null; } other.msgPattern = msgPattern == null ? null : (MessagePattern)msgPattern.clone(); other.stockDateFormatter = stockDateFormatter == null ? null : (DateFormat) stockDateFormatter.clone(); other.stockNumberFormatter = stockNumberFormatter == null ? null : (NumberFormat) stockNumberFormatter.clone(); other.pluralProvider = null; other.ordinalProvider = null; return other; } /** * {@inheritDoc} * @stable ICU 3.0 */ @Override public boolean equals(Object obj) { if (this == obj) // quick check return true; if (obj == null || getClass() != obj.getClass()) return false; MessageFormat other = (MessageFormat) obj; return Objects.equals(ulocale, other.ulocale) && Objects.equals(msgPattern, other.msgPattern) && Objects.equals(cachedFormatters, other.cachedFormatters) && Objects.equals(customFormatArgStarts, other.customFormatArgStarts); // Note: It might suffice to only compare custom formatters // rather than all formatters. } /** * {@inheritDoc} * @stable ICU 3.0 */ @Override public int hashCode() { return msgPattern.getPatternString().hashCode(); // enough for reasonable distribution } /** * Defines constants that are used as attribute keys in the * AttributedCharacterIterator returned * from MessageFormat.formatToCharacterIterator. * * @stable ICU 3.8 */ public static class Field extends Format.Field { private static final long serialVersionUID = 7510380454602616157L; /** * Create a Field with the specified name. * * @param name The name of the attribute * * @stable ICU 3.8 */ protected Field(String name) { super(name); } /** * Resolves instances being deserialized to the predefined constants. * * @return resolved MessageFormat.Field constant * @throws InvalidObjectException if the constant could not be resolved. * * @stable ICU 3.8 */ @Override protected Object readResolve() throws InvalidObjectException { if (this.getClass() != MessageFormat.Field.class) { throw new InvalidObjectException( "A subclass of MessageFormat.Field must implement readResolve."); } if (this.getName().equals(ARGUMENT.getName())) { return ARGUMENT; } else { throw new InvalidObjectException("Unknown attribute name."); } } /** * Constant identifying a portion of a message that was generated * from an argument passed into formatToCharacterIterator. * The value associated with the key will be an Integer * indicating the index in the arguments array of the * argument from which the text was generated. * * @stable ICU 3.8 */ public static final Field ARGUMENT = new Field("message argument field"); } // ===========================privates============================ // *Important*: All fields must be declared *transient* so that we can fully // control serialization! // See for example Joshua Bloch's "Effective Java", chapter 10 Serialization. /** * The locale to use for formatting numbers and dates. */ private transient ULocale ulocale; /** * The MessagePattern which contains the parsed structure of the pattern string. */ private transient MessagePattern msgPattern; /** * Cached formatters so we can just use them whenever needed instead of creating * them from scratch every time. */ private transient Map cachedFormatters; /** * Set of ARG_START part indexes where custom, user-provided Format objects * have been set via setFormat() or similar API. */ private transient Set customFormatArgStarts; /** * Stock formatters. Those are used when a format is not explicitly mentioned in * the message. The format is inferred from the argument. */ private transient DateFormat stockDateFormatter; private transient NumberFormat stockNumberFormatter; private transient PluralSelectorProvider pluralProvider; private transient PluralSelectorProvider ordinalProvider; private DateFormat getStockDateFormatter() { if (stockDateFormatter == null) { stockDateFormatter = DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT, ulocale);//fix } return stockDateFormatter; } private NumberFormat getStockNumberFormatter() { if (stockNumberFormatter == null) { stockNumberFormatter = NumberFormat.getInstance(ulocale); } return stockNumberFormatter; } // *Important*: All fields must be declared *transient*. // See the longer comment above ulocale. /** * Formats the arguments and writes the result into the * AppendableWrapper, updates the field position. * *

Exactly one of args and argsMap must be null, the other non-null. * * @param msgStart Index to msgPattern part to start formatting from. * @param pluralNumber null except when formatting a plural argument sub-message * where a '#' is replaced by the format string for this number. * @param args The formattable objects array. Non-null iff numbered values are used. * @param argsMap The key-value map of formattable objects. Non-null iff named values are used. * @param dest Output parameter to receive the result. * The result (string & attributes) is appended to existing contents. * @param fp Field position status. */ private void format(int msgStart, PluralSelectorContext pluralNumber, Object[] args, Map argsMap, AppendableWrapper dest, FieldPosition fp) { String msgString=msgPattern.getPatternString(); int prevIndex=msgPattern.getPart(msgStart).getLimit(); for(int i=msgStart+1;; ++i) { Part part=msgPattern.getPart(i); Part.Type type=part.getType(); int index=part.getIndex(); dest.append(msgString, prevIndex, index); if(type==Part.Type.MSG_LIMIT) { return; } prevIndex=part.getLimit(); if(type==Part.Type.REPLACE_NUMBER) { if(pluralNumber.forReplaceNumber) { // number-offset was already formatted. dest.formatAndAppend(pluralNumber.formatter, pluralNumber.number, pluralNumber.numberString); } else { dest.formatAndAppend(getStockNumberFormatter(), pluralNumber.number); } continue; } if(type!=Part.Type.ARG_START) { continue; } int argLimit=msgPattern.getLimitPartIndex(i); ArgType argType=part.getArgType(); part=msgPattern.getPart(++i); Object arg; boolean noArg=false; Object argId=null; String argName=msgPattern.getSubstring(part); if(args!=null) { int argNumber=part.getValue(); // ARG_NUMBER if (dest.attributes != null) { // We only need argId if we add it into the attributes. argId = argNumber; } if(0<=argNumber && argNumber= 0 || (subMsgString.indexOf('\'') >= 0 && !msgPattern.jdkAposMode())) { MessageFormat subMsgFormat = new MessageFormat(subMsgString, ulocale); subMsgFormat.format(0, null, args, argsMap, dest, null); } else if (dest.attributes == null) { dest.append(subMsgString); } else { // This formats the argument twice, once above to get the subMsgString // and then once more here. // It only happens in formatToCharacterIterator() // on a complex Format set via setFormat(), // and only when the selected subMsgString does not need further formatting. // This imitates ICU 4.6 behavior. dest.formatAndAppend(formatter, arg); } } else { dest.formatAndAppend(formatter, arg); } } else if( argType==ArgType.NONE || (cachedFormatters!=null && cachedFormatters.containsKey(i - 2))) { // ArgType.NONE, or // any argument which got reset to null via setFormat() or its siblings. if (arg instanceof Number) { // format number if can dest.formatAndAppend(getStockNumberFormatter(), arg); } else if (arg instanceof Date) { // format a Date if can dest.formatAndAppend(getStockDateFormatter(), arg); } else if (arg instanceof Calendar) { // format a Calendar if can dest.formatAndAppend(getStockDateFormatter(), arg); } else if (arg instanceof Temporal) { // format a Temporal if can dest.formatAndAppend(getStockDateFormatter(), arg); } else { dest.append(arg.toString()); } } else if(argType==ArgType.CHOICE) { if (!(arg instanceof Number)) { throw new IllegalArgumentException("'" + arg + "' is not a Number"); } double number = ((Number)arg).doubleValue(); int subMsgStart=findChoiceSubMessage(msgPattern, i, number); formatComplexSubMessage(subMsgStart, null, args, argsMap, dest); } else if(argType.hasPluralStyle()) { if (!(arg instanceof Number)) { throw new IllegalArgumentException("'" + arg + "' is not a Number"); } PluralSelectorProvider selector; if(argType == ArgType.PLURAL) { if (pluralProvider == null) { pluralProvider = new PluralSelectorProvider(this, PluralType.CARDINAL); } selector = pluralProvider; } else { if (ordinalProvider == null) { ordinalProvider = new PluralSelectorProvider(this, PluralType.ORDINAL); } selector = ordinalProvider; } Number number = (Number)arg; double offset=msgPattern.getPluralOffset(i); PluralSelectorContext context = new PluralSelectorContext(i, argName, number, offset); int subMsgStart=PluralFormat.findSubMessage( msgPattern, i, selector, context, number.doubleValue()); formatComplexSubMessage(subMsgStart, context, args, argsMap, dest); } else if(argType==ArgType.SELECT) { int subMsgStart=SelectFormat.findSubMessage(msgPattern, i, arg.toString()); formatComplexSubMessage(subMsgStart, null, args, argsMap, dest); } else { // This should never happen. throw new IllegalStateException("unexpected argType "+argType); } fp = updateMetaData(dest, prevDestLength, fp, argId); prevIndex=msgPattern.getPart(argLimit).getLimit(); i=argLimit; } } private void formatComplexSubMessage( int msgStart, PluralSelectorContext pluralNumber, Object[] args, Map argsMap, AppendableWrapper dest) { if (!msgPattern.jdkAposMode()) { format(msgStart, pluralNumber, args, argsMap, dest, null); return; } // JDK compatibility mode: (see JDK MessageFormat.format() API docs) // - remove SKIP_SYNTAX; that is, remove half of the apostrophes // - if the result string contains an open curly brace '{' then // instantiate a temporary MessageFormat object and format again; // otherwise just append the result string String msgString = msgPattern.getPatternString(); String subMsgString; StringBuilder sb = null; int prevIndex = msgPattern.getPart(msgStart).getLimit(); for (int i = msgStart;;) { Part part = msgPattern.getPart(++i); Part.Type type = part.getType(); int index = part.getIndex(); if (type == Part.Type.MSG_LIMIT) { if (sb == null) { subMsgString = msgString.substring(prevIndex, index); } else { subMsgString = sb.append(msgString, prevIndex, index).toString(); } break; } else if (type == Part.Type.REPLACE_NUMBER || type == Part.Type.SKIP_SYNTAX) { if (sb == null) { sb = new StringBuilder(); } sb.append(msgString, prevIndex, index); if (type == Part.Type.REPLACE_NUMBER) { if(pluralNumber.forReplaceNumber) { // number-offset was already formatted. sb.append(pluralNumber.numberString); } else { sb.append(getStockNumberFormatter().format(pluralNumber.number)); } } prevIndex = part.getLimit(); } else if (type == Part.Type.ARG_START) { if (sb == null) { sb = new StringBuilder(); } sb.append(msgString, prevIndex, index); prevIndex = index; i = msgPattern.getLimitPartIndex(i); index = msgPattern.getPart(i).getLimit(); MessagePattern.appendReducedApostrophes(msgString, prevIndex, index, sb); prevIndex = index; } } if (subMsgString.indexOf('{') >= 0) { MessageFormat subMsgFormat = new MessageFormat("", ulocale); subMsgFormat.applyPattern(subMsgString, MessagePattern.ApostropheMode.DOUBLE_REQUIRED); subMsgFormat.format(0, null, args, argsMap, dest, null); } else { dest.append(subMsgString); } } /** * Read as much literal string from the pattern string as possible. This stops * as soon as it finds an argument, or it reaches the end of the string. * @param from Index in the pattern string to start from. * @return A substring from the pattern string representing the longest possible * substring with no arguments. */ private String getLiteralStringUntilNextArgument(int from) { StringBuilder b = new StringBuilder(); String msgString=msgPattern.getPatternString(); int prevIndex=msgPattern.getPart(from).getLimit(); for(int i=from+1;; ++i) { Part part=msgPattern.getPart(i); Part.Type type=part.getType(); int index=part.getIndex(); b.append(msgString, prevIndex, index); if(type==Part.Type.ARG_START || type==Part.Type.MSG_LIMIT) { return b.toString(); } assert type==Part.Type.SKIP_SYNTAX || type==Part.Type.INSERT_CHAR : "Unexpected Part "+part+" in parsed message."; prevIndex=part.getLimit(); } } private FieldPosition updateMetaData(AppendableWrapper dest, int prevLength, FieldPosition fp, Object argId) { if (dest.attributes != null && prevLength < dest.length) { dest.attributes.add(new AttributeAndPosition(argId, prevLength, dest.length)); } if (fp != null && Field.ARGUMENT.equals(fp.getFieldAttribute())) { fp.setBeginIndex(prevLength); fp.setEndIndex(dest.length); return null; } return fp; } // This lives here because ICU4J does not have its own ChoiceFormat class. /** * Finds the ChoiceFormat sub-message for the given number. * @param pattern A MessagePattern. * @param partIndex the index of the first ChoiceFormat argument style part. * @param number a number to be mapped to one of the ChoiceFormat argument's intervals * @return the sub-message start part index. */ private static int findChoiceSubMessage(MessagePattern pattern, int partIndex, double number) { int count=pattern.countParts(); int msgStart; // Iterate over (ARG_INT|DOUBLE, ARG_SELECTOR, message) tuples // until ARG_LIMIT or end of choice-only pattern. // Ignore the first number and selector and start the loop on the first message. partIndex+=2; for(;;) { // Skip but remember the current sub-message. msgStart=partIndex; partIndex=pattern.getLimitPartIndex(partIndex); if(++partIndex>=count) { // Reached the end of the choice-only pattern. // Return with the last sub-message. break; } Part part=pattern.getPart(partIndex++); Part.Type type=part.getType(); if(type==Part.Type.ARG_LIMIT) { // Reached the end of the ChoiceFormat style. // Return with the last sub-message. break; } // part is an ARG_INT or ARG_DOUBLE assert type.hasNumericValue(); double boundary=pattern.getNumericValue(part); // Fetch the ARG_SELECTOR character. int selectorIndex=pattern.getPatternIndex(partIndex++); char boundaryChar=pattern.getPatternString().charAt(selectorIndex); if(boundaryChar=='<' ? !(number>boundary) : !(number>=boundary)) { // The number is in the interval between the previous boundary and the current one. // Return with the sub-message between them. // The !(a>b) and !(a>=b) comparisons are equivalent to // (a<=b) and (a= 0) { int newIndex = start + len; if (newIndex > furthest) { furthest = newIndex; bestNumber = tempNumber; if (furthest == source.length()) { break; } } } partIndex = msgLimit + 1; } if (furthest == start) { pos.setErrorIndex(start); } else { pos.setIndex(furthest); } return bestNumber; } /** * Matches the pattern string from the end of the partIndex to * the beginning of the limitPartIndex, * including all syntax except SKIP_SYNTAX, * against the source string starting at sourceOffset. * If they match, returns the length of the source string match. * Otherwise returns -1. */ private static int matchStringUntilLimitPart( MessagePattern pattern, int partIndex, int limitPartIndex, String source, int sourceOffset) { int matchingSourceLength = 0; String msgString = pattern.getPatternString(); int prevIndex = pattern.getPart(partIndex).getLimit(); for (;;) { Part part = pattern.getPart(++partIndex); if (partIndex == limitPartIndex || part.getType() == Part.Type.SKIP_SYNTAX) { int index = part.getIndex(); int length = index - prevIndex; if (length != 0 && !source.regionMatches(sourceOffset, msgString, prevIndex, length)) { return -1; // mismatch } matchingSourceLength += length; if (partIndex == limitPartIndex) { return matchingSourceLength; } prevIndex = part.getLimit(); // SKIP_SYNTAX } } } /** * Finds the "other" sub-message. * @param partIndex the index of the first PluralFormat argument style part. * @return the "other" sub-message start part index. */ private int findOtherSubMessage(int partIndex) { int count=msgPattern.countParts(); MessagePattern.Part part=msgPattern.getPart(partIndex); if(part.getType().hasNumericValue()) { ++partIndex; } // Iterate over (ARG_SELECTOR [ARG_INT|ARG_DOUBLE] message) tuples // until ARG_LIMIT or end of plural-only pattern. do { part=msgPattern.getPart(partIndex++); MessagePattern.Part.Type type=part.getType(); if(type==MessagePattern.Part.Type.ARG_LIMIT) { break; } assert type==MessagePattern.Part.Type.ARG_SELECTOR; // part is an ARG_SELECTOR followed by an optional explicit value, and then a message if(msgPattern.partSubstringMatches(part, "other")) { return partIndex; } if(msgPattern.getPartType(partIndex).hasNumericValue()) { ++partIndex; // skip the numeric-value part of "=1" etc. } partIndex=msgPattern.getLimitPartIndex(partIndex); } while(++partIndex0 ARG_START index */ int numberArgIndex; Format formatter; /** formatted argument number - plural offset */ String numberString; /** true if number-offset was formatted with the stock number formatter */ boolean forReplaceNumber; } /** * This provider helps defer instantiation of a PluralRules object * until we actually need to select a keyword. * For example, if the number matches an explicit-value selector like "=1" * we do not need any PluralRules. */ private static final class PluralSelectorProvider implements PluralFormat.PluralSelector { public PluralSelectorProvider(MessageFormat mf, PluralType type) { msgFormat = mf; this.type = type; } @Override public String select(Object ctx, double number) { if(rules == null) { rules = PluralRules.forLocale(msgFormat.ulocale, type); } // Select a sub-message according to how the number is formatted, // which is specified in the selected sub-message. // We avoid this circle by looking at how // the number is formatted in the "other" sub-message // which must always be present and usually contains the number. // Message authors should be consistent across sub-messages. PluralSelectorContext context = (PluralSelectorContext)ctx; int otherIndex = msgFormat.findOtherSubMessage(context.startIndex); context.numberArgIndex = msgFormat.findFirstPluralNumberArg(otherIndex, context.argName); if(context.numberArgIndex > 0 && msgFormat.cachedFormatters != null) { context.formatter = msgFormat.cachedFormatters.get(context.numberArgIndex); } if(context.formatter == null) { context.formatter = msgFormat.getStockNumberFormatter(); context.forReplaceNumber = true; } assert context.number.doubleValue() == number; // argument number minus the offset context.numberString = context.formatter.format(context.number); if(context.formatter instanceof DecimalFormat) { IFixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number); return rules.select(dec); } else { return rules.select(number); } } private MessageFormat msgFormat; private PluralRules rules; private PluralType type; } @SuppressWarnings("unchecked") private void format(Object arguments, AppendableWrapper result, FieldPosition fp) { if ((arguments == null || arguments instanceof Map)) { format(null, (Map)arguments, result, fp); } else { format((Object[])arguments, null, result, fp); } } /** * Internal routine used by format. * * @throws IllegalArgumentException if an argument in the * arguments map is not of the type * expected by the format element(s) that use it. */ private void format(Object[] arguments, Map argsMap, AppendableWrapper dest, FieldPosition fp) { if (arguments != null && msgPattern.hasNamedArguments()) { throw new IllegalArgumentException( "This method is not available in MessageFormat objects " + "that use alphanumeric argument names."); } format(0, null, arguments, argsMap, dest, fp); } private void resetPattern() { if (msgPattern != null) { msgPattern.clear(); } if (cachedFormatters != null) { cachedFormatters.clear(); } customFormatArgStarts = null; } private static final String[] typeList = { "number", "date", "time", "spellout", "ordinal", "duration" }; private static final int TYPE_NUMBER = 0, TYPE_DATE = 1, TYPE_TIME = 2, TYPE_SPELLOUT = 3, TYPE_ORDINAL = 4, TYPE_DURATION = 5; private static final String[] modifierList = {"", "currency", "percent", "integer"}; private static final int MODIFIER_EMPTY = 0, MODIFIER_CURRENCY = 1, MODIFIER_PERCENT = 2, MODIFIER_INTEGER = 3; private static final String[] dateModifierList = {"", "short", "medium", "long", "full"}; private static final int DATE_MODIFIER_EMPTY = 0, DATE_MODIFIER_SHORT = 1, DATE_MODIFIER_MEDIUM = 2, DATE_MODIFIER_LONG = 3, DATE_MODIFIER_FULL = 4; 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; /** * {@icu} Converts an 'apostrophe-friendly' pattern into a standard * pattern. * This is obsolete for ICU 4.8 and higher MessageFormat pattern strings. * It can still be useful together with {@link java.text.MessageFormat}. * *

See the class description for more about apostrophes and quoting, * and differences between ICU and {@link java.text.MessageFormat}. * *

{@link java.text.MessageFormat} and ICU 4.6 and earlier MessageFormat * treat all ASCII apostrophes as * quotes, which is problematic in some languages, e.g. * French, where apostrophe is commonly used. This utility * assumes that only an unpaired apostrophe immediately before * a brace is a true quote. Other unpaired apostrophes are paired, * and the resulting standard pattern string is returned. * *

Note: It is not guaranteed that the returned pattern * is indeed a valid pattern. The only effect is to convert * between patterns having different quoting semantics. * *

Note: This method only works on top-level messageText, * not messageText nested inside a complexArg. * * @param pattern the 'apostrophe-friendly' pattern to convert * @return the standard equivalent of the original pattern * @stable ICU 3.4 */ public static String autoQuoteApostrophe(String pattern) { StringBuilder buf = new StringBuilder(pattern.length() * 2); int state = STATE_INITIAL; int braceCount = 0; for (int i = 0, j = pattern.length(); i < j; ++i) { char c = pattern.charAt(i); switch (state) { case STATE_INITIAL: switch (c) { case SINGLE_QUOTE: state = STATE_SINGLE_QUOTE; break; case CURLY_BRACE_LEFT: state = STATE_MSG_ELEMENT; ++braceCount; break; } break; case STATE_SINGLE_QUOTE: switch (c) { case SINGLE_QUOTE: state = STATE_INITIAL; break; case CURLY_BRACE_LEFT: case CURLY_BRACE_RIGHT: state = STATE_IN_QUOTE; break; default: buf.append(SINGLE_QUOTE); state = STATE_INITIAL; break; } break; case STATE_IN_QUOTE: switch (c) { case SINGLE_QUOTE: state = STATE_INITIAL; break; } break; case STATE_MSG_ELEMENT: switch (c) { case CURLY_BRACE_LEFT: ++braceCount; break; case CURLY_BRACE_RIGHT: if (--braceCount == 0) { state = STATE_INITIAL; } break; } break; ///CLOVER:OFF default: // Never happens. break; ///CLOVER:ON } buf.append(c); } // End of scan if (state == STATE_SINGLE_QUOTE || state == STATE_IN_QUOTE) { buf.append(SINGLE_QUOTE); } return new String(buf); } /** * Convenience wrapper for Appendable, tracks the result string length. * Also, Appendable throws IOException, and we turn that into a RuntimeException * so that we need no throws clauses. */ private static final class AppendableWrapper { public AppendableWrapper(StringBuilder sb) { app = sb; length = sb.length(); attributes = null; } public AppendableWrapper(StringBuffer sb) { app = sb; length = sb.length(); attributes = null; } public void useAttributes() { attributes = new ArrayList<>(); } public void append(CharSequence s) { try { app.append(s); length += s.length(); } catch(IOException e) { throw new ICUUncheckedIOException(e); } } public void append(CharSequence s, int start, int limit) { try { app.append(s, start, limit); length += limit - start; } catch(IOException e) { throw new ICUUncheckedIOException(e); } } public void append(CharacterIterator iterator) { length += append(app, iterator); } public static int append(Appendable result, CharacterIterator iterator) { try { int start = iterator.getBeginIndex(); int limit = iterator.getEndIndex(); int length = limit - start; if (start < limit) { result.append(iterator.first()); while (++start < limit) { result.append(iterator.next()); } } return length; } catch(IOException e) { throw new ICUUncheckedIOException(e); } } public void formatAndAppend(Format formatter, Object arg) { if (attributes == null) { append(formatter.format(arg)); } else { AttributedCharacterIterator formattedArg = formatter.formatToCharacterIterator(arg); int prevLength = length; append(formattedArg); // Copy all of the attributes from formattedArg to our attributes list. formattedArg.first(); int start = formattedArg.getIndex(); // Should be 0 but might not be. int limit = formattedArg.getEndIndex(); // == start + length - prevLength int offset = prevLength - start; // Adjust attribute indexes for the result string. while (start < limit) { Map map = formattedArg.getAttributes(); int runLimit = formattedArg.getRunLimit(); if (map.size() != 0) { for (Map.Entry entry : map.entrySet()) { attributes.add( new AttributeAndPosition( entry.getKey(), entry.getValue(), offset + start, offset + runLimit)); } } start = runLimit; formattedArg.setIndex(start); } } } public void formatAndAppend(Format formatter, Object arg, String argString) { if (attributes == null && argString != null) { append(argString); } else { formatAndAppend(formatter, arg); } } private Appendable app; private int length; private List attributes; } private static final class AttributeAndPosition { /** * Defaults the field to Field.ARGUMENT. */ public AttributeAndPosition(Object fieldValue, int startIndex, int limitIndex) { init(Field.ARGUMENT, fieldValue, startIndex, limitIndex); } public AttributeAndPosition(Attribute field, Object fieldValue, int startIndex, int limitIndex) { init(field, fieldValue, startIndex, limitIndex); } public void init(Attribute field, Object fieldValue, int startIndex, int limitIndex) { key = field; value = fieldValue; start = startIndex; limit = limitIndex; } private Attribute key; private Object value; private int start; private int limit; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy