View Javadoc

1   /*
2    *  Copyright 2009-2013 Stephen Colebourne
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  package org.joda.money.format;
17  
18  import java.io.IOException;
19  import java.io.Serializable;
20  import java.util.Arrays;
21  import java.util.Locale;
22  
23  import org.joda.money.BigMoney;
24  import org.joda.money.BigMoneyProvider;
25  import org.joda.money.Money;
26  
27  /**
28   * Formats instances of money to and from a String.
29   * <p>
30   * Instances of {@code MoneyFormatter} can be created by
31   * {@code MoneyFormatterBuilder}.
32   * <p>
33   * This class is immutable and thread-safe.
34   */
35  public final class MoneyFormatter implements Serializable {
36  
37      /**
38       * Serialization version.
39       */
40      private static final long serialVersionUID = 2385346258L;
41  
42      /**
43       * The locale to use.
44       */
45      private final Locale locale;
46      /**
47       * The printers.
48       */
49      private final MoneyPrinter[] printers;
50      /**
51       * The parsers.
52       */
53      private final MoneyParser[] parsers;
54  
55      //-----------------------------------------------------------------------
56      /**
57       * Validates that the object specified is not null
58       *
59       * @param object  the object to check, null throws exception
60       * @param message  the message to use in the exception, not null
61       * @throws NullPointerException if the input value is null
62       */
63      static void checkNotNull(Object object, String message) {
64          if (object == null) {
65              throw new NullPointerException(message);
66          }
67      }
68  
69      //-----------------------------------------------------------------------
70      /**
71       * Constructor, creating a new formatter.
72       * 
73       * @param locale  the locale to use, not null
74       * @param printers  the printers, not null
75       * @param parsers  the parsers, not null
76       */
77      MoneyFormatter(
78              Locale locale,
79              MoneyPrinter[] printers,
80              MoneyParser[] parsers) {
81          assert locale != null;
82          assert printers != null;
83          assert parsers != null;
84          assert printers.length == parsers.length;
85          this.locale = locale;
86          this.printers = printers;
87          this.parsers = parsers;
88      }
89  
90      //-----------------------------------------------------------------------
91      /**
92       * Appends the printers and parsers from this formatter to the builder.
93       * 
94       * @param builder  the builder to append to not null
95       */
96      void appendTo(MoneyFormatterBuilder builder) {
97          for (int i = 0; i < printers.length; i++) {
98              builder.append(printers[i], parsers[i]);
99          }
100     }
101 
102     //-----------------------------------------------------------------------
103     /**
104      * Gets the locale to use.
105      * 
106      * @return the locale, never null
107      */
108     public Locale getLocale() {
109         return locale;
110     }
111 
112     /**
113      * Returns a copy of this instance with the specified locale.
114      * <p>
115      * Changing the locale may change the style of output depending on how the
116      * formatter has been configured.
117      * 
118      * @param locale  the locale, not null
119      * @return the new instance, never null
120      */
121     public MoneyFormatter withLocale(Locale locale) {
122         checkNotNull(locale, "Locale must not be null");
123         return new MoneyFormatter(locale, printers, parsers);
124     }
125 
126     //-----------------------------------------------------------------------
127     /**
128      * Checks whether this formatter can print.
129      * <p>
130      * If the formatter cannot print, an UnsupportedOperationException will
131      * be thrown from the print methods.
132      * 
133      * @return true if the formatter can print
134      */
135     public boolean isPrinter() {
136         return Arrays.asList(printers).contains(null) == false;
137     }
138 
139     /**
140      * Checks whether this formatter can parse.
141      * <p>
142      * If the formatter cannot parse, an UnsupportedOperationException will
143      * be thrown from the print methods.
144      * 
145      * @return true if the formatter can parse
146      */
147     public boolean isParser() {
148         return Arrays.asList(parsers).contains(null) == false;
149     }
150 
151     //-----------------------------------------------------------------------
152     /**
153      * Prints a monetary value to a {@code String}.
154      * 
155      * @param moneyProvider  the money to print, not null
156      * @return the string printed using the settings of this formatter
157      * @throws UnsupportedOperationException if the formatter is unable to print
158      * @throws MoneyFormatException if there is a problem while printing
159      */
160     public String print(BigMoneyProvider moneyProvider) {
161         StringBuilder buf = new StringBuilder();
162         print(buf, moneyProvider);
163         return buf.toString();
164     }
165 
166     /**
167      * Prints a monetary value to an {@code Appendable} converting
168      * any {@code IOException} to a {@code MoneyFormatException}.
169      * <p>
170      * Example implementations of {@code Appendable} are {@code StringBuilder},
171      * {@code StringBuffer} or {@code Writer}. Note that {@code StringBuilder}
172      * and {@code StringBuffer} never throw an {@code IOException}.
173      * 
174      * @param appendable  the appendable to add to, not null
175      * @param moneyProvider  the money to print, not null
176      * @throws UnsupportedOperationException if the formatter is unable to print
177      * @throws MoneyFormatException if there is a problem while printing
178      */
179     public void print(Appendable appendable, BigMoneyProvider moneyProvider) {
180         try {
181             printIO(appendable, moneyProvider);
182         } catch (IOException ex) {
183             throw new MoneyFormatException(ex.getMessage(), ex);
184         }
185     }
186 
187     /**
188      * Prints a monetary value to an {@code Appendable} potentially
189      * throwing an {@code IOException}.
190      * <p>
191      * Example implementations of {@code Appendable} are {@code StringBuilder},
192      * {@code StringBuffer} or {@code Writer}. Note that {@code StringBuilder}
193      * and {@code StringBuffer} never throw an {@code IOException}.
194      * 
195      * @param appendable  the appendable to add to, not null
196      * @param moneyProvider  the money to print, not null
197      * @throws UnsupportedOperationException if the formatter is unable to print
198      * @throws MoneyFormatException if there is a problem while printing
199      * @throws IOException if an IO error occurs
200      */
201     public void printIO(Appendable appendable, BigMoneyProvider moneyProvider) throws IOException {
202         checkNotNull(moneyProvider, "BigMoneyProvider must not be null");
203         if (isPrinter() == false) {
204             throw new UnsupportedOperationException("MoneyFomatter has not been configured to be able to print");
205         }
206         
207         BigMoney money = BigMoney.of(moneyProvider);
208         MoneyPrintContext context = new MoneyPrintContext(locale);
209         for (MoneyPrinter printer : printers) {
210             printer.print(context, appendable, money);
211         }
212     }
213 
214     //-----------------------------------------------------------------------
215     /**
216      * Fully parses the text into a {@code BigMoney}.
217      * <p>
218      * The parse must complete normally and parse the entire text (currency and amount).
219      * If the parse completes without reading the entire length of the text, an exception is thrown.
220      * If any other problem occurs during parsing, an exception is thrown.
221      * 
222      * @param text  the text to parse, not null
223      * @return the parsed monetary value, never null
224      * @throws UnsupportedOperationException if the formatter is unable to parse
225      * @throws MoneyFormatException if there is a problem while parsing
226      */
227     public BigMoney parseBigMoney(CharSequence text) {
228         checkNotNull(text, "Text must not be null");
229         MoneyParseContext result = parse(text, 0);
230         if (result.isError() || result.isFullyParsed() == false || result.isComplete() == false) {
231             String str = (text.length() > 64 ? text.subSequence(0, 64).toString() + "..." : text.toString());
232             if (result.isError()) {
233                 throw new MoneyFormatException("Text could not be parsed at index " + result.getErrorIndex() + ": " + str);
234             } else if (result.isFullyParsed() == false) {
235                 throw new MoneyFormatException("Unparsed text found at index " + result.getIndex() + ": " + str);
236             } else {
237                 throw new MoneyFormatException("Parsing did not find both currency and amount: " + str);
238             }
239         }
240         return result.toBigMoney();
241     }
242 
243     /**
244      * Fully parses the text into a {@code Money} requiring that the parsed
245      * amount has the correct number of decimal places.
246      * <p>
247      * The parse must complete normally and parse the entire text (currency and amount).
248      * If the parse completes without reading the entire length of the text, an exception is thrown.
249      * If any other problem occurs during parsing, an exception is thrown.
250      * 
251      * @param text  the text to parse, not null
252      * @return the parsed monetary value, never null
253      * @throws UnsupportedOperationException if the formatter is unable to parse
254      * @throws MoneyFormatException if there is a problem while parsing
255      * @throws ArithmeticException if the scale of the parsed money exceeds the scale of the currency
256      */
257     public Money parseMoney(CharSequence text) {
258         return parseBigMoney(text).toMoney();
259     }
260 
261     /**
262      * Parses the text extracting monetary information.
263      * <p>
264      * This method parses the input providing low-level access to the parsing state.
265      * The resulting context contains the parsed text, indicator of error, position
266      * following the parse and the parsed currency and amount.
267      * Together, these provide enough information for higher level APIs to use.
268      *
269      * @param text  the text to parse, not null
270      * @param startIndex  the start index to parse from
271      * @return the parsed monetary value, null only if the parse results in an error
272      * @throws IndexOutOfBoundsException if the start index is invalid
273      * @throws UnsupportedOperationException if this formatter cannot parse
274      */
275     public MoneyParseContext parse(CharSequence text, int startIndex) {
276         checkNotNull(text, "Text must not be null");
277         if (startIndex < 0 || startIndex > text.length()) {
278             throw new StringIndexOutOfBoundsException("Invalid start index: " + startIndex);
279         }
280         if (isParser() == false) {
281             throw new UnsupportedOperationException("MoneyFomatter has not been configured to be able to parse");
282         }
283         MoneyParseContext context = new MoneyParseContext(locale, text, startIndex);
284         for (MoneyParser parser : parsers) {
285             parser.parse(context);
286             if (context.isError()) {
287                 break;
288             }
289         }
290         return context;
291     }
292 
293     //-----------------------------------------------------------------------
294     /**
295      * Gets a string summary of the formatter.
296      * 
297      * @return a string summarising the formatter, never null
298      */
299     @Override
300     public String toString() {
301         StringBuilder buf1 = new StringBuilder();
302         if (isPrinter()) {
303             for (MoneyPrinter printer : printers) {
304                 buf1.append(printer.toString());
305             }
306         }
307         StringBuilder buf2 = new StringBuilder();
308         if (isParser()) {
309             for (MoneyParser parser : parsers) {
310                 buf2.append(parser.toString());
311             }
312         }
313         String str1 = buf1.toString();
314         String str2 = buf2.toString();
315         if (isPrinter() && isParser() == false) {
316             return str1;
317         } else if (isParser() && isPrinter() == false) {
318             return str2;
319         } else if (str1.equals(str2)) {
320             return str1;
321         } else {
322             return str1 + ":" + str2;
323         }
324     }
325 
326 }