| /* MessageFormat.java - Localized message formatting. |
| Copyright (C) 1999, 2001, 2002, 2004, 2005 Free Software Foundation, Inc. |
| |
| This file is part of GNU Classpath. |
| |
| GNU Classpath is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2, or (at your option) |
| any later version. |
| |
| GNU Classpath is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with GNU Classpath; see the file COPYING. If not, write to the |
| Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA |
| 02111-1307 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| |
| package java.text; |
| |
| import gnu.java.text.FormatCharacterIterator; |
| |
| import java.io.InvalidObjectException; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Vector; |
| |
| public class MessageFormat extends Format |
| { |
| /** |
| * @author Tom Tromey (tromey@cygnus.com) |
| * @author Jorge Aliss (jaliss@hotmail.com) |
| * @date March 3, 1999 |
| */ |
| /* Written using "Java Class Libraries", 2nd edition, plus online |
| * API docs for JDK 1.2 from http://www.javasoft.com. |
| * Status: Believed complete and correct to 1.2, except serialization. |
| * and parsing. |
| */ |
| private static final class MessageFormatElement |
| { |
| // Argument number. |
| int argNumber; |
| // Formatter to be used. This is the format set by setFormat. |
| Format setFormat; |
| // Formatter to be used based on the type. |
| Format format; |
| |
| // Argument will be checked to make sure it is an instance of this |
| // class. |
| Class formatClass; |
| |
| // Formatter type. |
| String type; |
| // Formatter style. |
| String style; |
| |
| // Text to follow this element. |
| String trailer; |
| |
| // Recompute the locale-based formatter. |
| void setLocale (Locale loc) |
| { |
| if (type == null) |
| ; |
| else if (type.equals("number")) |
| { |
| formatClass = java.lang.Number.class; |
| |
| if (style == null) |
| format = NumberFormat.getInstance(loc); |
| else if (style.equals("currency")) |
| format = NumberFormat.getCurrencyInstance(loc); |
| else if (style.equals("percent")) |
| format = NumberFormat.getPercentInstance(loc); |
| else if (style.equals("integer")) |
| { |
| NumberFormat nf = NumberFormat.getNumberInstance(loc); |
| nf.setMaximumFractionDigits(0); |
| nf.setGroupingUsed(false); |
| format = nf; |
| } |
| else |
| { |
| format = NumberFormat.getNumberInstance(loc); |
| DecimalFormat df = (DecimalFormat) format; |
| df.applyPattern(style); |
| } |
| } |
| else if (type.equals("time") || type.equals("date")) |
| { |
| formatClass = java.util.Date.class; |
| |
| int val = DateFormat.DEFAULT; |
| boolean styleIsPattern = false; |
| if (style == null) |
| ; |
| else if (style.equals("short")) |
| val = DateFormat.SHORT; |
| else if (style.equals("medium")) |
| val = DateFormat.MEDIUM; |
| else if (style.equals("long")) |
| val = DateFormat.LONG; |
| else if (style.equals("full")) |
| val = DateFormat.FULL; |
| else |
| styleIsPattern = true; |
| |
| if (type.equals("time")) |
| format = DateFormat.getTimeInstance(val, loc); |
| else |
| format = DateFormat.getDateInstance(val, loc); |
| |
| if (styleIsPattern) |
| { |
| SimpleDateFormat sdf = (SimpleDateFormat) format; |
| sdf.applyPattern(style); |
| } |
| } |
| else if (type.equals("choice")) |
| { |
| formatClass = java.lang.Number.class; |
| |
| if (style == null) |
| throw new |
| IllegalArgumentException ("style required for choice format"); |
| format = new ChoiceFormat (style); |
| } |
| } |
| } |
| |
| private static final long serialVersionUID = 6479157306784022952L; |
| |
| public static class Field extends Format.Field |
| { |
| static final long serialVersionUID = 7899943957617360810L; |
| |
| /** |
| * This is the attribute set for all characters produced |
| * by MessageFormat during a formatting. |
| */ |
| public static final MessageFormat.Field ARGUMENT = new MessageFormat.Field("argument"); |
| |
| // For deserialization |
| private Field() |
| { |
| super(""); |
| } |
| |
| protected Field(String s) |
| { |
| super(s); |
| } |
| |
| /** |
| * invoked to resolve the true static constant by |
| * comparing the deserialized object to know name. |
| * |
| * @return object constant |
| */ |
| protected Object readResolve() throws InvalidObjectException |
| { |
| if (getName().equals(ARGUMENT.getName())) |
| return ARGUMENT; |
| |
| throw new InvalidObjectException("no such MessageFormat field called " + getName()); |
| } |
| |
| } |
| |
| // Helper that returns the text up to the next format opener. The |
| // text is put into BUFFER. Returns index of character after end of |
| // string. Throws IllegalArgumentException on error. |
| private static int scanString(String pat, int index, StringBuffer buffer) |
| { |
| int max = pat.length(); |
| buffer.setLength(0); |
| boolean quoted = false; |
| for (; index < max; ++index) |
| { |
| char c = pat.charAt(index); |
| if (quoted) |
| { |
| // In a quoted context, a single quote ends the quoting. |
| if (c == '\'') |
| quoted = false; |
| else |
| buffer.append(c); |
| } |
| // Check for '', which is a single quote. |
| else if (c == '\'' && index + 1 < max && pat.charAt(index + 1) == '\'') |
| { |
| buffer.append(c); |
| ++index; |
| } |
| else if (c == '\'') |
| { |
| // Start quoting. |
| quoted = true; |
| } |
| else if (c == '{') |
| break; |
| else |
| buffer.append(c); |
| } |
| // Note that we explicitly allow an unterminated quote. This is |
| // done for compatibility. |
| return index; |
| } |
| |
| // This helper retrieves a single part of a format element. Returns |
| // the index of the terminating character. |
| private static int scanFormatElement(String pat, int index, |
| StringBuffer buffer, char term) |
| { |
| int max = pat.length(); |
| buffer.setLength(0); |
| int brace_depth = 1; |
| boolean quoted = false; |
| |
| for (; index < max; ++index) |
| { |
| char c = pat.charAt(index); |
| // First see if we should turn off quoting. |
| if (quoted) |
| { |
| if (c == '\'') |
| quoted = false; |
| // In both cases we fall through to inserting the |
| // character here. |
| } |
| // See if we have just a plain quote to insert. |
| else if (c == '\'' && index + 1 < max |
| && pat.charAt(index + 1) == '\'') |
| { |
| buffer.append(c); |
| ++index; |
| } |
| // See if quoting should turn on. |
| else if (c == '\'') |
| quoted = true; |
| else if (c == '{') |
| ++brace_depth; |
| else if (c == '}') |
| { |
| if (--brace_depth == 0) |
| break; |
| } |
| // Check for TERM after braces, because TERM might be `}'. |
| else if (c == term) |
| break; |
| // All characters, including opening and closing quotes, are |
| // inserted here. |
| buffer.append(c); |
| } |
| return index; |
| } |
| |
| // This is used to parse a format element and whatever non-format |
| // text might trail it. |
| private static int scanFormat(String pat, int index, StringBuffer buffer, |
| Vector elts, Locale locale) |
| { |
| MessageFormatElement mfe = new MessageFormatElement (); |
| elts.addElement(mfe); |
| |
| int max = pat.length(); |
| |
| // Skip the opening `{'. |
| ++index; |
| |
| // Fetch the argument number. |
| index = scanFormatElement (pat, index, buffer, ','); |
| try |
| { |
| mfe.argNumber = Integer.parseInt(buffer.toString()); |
| } |
| catch (NumberFormatException nfx) |
| { |
| IllegalArgumentException iae = new IllegalArgumentException(pat); |
| iae.initCause(nfx); |
| throw iae; |
| } |
| |
| // Extract the element format. |
| if (index < max && pat.charAt(index) == ',') |
| { |
| index = scanFormatElement (pat, index + 1, buffer, ','); |
| mfe.type = buffer.toString(); |
| |
| // Extract the style. |
| if (index < max && pat.charAt(index) == ',') |
| { |
| index = scanFormatElement (pat, index + 1, buffer, '}'); |
| mfe.style = buffer.toString (); |
| } |
| } |
| |
| // Advance past the last terminator. |
| if (index >= max || pat.charAt(index) != '}') |
| throw new IllegalArgumentException("Missing '}' at end of message format"); |
| ++index; |
| |
| // Now fetch trailing string. |
| index = scanString (pat, index, buffer); |
| mfe.trailer = buffer.toString (); |
| |
| mfe.setLocale(locale); |
| |
| return index; |
| } |
| |
| /** |
| * Applies the specified pattern to this MessageFormat. |
| * |
| * @param aPattern The Pattern |
| */ |
| public void applyPattern (String newPattern) |
| { |
| pattern = newPattern; |
| |
| StringBuffer tempBuffer = new StringBuffer (); |
| |
| int index = scanString (newPattern, 0, tempBuffer); |
| leader = tempBuffer.toString(); |
| |
| Vector elts = new Vector (); |
| while (index < newPattern.length()) |
| index = scanFormat (newPattern, index, tempBuffer, elts, locale); |
| |
| elements = new MessageFormatElement[elts.size()]; |
| elts.copyInto(elements); |
| } |
| |
| /** |
| * Overrides Format.clone() |
| */ |
| public Object clone () |
| { |
| MessageFormat c = (MessageFormat) super.clone (); |
| c.elements = (MessageFormatElement[]) elements.clone (); |
| return c; |
| } |
| |
| /** |
| * Overrides Format.equals(Object obj) |
| */ |
| public boolean equals (Object obj) |
| { |
| if (! (obj instanceof MessageFormat)) |
| return false; |
| MessageFormat mf = (MessageFormat) obj; |
| return (pattern.equals(mf.pattern) |
| && locale.equals(mf.locale)); |
| } |
| |
| /** |
| * A convinience method to format patterns. |
| * |
| * @param aPattern The pattern used when formatting. |
| * @param arguments The array containing the objects to be formatted. |
| */ |
| public AttributedCharacterIterator formatToCharacterIterator (Object arguments) |
| { |
| Object[] arguments_array = (Object[])arguments; |
| FormatCharacterIterator iterator = new FormatCharacterIterator(); |
| |
| formatInternal(arguments_array, new StringBuffer(), null, iterator); |
| |
| return iterator; |
| } |
| |
| /** |
| * A convinience method to format patterns. |
| * |
| * @param aPattern The pattern used when formatting. |
| * @param arguments The array containing the objects to be formatted. |
| */ |
| public static String format (String pattern, Object arguments[]) |
| { |
| MessageFormat mf = new MessageFormat (pattern); |
| StringBuffer sb = new StringBuffer (); |
| FieldPosition fp = new FieldPosition (NumberFormat.INTEGER_FIELD); |
| return mf.formatInternal(arguments, sb, fp, null).toString(); |
| } |
| |
| /** |
| * Returns the pattern with the formatted objects. |
| * |
| * @param source The array containing the objects to be formatted. |
| * @param result The StringBuffer where the text is appened. |
| * @param fp A FieldPosition object (it is ignored). |
| */ |
| public final StringBuffer format (Object arguments[], StringBuffer appendBuf, |
| FieldPosition fp) |
| { |
| return formatInternal(arguments, appendBuf, fp, null); |
| } |
| |
| private StringBuffer formatInternal (Object arguments[], |
| StringBuffer appendBuf, |
| FieldPosition fp, |
| FormatCharacterIterator output_iterator) |
| { |
| appendBuf.append(leader); |
| if (output_iterator != null) |
| output_iterator.append(leader); |
| |
| for (int i = 0; i < elements.length; ++i) |
| { |
| Object thisArg = null; |
| boolean unavailable = false; |
| if (arguments == null || elements[i].argNumber >= arguments.length) |
| unavailable = true; |
| else |
| thisArg = arguments[elements[i].argNumber]; |
| |
| AttributedCharacterIterator iterator = null; |
| |
| Format formatter = null; |
| |
| if (fp != null && i == fp.getField() && fp.getFieldAttribute() == Field.ARGUMENT) |
| fp.setBeginIndex(appendBuf.length()); |
| |
| if (unavailable) |
| appendBuf.append("{" + elements[i].argNumber + "}"); |
| else |
| { |
| if (elements[i].setFormat != null) |
| formatter = elements[i].setFormat; |
| else if (elements[i].format != null) |
| { |
| if (elements[i].formatClass != null |
| && ! elements[i].formatClass.isInstance(thisArg)) |
| throw new IllegalArgumentException("Wrong format class"); |
| |
| formatter = elements[i].format; |
| } |
| else if (thisArg instanceof Number) |
| formatter = NumberFormat.getInstance(locale); |
| else if (thisArg instanceof Date) |
| formatter = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale); |
| else |
| appendBuf.append(thisArg); |
| } |
| |
| if (fp != null && fp.getField() == i && fp.getFieldAttribute() == Field.ARGUMENT) |
| fp.setEndIndex(appendBuf.length()); |
| |
| if (formatter != null) |
| { |
| // Special-case ChoiceFormat. |
| if (formatter instanceof ChoiceFormat) |
| { |
| StringBuffer buf = new StringBuffer (); |
| formatter.format(thisArg, buf, fp); |
| MessageFormat mf = new MessageFormat (); |
| mf.setLocale(locale); |
| mf.applyPattern(buf.toString()); |
| mf.format(arguments, appendBuf, fp); |
| } |
| else |
| { |
| if (output_iterator != null) |
| iterator = formatter.formatToCharacterIterator(thisArg); |
| else |
| formatter.format(thisArg, appendBuf, fp); |
| } |
| |
| elements[i].format = formatter; |
| } |
| |
| if (output_iterator != null) |
| { |
| HashMap hash_argument = new HashMap(); |
| int position = output_iterator.getEndIndex(); |
| |
| hash_argument.put (MessageFormat.Field.ARGUMENT, |
| new Integer(elements[i].argNumber)); |
| |
| |
| if (iterator != null) |
| { |
| output_iterator.append(iterator); |
| output_iterator.addAttributes(hash_argument, position, |
| output_iterator.getEndIndex()); |
| } |
| else |
| output_iterator.append(thisArg.toString(), hash_argument); |
| |
| output_iterator.append(elements[i].trailer); |
| } |
| |
| appendBuf.append(elements[i].trailer); |
| } |
| |
| return appendBuf; |
| } |
| |
| /** |
| * Returns the pattern with the formatted objects. The first argument |
| * must be a array of Objects. |
| * This is equivalent to format((Object[]) objectArray, appendBuf, fpos) |
| * |
| * @param objectArray The object array to be formatted. |
| * @param appendBuf The StringBuffer where the text is appened. |
| * @param fpos A FieldPosition object (it is ignored). |
| */ |
| public final StringBuffer format (Object objectArray, StringBuffer appendBuf, |
| FieldPosition fpos) |
| { |
| return format ((Object[])objectArray, appendBuf, fpos); |
| } |
| |
| /** |
| * Returns an array with the Formats for |
| * the arguments. |
| */ |
| public Format[] getFormats () |
| { |
| Format[] f = new Format[elements.length]; |
| for (int i = elements.length - 1; i >= 0; --i) |
| f[i] = elements[i].setFormat; |
| return f; |
| } |
| |
| /** |
| * Returns the locale. |
| */ |
| public Locale getLocale () |
| { |
| return locale; |
| } |
| |
| /** |
| * Overrides Format.hashCode() |
| */ |
| public int hashCode () |
| { |
| // FIXME: not a very good hash. |
| return pattern.hashCode() + locale.hashCode(); |
| } |
| |
| private MessageFormat () |
| { |
| } |
| |
| /** |
| * Creates a new MessageFormat object with |
| * the specified pattern |
| * |
| * @param pattern The Pattern |
| */ |
| public MessageFormat(String pattern) |
| { |
| this(pattern, Locale.getDefault()); |
| } |
| |
| /** |
| * Creates a new MessageFormat object with |
| * the specified pattern |
| * |
| * @param pattern The Pattern |
| * @param locale The Locale to use |
| * |
| * @since 1.4 |
| */ |
| public MessageFormat(String pattern, Locale locale) |
| { |
| this.locale = locale; |
| applyPattern (pattern); |
| } |
| |
| /** |
| * Parse a string <code>sourceStr</code> against the pattern specified |
| * to the MessageFormat constructor. |
| * |
| * @param sourceStr the string to be parsed. |
| * @param pos the current parse position (and eventually the error position). |
| * @return the array of parsed objects sorted according to their argument number |
| * in the pattern. |
| */ |
| public Object[] parse (String sourceStr, ParsePosition pos) |
| { |
| // Check initial text. |
| int index = pos.getIndex(); |
| if (! sourceStr.startsWith(leader, index)) |
| { |
| pos.setErrorIndex(index); |
| return null; |
| } |
| index += leader.length(); |
| |
| Vector results = new Vector (elements.length, 1); |
| // Now check each format. |
| for (int i = 0; i < elements.length; ++i) |
| { |
| Format formatter = null; |
| if (elements[i].setFormat != null) |
| formatter = elements[i].setFormat; |
| else if (elements[i].format != null) |
| formatter = elements[i].format; |
| |
| Object value = null; |
| if (formatter instanceof ChoiceFormat) |
| { |
| // We must special-case a ChoiceFormat because it might |
| // have recursive formatting. |
| ChoiceFormat cf = (ChoiceFormat) formatter; |
| String[] formats = (String[]) cf.getFormats(); |
| double[] limits = (double[]) cf.getLimits(); |
| MessageFormat subfmt = new MessageFormat (); |
| subfmt.setLocale(locale); |
| ParsePosition subpos = new ParsePosition (index); |
| |
| int j; |
| for (j = 0; value == null && j < limits.length; ++j) |
| { |
| subfmt.applyPattern(formats[j]); |
| subpos.setIndex(index); |
| value = subfmt.parse(sourceStr, subpos); |
| } |
| if (value != null) |
| { |
| index = subpos.getIndex(); |
| value = new Double (limits[j]); |
| } |
| } |
| else if (formatter != null) |
| { |
| pos.setIndex(index); |
| value = formatter.parseObject(sourceStr, pos); |
| if (value != null) |
| index = pos.getIndex(); |
| } |
| else |
| { |
| // We have a String format. This can lose in a number |
| // of ways, but we give it a shot. |
| int next_index = sourceStr.indexOf(elements[i].trailer, index); |
| if (next_index == -1) |
| { |
| pos.setErrorIndex(index); |
| return null; |
| } |
| value = sourceStr.substring(index, next_index); |
| index = next_index; |
| } |
| |
| if (value == null |
| || ! sourceStr.startsWith(elements[i].trailer, index)) |
| { |
| pos.setErrorIndex(index); |
| return null; |
| } |
| |
| if (elements[i].argNumber >= results.size()) |
| results.setSize(elements[i].argNumber + 1); |
| results.setElementAt(value, elements[i].argNumber); |
| |
| index += elements[i].trailer.length(); |
| } |
| |
| Object[] r = new Object[results.size()]; |
| results.copyInto(r); |
| return r; |
| } |
| |
| public Object[] parse (String sourceStr) throws ParseException |
| { |
| ParsePosition pp = new ParsePosition (0); |
| Object[] r = parse (sourceStr, pp); |
| if (r == null) |
| throw new ParseException ("couldn't parse string", pp.getErrorIndex()); |
| return r; |
| } |
| |
| public Object parseObject (String sourceStr, ParsePosition pos) |
| { |
| return parse (sourceStr, pos); |
| } |
| |
| /** |
| * Sets the format for the argument at an specified |
| * index. |
| * |
| * @param index The index. |
| * @format The Format object. |
| */ |
| public void setFormat (int variableNum, Format newFormat) |
| { |
| elements[variableNum].setFormat = newFormat; |
| } |
| |
| /** |
| * Sets the formats for the arguments. |
| * |
| * @param formats An array of Format objects. |
| */ |
| public void setFormats (Format[] newFormats) |
| { |
| if (newFormats.length < elements.length) |
| throw new IllegalArgumentException("Not enough format objects"); |
| |
| int len = Math.min(newFormats.length, elements.length); |
| for (int i = 0; i < len; ++i) |
| elements[i].setFormat = newFormats[i]; |
| } |
| |
| /** |
| * Sets the locale. |
| * |
| * @param locale A Locale |
| */ |
| public void setLocale (Locale loc) |
| { |
| locale = loc; |
| if (elements != null) |
| { |
| for (int i = 0; i < elements.length; ++i) |
| elements[i].setLocale(loc); |
| } |
| } |
| |
| /** |
| * Returns the pattern. |
| */ |
| public String toPattern () |
| { |
| return pattern; |
| } |
| |
| /** |
| * Return the formatters used sorted by argument index. It uses the |
| * internal table to fill in this array: if a format has been |
| * set using <code>setFormat</code> or <code>setFormatByArgumentIndex</code> |
| * then it returns it at the right index. If not it uses the detected |
| * formatters during a <code>format</code> call. If nothing is known |
| * about that argument index it just puts null at that position. |
| * To get useful informations you may have to call <code>format</code> |
| * at least once. |
| * |
| * @return an array of formatters sorted by argument index. |
| */ |
| public Format[] getFormatsByArgumentIndex() |
| { |
| int argNumMax = 0; |
| // First, find the greatest argument number. |
| for (int i=0;i<elements.length;i++) |
| if (elements[i].argNumber > argNumMax) |
| argNumMax = elements[i].argNumber; |
| |
| Format[] formats = new Format[argNumMax]; |
| for (int i=0;i<elements.length;i++) |
| { |
| if (elements[i].setFormat != null) |
| formats[elements[i].argNumber] = elements[i].setFormat; |
| else if (elements[i].format != null) |
| formats[elements[i].argNumber] = elements[i].format; |
| } |
| return formats; |
| } |
| |
| /** |
| * Set the format to used using the argument index number. |
| * |
| * @param argumentIndex the argument index. |
| * @param newFormat the format to use for this argument. |
| */ |
| public void setFormatByArgumentIndex(int argumentIndex, |
| Format newFormat) |
| { |
| for (int i=0;i<elements.length;i++) |
| { |
| if (elements[i].argNumber == argumentIndex) |
| elements[i].setFormat = newFormat; |
| } |
| } |
| |
| /** |
| * Set the format for argument using a specified array of formatters |
| * which is sorted according to the argument index. If the number of |
| * elements in the array is fewer than the number of arguments only |
| * the arguments specified by the array are touched. |
| * |
| * @param newFormats array containing the new formats to set. |
| * |
| * @throws NullPointerException if newFormats is null |
| */ |
| public void setFormatsByArgumentIndex(Format[] newFormats) |
| { |
| for (int i=0;i<newFormats.length;i++) |
| { |
| // Nothing better than that can exist here. |
| setFormatByArgumentIndex(i, newFormats[i]); |
| } |
| } |
| |
| // The pattern string. |
| private String pattern; |
| // The locale. |
| private Locale locale; |
| // Variables. |
| private MessageFormatElement[] elements; |
| // Leader text. |
| private String leader; |
| } |