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 }