package com.robonobo.wang.client;
import java.io.File;
import java.util.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import com.robonobo.common.concurrent.CatchingRunnable;
import com.robonobo.common.exceptions.SeekInnerCalmException;
import com.robonobo.common.http.PreemptiveHttpClient;
import com.robonobo.common.util.TextUtil;
import com.robonobo.wang.*;
import com.robonobo.wang.beans.*;
import com.robonobo.wang.proto.WangProtocol.BlindedCoinListMsg;
import com.robonobo.wang.proto.WangProtocol.BlindedCoinListMsg.Status;
import com.robonobo.wang.proto.WangProtocol.CoinListMsg;
import com.robonobo.wang.proto.WangProtocol.CoinMsg;
import com.robonobo.wang.proto.WangProtocol.CoinRequestListMsg;
import com.robonobo.wang.proto.WangProtocol.DenominationListMsg;
import com.robonobo.wang.proto.WangProtocol.DenominationMsg;
import com.robonobo.wang.proto.WangProtocol.DepositStatusMsg;
public class WangClient {
private Log log = LogFactory.getLog(getClass());
private WangConfig config;
private BankFacade bank;
private LucreFacade lucre;
/**
* Take note of the smallest denomination, as we round up any withdrawal requests
*/
private int smallestDenom;
/** When we do below this value of local coins, request more */
private double thresholdFloatLevel;
/** What coins we should request <denom, num to req> */
private Map<Integer, Integer> floatCoinsToReq;
/**
* Withdraws from the bank as necessary to maintain our float of coinage
*/
private Thread floatUpdaterThread;
// This map is in descending key order
private SortedMap<Integer, DenominationPublic> denoms = new TreeMap<Integer, DenominationPublic>(
new DescendingIntComp());
private CoinStore coinStore;
public WangClient(WangConfig config, PreemptiveHttpClient client) {
this.config = config;
bank = new BankFacade(config.getBankUrl(), config.getAccountEmail(), config.getAccountPwd(), client);
lucre = new LucreFacade();
}
public void start() throws WangException {
File coinStoreDir = new File(config.getCoinStoreDir());
log.info("Wang client starting with bank url "+config.getBankUrl()+" and coin store dir "+coinStoreDir.getAbsolutePath());
coinStore = new CoinStore(coinStoreDir, config.getAccountPwd());
// Fetch our set of denominations
DenominationListMsg list = bank.getDenominations();
smallestDenom = Integer.MAX_VALUE;
for (DenominationMsg denomMsg : list.getDenominationList()) {
denoms.put(denomMsg.getDenom(), new DenominationPublic(denomMsg));
if (denomMsg.getDenom() < smallestDenom)
smallestDenom = denomMsg.getDenom();
}
log.info("Got coin denominations: " + TextUtil.commaSepList(denoms.keySet()));
// Withdraw initial float
// TODO What if our balance is less than this float value? Check
double totalFloatVal = 0;
floatCoinsToReq = new HashMap<Integer, Integer>();
String floatStr = config.getFloatLevel();
String[] outerToks = floatStr.split(",");
for (String outerTok : outerToks) {
String[] innerToks = outerTok.split(":");
if (innerToks.length != 2)
throw new WangConfigException("Invalid float string: " + floatStr);
int denom = Integer.parseInt(innerToks[0]);
int numToGet = Integer.parseInt(innerToks[1]);
floatCoinsToReq.put(denom, numToGet);
totalFloatVal += numToGet * getDenomValue(denom);
}
// Withdraw more when we get to half this much
thresholdFloatLevel = totalFloatVal / 2d;
withdrawCoins(floatCoinsToReq);
}
public void stop() throws WangException {
log.info("Wang client stopping");
waitForFloatUpdater();
// Return our coins to the bank
CoinListMsg.Builder clBldr = CoinListMsg.newBuilder();
for (int denom : denoms.keySet()) {
int numCoins = coinStore.numCoins(denom);
for (int i = 0; i < numCoins; i++) {
clBldr.addCoin(coinStore.getCoin(denom));
}
}
CoinListMsg cl = clBldr.build();
try {
putCoins(cl);
} catch (WangException e) {
log.error("Error returning on-hand coins to the bank - persisting in local store");
for (CoinMsg coin : cl.getCoinList()) {
coinStore.putCoin(coin);
}
}
log.info("Wang client stopped");
}
/**
* Returns a list of coins that are at least as much as the total value, and as close as possible to it (might be
* slightly over due to the requested amount not being representable in our denoms). Once these coins are withdrawn,
* the withdrawer has responsibility for them.
*/
public CoinListMsg getCoins(double totalValue) throws WangException {
double valSoFar = 0;
CoinListMsg.Builder clBldr = CoinListMsg.newBuilder();
// First, get any coins we have in our store
denomLoop: for (Integer denom : denoms.keySet()) {
while (valSoFar < totalValue && coinStore.numCoins(denom) > 0) {
double coinVal = getDenomValue(denom);
if (totalValue - valSoFar >= coinVal) {
clBldr.addCoin(coinStore.getCoin(denom));
valSoFar += coinVal;
} else {
continue denomLoop;
}
}
}
// If we have a shortfall, check to see if adding a smallest-denom coin
// will make it up
if (valSoFar < totalValue && valSoFar + getDenomValue(smallestDenom) >= totalValue
&& coinStore.numCoins(smallestDenom) > 0) {
clBldr.addCoin(coinStore.getCoin(smallestDenom));
valSoFar += getDenomValue(smallestDenom);
}
try {
if (valSoFar >= totalValue)
return clBldr.build();
else {
// We don't have enough locally - withdraw more
double amtToWithdraw = totalValue - valSoFar;
// Decide how many of each denom we want to withdraw
Map<Integer, Integer> toWithdraw = new HashMap<Integer, Integer>();
// Biggest denomination first
for (int denom : denoms.keySet()) {
int numThisDenom = (int) (amtToWithdraw / getDenomValue(denom));
if (numThisDenom > 0) {
toWithdraw.put(denom, numThisDenom);
amtToWithdraw -= getDenomValue(denom) * numThisDenom;
}
}
// We might need to add a smallest-denom coin to make up
// rounding
if (amtToWithdraw > 0) {
if (amtToWithdraw > getDenomValue(smallestDenom))
throw new SeekInnerCalmException();
int numSmallest = toWithdraw.containsKey(smallestDenom) ? toWithdraw.get(smallestDenom) : 0;
toWithdraw.put(smallestDenom, numSmallest + 1);
amtToWithdraw -= getDenomValue(smallestDenom);
}
try {
withdrawCoins(toWithdraw);
} catch (WangException e) {
// Oopsie... return our coins to our local store before
// re-throwing exception
for (CoinMsg coin : clBldr.getCoinList()) {
coinStore.putCoin(coin);
}
throw e;
}
clBldr.addAllCoin(getCoins(totalValue - valSoFar).getCoinList());
return clBldr.build();
}
} finally {
updateFloat();
}
}
public void putCoins(CoinListMsg coins) throws WangException {
DepositStatusMsg status = bank.depositCoins(coins);
if (status.getStatus() != DepositStatusMsg.Status.OK)
throw new BadCoinException();
if (log.isInfoEnabled()) {
Map<Integer, Integer> cMap = new HashMap<Integer, Integer>();
for (CoinMsg coin : coins.getCoinList()) {
int d = coin.getDenom();
if (cMap.containsKey(d))
cMap.put(d, cMap.get(d) + 1);
else
cMap.put(d, 1);
}
StringBuffer sb = new StringBuffer("Deposited coins: ");
boolean first = true;
for (Integer d : cMap.keySet()) {
if (first)
first = false;
else
sb.append(", ");
sb.append(cMap.get(d)).append("x").append(d);
}
log.info(sb);
}
}
/**
* Gets the total value of the coins we have on hand
*/
public double getOnHandBalance() {
double balance = 0;
for (Integer denom : denoms.keySet()) {
int numCoins = coinStore.numCoins(denom);
double denomValue = getDenomValue(denom);
balance += (denomValue * numCoins);
}
return balance;
}
/**
* Gets the balance of this account stored in the bank
*/
public double getBankBalance() throws WangException {
return bank.getBalance();
}
/**
* Gets the balance from the bank - waits for any pending withdrawal requests to complete before returning
*/
public double getAccurateBankBalance() throws WangException {
waitForFloatUpdater();
return bank.getBalance();
}
private double getDenomValue(Integer denom) {
return Math.pow(2, denom);
}
/**
*
* @param numCoins
* map<denomination, num to withdraw>
*/
private void withdrawCoins(Map<Integer, Integer> numCoins) throws WangException {
List<CoinRequestPrivate> privReqs = new ArrayList<CoinRequestPrivate>();
CoinRequestListMsg.Builder crlBldr = CoinRequestListMsg.newBuilder();
// Create coin requests
for (Integer denomExp : numCoins.keySet()) {
DenominationPublic denom = denoms.get(denomExp);
int numToGet = numCoins.get(denomExp);
for (int i = 0; i < numToGet; i++) {
CoinRequestPrivate privReq = lucre.createCoinRequest(denom);
privReqs.add(privReq);
crlBldr.addCoinRequest(new CoinRequestPublic(privReq).toMsg());
}
}
// Send coin requests to bank, get back coin signatures
CoinRequestListMsg crl = crlBldr.build();
BlindedCoinListMsg blCoins = bank.getCoins(crl);
if (blCoins.getStatus() == Status.InsufficientWang)
throw new InsufficientWangException();
if (blCoins.getCoinCount() != crl.getCoinRequestCount())
throw new WangServerException("Bank returned incorrect number of coins!");
// Unblind signatures to get coins
List<Coin> coins = new ArrayList<Coin>();
for (int i = 0; i < crl.getCoinRequestCount(); i++) {
CoinRequestPrivate privReq = privReqs.get(i);
BlindedCoin blCoin = new BlindedCoin(blCoins.getCoin(i));
DenominationPublic denom = denoms.get(privReq.getDenom());
Coin coin = lucre.unblindCoin(denom, blCoin, privReq);
coins.add(coin);
}
// Store coins locally
for (Coin coin : coins) {
coinStore.putCoin(coin.toMsg());
}
if (log.isInfoEnabled()) {
Map<Integer, Integer> cMap = new HashMap<Integer, Integer>();
for (Coin coin : coins) {
int d = coin.getDenom();
if (cMap.containsKey(d))
cMap.put(d, cMap.get(d) + 1);
else
cMap.put(d, 1);
}
StringBuffer sb = new StringBuffer("Withdrew coins: ");
boolean first = true;
for (Integer d : cMap.keySet()) {
if (first)
first = false;
else
sb.append(", ");
sb.append(cMap.get(d)).append("x").append(d);
}
log.info(sb);
}
}
private synchronized void updateFloat() {
if (getOnHandBalance() > thresholdFloatLevel)
return;
if (floatUpdaterThread != null)
return;
floatUpdaterThread = new Thread(new CatchingRunnable() {
public void doRun() throws Exception {
withdrawCoins(floatCoinsToReq);
floatUpdaterThread = null;
}
});
floatUpdaterThread.setName("Wang Float Updater");
floatUpdaterThread.start();
}
private void waitForFloatUpdater() {
Thread ft = floatUpdaterThread;
if (ft != null) {
try {
ft.join(10000);
} catch (InterruptedException ignore) {
}
}
}
/**
* Reverses the sort order (to become descending)
*/
private class DescendingIntComp implements Comparator<Integer> {
public int compare(Integer i1, Integer i2) {
return 0 - i1.compareTo(i2);
}
}
}