task4/solution06/src/org/apidesign/apifest08/currency/Convertor.java
author Jaroslav Tulach <jaroslav.tulach@apidesign.org>
Fri, 17 Oct 2008 17:32:34 +0200
changeset 64 51f7b894eba6
parent 61 58ec6da75f6f
permissions -rw-r--r--
Solution 6, task 4
     1 package org.apidesign.apifest08.currency;
     2 
     3 import static org.apidesign.apifest08.currency.Assert.notNull;
     4 
     5 import java.math.BigDecimal;
     6 import java.math.RoundingMode;
     7 import java.util.ArrayList;
     8 import java.util.Calendar;
     9 import java.util.Currency;
    10 import java.util.Date;
    11 import java.util.GregorianCalendar;
    12 import java.util.List;
    13 import java.util.TimeZone;
    14 
    15 /**
    16  * Currency covertor.
    17  */
    18 public final class Convertor {
    19 	
    20 	private List<ConvertorDelegate> convertorDelegates = new ArrayList<ConvertorDelegate>();
    21 	
    22 	
    23 	/**
    24 	 * Create new instance of the converter for the given currencies and its rate.
    25 	 * 
    26 	 * @param rateValue the rate between the first and the second currency
    27 	 * @param currencyFirst the first currency
    28 	 * @param currencySecond the second currency
    29 	 */
    30 	public Convertor(BigDecimal rateValue, Currency currencyFirst, Currency currencySecond) {
    31 		notNull(currencyFirst, "currencyFirst");
    32 		notNull(currencySecond, "currencySecond");		
    33 		notNull(rateValue, "rateValue");	
    34 		convertorDelegates.add(new ConvertorDelegate(new StaticRateProvider(rateValue), currencyFirst, currencySecond, null, null));
    35 	}
    36 	
    37 	/**
    38 	 * Create new instance of the converter for the given currencies and its rate. 
    39 	 * A rate value is provided by {@link RateProvider}.
    40 	 * 
    41 	 * @param rateProvider the rate provider
    42 	 * @param currencyFirst the first currency
    43 	 * @param currencySecond the second currency
    44 	 */
    45 	public Convertor(RateProvider rateProvider, Currency currencyFirst, Currency currencySecond) {
    46 		notNull(currencyFirst, "currencyFirst");
    47 		notNull(currencySecond, "currencySecond");		
    48 		notNull(rateProvider, "rateProvider");
    49 		convertorDelegates.add(new ConvertorDelegate(rateProvider, currencyFirst, currencySecond, null, null));
    50 	}
    51 	
    52 	/**
    53 	 * Create new instance of the convertor. The associated rate(s) is timely limited
    54 	 * by a given 'from' and 'till'. 
    55 	 * @param convertor
    56 	 * @param from
    57 	 * @param till
    58 	 */
    59 	public Convertor(Convertor convertor, Date from, Date till) {
    60 	    notNull(convertor, "convertor");
    61 		notNull(from, "from");
    62 		notNull(till, "till");				
    63 		for(ConvertorDelegate delegate: convertor.convertorDelegates) {
    64 			convertorDelegates.add(new ConvertorDelegate(delegate.rateProvider, delegate.first, delegate.second, from, till));
    65 		}
    66 	}
    67 	
    68 	/**
    69 	 * Create new instance of the convertor from the given convertors. 
    70 	 * @param convertors the convertors
    71 	 */
    72 	public Convertor(Convertor... convertors) {
    73 		notNull(convertors, "convertors");
    74 		if(convertors.length == 0) {
    75 			throw new IllegalArgumentException("There must be at least one converter.");
    76 		}
    77 		
    78 		for(Convertor convertor: convertors) {
    79 			if(convertor != null) {
    80 				for(ConvertorDelegate delegate: convertor.convertorDelegates) {
    81 					convertorDelegates.add(new ConvertorDelegate(delegate.rateProvider, delegate.first, delegate.second, delegate.from, delegate.till));
    82 				}
    83 			}
    84 		}
    85 	}
    86     	
    87 	/**
    88 	 * Converts an amount value between the two currencies of this converter. The rate is taken 
    89 	 * for current time.
    90 	 *  
    91 	 * @param amount an amount
    92 	 * @param fromCurrency an amount currency
    93 	 * @param toCurrency to a target currency
    94 	 * @return a converted amount value
    95 	 * 
    96 	 * @throws ConversionException if the conversion fails
    97 	 * @throws UnsupportedConversionException if the conversion between a given currencies is not supported.
    98 	 */
    99 	public Amount convert(BigDecimal amount, Currency fromCurrency, Currency toCurrency) throws ConversionException {
   100 		return convert(amount, fromCurrency, toCurrency, new Date(System.currentTimeMillis()));
   101 	}
   102 
   103 	/**
   104 	 * Converts an amount value between the two currencies of this converter and its 
   105 	 * associated rate for a given time.
   106 	 *  
   107 	 * @param amount an amount
   108 	 * @param fromCurrency an amount currency
   109 	 * @param toCurrency to a target currency
   110 	 * @param time time
   111 	 * @return a converted amount value
   112 	 * 
   113 	 * @throws ConversionException if the conversion fails
   114 	 * @throws UnsupportedConversionException if the conversion between a given currencies and time is not supported.
   115 	 */
   116 	public Amount convert(BigDecimal amount, Currency fromCurrency, Currency toCurrency, Date time) throws ConversionException {
   117 		notNull(amount, "amount");
   118 		notNull(fromCurrency, "fromCurrency");
   119 		notNull(toCurrency, "toCurrency");		
   120 		notNull(time, "time");
   121 		
   122 		ConvertorDelegate appropriateDelegate = null;
   123 		//try find an appropriate delegate for conversion
   124 		for(ConvertorDelegate delegate : convertorDelegates) {
   125 			if(delegate.isConversionSupported(fromCurrency, toCurrency) && delegate.isConversionSupported(time)) {				
   126 				appropriateDelegate = delegate;				
   127 			}
   128 		}
   129 		if(appropriateDelegate == null) {
   130 			throw new UnsupportedConversionException(fromCurrency, toCurrency, time);
   131 		}
   132 		
   133 		return appropriateDelegate.convert(amount, fromCurrency, toCurrency);
   134 	}
   135 	
   136 	/**
   137 	 * Internal delegate implements a logic for conversion between two currencies
   138 	 * and vice versa. There could be time limitation for the associated rate.
   139 	 * 
   140 	 * @see #isConversionSupported(Currency, Currency)
   141 	 */
   142 	private static class ConvertorDelegate {
   143 		
   144 		private final Currency first;
   145 		private final Currency second;
   146 		private final RateProvider rateProvider;
   147 		private final Date from;
   148 		private final Date till;
   149 		public static final BigDecimal one = new BigDecimal(1);
   150 		
   151 		
   152 		private ConvertorDelegate(RateProvider rateProvider, Currency currencyFirst, Currency currencySecond, Date from, Date till) {
   153 			this.rateProvider = rateProvider;
   154 			this.first = currencyFirst;
   155 			this.second = currencySecond;
   156 			this.from = from;
   157 			this.till = till;
   158 		}
   159 		
   160 		private Amount convert(BigDecimal amount, Currency fromCurrency, Currency toCurrency) throws ConversionException {
   161 			BigDecimal rateValue = getRateValue(fromCurrency, toCurrency);
   162 			BigDecimal result = rateValue.multiply(amount);			
   163 			return new Amount(result, toCurrency);	
   164 		}
   165 		
   166 		private BigDecimal getRateValue(Currency fromCurrency, Currency toCurrency) {		
   167 			
   168 			BigDecimal retVal;
   169 			
   170 			if(first == fromCurrency) {
   171 				BigDecimal rateValue = rateProvider.getRate();
   172 				if(rateValue == null) {
   173 					throw new NullPointerException("Rate cannot be null!");
   174 				}
   175 				retVal = rateValue;
   176 			} else {	
   177 				BigDecimal rateValue = rateProvider.getRate();
   178 				if(rateValue == null) {
   179 					throw new NullPointerException("Rate cannot be null!");
   180 				}
   181 				//reverse rate	
   182 				retVal = one.divide(rateValue, 10 ,RoundingMode.HALF_UP);
   183 			}
   184 			
   185 			return retVal;
   186 		}
   187 		
   188 		/**
   189 		 * @return <code>true</code> if the delegate is able to convert from the given currency
   190 		 * to the given currency and vice versa otherwise <code>false</code>.
   191 		 */
   192 		private boolean isConversionSupported(Currency fromCurrency, Currency toCurrency) {
   193 			return ((fromCurrency == first || fromCurrency == second) && (toCurrency == first || toCurrency == second));
   194 		}
   195 		
   196 		/**
   197 		 * @return <code>true</code> if the delegate is able to convert in a given
   198 		 * time.
   199 		 */
   200 		private boolean isConversionSupported(Date time) {
   201 			boolean retVal;
   202 			if(this.from != null && this.till != null) {
   203 				retVal = getDateInGMT(from).getTime() <= getDateInGMT(time).getTime() && getDateInGMT(till).getTime() >= getDateInGMT(time).getTime();  
   204 		    } else {
   205 				retVal = true; //delegate is applicable "for eternity"				
   206 			}
   207 			return retVal;
   208 		}
   209 		
   210 		private Date getDateInGMT(Date currentDate) {
   211 			TimeZone tz = TimeZone.getTimeZone("GMT");
   212 			
   213 			Calendar mbCal = new GregorianCalendar(tz);
   214 			mbCal.setTimeInMillis(currentDate.getTime());
   215 
   216 			Calendar cal = Calendar.getInstance();
   217 			cal.set(Calendar.YEAR, mbCal.get(Calendar.YEAR));
   218 			cal.set(Calendar.MONTH, mbCal.get(Calendar.MONTH));
   219 			cal.set(Calendar.DAY_OF_MONTH, mbCal.get(Calendar.DAY_OF_MONTH));
   220 			cal.set(Calendar.HOUR_OF_DAY, mbCal.get(Calendar.HOUR_OF_DAY));
   221 			cal.set(Calendar.MINUTE, mbCal.get(Calendar.MINUTE));
   222 			cal.set(Calendar.SECOND, mbCal.get(Calendar.SECOND));
   223 			cal.set(Calendar.MILLISECOND, mbCal.get(Calendar.MILLISECOND));
   224 
   225 			return cal.getTime();
   226 		}
   227 	}
   228 	
   229 	/**
   230 	 * A rate provider. This class represents a way how could be "static" convertor
   231 	 * extended in order converts according to current rate.
   232 	 */
   233 	public static abstract class RateProvider { 
   234 		
   235 		/**
   236 		 * @return a rate between the from currency and the to currency associated with
   237 		 * a given convertor.
   238 		 */
   239 		public abstract BigDecimal getRate();
   240 	}
   241 	
   242 	private static class StaticRateProvider extends RateProvider{
   243 		private final BigDecimal rateValue;
   244 		
   245 		private StaticRateProvider(BigDecimal rateValue){
   246 			this.rateValue = rateValue;
   247 		}
   248 		
   249 		public BigDecimal getRate() {
   250 			return this.rateValue;
   251 		}
   252 	}
   253 }