task4/solution04/src/org/apidesign/apifest08/currency/CompositeConvertorImpl.java
1 package org.apidesign.apifest08.currency;
4 import java.math.BigDecimal;
5 import java.math.RoundingMode;
6 import java.util.Collections;
7 import java.util.Currency;
8 import java.util.HashMap;
9 import java.util.HashSet;
15 * A composite convertor allows conversions between many currencies by forwarding conversion requests to stored convertors.
16 * A composite convertor will build all possible conversions that are allowed by the underlying set of convertors.
18 * @author D'Arcy Smith
21 final class CompositeConvertorImpl
25 * The convertors that are supported.
27 private final Convertor[] convertors;
30 * Keeps track of what convertors to use to convert between currencies.
32 private final Map<Currency, Map<Currency, Convertor>> possibleConversions;
35 possibleConversions = new HashMap<Currency, Map<Currency, Convertor>>();
39 * Construct a ComositeConvertorImpl with the specified convertors.
40 * This will result in all possible conversions between the supplied currencies being made.
42 * @param cs the convertors to use.
43 * @throws IllegalArgumentException if any of the items in cs are null.
45 CompositeConvertorImpl(Convertor ... cs)
51 // track all of the known conversion
52 for(final Convertor convertor : convertors)
54 final Set<Currency> currencies;
55 Map<Currency, Convertor> possible;
59 throw new IllegalArgumentException("cs cannot contain null");
62 currencies = convertor.getCurrencies();
64 for(final Currency currency : currencies)
66 possible = possibleConversions.get(currency);
70 possible = new HashMap<Currency, Convertor>();
71 possibleConversions.put(currency, possible);
74 for(final Currency c : currencies)
76 possible.put(c, convertor);
81 // make up conversions that can be derived... eg:
96 // todo... need to loop this until all the ones that can be handled are done.
97 for(final Currency from : getCurrencies())
99 for(final Currency to : getCurrencies())
101 if(!(canConvert(from, to)))
103 final Set<Currency> fromCurrencies;
104 final Set<Currency> toCurrencies;
105 final Set<Currency> common;
107 fromCurrencies = possibleConversions.get(from).keySet();
108 toCurrencies = possibleConversions.get(to).keySet();
109 common = new HashSet<Currency>();
111 for(final Currency currency : fromCurrencies)
113 if(toCurrencies.contains(currency))
115 common.add(currency);
119 for(final Currency currency : common)
121 final Convertor convertor;
123 convertor = createConvertor(from, to, currency);
124 possibleConversions.get(from).put(to, convertor);
125 possibleConversions.get(to).put(from, convertor);
132 while(newConvertors > 0);
136 * Check to see if converting between the two currencies is possible.
138 * @param from the currency to convert from.
139 * @param to the currency to convert to.
140 * @return true if the conversion is possible.
141 * @throws IllegalArgumentException if either from or to are null.
143 public boolean canConvert(final Currency from, final Currency to)
145 final Map<Currency, Convertor> possible;
149 throw new IllegalArgumentException("from cannot be null");
154 throw new IllegalArgumentException("to cannot be null");
157 possible = possibleConversions.get(from);
159 if(possible.containsKey(to))
168 * Get the currencies that the convertor supports. Just because a currency is
169 * supported does not mean that canConvert will return true.
171 * @return the supported currencies.
173 public Set<Currency> getCurrencies()
175 final Set<Currency> currencies;
177 currencies = possibleConversions.keySet();
179 return (Collections.unmodifiableSet(currencies));
183 * Get the conversion rate between two currencies.
185 * @param from the currency to convert from.
186 * @param to the currency to convert to.
187 * @return the conversion rate between the two currencies.
188 * @throws IllegalArgumentException if either from or to is null.
189 * @throws InvalidConversionException if canConvert would return false.
191 public BigDecimal getConversionRate(final Currency from, final Currency to)
192 throws InvalidConversionException
194 final Map<Currency, Convertor> possible;
199 throw new IllegalArgumentException("from cannot be null");
204 throw new IllegalArgumentException("to cannot be null");
207 if(!(canConvert(from, to)))
209 throw new InvalidConversionException("cannot convert", to);
212 possible = possibleConversions.get(from);
213 convertor = possible.get(to);
215 if(convertor == null)
220 return (convertor.getConversionRate(from, to));
223 private Convertor getConvertor(final Currency from, final Currency to)
224 throws InvalidConversionException
226 final Map<Currency, Convertor> possible;
231 throw new IllegalArgumentException("from cannot be null");
236 throw new IllegalArgumentException("to cannot be null");
239 if(!(canConvert(from, to)))
241 throw new InvalidConversionException("cannot convert", to);
244 possible = possibleConversions.get(from);
245 convertor = possible.get(to);
247 if(convertor == null)
256 * Convert an amount from one currency to another.
258 * @param from the currency to convert from.
259 * @param to the currency to convert to.
260 * @param amount the amount to convert.
261 * @return the converted amount.
262 * @throws IllegalArgumentException if any of the arguments are null.
263 * @throws InvalidConversionException if either from or to are not valid for the convertor.
265 public BigDecimal convert(final Currency from,
267 final BigDecimal amount)
268 throws InvalidConversionException
270 final BigDecimal result;
271 final Convertor convertor;
275 throw new IllegalArgumentException("amount cannot be null");
280 throw new IllegalArgumentException("from cannot be null");
285 throw new IllegalArgumentException("to cannot be null");
288 // fixed a bug from Task2 that showed up in Task3... before we did the conversion here,
289 // but that meant that the underlying covnerter convert method never got called... which
290 // meant that in Task3 the exchange rate never changed.
291 convertor = getConvertor(from, to);
292 result = convertor.convert(from, to, amount);
294 return (result.setScale(2, RoundingMode.HALF_DOWN));
298 * Create a convertor between two currencies using another currency that is able to convert between both.
300 * @param from the currency to convert from.
301 * @param to the currency to convert to.
302 * @param intermediary the currency to use as a go-between.
303 * @return a Convertor that is able to convert between from an to.
304 * @throws IllegalArgumentException if any of the arguments are null.
306 private Convertor createConvertor(final Currency from,
308 final Currency intermediary)
310 final Convertor fromIntermediary;
311 final Convertor toIntermediary;
315 throw new IllegalArgumentException("from cannot be null");
320 throw new IllegalArgumentException("to cannot be null");
323 if(intermediary == null)
325 throw new IllegalArgumentException("intermediary cannot be null");
328 fromIntermediary = possibleConversions.get(from).get(intermediary);
329 toIntermediary = possibleConversions.get(to).get(intermediary);
333 final BigDecimal fromRate;
334 final BigDecimal toRate;
335 final BigDecimal rate;
336 final Convertor convertor;
338 fromRate = fromIntermediary.getConversionRate(from, intermediary);
339 toRate = toIntermediary.getConversionRate(intermediary, to);
340 rate = fromRate.multiply(toRate);
341 convertor = ConvertorFactory.getConvertor(from, BigDecimal.ONE, to, rate);
345 catch (InvalidConversionException ex)