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 }