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 }