/**
* Copyright 2012 multibit.org
*
* Licensed under the MIT license (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License
* at
*
* http://opensource.org/licenses/mit-license.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.multibit.controller.bitcoin;
import com.google.bitcoin.core.*;
import com.google.bitcoin.script.Script;
import com.google.bitcoin.uri.BitcoinURI;
import com.google.bitcoin.uri.BitcoinURIParseException;
import org.multibit.controller.AbstractController;
import org.multibit.controller.AbstractEventHandler;
import org.multibit.controller.core.CoreController;
import org.multibit.file.FileHandler;
import org.multibit.message.MessageManager;
import org.multibit.model.bitcoin.BitcoinModel;
import org.multibit.model.bitcoin.WalletBusyListener;
import org.multibit.model.bitcoin.WalletData;
import org.multibit.network.MultiBitService;
import org.multibit.viewsystem.View;
import org.multibit.viewsystem.ViewSystem;
import org.multibit.viewsystem.swing.action.ExitAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URI;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
/**
* The MVC controller for MultiBit.
*
* @author jim
*/
public class BitcoinController extends AbstractController<CoreController> implements WalletEventListener, TransactionConfidence.Listener {
public static final String ENCODED_SPACE_CHARACTER = "%20";
private Logger log = LoggerFactory.getLogger(BitcoinController.class);
/**
* The WalletBusy listeners
*/
private final Collection<WalletBusyListener> walletBusyListeners;
private EventHandler eventHandler;
/**
* The bitcoinj network interface.
*/
private MultiBitService multiBitService;
/**
* Class encapsulating File IO.
*/
private final FileHandler fileHandler;
/**
* The listener handling Peer events.
*/
private final PeerEventListener peerEventListener;
/**
* The data model backing the views.
*/
private BitcoinModel model;
/**
* Used for testing only.
*/
public BitcoinController(CoreController coreController) {
super(coreController);
this.walletBusyListeners = new ArrayList<WalletBusyListener>();
this.fileHandler = new FileHandler(this);
this.eventHandler = new EventHandler(this);
this.peerEventListener = new BitcoinPeerEventListener(this);
this.addEventHandler(this.getEventHandler());
}
@Override
public BitcoinModel getModel() {
return model;
}
public void setModel(BitcoinModel model) {
this.model = model;
}
/**
* Register a new WalletBusyListener.
*/
public void registerWalletBusyListener(WalletBusyListener walletBusyListener) {
walletBusyListeners.add(walletBusyListener);
}
/**
* Clear the wallet busy listeners
*/
public void clearWalletBusyListeners() {
walletBusyListeners.clear();
}
/**
* Log the number of wallet busy listeners
*/
public void logNumberOfWalletBusyListeners() {
log.debug("There are " + walletBusyListeners.size() + " walletBusyListeners.");
}
/**
* Add a wallet to multibit from a filename.
*
* @param walletFilename The wallet filename
*
* @return The model data
*/
public WalletData addWalletFromFilename(String walletFilename) throws IOException {
WalletData perWalletModelDataToReturn = null;
if (multiBitService != null) {
perWalletModelDataToReturn = multiBitService.addWalletFromFilename(walletFilename);
}
return perWalletModelDataToReturn;
}
/**
* Fire that a wallet has changed its busy state.
*/
public void fireWalletBusyChange(boolean newWalletIsBusy) {
//log.debug("fireWalletBusyChange called");
for( Iterator<WalletBusyListener> it = walletBusyListeners.iterator(); it.hasNext();) {
WalletBusyListener walletBusyListener = it.next();
walletBusyListener.walletBusyChange(newWalletIsBusy);
}
}
/**
* Method called by downloadListener whenever a block is downloaded.
*/
public void fireBlockDownloaded() {
//log.debug("Fire blockdownloaded");
for (ViewSystem viewSystem : super.getViewSystem()) {
viewSystem.blockDownloaded();
}
// Mark all the wallets as dirty as their lastBlockSeenHeight will need changing.
if (getModel() != null) {
List<WalletData> perWalletModelDataList = getModel().getPerWalletModelDataList();
if (perWalletModelDataList != null) {
for (WalletData loopPerWalletModelData : perWalletModelDataList) {
if (loopPerWalletModelData.getWalletInfo() != null) {
synchronized(loopPerWalletModelData.getWalletInfo()) {
loopPerWalletModelData.setDirty(true);
}
} else {
loopPerWalletModelData.setDirty(true);
}
}
}
}
}
@Override
public void onCoinsReceived(Wallet wallet, Transaction transaction, BigInteger prevBalance, BigInteger newBalance) {
//log.debug("onCoinsReceived called");
for (ViewSystem viewSystem : super.getViewSystem()) {
viewSystem.onCoinsReceived(wallet, transaction, prevBalance, newBalance);
}
}
@Override
public void onCoinsSent(Wallet wallet, Transaction transaction, BigInteger prevBalance, BigInteger newBalance) {
//log.debug("onCoinsSent called");
for (ViewSystem viewSystem : super.getViewSystem()) {
viewSystem.onCoinsSent(wallet, transaction, prevBalance, newBalance);
}
}
@Override
public void onWalletChanged(Wallet wallet) {
if (wallet == null) {
return;
}
// log.debug("onWalletChanged called");
final int walletIdentityHashCode = System.identityHashCode(wallet);
for (WalletData loopPerWalletModelData : getModel().getPerWalletModelDataList()) {
// Find the wallet object and mark as dirty.
if (System.identityHashCode(loopPerWalletModelData.getWallet()) == walletIdentityHashCode) {
loopPerWalletModelData.setDirty(true);
break;
}
}
fireDataChangedUpdateLater();
}
@Override
public void onTransactionConfidenceChanged(Wallet wallet, Transaction transaction) {
//log.debug("onTransactionConfidenceChanged called");
for (ViewSystem viewSystem : super.getViewSystem()) {
viewSystem.onTransactionConfidenceChanged(wallet, transaction);
}
}
@Override
public void onKeysAdded(Wallet wallet, List<ECKey> keys) {
log.debug("Keys added : " + keys.toString());
}
@Override
public void onScriptsAdded(Wallet wallet, List<Script> scripts) {
log.debug("Scripts added : " + scripts.toString());
}
@Override
public void onReorganize(Wallet wallet) {
log.debug("onReorganize called");
List<WalletData> perWalletModelDataList = getModel().getPerWalletModelDataList();
for (WalletData loopPerWalletModelData : perWalletModelDataList) {
if (loopPerWalletModelData.getWallet().equals(wallet)) {
loopPerWalletModelData.setDirty(true);
log.debug("Marking wallet '" + loopPerWalletModelData.getWalletFilename() + "' as dirty.");
}
}
for (ViewSystem viewSystem : super.getViewSystem()) {
viewSystem.onReorganize(wallet);
}
}
public MultiBitService getMultiBitService() {
return multiBitService;
}
public void setMultiBitService(MultiBitService multiBitService) {
this.multiBitService = multiBitService;
}
public FileHandler getFileHandler() {
return fileHandler;
}
public synchronized void handleOpenURI() {
log.debug("handleOpenURI.1 called and rawBitcoinURI ='" + eventHandler.rawBitcoinURI + "'");
if (eventHandler.rawBitcoinURI != null) {
handleOpenURI(eventHandler.rawBitcoinURI.toString());
}
}
public synchronized void handleOpenURI(String rawBitcoinURIString) {
log.debug("handleOpenURI.2 called and rawBitcoinURIString ='" + rawBitcoinURIString + "'");
// get the open URI configuration information
String showOpenUriDialogText = getModel().getUserPreference(BitcoinModel.OPEN_URI_SHOW_DIALOG);
String useUriText = getModel().getUserPreference(BitcoinModel.OPEN_URI_USE_URI);
if (Boolean.FALSE.toString().equalsIgnoreCase(useUriText)
&& Boolean.FALSE.toString().equalsIgnoreCase(showOpenUriDialogText)) {
// ignore open URI request
log.debug("Bitcoin URI ignored because useUriText = '" + useUriText + "', showOpenUriDialogText = '"
+ showOpenUriDialogText + "'");
org.multibit.message.Message message = new org.multibit.message.Message(super.getLocaliser().getString("showOpenUriView.paymentRequestIgnored"));
MessageManager.INSTANCE.addMessage(message);
return;
}
if (rawBitcoinURIString == null || rawBitcoinURIString.equals("")) {
log.debug("No Bitcoin URI found to handle");
return;
}
// Process the URI
// TODO Consider handling the possible runtime exception at a suitable
// level for recovery.
// Early MultiBit versions did not URL encode the label hence may
// have illegal embedded spaces - convert to ENCODED_SPACE_CHARACTER i.e
// be lenient
String uriString = rawBitcoinURIString.replace(" ", ENCODED_SPACE_CHARACTER);
BitcoinURI bitcoinURI;
try {
bitcoinURI = new BitcoinURI(getModel().getNetworkParameters(), uriString);
} catch (BitcoinURIParseException pe) {
log.error("Could not parse the uriString '" + uriString + "', aborting");
return;
}
// Convert the URI data into suitably formatted view data.
String address = bitcoinURI.getAddress().toString();
String label = "";
try {
// No label? Set it to a blank String otherwise perform a URL decode
// on it just to be sure.
label = null == bitcoinURI.getLabel() ? "" : URLDecoder.decode(bitcoinURI.getLabel(), "UTF-8");
} catch (UnsupportedEncodingException e) {
log.error("Could not decode the label in UTF-8. Unusual URI entry or platform.");
}
// No amount? Set it to zero.
BigInteger numericAmount = null == bitcoinURI.getAmount() ? BigInteger.ZERO : bitcoinURI.getAmount();
String amount = getLocaliser().bitcoinValueToStringNotLocalised(numericAmount, false, false);
if (Boolean.FALSE.toString().equalsIgnoreCase(showOpenUriDialogText)) {
// Do not show confirm dialog - go straight to send view.
// Populate the model with the URI data.
getModel().setActiveWalletPreference(BitcoinModel.SEND_ADDRESS, address);
getModel().setActiveWalletPreference(BitcoinModel.SEND_LABEL, label);
getModel().setActiveWalletPreference(BitcoinModel.SEND_AMOUNT, amount);
getModel().setActiveWalletPreference(BitcoinModel.SEND_PERFORM_PASTE_NOW, "true");
log.debug("Routing straight to send view for address = " + address);
getModel().setUserPreference(BitcoinModel.BRING_TO_FRONT, "true");
displayView(View.SEND_BITCOIN_VIEW);
return;
} else {
// Show the confirm dialog to see if the user wants to use URI.
// Populate the model with the URI data.
getModel().setUserPreference(BitcoinModel.OPEN_URI_ADDRESS, address);
getModel().setUserPreference(BitcoinModel.OPEN_URI_LABEL, label);
getModel().setUserPreference(BitcoinModel.OPEN_URI_AMOUNT, amount);
log.debug("Routing to show open uri view for address = " + address);
displayView(View.SHOW_OPEN_URI_DIALOG_VIEW);
return;
}
}
public PeerEventListener getPeerEventListener() {
return peerEventListener;
}
@Override
public final AbstractEventHandler getEventHandler() {
return this.eventHandler;
}
private class EventHandler extends AbstractEventHandler<BitcoinController> {
/**
* Multiple threads will write to this variable so require it to be
* volatile to ensure that latest write is what gets read
*/
private volatile URI rawBitcoinURI = null;
public EventHandler(BitcoinController coreController) {
super(coreController);
}
@Override
public void handleOpenURIEvent(URI rawBitcoinURI) {
this.rawBitcoinURI = rawBitcoinURI;
handleOpenURI();
}
@Override
public void handleQuitEvent(ExitAction exitAction) {
exitAction.setBitcoinController(super.controller);
}
}
@Override
public void onConfidenceChanged(Transaction tx, ChangeReason reason) {
}
}