001 /*
002 * Copyright 2009-2013 Stephen Colebourne
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.joda.money.format;
017
018 import java.io.IOException;
019 import java.io.Serializable;
020 import java.util.Arrays;
021 import java.util.Locale;
022
023 import org.joda.money.BigMoney;
024 import org.joda.money.BigMoneyProvider;
025 import org.joda.money.Money;
026
027 /**
028 * Formats instances of money to and from a String.
029 * <p>
030 * Instances of {@code MoneyFormatter} can be created by
031 * {@code MoneyFormatterBuilder}.
032 * <p>
033 * This class is immutable and thread-safe.
034 */
035 public final class MoneyFormatter implements Serializable {
036
037 /**
038 * Serialization version.
039 */
040 private static final long serialVersionUID = 2385346258L;
041
042 /**
043 * The locale to use.
044 */
045 private final Locale locale;
046 /**
047 * The printers.
048 */
049 private final MoneyPrinter[] printers;
050 /**
051 * The parsers.
052 */
053 private final MoneyParser[] parsers;
054
055 //-----------------------------------------------------------------------
056 /**
057 * Validates that the object specified is not null
058 *
059 * @param object the object to check, null throws exception
060 * @param message the message to use in the exception, not null
061 * @throws NullPointerException if the input value is null
062 */
063 static void checkNotNull(Object object, String message) {
064 if (object == null) {
065 throw new NullPointerException(message);
066 }
067 }
068
069 //-----------------------------------------------------------------------
070 /**
071 * Constructor, creating a new formatter.
072 *
073 * @param locale the locale to use, not null
074 * @param printers the printers, not null
075 * @param parsers the parsers, not null
076 */
077 MoneyFormatter(
078 Locale locale,
079 MoneyPrinter[] printers,
080 MoneyParser[] parsers) {
081 assert locale != null;
082 assert printers != null;
083 assert parsers != null;
084 assert printers.length == parsers.length;
085 this.locale = locale;
086 this.printers = printers;
087 this.parsers = parsers;
088 }
089
090 //-----------------------------------------------------------------------
091 /**
092 * Appends the printers and parsers from this formatter to the builder.
093 *
094 * @param builder the builder to append to not null
095 */
096 void appendTo(MoneyFormatterBuilder builder) {
097 for (int i = 0; i < printers.length; i++) {
098 builder.append(printers[i], parsers[i]);
099 }
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 }