/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2017 Cinchapi Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.cinchapi.concourse.example.bank;
import java.util.ArrayDeque;
import java.util.Set;
import com.cinchapi.concourse.Concourse;
import com.cinchapi.concourse.Link;
import com.cinchapi.concourse.TransactionException;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
/**
* An implementation of the {@link Account} interface that uses Concourse for
* data storage.
*
* @author Jeff Nelson
*/
public class ConcourseAccount implements Account {
// NOTE: For the purpose of this example, we don't store any data about the
// Account locally so that we can illustrate how you would query Concourse
// to get the data. In a real application, you always want to cache
// repeatedly used data locally.
/**
* Since Concourse does not have tables, we store a special key in each
* record to indicate the class to which the record/object belongs. This
* isn't necessary, but it helps to ensure logical consistency between the
* application and the database.
*/
private final static String CLASS_KEY_NAME = "_class";
/**
* The id of the Concourse record that holds the data for an instance of
* this class.
*/
private final long id;
/**
* This constructor creates a new record in Concourse and inserts the data
* expressed in the parameters.
*
* @param balance the initial balance for the account
* @param owners {@link Customer customers} that are owners on the account
*/
public ConcourseAccount(double balance, Customer... owners) {
Preconditions.checkArgument(balance > 0);
Concourse concourse = Constants.CONCOURSE_CONNECTIONS.request();
try {
Multimap<String, Object> data = HashMultimap.create();
data.put(CLASS_KEY_NAME, getClass().getName());
data.put("balance", balance);
for (Customer owner : owners) {
data.put("owners", Link.to(owner.getId()));
}
this.id = concourse.insert(data); // The #insert method is atomic,
// so we don't need a transaction
// to safely commit all the data
}
finally {
Constants.CONCOURSE_CONNECTIONS.release(concourse);
}
}
/**
* This constructor loads an existing object from Concourse.
*
* @param id the id of the record that holds the data for the object we want
* to load
*/
public ConcourseAccount(long id) {
Concourse concourse = Constants.CONCOURSE_CONNECTIONS.request();
try {
Preconditions.checkArgument(getClass().getName().equals(
concourse.get(CLASS_KEY_NAME, id)));
this.id = id;
// NOTE: If this were a real application, it would be a god idea to
// preload frequently used attributes here and cache them locally
// (or maybe even in an external cache like Memcache or Redis).
}
finally {
Constants.CONCOURSE_CONNECTIONS.release(concourse);
}
}
@Override
public boolean debit(String charge, double amount) {
Preconditions.checkArgument(amount > 0);
Concourse concourse = Constants.CONCOURSE_CONNECTIONS.request();
try {
concourse.stage();
if(withdrawImpl(concourse, amount)) {
// By using the #add method, we can store multiple charges in
// the record at the same time
concourse.add("charges", charge, id);
return concourse.commit();
}
else {
concourse.abort();
return false;
}
}
catch (TransactionException e) {
concourse.abort();
return false;
}
finally {
Constants.CONCOURSE_CONNECTIONS.release(concourse);
}
}
@Override
public boolean deposit(double amount) {
Preconditions.checkArgument(amount > 0);
// This implementation uses verifyAndSwap to atomically increment the
// account balance (without a using a transaction!) which ensures that
// money isn't lost if two people make a deposit at the same time.
Concourse concourse = Constants.CONCOURSE_CONNECTIONS.request();
try {
boolean incremented = false;
while (!incremented) {
double balance = concourse.get("balance", id);
double newBalance = balance + amount;
if(concourse.verifyAndSwap("balance", balance, id, newBalance)) {
incremented = true;
}
else {
// verifyAndSwap will fail if the balance changed since the
// initial read. If that happens, we simply retry
continue;
}
}
return true;
}
finally {
Constants.CONCOURSE_CONNECTIONS.release(concourse);
}
}
@Override
public double getBalance() {
Concourse concourse = Constants.CONCOURSE_CONNECTIONS.request();
try {
return concourse.get("balance", id);
}
finally {
Constants.CONCOURSE_CONNECTIONS.release(concourse);
}
}
@Override
public long getId() {
return id;
}
@Override
public Customer[] getOwners() {
Concourse concourse = Constants.CONCOURSE_CONNECTIONS.request();
try {
Set<Link> customerLinks = concourse.select("owners", id);
Customer[] owners = new Customer[customerLinks.size()];
int index = 0;
for (Link link : customerLinks) {
owners[index] = new ConcourseCustomer(link.longValue());
++index;
}
return owners;
}
finally {
Constants.CONCOURSE_CONNECTIONS.release(concourse);
}
}
@Override
public boolean withdraw(double amount) {
return debit("withdraw " + amount, amount);
}
/**
* An internal method that transfers money (if possible) from this account
* to an {@code other} one. This method doesn't start a transaction because
* it assumes that the caller has already done so.
*
* @param concourse the connection to Concourse that is retrieved from
* {@link Constants#CONCOURSE_CONNECTIONS}
* @param other the recipient {@link ConcourseAccount account} for the
* transferred funds
* @param amount the amount to transfer from this account.
* @return the amount of money that is actually transferred from this
* account to {@code other}. An account can only transfer as much
* money as it has. So, if this account has a balance that is
* smaller than {@code amount}, it will transfer only how much it
* has.
*/
private double transferTo(Concourse concourse, ConcourseAccount other,
double amount) {
double balance = concourse.get("balance", id);
if(balance > 0) {
double toTransfer = balance > amount ? amount : balance;
balance -= toTransfer;
double otherBalance = concourse.get("balance", other.getId());
otherBalance += toTransfer;
concourse.set("balance", balance, id);
concourse.set("balance", otherBalance, other.getId());
concourse.add("charges", "transfer " + toTransfer + " to account "
+ other.getId(), id);
return toTransfer;
}
else {
return 0;
}
}
/**
* An implementation that withdraws {@code amount} of money from this
* account using the provided {@code concourse} connection. This method does
* not start a new transaction because it assumes that the caller has
* already done so.
*
* @param concourse the connection to Concourse that is retrieved from
* {@link Constants#CONCOURSE_CONNECTIONS}
* @param amount the amount to withdraw
* @return {@code true} if the withdrawal is successful (e.g. there is
* enough money in the account (possibly after transferring from
* other accounts) to withdraw the money without leaving a negative
* balance).
*/
private boolean withdrawImpl(Concourse concourse, double amount) {
double balance = concourse.get("balance", id);
double need = amount - balance;
if(need > 0) {
// Get all the other accounts that are owned by the owners of this
// account
StringBuilder criteria = new StringBuilder();
criteria.append(CLASS_KEY_NAME).append(" = ")
.append(getClass().getName());
criteria.append(" AND (");
boolean first = true;
for (Customer owner : getOwners()) {
if(!first) {
criteria.append(" OR ");
}
first = false;
criteria.append("owners lnks2 ").append(owner.getId());
}
criteria.append(")");
Set<Long> otherAccounts = concourse.find(criteria.toString());
ArrayDeque<Long> stack = new ArrayDeque<Long>(otherAccounts);
while (need > 0 && !stack.isEmpty()) {
long rid = stack.pop();
if(rid == id) {
// Don't try to transfer money from the same account as
// this!
continue;
}
ConcourseAccount other = new ConcourseAccount(rid);
double transferred = other.transferTo(concourse, this, need);
// NOTE: We don't need to worry about the balance changing by
// another client. If that happens, the transaction will
// automatically fail
balance += transferred;
need -= transferred;
}
if(need > 0) {
return false;
}
}
concourse.set("balance", balance - amount, id);
return true;
}
}