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; 17 18 import java.io.InvalidObjectException; 19 import java.io.ObjectInputStream; 20 import java.io.Serializable; 21 import java.util.ArrayList; 22 import java.util.Collections; 23 import java.util.Currency; 24 import java.util.HashSet; 25 import java.util.List; 26 import java.util.Locale; 27 import java.util.Map.Entry; 28 import java.util.Set; 29 import java.util.concurrent.ConcurrentHashMap; 30 import java.util.concurrent.ConcurrentMap; 31 32 import org.joda.convert.FromString; 33 import org.joda.convert.ToString; 34 35 /** 36 * A unit of currency. 37 * <p> 38 * This class represents a unit of currency such as the British Pound, Euro 39 * or US Dollar. 40 * <p> 41 * The set of loaded currencies is provided by an instance of {@link CurrencyUnitDataProvider}. 42 * The provider used is determined by the system property {@code org.joda.money.CurrencyUnitDataProvider} 43 * which should be the fully qualified class name of the provider. The default provider loads the first 44 * resource named {@code /org/joda/money/MoneyData.csv} on the classpath. 45 * <p> 46 * This class is immutable and thread-safe. 47 */ 48 public final class CurrencyUnit implements Comparable<CurrencyUnit>, Serializable { 49 50 /** 51 * The serialisation version. 52 */ 53 private static final long serialVersionUID = 327835287287L; 54 /** 55 * Map of registered currencies by text code. 56 */ 57 private static final ConcurrentMap<String, CurrencyUnit> currenciesByCode = new ConcurrentHashMap<String, CurrencyUnit>(); 58 /** 59 * Map of registered currencies by numeric code. 60 */ 61 private static final ConcurrentMap<Integer, CurrencyUnit> currenciesByNumericCode = new ConcurrentHashMap<Integer, CurrencyUnit>(); 62 /** 63 * Map of registered currencies by country. 64 */ 65 private static final ConcurrentMap<String, CurrencyUnit> currenciesByCountry = new ConcurrentHashMap<String, CurrencyUnit>(); 66 static { 67 // load one data provider by system property 68 try { 69 try { 70 String clsName = System.getProperty( 71 "org.joda.money.CurrencyUnitDataProvider", "org.joda.money.DefaultCurrencyUnitDataProvider"); 72 Class<? extends CurrencyUnitDataProvider> cls = 73 CurrencyUnit.class.getClassLoader().loadClass(clsName).asSubclass(CurrencyUnitDataProvider.class); 74 cls.newInstance().registerCurrencies(); 75 } catch (SecurityException ex) { 76 new DefaultCurrencyUnitDataProvider().registerCurrencies(); 77 } 78 } catch (RuntimeException ex) { 79 throw ex; 80 } catch (Exception ex) { 81 throw new RuntimeException(ex.toString(), ex); 82 } 83 } 84 85 // a selection of commonly traded, stable currencies 86 /** 87 * The currency 'USD' - United States Dollar. 88 */ 89 public static final CurrencyUnit USD = of("USD"); 90 /** 91 * The currency 'EUR' - Euro. 92 */ 93 public static final CurrencyUnit EUR = of("EUR"); 94 /** 95 * The currency 'JPY' - Japanese Yen. 96 */ 97 public static final CurrencyUnit JPY = of("JPY"); 98 /** 99 * The currency 'GBP' - British pound. 100 */ 101 public static final CurrencyUnit GBP = of("GBP"); 102 /** 103 * The currency 'CHF' - Swiss Franc. 104 */ 105 public static final CurrencyUnit CHF = of("CHF"); 106 /** 107 * The currency 'AUD' - Australian Dollar. 108 */ 109 public static final CurrencyUnit AUD = of("AUD"); 110 /** 111 * The currency 'CAD' - Canadian Dollar. 112 */ 113 public static final CurrencyUnit CAD = of("CAD"); 114 115 /** 116 * The currency code, not null. 117 */ 118 private final String code; 119 /** 120 * The numeric currency code. 121 */ 122 private final short numericCode; 123 /** 124 * The number of decimal places. 125 */ 126 private final short decimalPlaces; 127 128 //----------------------------------------------------------------------- 129 /** 130 * Registers a currency allowing it to be used. 131 * <p> 132 * This class only permits known currencies to be returned. 133 * To achieve this, all currencies have to be registered in advance. 134 * <p> 135 * Since this method is public, it is possible to add currencies in 136 * application code. It is recommended to do this only at startup, however 137 * it is safe to do so later as the internal implementation is thread-safe. 138 * 139 * @param currencyCode the currency code, not null 140 * @param numericCurrencyCode the numeric currency code, -1 if none 141 * @param decimalPlaces the number of decimal places that the currency 142 * normally has, from 0 to 9 (normally 0, 2 or 3), or -1 for a pseudo-currency 143 * @param countryCodes the country codes to register the currency under, not null 144 * @return the new instance, never null 145 * @throws IllegalArgumentException if the code is already registered, or the 146 * specified data is invalid 147 */ 148 public static synchronized CurrencyUnit registerCurrency( 149 String currencyCode, int numericCurrencyCode, int decimalPlaces, List<String> countryCodes) { 150 return registerCurrency(currencyCode, numericCurrencyCode, decimalPlaces, countryCodes, false); 151 } 152 153 /** 154 * Registers a currency allowing it to be used, allowing replacement. 155 * <p> 156 * This class only permits known currencies to be returned. 157 * To achieve this, all currencies have to be registered in advance. 158 * <p> 159 * Since this method is public, it is possible to add currencies in 160 * application code. It is recommended to do this only at startup, however 161 * it is safe to do so later as the internal implementation is thread-safe. 162 * <p> 163 * This method uses a flag to determine whether the registered currency 164 * must be new, or can replace an existing currency. 165 * 166 * @param currencyCode the currency code, not null 167 * @param numericCurrencyCode the numeric currency code, -1 if none 168 * @param decimalPlaces the number of decimal places that the currency 169 * normally has, from 0 to 9 (normally 0, 2 or 3), or -1 for a pseudo-currency 170 * @param countryCodes the country codes to register the currency under, 171 * use of ISO-3166 is recommended, not null 172 * @param force true to register forcefully, replacing any existing matching currency, 173 * false to validate that there is no existing matching currency 174 * @return the new instance, never null 175 * @throws IllegalArgumentException if the code is already registered and {@code force} is false; 176 * or if the specified data is invalid 177 */ 178 public static synchronized CurrencyUnit registerCurrency( 179 String currencyCode, int numericCurrencyCode, int decimalPlaces, List<String> countryCodes, boolean force) { 180 MoneyUtils.checkNotNull(currencyCode, "Currency code must not be null"); 181 if (numericCurrencyCode < -1 || numericCurrencyCode > 999) { 182 throw new IllegalArgumentException("Invalid numeric code"); 183 } 184 if (decimalPlaces < -1 || decimalPlaces > 9) { 185 throw new IllegalArgumentException("Invalid number of decimal places"); 186 } 187 MoneyUtils.checkNotNull(countryCodes, "Country codes must not be null"); 188 189 CurrencyUnit currency = new CurrencyUnit(currencyCode, (short) numericCurrencyCode, (short) decimalPlaces); 190 if (force) { 191 currenciesByCode.remove(currencyCode); 192 currenciesByNumericCode.remove(numericCurrencyCode); 193 for (String countryCode : countryCodes) { 194 currenciesByCountry.remove(countryCode); 195 } 196 } else { 197 if (currenciesByCode.containsKey(currencyCode) || currenciesByNumericCode.containsKey(numericCurrencyCode)) { 198 throw new IllegalArgumentException("Currency already registered: " + currencyCode); 199 } 200 for (String countryCode : countryCodes) { 201 if (currenciesByCountry.containsKey(countryCode)) { 202 throw new IllegalArgumentException("Currency already registered for country: " + countryCode); 203 } 204 } 205 } 206 currenciesByCode.putIfAbsent(currencyCode, currency); 207 if (numericCurrencyCode >= 0) { 208 currenciesByNumericCode.putIfAbsent(numericCurrencyCode, currency); 209 } 210 for (String countryCode : countryCodes) { 211 currenciesByCountry.put(countryCode, currency); 212 } 213 return currenciesByCode.get(currencyCode); 214 } 215 216 /** 217 * Gets the list of all registered currencies. 218 * <p> 219 * This class only permits known currencies to be returned, thus this list is 220 * the complete list of valid singleton currencies. The list may change after 221 * application startup, however this isn't recommended. 222 * 223 * @return the sorted, independent, list of all registered currencies, never null 224 */ 225 public static List<CurrencyUnit> registeredCurrencies() { 226 ArrayList<CurrencyUnit> list = new ArrayList<CurrencyUnit>(currenciesByCode.values()); 227 Collections.sort(list); 228 return list; 229 } 230 231 //----------------------------------------------------------------------- 232 /** 233 * Obtains an instance of {@code CurrencyUnit} matching the specified JDK currency. 234 * <p> 235 * This converts the JDK currency instance to a currency unit using the code. 236 * 237 * @param currency the currency, not null 238 * @return the singleton instance, never null 239 * @throws IllegalCurrencyException if the currency is unknown 240 */ 241 public static CurrencyUnit of(Currency currency) { 242 MoneyUtils.checkNotNull(currency, "Currency must not be null"); 243 return of(currency.getCurrencyCode()); 244 } 245 246 /** 247 * Obtains an instance of {@code CurrencyUnit} for the specified ISO-4217 three letter currency code. 248 * <p> 249 * A currency is uniquely identified by ISO-4217 three letter code. 250 * 251 * @param currencyCode the currency code, not null 252 * @return the singleton instance, never null 253 * @throws IllegalCurrencyException if the currency is unknown 254 */ 255 @FromString 256 public static CurrencyUnit of(String currencyCode) { 257 MoneyUtils.checkNotNull(currencyCode, "Currency code must not be null"); 258 CurrencyUnit currency = currenciesByCode.get(currencyCode); 259 if (currency == null) { 260 throw new IllegalCurrencyException("Unknown currency '" + currencyCode + '\''); 261 } 262 return currency; 263 } 264 265 /** 266 * Obtains an instance of {@code CurrencyUnit} for the specified ISO-4217 numeric currency code. 267 * <p> 268 * The numeric code is an alternative to the three letter code. 269 * This method is lenient and does not require the string to be left padded with zeroes. 270 * 271 * @param numericCurrencyCode the currency code, not null 272 * @return the singleton instance, never null 273 * @throws IllegalCurrencyException if the currency is unknown 274 */ 275 public static CurrencyUnit ofNumericCode(String numericCurrencyCode) { 276 MoneyUtils.checkNotNull(numericCurrencyCode, "Currency code must not be null"); 277 switch (numericCurrencyCode.length()) { 278 case 1: 279 return ofNumericCode(numericCurrencyCode.charAt(0) - '0'); 280 case 2: 281 return ofNumericCode((numericCurrencyCode.charAt(0) - '0') * 10 + 282 numericCurrencyCode.charAt(1) - '0'); 283 case 3: 284 return ofNumericCode((numericCurrencyCode.charAt(0) - '0') * 100 + 285 (numericCurrencyCode.charAt(1) - '0') * 10 + 286 numericCurrencyCode.charAt(2) - '0'); 287 default: 288 throw new IllegalCurrencyException("Unknown currency '" + numericCurrencyCode + '\''); 289 } 290 } 291 292 /** 293 * Obtains an instance of {@code CurrencyUnit} for the specified ISO-4217 numeric currency code. 294 * <p> 295 * The numeric code is an alternative to the three letter code. 296 * 297 * @param numericCurrencyCode the numeric currency code, not null 298 * @return the singleton instance, never null 299 * @throws IllegalCurrencyException if the currency is unknown 300 */ 301 public static CurrencyUnit ofNumericCode(int numericCurrencyCode) { 302 CurrencyUnit currency = currenciesByNumericCode.get(numericCurrencyCode); 303 if (currency == null) { 304 throw new IllegalCurrencyException("Unknown currency '" + numericCurrencyCode + '\''); 305 } 306 return currency; 307 } 308 309 /** 310 * Obtains an instance of {@code CurrencyUnit} for the specified locale. 311 * <p> 312 * Only the country is used from the locale. 313 * 314 * @param locale the locale, not null 315 * @return the singleton instance, never null 316 * @throws IllegalCurrencyException if the currency is unknown 317 */ 318 public static CurrencyUnit of(Locale locale) { 319 MoneyUtils.checkNotNull(locale, "Locale must not be null"); 320 CurrencyUnit currency = currenciesByCountry.get(locale.getCountry()); 321 if (currency == null) { 322 throw new IllegalCurrencyException("Unknown currency for locale '" + locale + '\''); 323 } 324 return currency; 325 } 326 327 /** 328 * Obtains an instance of {@code CurrencyUnit} for the specified ISO-3166 country code. 329 * <p> 330 * Country codes should generally be in upper case. 331 * This method is case sensitive. 332 * 333 * @param countryCode the country code, typically ISO-3166, not null 334 * @return the singleton instance, never null 335 * @throws IllegalCurrencyException if the currency is unknown 336 */ 337 public static CurrencyUnit ofCountry(String countryCode) { 338 MoneyUtils.checkNotNull(countryCode, "Country code must not be null"); 339 CurrencyUnit currency = currenciesByCountry.get(countryCode); 340 if (currency == null) { 341 throw new IllegalCurrencyException("Unknown currency for country '" + countryCode + '\''); 342 } 343 return currency; 344 } 345 346 //----------------------------------------------------------------------- 347 /** 348 * Obtains an instance of {@code CurrencyUnit} for the specified currency code. 349 * <p> 350 * This method exists to match the API of {@link Currency}. 351 * 352 * @param currencyCode the currency code, not null 353 * @return the singleton instance, never null 354 * @throws IllegalCurrencyException if the currency is unknown 355 */ 356 public static CurrencyUnit getInstance(String currencyCode) { 357 return CurrencyUnit.of(currencyCode); 358 } 359 360 /** 361 * Obtains an instance of {@code CurrencyUnit} for the specified locale. 362 * <p> 363 * This method exists to match the API of {@link Currency}. 364 * 365 * @param locale the locale, not null 366 * @return the singleton instance, never null 367 * @throws IllegalCurrencyException if the currency is unknown 368 */ 369 public static CurrencyUnit getInstance(Locale locale) { 370 return CurrencyUnit.of(locale); 371 } 372 373 //----------------------------------------------------------------------- 374 /** 375 * Constructor, creating a new currency instance. 376 * 377 * @param code the currency code, not null 378 * @param numericCurrencyCode the numeric currency code, -1 if none 379 * @param decimalPlaces the decimal places, not null 380 */ 381 CurrencyUnit(String code, short numericCurrencyCode, short decimalPlaces) { 382 assert code != null : "Joda-Money bug: Currency code must not be null"; 383 this.code = code; 384 this.numericCode = numericCurrencyCode; 385 this.decimalPlaces = decimalPlaces; 386 } 387 388 /** 389 * Block malicious data streams. 390 * 391 * @param ois the input stream, not null 392 * @throws InvalidObjectException 393 */ 394 private void readObject(ObjectInputStream ois) throws InvalidObjectException { 395 throw new InvalidObjectException("Serialization delegate required"); 396 } 397 398 /** 399 * Uses a serialization delegate. 400 * 401 * @return the replacing object, never null 402 */ 403 private Object writeReplace() { 404 return new Ser(Ser.CURRENCY_UNIT, this); 405 } 406 407 //----------------------------------------------------------------------- 408 /** 409 * Gets the ISO-4217 three letter currency code. 410 * <p> 411 * Each currency is uniquely identified by a three-letter code. 412 * 413 * @return the three letter ISO-4217 currency code, never null 414 */ 415 public String getCode() { 416 return code; 417 } 418 419 /** 420 * Gets the ISO-4217 numeric currency code. 421 * <p> 422 * The numeric code is an alternative to the standard three letter code. 423 * 424 * @return the numeric currency code 425 */ 426 public int getNumericCode() { 427 return numericCode; 428 } 429 430 /** 431 * Gets the ISO-4217 numeric currency code as a three digit string. 432 * <p> 433 * This formats the numeric code as a three digit string prefixed by zeroes if necessary. 434 * If there is no valid code, then an empty string is returned. 435 * 436 * @return the three digit numeric currency code, empty is no code, never null 437 */ 438 public String getNumeric3Code() { 439 if (numericCode < 0) { 440 return ""; 441 } 442 String str = Integer.toString(numericCode); 443 if (str.length() == 1) { 444 return "00" + str; 445 } 446 if (str.length() == 2) { 447 return "0" + str; 448 } 449 return str; 450 } 451 452 /** 453 * Gets the country codes applicable to this currency. 454 * <p> 455 * A currency is typically valid in one or more countries. 456 * The codes are typically defined by ISO-3166. 457 * An empty set indicates that no the currency is not associated with a country code. 458 * 459 * @return the country codes, may be empty, not null 460 */ 461 public Set<String> getCountryCodes() { 462 Set<String> countryCodes = new HashSet<String>(); 463 for (Entry<String, CurrencyUnit> entry : currenciesByCountry.entrySet()) { 464 if (this.equals(entry.getValue())) { 465 countryCodes.add(entry.getKey()); 466 } 467 } 468 return countryCodes; 469 } 470 471 //----------------------------------------------------------------------- 472 /** 473 * Gets the number of decimal places typically used by this currency. 474 * <p> 475 * Different currencies have different numbers of decimal places by default. 476 * For example, 'GBP' has 2 decimal places, but 'JPY' has zero. 477 * Pseudo-currencies will return zero. 478 * <p> 479 * See also {@link #getDefaultFractionDigits()}. 480 * 481 * @return the decimal places, from 0 to 9 (normally 0, 2 or 3) 482 */ 483 public int getDecimalPlaces() { 484 return decimalPlaces < 0 ? 0 : decimalPlaces; 485 } 486 487 /** 488 * Checks if this is a pseudo-currency. 489 * 490 * @return true if this is a pseudo-currency 491 */ 492 public boolean isPseudoCurrency() { 493 return decimalPlaces < 0; 494 } 495 496 //----------------------------------------------------------------------- 497 /** 498 * Gets the ISO-4217 three-letter currency code. 499 * <p> 500 * This method matches the API of {@link Currency}. 501 * 502 * @return the currency code, never null 503 */ 504 public String getCurrencyCode() { 505 return code; 506 } 507 508 /** 509 * Gets the number of fractional digits typically used by this currency. 510 * <p> 511 * Different currencies have different numbers of fractional digits by default. 512 * For example, 'GBP' has 2 fractional digits, but 'JPY' has zero. 513 * Pseudo-currencies are indicated by -1. 514 * <p> 515 * This method matches the API of {@link Currency}. 516 * The alternative method {@link #getDecimalPlaces()} may be more useful. 517 * 518 * @return the fractional digits, from 0 to 9 (normally 0, 2 or 3), or -1 for pseudo-currencies 519 */ 520 public int getDefaultFractionDigits() { 521 return decimalPlaces; 522 } 523 524 //----------------------------------------------------------------------- 525 /** 526 * Gets the symbol for this locale from the JDK. 527 * <p> 528 * If this currency doesn't have a JDK equivalent, then the currency code 529 * is returned. 530 * <p> 531 * This method matches the API of {@link Currency}. 532 * 533 * @return the JDK currency instance, never null 534 */ 535 public String getSymbol() { 536 try { 537 return Currency.getInstance(code).getSymbol(); 538 } catch (IllegalArgumentException ex) { 539 return code; 540 } 541 } 542 543 /** 544 * Gets the symbol for this locale from the JDK. 545 * <p> 546 * If this currency doesn't have a JDK equivalent, then the currency code 547 * is returned. 548 * <p> 549 * This method matches the API of {@link Currency}. 550 * 551 * @param locale the locale to get the symbol for, not null 552 * @return the JDK currency instance, never null 553 */ 554 public String getSymbol(Locale locale) { 555 MoneyUtils.checkNotNull(locale, "Locale must not be null"); 556 try { 557 return Currency.getInstance(code).getSymbol(locale); 558 } catch (IllegalArgumentException ex) { 559 return code; 560 } 561 } 562 563 //----------------------------------------------------------------------- 564 /** 565 * Gets the JDK currency instance equivalent to this currency. 566 * <p> 567 * This attempts to convert a {@code CurrencyUnit} to a JDK {@code Currency}. 568 * 569 * @return the JDK currency instance, never null 570 * @throws IllegalArgumentException if no matching currency exists in the JDK 571 */ 572 public Currency toCurrency() { 573 return Currency.getInstance(code); 574 } 575 576 //----------------------------------------------------------------------- 577 /** 578 * Compares this currency to another by alphabetical comparison of the code. 579 * 580 * @param other the other currency, not null 581 * @return negative if earlier alphabetically, 0 if equal, positive if greater alphabetically 582 */ 583 public int compareTo(CurrencyUnit other) { 584 return code.compareTo(other.code); 585 } 586 587 /** 588 * Checks if this currency equals another currency. 589 * <p> 590 * The comparison checks the 3 letter currency code. 591 * 592 * @param obj the other currency, null returns false 593 * @return true if equal 594 */ 595 @Override 596 public boolean equals(Object obj) { 597 if (obj == this) { 598 return true; 599 } 600 if (obj instanceof CurrencyUnit) { 601 return code.equals(((CurrencyUnit) obj).code); 602 } 603 return false; 604 } 605 606 /** 607 * Returns a suitable hash code for the currency. 608 * 609 * @return the hash code 610 */ 611 @Override 612 public int hashCode() { 613 return code.hashCode(); 614 } 615 616 //----------------------------------------------------------------------- 617 /** 618 * Gets the currency code as a string. 619 * 620 * @return the currency code, never null 621 */ 622 @Override 623 @ToString 624 public String toString() { 625 return code; 626 } 627 628 }