001    /* MessageFormat.java - Localized message formatting.
002       Copyright (C) 1999, 2001, 2002, 2004, 2005 Free Software Foundation, Inc.
003    
004    This file is part of GNU Classpath.
005    
006    GNU Classpath is free software; you can redistribute it and/or modify
007    it under the terms of the GNU General Public License as published by
008    the Free Software Foundation; either version 2, or (at your option)
009    any later version.
010    
011    GNU Classpath is distributed in the hope that it will be useful, but
012    WITHOUT ANY WARRANTY; without even the implied warranty of
013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014    General Public License for more details.
015    
016    You should have received a copy of the GNU General Public License
017    along with GNU Classpath; see the file COPYING.  If not, write to the
018    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019    02110-1301 USA.
020    
021    Linking this library statically or dynamically with other modules is
022    making a combined work based on this library.  Thus, the terms and
023    conditions of the GNU General Public License cover the whole
024    combination.
025    
026    As a special exception, the copyright holders of this library give you
027    permission to link this library with independent modules to produce an
028    executable, regardless of the license terms of these independent
029    modules, and to copy and distribute the resulting executable under
030    terms of your choice, provided that you also meet, for each linked
031    independent module, the terms and conditions of the license of that
032    module.  An independent module is a module which is not derived from
033    or based on this library.  If you modify this library, you may extend
034    this exception to your version of the library, but you are not
035    obligated to do so.  If you do not wish to do so, delete this
036    exception statement from your version. */
037    
038    
039    package java.text;
040    
041    import gnu.java.lang.CPStringBuilder;
042    
043    import gnu.java.text.FormatCharacterIterator;
044    
045    import java.io.InvalidObjectException;
046    
047    import java.util.ArrayList;
048    import java.util.Date;
049    import java.util.HashMap;
050    import java.util.List;
051    import java.util.Locale;
052    
053    public class MessageFormat extends Format
054    {
055      /**
056       * @author Tom Tromey (tromey@cygnus.com)
057       * @author Jorge Aliss (jaliss@hotmail.com)
058       * @date March 3, 1999
059       */
060      /* Written using "Java Class Libraries", 2nd edition, plus online
061       * API docs for JDK 1.2 from http://www.javasoft.com.
062       * Status:  Believed complete and correct to 1.2, except serialization.
063       *          and parsing.
064       */
065      private static final class MessageFormatElement
066      {
067        // Argument number.
068        int argNumber;
069        // Formatter to be used.  This is the format set by setFormat.
070        Format setFormat;
071        // Formatter to be used based on the type.
072        Format format;
073    
074        // Argument will be checked to make sure it is an instance of this
075        // class.
076        Class<?> formatClass;
077    
078        // Formatter type.
079        String type;
080        // Formatter style.
081        String style;
082    
083        // Text to follow this element.
084        String trailer;
085    
086        // Recompute the locale-based formatter.
087        void setLocale (Locale loc)
088        {
089          if (type != null)
090            {
091              if (type.equals("number"))
092                {
093                  formatClass = java.lang.Number.class;
094    
095                  if (style == null)
096                    format = NumberFormat.getInstance(loc);
097                  else if (style.equals("currency"))
098                    format = NumberFormat.getCurrencyInstance(loc);
099                  else if (style.equals("percent"))
100                    format = NumberFormat.getPercentInstance(loc);
101                  else if (style.equals("integer"))
102                    format = NumberFormat.getIntegerInstance(loc);
103                  else
104                    {
105                      format = NumberFormat.getNumberInstance(loc);
106                      DecimalFormat df = (DecimalFormat) format;
107                      df.applyPattern(style);
108                    }
109                }
110              else if (type.equals("time") || type.equals("date"))
111                {
112                  formatClass = java.util.Date.class;
113    
114                  int val = DateFormat.DEFAULT;
115                  boolean styleIsPattern = false;
116                  if (style != null)
117                    {
118                      if (style.equals("short"))
119                        val = DateFormat.SHORT;
120                      else if (style.equals("medium"))
121                        val = DateFormat.MEDIUM;
122                      else if (style.equals("long"))
123                        val = DateFormat.LONG;
124                      else if (style.equals("full"))
125                        val = DateFormat.FULL;
126                      else
127                        styleIsPattern = true;
128                    }
129    
130                  if (type.equals("time"))
131                    format = DateFormat.getTimeInstance(val, loc);
132                  else
133                    format = DateFormat.getDateInstance(val, loc);
134    
135                  if (styleIsPattern)
136                    {
137                      SimpleDateFormat sdf = (SimpleDateFormat) format;
138                      sdf.applyPattern(style);
139                    }
140                }
141              else if (type.equals("choice"))
142                {
143                  formatClass = java.lang.Number.class;
144    
145                  if (style == null)
146                    throw new
147                    IllegalArgumentException ("style required for choice format");
148                  format = new ChoiceFormat (style);
149                }
150            }
151        }
152      }
153    
154      private static final long serialVersionUID = 6479157306784022952L;
155    
156      public static class Field extends Format.Field
157      {
158        static final long serialVersionUID = 7899943957617360810L;
159    
160        /**
161         * This is the attribute set for all characters produced
162         * by MessageFormat during a formatting.
163         */
164        public static final MessageFormat.Field ARGUMENT = new MessageFormat.Field("argument");
165    
166        // For deserialization
167        @SuppressWarnings("unused")
168        private Field()
169        {
170          super("");
171        }
172    
173        protected Field(String s)
174        {
175          super(s);
176        }
177    
178        /**
179         * invoked to resolve the true static constant by
180         * comparing the deserialized object to know name.
181         *
182         * @return object constant
183         */
184        protected Object readResolve() throws InvalidObjectException
185        {
186          if (getName().equals(ARGUMENT.getName()))
187            return ARGUMENT;
188    
189          throw new InvalidObjectException("no such MessageFormat field called " + getName());
190        }
191    
192      }
193    
194      // Helper that returns the text up to the next format opener.  The
195      // text is put into BUFFER.  Returns index of character after end of
196      // string.  Throws IllegalArgumentException on error.
197      private static int scanString(String pat, int index, CPStringBuilder buffer)
198      {
199        int max = pat.length();
200        buffer.setLength(0);
201        boolean quoted = false;
202        for (; index < max; ++index)
203          {
204            char c = pat.charAt(index);
205            if (quoted)
206              {
207                // In a quoted context, a single quote ends the quoting.
208                if (c == '\'')
209                  quoted = false;
210                else
211                  buffer.append(c);
212              }
213            // Check for '', which is a single quote.
214            else if (c == '\'' && index + 1 < max && pat.charAt(index + 1) == '\'')
215              {
216                buffer.append(c);
217                ++index;
218              }
219            else if (c == '\'')
220              {
221                // Start quoting.
222                quoted = true;
223              }
224            else if (c == '{')
225              break;
226            else
227              buffer.append(c);
228          }
229        // Note that we explicitly allow an unterminated quote.  This is
230        // done for compatibility.
231        return index;
232      }
233    
234      // This helper retrieves a single part of a format element.  Returns
235      // the index of the terminating character.
236      private static int scanFormatElement(String pat, int index,
237                                           CPStringBuilder buffer, char term)
238      {
239        int max = pat.length();
240        buffer.setLength(0);
241        int brace_depth = 1;
242        boolean quoted = false;
243    
244        for (; index < max; ++index)
245          {
246            char c = pat.charAt(index);
247            // First see if we should turn off quoting.
248            if (quoted)
249              {
250                if (c == '\'')
251                  quoted = false;
252                // In both cases we fall through to inserting the
253                // character here.
254              }
255            // See if we have just a plain quote to insert.
256            else if (c == '\'' && index + 1 < max
257                     && pat.charAt(index + 1) == '\'')
258              {
259                buffer.append(c);
260                ++index;
261              }
262            // See if quoting should turn on.
263            else if (c == '\'')
264              quoted = true;
265            else if (c == '{')
266              ++brace_depth;
267            else if (c == '}')
268              {
269                if (--brace_depth == 0)
270                  break;
271              }
272            // Check for TERM after braces, because TERM might be `}'.
273            else if (c == term)
274              break;
275            // All characters, including opening and closing quotes, are
276            // inserted here.
277            buffer.append(c);
278          }
279        return index;
280      }
281    
282      // This is used to parse a format element and whatever non-format
283      // text might trail it.
284      private static int scanFormat(String pat, int index, CPStringBuilder buffer,
285                                    List<MessageFormatElement> elts, Locale locale)
286      {
287        MessageFormatElement mfe = new MessageFormatElement ();
288        elts.add(mfe);
289    
290        int max = pat.length();
291    
292        // Skip the opening `{'.
293        ++index;
294    
295        // Fetch the argument number.
296        index = scanFormatElement (pat, index, buffer, ',');
297        try
298          {
299            mfe.argNumber = Integer.parseInt(buffer.toString());
300          }
301        catch (NumberFormatException nfx)
302          {
303            IllegalArgumentException iae = new IllegalArgumentException(pat);
304            iae.initCause(nfx);
305            throw iae;
306          }
307    
308        // Extract the element format.
309        if (index < max && pat.charAt(index) == ',')
310          {
311            index = scanFormatElement (pat, index + 1, buffer, ',');
312            mfe.type = buffer.toString();
313    
314            // Extract the style.
315            if (index < max && pat.charAt(index) == ',')
316              {
317                index = scanFormatElement (pat, index + 1, buffer, '}');
318                mfe.style = buffer.toString ();
319              }
320          }
321    
322        // Advance past the last terminator.
323        if (index >= max || pat.charAt(index) != '}')
324          throw new IllegalArgumentException("Missing '}' at end of message format");
325        ++index;
326    
327        // Now fetch trailing string.
328        index = scanString (pat, index, buffer);
329        mfe.trailer = buffer.toString ();
330    
331        mfe.setLocale(locale);
332    
333        return index;
334      }
335    
336      /**
337       * Applies the specified pattern to this MessageFormat.
338       *
339       * @param newPattern The Pattern
340       */
341      public void applyPattern (String newPattern)
342      {
343        pattern = newPattern;
344    
345        CPStringBuilder tempBuffer = new CPStringBuilder ();
346    
347        int index = scanString (newPattern, 0, tempBuffer);
348        leader = tempBuffer.toString();
349    
350        List<MessageFormatElement> elts = new ArrayList<MessageFormatElement>();
351        while (index < newPattern.length())
352          index = scanFormat (newPattern, index, tempBuffer, elts, locale);
353    
354        elements = elts.toArray(new MessageFormatElement[elts.size()]);
355      }
356    
357      /**
358       * Overrides Format.clone()
359       */
360      public Object clone ()
361      {
362        MessageFormat c = (MessageFormat) super.clone ();
363        c.elements = (MessageFormatElement[]) elements.clone ();
364        return c;
365      }
366    
367      /**
368       * Overrides Format.equals(Object obj)
369       */
370      public boolean equals (Object obj)
371      {
372        if (! (obj instanceof MessageFormat))
373          return false;
374        MessageFormat mf = (MessageFormat) obj;
375        return (pattern.equals(mf.pattern)
376                && locale.equals(mf.locale));
377      }
378    
379      /**
380       * A convinience method to format patterns.
381       *
382       * @param arguments The array containing the objects to be formatted.
383       */
384      public AttributedCharacterIterator formatToCharacterIterator (Object arguments)
385      {
386        Object[] arguments_array = (Object[])arguments;
387        FormatCharacterIterator iterator = new FormatCharacterIterator();
388    
389        formatInternal(arguments_array, new StringBuffer(), null, iterator);
390    
391        return iterator;
392      }
393    
394      /**
395       * A convinience method to format patterns.
396       *
397       * @param pattern The pattern used when formatting.
398       * @param arguments The array containing the objects to be formatted.
399       */
400      public static String format (String pattern, Object... arguments)
401      {
402        MessageFormat mf = new MessageFormat (pattern);
403        StringBuffer sb = new StringBuffer ();
404        FieldPosition fp = new FieldPosition (NumberFormat.INTEGER_FIELD);
405        return mf.formatInternal(arguments, sb, fp, null).toString();
406      }
407    
408      /**
409       * Returns the pattern with the formatted objects.
410       *
411       * @param arguments The array containing the objects to be formatted.
412       * @param appendBuf The StringBuffer where the text is appened.
413       * @param fp A FieldPosition object (it is ignored).
414       */
415      public final StringBuffer format (Object arguments[], StringBuffer appendBuf,
416                                        FieldPosition fp)
417      {
418        return formatInternal(arguments, appendBuf, fp, null);
419      }
420    
421      private StringBuffer formatInternal (Object arguments[],
422                                           StringBuffer appendBuf,
423                                           FieldPosition fp,
424                                           FormatCharacterIterator output_iterator)
425      {
426        appendBuf.append(leader);
427        if (output_iterator != null)
428          output_iterator.append(leader);
429    
430        for (int i = 0; i < elements.length; ++i)
431          {
432            Object thisArg = null;
433            boolean unavailable = false;
434            if (arguments == null || elements[i].argNumber >= arguments.length)
435              unavailable = true;
436            else
437              thisArg = arguments[elements[i].argNumber];
438    
439            AttributedCharacterIterator iterator = null;
440    
441            Format formatter = null;
442    
443            if (fp != null && i == fp.getField() && fp.getFieldAttribute() == Field.ARGUMENT)
444              fp.setBeginIndex(appendBuf.length());
445    
446            if (unavailable)
447              appendBuf.append("{" + elements[i].argNumber + "}");
448            else
449              {
450                if (elements[i].setFormat != null)
451                  formatter = elements[i].setFormat;
452                else if (elements[i].format != null)
453                  {
454                    if (elements[i].formatClass != null
455                        && ! elements[i].formatClass.isInstance(thisArg))
456                      throw new IllegalArgumentException("Wrong format class");
457    
458                    formatter = elements[i].format;
459                  }
460                else if (thisArg instanceof Number)
461                  formatter = NumberFormat.getInstance(locale);
462                else if (thisArg instanceof Date)
463                  formatter = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale);
464                else
465                  appendBuf.append(thisArg);
466              }
467    
468            if (fp != null && fp.getField() == i && fp.getFieldAttribute() == Field.ARGUMENT)
469              fp.setEndIndex(appendBuf.length());
470    
471            if (formatter != null)
472              {
473                // Special-case ChoiceFormat.
474                if (formatter instanceof ChoiceFormat)
475                  {
476                    StringBuffer buf = new StringBuffer ();
477                    formatter.format(thisArg, buf, fp);
478                    MessageFormat mf = new MessageFormat ();
479                    mf.setLocale(locale);
480                    mf.applyPattern(buf.toString());
481                    mf.format(arguments, appendBuf, fp);
482                  }
483                else
484                  {
485                    if (output_iterator != null)
486                      iterator = formatter.formatToCharacterIterator(thisArg);
487                    else
488                      formatter.format(thisArg, appendBuf, fp);
489                  }
490    
491                elements[i].format = formatter;
492              }
493    
494            if (output_iterator != null)
495              {
496                HashMap<MessageFormat.Field, Integer> hash_argument =
497                  new HashMap<MessageFormat.Field, Integer>();
498                int position = output_iterator.getEndIndex();
499    
500                hash_argument.put (MessageFormat.Field.ARGUMENT,
501                                   Integer.valueOf(elements[i].argNumber));
502    
503    
504                if (iterator != null)
505                  {
506                    output_iterator.append(iterator);
507                    output_iterator.addAttributes(hash_argument, position,
508                                                  output_iterator.getEndIndex());
509                  }
510                else
511                  output_iterator.append(thisArg.toString(), hash_argument);
512    
513                output_iterator.append(elements[i].trailer);
514              }
515    
516            appendBuf.append(elements[i].trailer);
517          }
518    
519        return appendBuf;
520      }
521    
522      /**
523       * Returns the pattern with the formatted objects.  The first argument
524       * must be a array of Objects.
525       * This is equivalent to format((Object[]) objectArray, appendBuf, fpos)
526       *
527       * @param objectArray The object array to be formatted.
528       * @param appendBuf The StringBuffer where the text is appened.
529       * @param fpos A FieldPosition object (it is ignored).
530       */
531      public final StringBuffer format (Object objectArray, StringBuffer appendBuf,
532                                        FieldPosition fpos)
533      {
534        return format ((Object[])objectArray, appendBuf, fpos);
535      }
536    
537      /**
538       * Returns an array with the Formats for
539       * the arguments.
540       */
541      public Format[] getFormats ()
542      {
543        Format[] f = new Format[elements.length];
544        for (int i = elements.length - 1; i >= 0; --i)
545          f[i] = elements[i].setFormat;
546        return f;
547      }
548    
549      /**
550       * Returns the locale.
551       */
552      public Locale getLocale ()
553      {
554        return locale;
555      }
556    
557      /**
558       * Overrides Format.hashCode()
559       */
560      public int hashCode ()
561      {
562        // FIXME: not a very good hash.
563        return pattern.hashCode() + locale.hashCode();
564      }
565    
566      private MessageFormat ()
567      {
568      }
569    
570      /**
571       * Creates a new MessageFormat object with
572       * the specified pattern
573       *
574       * @param pattern The Pattern
575       */
576      public MessageFormat(String pattern)
577      {
578        this(pattern, Locale.getDefault());
579      }
580    
581      /**
582       * Creates a new MessageFormat object with
583       * the specified pattern
584       *
585       * @param pattern The Pattern
586       * @param locale The Locale to use
587       *
588       * @since 1.4
589       */
590      public MessageFormat(String pattern, Locale locale)
591      {
592        this.locale = locale;
593        applyPattern (pattern);
594      }
595    
596      /**
597       * Parse a string <code>sourceStr</code> against the pattern specified
598       * to the MessageFormat constructor.
599       *
600       * @param sourceStr the string to be parsed.
601       * @param pos the current parse position (and eventually the error position).
602       * @return the array of parsed objects sorted according to their argument number
603       * in the pattern.
604       */
605      public Object[] parse (String sourceStr, ParsePosition pos)
606      {
607        // Check initial text.
608        int index = pos.getIndex();
609        if (! sourceStr.startsWith(leader, index))
610          {
611            pos.setErrorIndex(index);
612            return null;
613          }
614        index += leader.length();
615    
616        ArrayList<Object> results = new ArrayList<Object>(elements.length);
617        // Now check each format.
618        for (int i = 0; i < elements.length; ++i)
619          {
620            Format formatter = null;
621            if (elements[i].setFormat != null)
622              formatter = elements[i].setFormat;
623            else if (elements[i].format != null)
624              formatter = elements[i].format;
625    
626            Object value = null;
627            if (formatter instanceof ChoiceFormat)
628              {
629                // We must special-case a ChoiceFormat because it might
630                // have recursive formatting.
631                ChoiceFormat cf = (ChoiceFormat) formatter;
632                String[] formats = (String[]) cf.getFormats();
633                double[] limits = cf.getLimits();
634                MessageFormat subfmt = new MessageFormat ();
635                subfmt.setLocale(locale);
636                ParsePosition subpos = new ParsePosition (index);
637    
638                int j;
639                for (j = 0; value == null && j < limits.length; ++j)
640                  {
641                    subfmt.applyPattern(formats[j]);
642                    subpos.setIndex(index);
643                    value = subfmt.parse(sourceStr, subpos);
644                  }
645                if (value != null)
646                  {
647                    index = subpos.getIndex();
648                    value = new Double (limits[j]);
649                  }
650              }
651            else if (formatter != null)
652              {
653                pos.setIndex(index);
654                value = formatter.parseObject(sourceStr, pos);
655                if (value != null)
656                  index = pos.getIndex();
657              }
658            else
659              {
660                // We have a String format.  This can lose in a number
661                // of ways, but we give it a shot.
662                int next_index;
663                if (elements[i].trailer.length() > 0)
664                  next_index = sourceStr.indexOf(elements[i].trailer, index);
665                else
666                  next_index = sourceStr.length();
667                if (next_index == -1)
668                  {
669                    pos.setErrorIndex(index);
670                    return null;
671                  }
672                value = sourceStr.substring(index, next_index);
673                index = next_index;
674              }
675    
676            if (value == null
677                || ! sourceStr.startsWith(elements[i].trailer, index))
678              {
679                pos.setErrorIndex(index);
680                return null;
681              }
682    
683            if (elements[i].argNumber >= results.size())
684              {
685                // Emulate padding behaviour of Vector.setSize() with ArrayList
686                results.ensureCapacity(elements[i].argNumber + 1);
687                for (int a = results.size(); a <= elements[i].argNumber; ++a)
688                  results.add(a, null);
689              }
690            results.set(elements[i].argNumber, value);
691    
692            index += elements[i].trailer.length();
693          }
694    
695        return results.toArray(new Object[results.size()]);
696      }
697    
698      public Object[] parse (String sourceStr) throws ParseException
699      {
700        ParsePosition pp = new ParsePosition (0);
701        Object[] r = parse (sourceStr, pp);
702        if (r == null)
703          throw new ParseException ("couldn't parse string", pp.getErrorIndex());
704        return r;
705      }
706    
707      public Object parseObject (String sourceStr, ParsePosition pos)
708      {
709        return parse (sourceStr, pos);
710      }
711    
712      /**
713       * Sets the format for the argument at an specified
714       * index.
715       *
716       * @param variableNum The index.
717       * @param newFormat The Format object.
718       */
719      public void setFormat (int variableNum, Format newFormat)
720      {
721        elements[variableNum].setFormat = newFormat;
722      }
723    
724      /**
725       * Sets the formats for the arguments.
726       *
727       * @param newFormats An array of Format objects.
728       */
729      public void setFormats (Format[] newFormats)
730      {
731        if (newFormats.length < elements.length)
732          throw new IllegalArgumentException("Not enough format objects");
733    
734        int len = Math.min(newFormats.length, elements.length);
735        for (int i = 0; i < len; ++i)
736          elements[i].setFormat = newFormats[i];
737      }
738    
739      /**
740       * Sets the locale.
741       *
742       * @param loc A Locale
743       */
744      public void setLocale (Locale loc)
745      {
746        locale = loc;
747        if (elements != null)
748          {
749            for (int i = 0; i < elements.length; ++i)
750              elements[i].setLocale(loc);
751          }
752      }
753    
754      /**
755       * Returns the pattern.
756       */
757      public String toPattern ()
758      {
759        return pattern;
760      }
761    
762      /**
763       * Return the formatters used sorted by argument index. It uses the
764       * internal table to fill in this array: if a format has been
765       * set using <code>setFormat</code> or <code>setFormatByArgumentIndex</code>
766       * then it returns it at the right index. If not it uses the detected
767       * formatters during a <code>format</code> call. If nothing is known
768       * about that argument index it just puts null at that position.
769       * To get useful informations you may have to call <code>format</code>
770       * at least once.
771       *
772       * @return an array of formatters sorted by argument index.
773       */
774      public Format[] getFormatsByArgumentIndex()
775      {
776        int argNumMax = 0;
777        // First, find the greatest argument number.
778        for (int i=0;i<elements.length;i++)
779          if (elements[i].argNumber > argNumMax)
780            argNumMax = elements[i].argNumber;
781    
782        Format[] formats = new Format[argNumMax];
783        for (int i=0;i<elements.length;i++)
784          {
785            if (elements[i].setFormat != null)
786              formats[elements[i].argNumber] = elements[i].setFormat;
787            else if (elements[i].format != null)
788              formats[elements[i].argNumber] = elements[i].format;
789          }
790        return formats;
791      }
792    
793      /**
794       * Set the format to used using the argument index number.
795       *
796       * @param argumentIndex the argument index.
797       * @param newFormat the format to use for this argument.
798       */
799      public void setFormatByArgumentIndex(int argumentIndex,
800                                           Format newFormat)
801      {
802        for (int i=0;i<elements.length;i++)
803          {
804            if (elements[i].argNumber == argumentIndex)
805              elements[i].setFormat = newFormat;
806          }
807      }
808    
809      /**
810       * Set the format for argument using a specified array of formatters
811       * which is sorted according to the argument index. If the number of
812       * elements in the array is fewer than the number of arguments only
813       * the arguments specified by the array are touched.
814       *
815       * @param newFormats array containing the new formats to set.
816       *
817       * @throws NullPointerException if newFormats is null
818       */
819      public void setFormatsByArgumentIndex(Format[] newFormats)
820      {
821        for (int i=0;i<newFormats.length;i++)
822          {
823            // Nothing better than that can exist here.
824            setFormatByArgumentIndex(i, newFormats[i]);
825          }
826      }
827    
828      // The pattern string.
829      private String pattern;
830      // The locale.
831      private Locale locale;
832      // Variables.
833      private MessageFormatElement[] elements;
834      // Leader text.
835      private String leader;
836    }