1. /*
  2. * @(#)MessageFormat.java 1.33 01/11/29
  3. *
  4. * Copyright 2002 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. /*
  8. * @(#)MessageFormat.java 1.33 01/11/29
  9. *
  10. * (C) Copyright Taligent, Inc. 1996, 1997 - All Rights Reserved
  11. * (C) Copyright IBM Corp. 1996 - 1998 - All Rights Reserved
  12. *
  13. * Portions copyright (c) 1996-1998 Sun Microsystems, Inc.
  14. * All Rights Reserved.
  15. *
  16. * The original version of this source code and documentation is copyrighted
  17. * and owned by Taligent, Inc., a wholly-owned subsidiary of IBM. These
  18. * materials are provided under terms of a License Agreement between Taligent
  19. * and Sun. This technology is protected by multiple US and International
  20. * patents. This notice and attribution to Taligent may not be removed.
  21. * Taligent is a registered trademark of Taligent, Inc.
  22. *
  23. * Permission to use, copy, modify, and distribute this software
  24. * and its documentation for NON-COMMERCIAL purposes and without
  25. * fee is hereby granted provided that this copyright notice
  26. * appears in all copies. Please refer to the file "copyright.html"
  27. * for further important copyright and licensing information.
  28. *
  29. * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF
  30. * THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
  31. * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
  32. * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR
  33. * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
  34. * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
  35. *
  36. */
  37. package java.text;
  38. import java.util.Date;
  39. import java.util.Locale;
  40. import java.text.DecimalFormat;
  41. import java.text.Utility;
  42. /**
  43. * <code>MessageFormat</code> provides a means to produce concatenated
  44. * messages in language-neutral way. Use this to construct messages
  45. * displayed for end users.
  46. *
  47. * <p>
  48. * <code>MessageFormat</code> takes a set of objects, formats them, then
  49. * inserts the formatted strings into the pattern at the appropriate places.
  50. *
  51. * <p>
  52. * <strong>Note:</strong>
  53. * <code>MessageFormat</code> differs from the other <code>Format</code>
  54. * classes in that you create a <code>MessageFormat</code> object with one
  55. * of its constructors (not with a <code>getInstance</code> style factory
  56. * method). The factory methods aren't necessary because <code>MessageFormat</code>
  57. * doesn't require any complex setup for a given locale. In fact,
  58. * <code>MessageFormat</code> doesn't implement any locale specific behavior
  59. * at all. It just needs to be set up on a sentence by sentence basis.
  60. *
  61. * <p>
  62. * Here are some examples of usage:
  63. * <blockquote>
  64. * <pre>
  65. * Object[] arguments = {
  66. * new Integer(7),
  67. * new Date(System.currentTimeMillis()),
  68. * "a disturbance in the Force"
  69. * };
  70. *
  71. * String result = MessageFormat.format(
  72. * "At {1,time} on {1,date}, there was {2} on planet {0,number,integer}.",
  73. * arguments);
  74. *
  75. * <em>output</em>: At 12:30 PM on Jul 3, 2053, there was a disturbance
  76. * in the Force on planet 7.
  77. *
  78. * </pre>
  79. * </blockquote>
  80. * Typically, the message format will come from resources, and the
  81. * arguments will be dynamically set at runtime.
  82. *
  83. * <p>
  84. * Example 2:
  85. * <blockquote>
  86. * <pre>
  87. * Object[] testArgs = {new Long(3), "MyDisk"};
  88. *
  89. * MessageFormat form = new MessageFormat(
  90. * "The disk \"{1}\" contains {0} file(s).");
  91. *
  92. * System.out.println(form.format(testArgs));
  93. *
  94. * // output, with different testArgs
  95. * <em>output</em>: The disk "MyDisk" contains 0 file(s).
  96. * <em>output</em>: The disk "MyDisk" contains 1 file(s).
  97. * <em>output</em>: The disk "MyDisk" contains 1,273 file(s).
  98. * </pre>
  99. * </blockquote>
  100. *
  101. * <p>
  102. * The pattern is of the form:
  103. * <blockquote>
  104. * <pre>
  105. * messageFormatPattern := string ( "{" messageFormatElement "}" string )*
  106. *
  107. * messageFormatElement := argument { "," elementFormat }
  108. *
  109. * elementFormat := "time" { "," datetimeStyle }
  110. * | "date" { "," datetimeStyle }
  111. * | "number" { "," numberStyle }
  112. * | "choice" { "," choiceStyle }
  113. *
  114. * datetimeStyle := "short"
  115. * | "medium"
  116. * | "long"
  117. * | "full"
  118. * | dateFormatPattern
  119. *
  120. * numberStyle := "currency"
  121. * | "percent"
  122. * | "integer"
  123. * | numberFormatPattern
  124. *
  125. * choiceStyle := choiceFormatPattern
  126. * </pre>
  127. * </blockquote>
  128. * If there is no <code>elementFormat</code>,
  129. * then the argument must be a string, which is substituted. If there is
  130. * no <code>dateTimeStyle</code> or <code>numberStyle</code>, then the
  131. * default format is used (for example, <code>NumberFormat.getInstance</code>,
  132. * <code>DateFormat.getTimeInstance</code>, or <code>DateFormat.getInstance</code>).
  133. *
  134. * <p>
  135. * In strings, single quotes can be used to quote the "{"
  136. * (curly brace) if necessary. A real single quote is represented by ''.
  137. * Inside a <code>messageFormatElement</code>, quotes are <strong>not</strong>
  138. * removed. For example, {1,number,$'#',##} will produce a number format
  139. * with the pound-sign quoted, with a result such as: "$#31,45".
  140. *
  141. * <p>
  142. * If a pattern is used, then unquoted braces in the pattern, if any, must match:
  143. * that is, "ab {0} de" and "ab '}' de" are ok, but "ab {0'}' de" and "ab } de" are
  144. * not.
  145. *
  146. * <p>
  147. * The argument is a number from 0 to 9, which corresponds to the
  148. * arguments presented in an array to be formatted.
  149. *
  150. * <p>
  151. * It is ok to have unused arguments in the array.
  152. * With missing arguments or arguments that are not of the right class for
  153. * the specified format, a <code>ParseException</code> is thrown.
  154. * First, <code>format</code> checks to see if a <code>Format</code> object has been
  155. * specified for the argument with the <code>setFormats</code> method.
  156. * If so, then <code>format</code> uses that <code>Format</code> object to format the
  157. * argument. Otherwise, the argument is formatted based on the object's
  158. * type. If the argument is a <code>Number</code>, then <code>format</code>
  159. * uses <code>NumberFormat.getInstance</code> to format the argument; if the
  160. * argument is a <code>Date</code>, then <code>format</code> uses
  161. * <code>DateFormat.getDateTimeInstance</code> to format the argument.
  162. * Otherwise, it uses the <code>toString</code> method.
  163. *
  164. * <p>
  165. * For more sophisticated patterns, you can use a <code>ChoiceFormat</code> to get
  166. * output such as:
  167. * <blockquote>
  168. * <pre>
  169. * MessageFormat form = new MessageFormat("The disk \"{1}\" contains {0}.");
  170. * double[] filelimits = {0,1,2};
  171. * String[] filepart = {"no files","one file","{0,number} files"};
  172. * ChoiceFormat fileform = new ChoiceFormat(filelimits, filepart);
  173. * form.setFormat(1,fileform); // NOT zero, see below
  174. *
  175. * Object[] testArgs = {new Long(12373), "MyDisk"};
  176. *
  177. * System.out.println(form.format(testArgs));
  178. *
  179. * // output, with different testArgs
  180. * output: The disk "MyDisk" contains no files.
  181. * output: The disk "MyDisk" contains one file.
  182. * output: The disk "MyDisk" contains 1,273 files.
  183. * </pre>
  184. * </blockquote>
  185. * You can either do this programmatically, as in the above example,
  186. * or by using a pattern (see
  187. * {@link ChoiceFormat}
  188. * for more information) as in:
  189. * <blockquote>
  190. * <pre>
  191. * form.applyPattern(
  192. * "There {0,choice,0#are no files|1#is one file|1#are {0,number,integer} files}.");
  193. * </pre>
  194. * </blockquote>
  195. * <p>
  196. * <strong>Note:</strong> As we see above, the string produced
  197. * by a <code>ChoiceFormat</code> in <code>MessageFormat</code> is treated specially;
  198. * occurances of '{' are used to indicated subformats, and cause recursion.
  199. * If you create both a <code>MessageFormat</code> and <code>ChoiceFormat</code>
  200. * programmatically (instead of using the string patterns), then be careful not to
  201. * produce a format that recurses on itself, which will cause an infinite loop.
  202. * <p>
  203. * <strong>Note:</strong> formats are numbered by order of
  204. * variable in the string.
  205. * This is <strong>not</strong> the same as the argument numbering!
  206. * For example: with "abc{2}def{3}ghi{0}...",
  207. * <ul>
  208. * <li>format0 affects the first variable {2}
  209. * <li>format1 affects the second variable {3}
  210. * <li>format2 affects the second variable {0}
  211. * <li>and so on.
  212. * </ul>
  213. * <p>
  214. * When a single argument is parsed more than once in the string, the last match
  215. * will be the final result of the parsing. For example,
  216. * <pre>
  217. * MessageFormat mf = new MessageFormat("{0,number,#.##}, {0,number,#.#}");
  218. * Object[] objs = {new Double(3.1415)};
  219. * String result = mf.format( objs );
  220. * // result now equals "3.14, 3.1"
  221. * objs = null;
  222. * objs = mf.parse(result, new ParsePosition(0));
  223. * // objs now equals {new Double(3.1)}
  224. * </pre>
  225. * <p>
  226. * Likewise, parsing with a MessageFormat object using patterns containing
  227. * multiple occurances of the same argument would return the last match. For
  228. * example,
  229. * <pre>
  230. * MessageFormat mf = new MessageFormat("{0}, {0}, {0}");
  231. * String forParsing = "x, y, z";
  232. * Object[] objs = mf.parse(forParsing, new ParsePosition(0));
  233. * // result now equals {new String("z")}
  234. * </pre>
  235. * <p>
  236. * You can use <code>setLocale</code> followed by <code>applyPattern</code>
  237. * (and then possibly <code>setFormat</code>) to re-initialize a
  238. * <code>MessageFormat</code> with a different locale.
  239. *
  240. * @see java.util.Locale
  241. * @see Format
  242. * @see NumberFormat
  243. * @see DecimalFormat
  244. * @see ChoiceFormat
  245. * @version 1.15 29 Jan 1997
  246. * @author Mark Davis
  247. */
  248. public class MessageFormat extends Format {
  249. /**
  250. * Constructs with the specified pattern.
  251. * @see MessageFormat#applyPattern
  252. */
  253. public MessageFormat(String pattern) {
  254. applyPattern(pattern);
  255. }
  256. /**
  257. * Constructs with the specified pattern and formats for the
  258. * arguments in that pattern.
  259. */
  260. public void setLocale(Locale theLocale) {
  261. locale = theLocale;
  262. }
  263. /**
  264. * Gets the locale. This locale is used for fetching default number or date
  265. * format information.
  266. */
  267. public Locale getLocale() {
  268. return locale;
  269. }
  270. /**
  271. * Sets the pattern. See the class description.
  272. */
  273. public void applyPattern(String newPattern) {
  274. StringBuffer[] segments = new StringBuffer[4];
  275. for (int i = 0; i < segments.length; ++i) {
  276. segments[i] = new StringBuffer();
  277. }
  278. int part = 0;
  279. int formatNumber = 0;
  280. boolean inQuote = false;
  281. int braceStack = 0;
  282. maxOffset = -1;
  283. for (int i = 0; i < newPattern.length(); ++i) {
  284. char ch = newPattern.charAt(i);
  285. if (part == 0) {
  286. if (ch == '\'') {
  287. if (i + 1 < newPattern.length()
  288. && newPattern.charAt(i+1) == '\'') {
  289. segments[part].append(ch); // handle doubles
  290. ++i;
  291. } else {
  292. inQuote = !inQuote;
  293. }
  294. } else if (ch == '{' && !inQuote) {
  295. part = 1;
  296. } else {
  297. segments[part].append(ch);
  298. }
  299. } else if (inQuote) { // just copy quotes in parts
  300. segments[part].append(ch);
  301. if (ch == '\'') {
  302. inQuote = false;
  303. }
  304. } else {
  305. switch (ch) {
  306. case ',':
  307. if (part < 3)
  308. part += 1;
  309. else
  310. segments[part].append(ch);
  311. break;
  312. case '{':
  313. ++braceStack;
  314. segments[part].append(ch);
  315. break;
  316. case '}':
  317. if (braceStack == 0) {
  318. part = 0;
  319. makeFormat(i, formatNumber, segments);
  320. formatNumber++;
  321. } else {
  322. --braceStack;
  323. segments[part].append(ch);
  324. }
  325. break;
  326. case '\'':
  327. inQuote = true;
  328. // fall through, so we keep quotes in other parts
  329. default:
  330. segments[part].append(ch);
  331. break;
  332. }
  333. }
  334. }
  335. if (braceStack == 0 && part != 0) {
  336. maxOffset = -1;
  337. throw new IllegalArgumentException("Unmatched braces in the pattern.");
  338. }
  339. pattern = segments[0].toString();
  340. }
  341. /**
  342. * Gets the pattern. See the class description.
  343. */
  344. public String toPattern() {
  345. // later, make this more extensible
  346. int lastOffset = 0;
  347. StringBuffer result = new StringBuffer();
  348. for (int i = 0; i <= maxOffset; ++i) {
  349. copyAndFixQuotes(pattern, lastOffset, offsets[i],result);
  350. lastOffset = offsets[i];
  351. result.append('{');
  352. result.append(argumentNumbers[i]);
  353. if (formats[i] == null) {
  354. // do nothing, string format
  355. } else if (formats[i] instanceof DecimalFormat) {
  356. if (formats[i].equals(NumberFormat.getInstance(locale))) {
  357. result.append(",number");
  358. } else if (formats[i].equals(
  359. NumberFormat.getCurrencyInstance(locale))) {
  360. result.append(",number,currency");
  361. } else if (formats[i].equals(
  362. NumberFormat.getPercentInstance(locale))) {
  363. result.append(",number,percent");
  364. } else if (formats[i].equals(getIntegerFormat(locale))) {
  365. result.append(",number,integer");
  366. } else {
  367. result.append(",number," +
  368. ((DecimalFormat)formats[i]).toPattern());
  369. }
  370. } else if (formats[i] instanceof SimpleDateFormat) {
  371. if (formats[i].equals(DateFormat.getDateInstance(
  372. DateFormat.DEFAULT,locale))) {
  373. result.append(",date");
  374. } else if (formats[i].equals(DateFormat.getDateInstance(
  375. DateFormat.SHORT,locale))) {
  376. result.append(",date,short");
  377. } else if (formats[i].equals(DateFormat.getDateInstance(
  378. DateFormat.DEFAULT,locale))) {
  379. result.append(",date,medium");
  380. } else if (formats[i].equals(DateFormat.getDateInstance(
  381. DateFormat.LONG,locale))) {
  382. result.append(",date,long");
  383. } else if (formats[i].equals(DateFormat.getDateInstance(
  384. DateFormat.FULL,locale))) {
  385. result.append(",date,full");
  386. } else if (formats[i].equals(DateFormat.getTimeInstance(
  387. DateFormat.DEFAULT,locale))) {
  388. result.append(",time");
  389. } else if (formats[i].equals(DateFormat.getTimeInstance(
  390. DateFormat.SHORT,locale))) {
  391. result.append(",time,short");
  392. } else if (formats[i].equals(DateFormat.getTimeInstance(
  393. DateFormat.DEFAULT,locale))) {
  394. result.append(",time,medium");
  395. } else if (formats[i].equals(DateFormat.getTimeInstance(
  396. DateFormat.LONG,locale))) {
  397. result.append(",time,long");
  398. } else if (formats[i].equals(DateFormat.getTimeInstance(
  399. DateFormat.FULL,locale))) {
  400. result.append(",time,full");
  401. } else {
  402. result.append(",date,"
  403. + ((SimpleDateFormat)formats[i]).toPattern());
  404. }
  405. } else if (formats[i] instanceof ChoiceFormat) {
  406. result.append(",choice,"
  407. + ((ChoiceFormat)formats[i]).toPattern());
  408. } else {
  409. //result.append(", unknown");
  410. }
  411. result.append('}');
  412. }
  413. copyAndFixQuotes(pattern, lastOffset, pattern.length(), result);
  414. return result.toString();
  415. }
  416. /**
  417. * Sets formats to use on parameters.
  418. * See the class description about format numbering.
  419. */
  420. public void setFormats(Format[] newFormats) {
  421. try {
  422. formats = (Format[]) newFormats.clone();
  423. } catch (Exception e) {
  424. return; // should never occur!
  425. }
  426. }
  427. /**
  428. * Set a format to be used on a variable in the pattern.
  429. * @param variable the zero-based number of the variable in the format.
  430. * This is <em>not</em> the argument number. If <code>variable</code>
  431. * is out of range, an <code>ArrayIndexOutOfBoundsException</code> is
  432. * thrown.
  433. * @param newFormat the format to use for the specified variable
  434. */
  435. public void setFormat(int variable, Format newFormat) {
  436. formats[variable] = newFormat;
  437. }
  438. /**
  439. * Gets formats that were set with setFormats.
  440. * See the class description about format numbering.
  441. */
  442. public Format[] getFormats() {
  443. try {
  444. return (Format[]) formats.clone();
  445. } catch (Exception e) {
  446. return formats; // should never occur!
  447. }
  448. }
  449. /**
  450. * Returns pattern with formatted objects. If source is null, the
  451. * original pattern is returned, if source contains null objects, the
  452. * formatted result will substitute each argument with the string "null".
  453. * @param source an array of objects to be formatted & substituted.
  454. * @param result where text is appended.
  455. * @param ignore no useful status is returned.
  456. */
  457. public final StringBuffer format(Object[] source, StringBuffer result,
  458. FieldPosition ignore)
  459. {
  460. return format(source,result,ignore, 0);
  461. }
  462. /**
  463. * Convenience routine.
  464. * Avoids explicit creation of MessageFormat,
  465. * but doesn't allow future optimizations.
  466. */
  467. public static String format(String pattern, Object[] arguments) {
  468. MessageFormat temp = new MessageFormat(pattern);
  469. return temp.format(arguments);
  470. }
  471. // Overrides
  472. public final StringBuffer format(Object source, StringBuffer result,
  473. FieldPosition ignore)
  474. {
  475. return format((Object[])source,result,ignore, 0);
  476. }
  477. /**
  478. * Parses the string.
  479. *
  480. * <p>Caveats: The parse may fail in a number of circumstances.
  481. * For example:
  482. * <ul>
  483. * <li>If one of the arguments does not occur in the pattern.
  484. * <li>If the format of an argument loses information, such as
  485. * with a choice format where a large number formats to "many".
  486. * <li>Does not yet handle recursion (where
  487. * the substituted strings contain {n} references.)
  488. * <li>Will not always find a match (or the correct match)
  489. * if some part of the parse is ambiguous.
  490. * For example, if the pattern "{1},{2}" is used with the
  491. * string arguments {"a,b", "c"}, it will format as "a,b,c".
  492. * When the result is parsed, it will return {"a", "b,c"}.
  493. * <li>If a single argument is parsed more than once in the string,
  494. * then the later parse wins.
  495. * </ul>
  496. * When the parse fails, use ParsePosition.getErrorIndex() to find out
  497. * where in the string did the parsing failed. The returned error
  498. * index is the starting offset of the sub-patterns that the string
  499. * is comparing with. For example, if the parsing string "AAA {0} BBB"
  500. * is comparing against the pattern "AAD {0} BBB", the error index is
  501. * 0. When an error occurs, the call to this method will return null.
  502. * If the soruce is null, return an empty array.
  503. */
  504. public Object[] parse(String source, ParsePosition status) {
  505. Object[] empty = {};
  506. if (source == null) return empty;
  507. Object[] resultArray = new Object[10];
  508. int patternOffset = 0;
  509. int sourceOffset = status.index;
  510. ParsePosition tempStatus = new ParsePosition(0);
  511. for (int i = 0; i <= maxOffset; ++i) {
  512. // match up to format
  513. int len = offsets[i] - patternOffset;
  514. if (len == 0 || pattern.regionMatches(patternOffset,
  515. source, sourceOffset, len)) {
  516. sourceOffset += len;
  517. patternOffset += len;
  518. } else {
  519. status.errorIndex = sourceOffset;
  520. return null; // leave index as is to signal error
  521. }
  522. // now use format
  523. if (formats[i] == null) { // string format
  524. // if at end, use longest possible match
  525. // otherwise uses first match to intervening string
  526. // does NOT recursively try all possibilities
  527. int tempLength = (i != maxOffset) ? offsets[i+1] : pattern.length();
  528. int next;
  529. if (patternOffset >= tempLength) {
  530. next = source.length();
  531. }else{
  532. next = source.indexOf( pattern.substring(patternOffset,tempLength), sourceOffset);
  533. }
  534. if (next < 0) {
  535. status.errorIndex = sourceOffset;
  536. return null; // leave index as is to signal error
  537. } else {
  538. String strValue= source.substring(sourceOffset,next);
  539. if (!strValue.equals("{"+argumentNumbers[i]+"}"))
  540. resultArray[argumentNumbers[i]]
  541. = source.substring(sourceOffset,next);
  542. sourceOffset = next;
  543. }
  544. } else {
  545. tempStatus.index = sourceOffset;
  546. resultArray[argumentNumbers[i]]
  547. = formats[i].parseObject(source,tempStatus);
  548. if (tempStatus.index == sourceOffset) {
  549. status.errorIndex = sourceOffset;
  550. return null; // leave index as is to signal error
  551. }
  552. sourceOffset = tempStatus.index; // update
  553. }
  554. }
  555. int len = pattern.length() - patternOffset;
  556. if (len == 0 || pattern.regionMatches(patternOffset,
  557. source, sourceOffset, len)) {
  558. status.index = sourceOffset + len;
  559. } else {
  560. status.errorIndex = sourceOffset;
  561. return null; // leave index as is to signal error
  562. }
  563. return resultArray;
  564. }
  565. /**
  566. * Parses the string. Does not yet handle recursion (where
  567. * the substituted strings contain {n} references.)
  568. * @exception ParseException if the string can't be parsed.
  569. */
  570. public Object[] parse(String source) throws ParseException {
  571. ParsePosition status = new ParsePosition(0);
  572. Object[] result = parse(source, status);
  573. if (status.index == 0) // unchanged, returned object is null
  574. throw new ParseException("MessageFormat parse error!", status.errorIndex);
  575. return result;
  576. }
  577. /**
  578. * Parses the string. Does not yet handle recursion (where
  579. * the substituted strings contain %n references.)
  580. */
  581. public Object parseObject (String text, ParsePosition status) {
  582. return parse(text, status);
  583. }
  584. /**
  585. * Overrides Cloneable
  586. */
  587. public Object clone()
  588. {
  589. MessageFormat other = (MessageFormat) super.clone();
  590. // clone arrays. Can't do with utility because of bug in Cloneable
  591. other.formats = (Format[]) formats.clone(); // shallow clone
  592. for (int i = 0; i < formats.length; ++i) {
  593. if (formats[i] != null)
  594. other.formats[i] = (Format)formats[i].clone();
  595. }
  596. // for primitives or immutables, shallow clone is enough
  597. other.offsets = (int[]) offsets.clone();
  598. other.argumentNumbers = (int[]) argumentNumbers.clone();
  599. return other;
  600. }
  601. /**
  602. * Equality comparision between two message format objects
  603. */
  604. public boolean equals(Object obj) {
  605. if (this == obj) // quick check
  606. return true;
  607. if (obj == null || getClass() != obj.getClass())
  608. return false;
  609. MessageFormat other = (MessageFormat) obj;
  610. return (maxOffset == other.maxOffset
  611. && pattern.equals(other.pattern)
  612. && Utility.objectEquals(locale, other.locale) // does null check
  613. && Utility.arrayEquals(offsets,other.offsets)
  614. && Utility.arrayEquals(argumentNumbers,other.argumentNumbers)
  615. && Utility.arrayEquals(formats,other.formats));
  616. }
  617. /**
  618. * Generates a hash code for the message format object.
  619. */
  620. public int hashCode() {
  621. return pattern.hashCode(); // enough for reasonable distribution
  622. }
  623. // ===========================privates============================
  624. /**
  625. * The locale to use for formatting numbers and dates.
  626. * @serial
  627. */
  628. private Locale locale = Locale.getDefault();
  629. /**
  630. * The string that the formatted values are to be plugged into. In other words, this
  631. * is the pattern supplied on construction with all of the {} expressions taken out.
  632. * @serial
  633. */
  634. private String pattern = "";
  635. /**
  636. * An array of ten formatters, which are used to format the first ten arguments.
  637. * @serial
  638. */
  639. // later, allow more than ten items
  640. private Format[] formats = new Format[10];
  641. /**
  642. * The positions where the results of formatting each argument are to be inserted
  643. * into the pattern.
  644. * @serial
  645. */
  646. private int[] offsets = new int[10];
  647. /**
  648. * The argument numbers corresponding to each formatter. (The formatters are stored
  649. * in the order they occur in the pattern, not in the order in which the arguments
  650. * are specified.)
  651. * @serial
  652. */
  653. private int[] argumentNumbers = new int[10];
  654. /**
  655. * One less than the number of entries in <code>offsets</code>. Can also be thought of
  656. * as the index of the highest-numbered element in <code>offsets</code> that is being used.
  657. * All of these arrays should have the same number of elements being used as <code>offsets</code>
  658. * does, and so this variable suffices to tell us how many entries are in all of them.
  659. * @serial
  660. */
  661. private int maxOffset = -1;
  662. /**
  663. * Constructs with the specified pattern.
  664. * @see MessageFormat#applyPattern
  665. */
  666. private MessageFormat(String pattern, Locale loc) {
  667. locale = (Locale)loc.clone();
  668. applyPattern(pattern);
  669. }
  670. /**
  671. * Internal routine used by format.
  672. * @param recursionProtection Initially zero. Bits 0..9 are used to indicate
  673. * that a parameter has already been seen, to avoid recursion. Currently
  674. * unused.
  675. */
  676. private StringBuffer format(Object[] arguments, StringBuffer result,
  677. FieldPosition status, int recursionProtection) {
  678. // note: this implementation assumes a fast substring & index.
  679. // if this is not true, would be better to append chars one by one.
  680. int lastOffset = 0;
  681. for (int i = 0; i <= maxOffset; ++i) {
  682. result.append(pattern.substring(lastOffset, offsets[i]));
  683. lastOffset = offsets[i];
  684. int argumentNumber = argumentNumbers[i];
  685. if (arguments == null || argumentNumber >= arguments.length) {
  686. result.append("{" + argumentNumber + "}");
  687. continue;
  688. }
  689. // int argRecursion = ((recursionProtection >> (argumentNumber*2)) & 0x3);
  690. if (false) { // if (argRecursion == 3){
  691. // prevent loop!!!
  692. result.append('\uFFFD');
  693. } else {
  694. Object obj = arguments[argumentNumber];
  695. String arg;
  696. boolean tryRecursion = false;
  697. if (obj == null) {
  698. arg = "null";
  699. } else if (formats[i] != null) {
  700. arg = formats[i].format(obj);
  701. tryRecursion = formats[i] instanceof ChoiceFormat;
  702. } else if (obj instanceof Number) {
  703. // format number if can
  704. arg = NumberFormat.getInstance(locale).format(obj); // fix
  705. } else if (obj instanceof Date) {
  706. // format a Date if can
  707. arg = DateFormat.getDateTimeInstance(DateFormat.SHORT,
  708. DateFormat.SHORT,
  709. locale).format(obj);//fix
  710. } else if (obj instanceof String) {
  711. arg = (String) obj;
  712. } else {
  713. arg = obj.toString();
  714. if (arg == null) arg = "null";
  715. }
  716. // recurse if necessary
  717. if (tryRecursion && arg.indexOf('{') >= 0) {
  718. MessageFormat temp = new MessageFormat(arg, locale);
  719. temp.format(arguments,result,status,recursionProtection);
  720. } else {
  721. result.append(arg);
  722. }
  723. }
  724. }
  725. result.append(pattern.substring(lastOffset, pattern.length()));
  726. return result;
  727. }
  728. private static final String[] typeList =
  729. {"", "", "number", "", "date", "", "time", "", "choice"};
  730. private static final String[] modifierList =
  731. {"", "", "currency", "", "percent", "", "integer"};
  732. private static final String[] dateModifierList =
  733. {"", "", "short", "", "medium", "", "long", "", "full"};
  734. private void makeFormat(int position, int offsetNumber,
  735. StringBuffer[] segments)
  736. {
  737. // get the number
  738. int argumentNumber;
  739. int oldMaxOffset = maxOffset;
  740. try {
  741. argumentNumber = Integer.parseInt(segments[1].toString()); // always unlocalized!
  742. if (argumentNumber < 0 || argumentNumber > 9) {
  743. throw new NumberFormatException();
  744. }
  745. maxOffset = offsetNumber;
  746. offsets[offsetNumber] = segments[0].length();
  747. argumentNumbers[offsetNumber] = argumentNumber;
  748. } catch (Exception e) {
  749. throw new IllegalArgumentException("argument number too large at ");
  750. }
  751. // now get the format
  752. Format newFormat = null;
  753. switch (findKeyword(segments[2].toString(), typeList)) {
  754. case 0:
  755. break;
  756. case 1: case 2:// number
  757. switch (findKeyword(segments[3].toString(), modifierList)) {
  758. case 0: // default;
  759. newFormat = NumberFormat.getInstance(locale);
  760. break;
  761. case 1: case 2:// currency
  762. newFormat = NumberFormat.getCurrencyInstance(locale);
  763. break;
  764. case 3: case 4:// percent
  765. newFormat = NumberFormat.getPercentInstance(locale);
  766. break;
  767. case 5: case 6:// integer
  768. newFormat = getIntegerFormat(locale);
  769. break;
  770. default: // pattern
  771. newFormat = NumberFormat.getInstance(locale);
  772. try {
  773. ((DecimalFormat)newFormat).applyPattern(segments[3].toString());
  774. } catch (Exception e) {
  775. maxOffset = oldMaxOffset;
  776. throw new IllegalArgumentException(
  777. "Pattern incorrect or locale does not support formats, error at ");
  778. }
  779. break;
  780. }
  781. break;
  782. case 3: case 4: // date
  783. switch (findKeyword(segments[3].toString(), dateModifierList)) {
  784. case 0: // default
  785. newFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, locale);
  786. break;
  787. case 1: case 2: // short
  788. newFormat = DateFormat.getDateInstance(DateFormat.SHORT, locale);
  789. break;
  790. case 3: case 4: // medium
  791. newFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, locale);
  792. break;
  793. case 5: case 6: // long
  794. newFormat = DateFormat.getDateInstance(DateFormat.LONG, locale);
  795. break;
  796. case 7: case 8: // full
  797. newFormat = DateFormat.getDateInstance(DateFormat.FULL, locale);
  798. break;
  799. default:
  800. newFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, locale);
  801. try {
  802. ((SimpleDateFormat)newFormat).applyPattern(segments[3].toString());
  803. } catch (Exception e) {
  804. maxOffset = oldMaxOffset;
  805. throw new IllegalArgumentException(
  806. "Pattern incorrect or locale does not support formats, error at ");
  807. }
  808. break;
  809. }
  810. break;
  811. case 5: case 6:// time
  812. switch (findKeyword(segments[3].toString(), dateModifierList)) {
  813. case 0: // default
  814. newFormat = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale);
  815. break;
  816. case 1: case 2: // short
  817. newFormat = DateFormat.getTimeInstance(DateFormat.SHORT, locale);
  818. break;
  819. case 3: case 4: // medium
  820. newFormat = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale);
  821. break;
  822. case 5: case 6: // long
  823. newFormat = DateFormat.getTimeInstance(DateFormat.LONG, locale);
  824. break;
  825. case 7: case 8: // full
  826. newFormat = DateFormat.getTimeInstance(DateFormat.FULL, locale);
  827. break;
  828. default:
  829. newFormat = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale);
  830. try {
  831. ((SimpleDateFormat)newFormat).applyPattern(segments[3].toString());
  832. } catch (Exception e) {
  833. maxOffset = oldMaxOffset;
  834. throw new IllegalArgumentException(
  835. "Pattern incorrect or locale does not support formats, error at ");
  836. }
  837. break;
  838. }
  839. break;
  840. case 7: case 8:// choice
  841. try {
  842. newFormat = new ChoiceFormat(segments[3].toString());
  843. } catch (Exception e) {
  844. maxOffset = oldMaxOffset;
  845. throw new IllegalArgumentException(
  846. "Choice Pattern incorrect, error at ");
  847. }
  848. break;
  849. default:
  850. maxOffset = oldMaxOffset;
  851. throw new IllegalArgumentException("unknown format type at ");
  852. }
  853. formats[offsetNumber] = newFormat;
  854. segments[1].setLength(0); // throw away other segments
  855. segments[2].setLength(0);
  856. segments[3].setLength(0);
  857. }
  858. private static final int findKeyword(String s, String[] list) {
  859. s = s.trim().toLowerCase();
  860. for (int i = 0; i < list.length; ++i) {
  861. if (s.equals(list[i]))
  862. return i;
  863. }
  864. return -1;
  865. }
  866. /**
  867. * Convenience method that ought to be in NumberFormat
  868. */
  869. NumberFormat getIntegerFormat(Locale locale) {
  870. NumberFormat temp = NumberFormat.getInstance(locale);
  871. if (temp instanceof DecimalFormat) {
  872. DecimalFormat temp2 = (DecimalFormat) temp;
  873. temp2.setMaximumFractionDigits(0);
  874. temp2.setDecimalSeparatorAlwaysShown(false);
  875. temp2.setParseIntegerOnly(true);
  876. }
  877. return temp;
  878. }
  879. private static final void copyAndFixQuotes(
  880. String source, int start, int end, StringBuffer target) {
  881. for (int i = start; i < end; ++i) {
  882. char ch = source.charAt(i);
  883. if (ch == '{') {
  884. target.append("'{'");
  885. } else if (ch == '}') {
  886. target.append("'}'");
  887. } else if (ch == '\'') {
  888. target.append("''");
  889. } else {
  890. target.append(ch);
  891. }
  892. }
  893. }
  894. }