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;
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 }