Java tutorial
/* * BTC-e client for JavaFX * Copyright (C) 2014 QuarkDev Solutions <quarkdev.solutions@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.QuarkLabs.BTCeClientJavaFX; import com.QuarkLabs.BTCeClientJavaFX.models.ActiveOrder; import com.QuarkLabs.BTCeClientJavaFX.models.Fund; import com.QuarkLabs.BTCeClientJavaFX.models.Ticker; import com.QuarkLabs.BTCeClientJavaFX.networking.App; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.concurrent.Task; import javafx.concurrent.WorkerStateEvent; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.control.cell.PropertyValueFactory; import javafx.stage.Stage; import javafx.util.Callback; import javafx.util.Duration; import org.json.JSONObject; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URL; import java.text.DateFormat; import java.util.*; /** * Controller for main screen */ public class MainController { private static final String SOMETHING_WENT_WRONG = "Something went wrong"; private static final String ERROR_TITLE = "Error: "; private static final String PATH_TO_ORDERS_BOOK_LAYOUT = "layouts/ordersbooklayout.fxml"; private static final String PATH_TO_TRADES_LAYOUT = "layouts/markettrades.fxml"; private static final String EXCHANGE_CONFIG_PATH = "config/exchange.xml"; private App app; @FXML private ResourceBundle resources; @FXML private URL location; @FXML private Button clearLogButton; @FXML private TableView<Fund> fundsTable; @FXML private TextArea logField; @FXML private Button sellButton; @FXML private Button buyButton; @FXML private Button showActiveOrdersButton; @FXML private TableColumn<Ticker, Double> tickersTableLastColumn; @FXML private TableColumn<Ticker, String> tickersTablePairColumn; @FXML private TableView<Ticker> tickersTable; @FXML private TableColumn<Ticker, Double> tickersTableBuyColumn; @FXML private TableColumn<Ticker, Double> tickersTableFeeColumn; @FXML private TableColumn<Ticker, Double> tickersTableSellColumn; @FXML private TableColumn<Fund, String> fundsTableCurrencyColumn; @FXML private TableColumn<Fund, Double> fundsTableValueColumn; @FXML private TextField tradeAmountValue; @FXML private ChoiceBox<String> tradePriceCurrencyType; @FXML private ChoiceBox<String> tradeCurrencyType; @FXML private TextField tradePriceValue; @FXML private Button updateFundsButton; @FXML private TableView<ActiveOrder> activeOrdersTable; @FXML private TableColumn<ActiveOrder, Double> activeOrdersAmountColumn; @FXML private TableColumn<ActiveOrder, Boolean> activeOrdersCancelColumn; @FXML private TableColumn<ActiveOrder, String> activeOrdersPairColumn; @FXML private TableColumn<ActiveOrder, Double> activeOrdersRateColumn; @FXML private TableColumn<ActiveOrder, String> activeOrdersTimeColumn; @FXML private TableColumn<ActiveOrder, String> activeOrdersTypeColumn; private List<String> currencies = new ArrayList<>(); private List<String> pairs = new ArrayList<>(); private Map<String, Ticker> tickersData = new HashMap<>(); private ObservableList<ActiveOrder> activeOrders = FXCollections.observableArrayList(); private ObservableList<Fund> fundsData = FXCollections.observableArrayList(); private ObservableList<Ticker> tickers = FXCollections.observableArrayList(); @FXML void initialize() { assert clearLogButton != null : "fx:id=\"clearLogButton\" was not injected: check your FXML file 'mainlayout.fxml'."; assert fundsTable != null : "fx:id=\"fundsTable\" was not injected: check your FXML file 'mainlayout.fxml'."; assert logField != null : "fx:id=\"logField\" was not injected: check your FXML file 'mainlayout.fxml'."; assert buyButton != null : "fx:id=\"buyButton\" was not injected: check your FXML file 'mainlayout.fxml'."; assert sellButton != null : "fx:id=\"sellButton\" was not injected: check your FXML file 'mainlayout.fxml'."; assert showActiveOrdersButton != null : "fx:id=\"showActiveOrdersButton\" was not injected: check your FXML file 'mainlayout.fxml'."; assert tickersTableLastColumn != null : "fx:id=\"tickerTableLastColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; assert tickersTablePairColumn != null : "fx:id=\"tickerTablePairColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; assert tickersTable != null : "fx:id=\"tickersTable\" was not injected: check your FXML file 'mainlayout.fxml'."; assert tickersTableBuyColumn != null : "fx:id=\"tickersTableBuyColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; assert tickersTableFeeColumn != null : "fx:id=\"tickersTableFeeColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; assert tickersTableSellColumn != null : "fx:id=\"tickersTableSellColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; assert tradeAmountValue != null : "fx:id=\"tradeAmountValue\" was not injected: check your FXML file 'mainlayout.fxml'."; assert tradePriceCurrencyType != null : "fx:id=\"tradeCurrencyPriceValue\" was not injected: check your FXML file 'mainlayout.fxml'."; assert tradeCurrencyType != null : "fx:id=\"tradeCurrencyType\" was not injected: check your FXML file 'mainlayout.fxml'."; assert tradePriceValue != null : "fx:id=\"tradePriceValue\" was not injected: check your FXML file 'mainlayout.fxml'."; assert updateFundsButton != null : "fx:id=\"updateFundsButton\" was not injected: check your FXML file 'mainlayout.fxml'."; assert fundsTableCurrencyColumn != null : "fx:id=\"fundsTableCurrencyColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; assert fundsTableValueColumn != null : "fx:id=\"fundsTableValueColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; assert activeOrdersTable != null : "fx:id=\"fundsTableValueColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; assert activeOrdersAmountColumn != null : "fx:id=\"activeOrdersAmountColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; assert activeOrdersPairColumn != null : "fx:id=\"activeOrdersPairColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; assert activeOrdersRateColumn != null : "fx:id=\"activeOrdersRateColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; assert activeOrdersTimeColumn != null : "fx:id=\"activeOrdersTimeColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; assert activeOrdersTypeColumn != null : "fx:id=\"activeOrdersTypeColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; assert activeOrdersCancelColumn != null : "fx:id=\"activeOrdersCancelColumn\" was not injected: check your FXML file 'mainlayout.fxml'."; //Holder for all main API methods of exchange app = new App(); //Loading configs loadExchangeConfig(); //Populate choiceboxes at the trading section tradeCurrencyType.setItems(FXCollections.observableArrayList(currencies)); tradeCurrencyType.setValue(currencies.get(0)); tradePriceCurrencyType.setItems(FXCollections.observableArrayList(currencies)); tradePriceCurrencyType.setValue(currencies.get(0)); //Active Orders table activeOrdersAmountColumn.setCellValueFactory(new PropertyValueFactory<ActiveOrder, Double>("amount")); activeOrdersPairColumn.setCellValueFactory(new PropertyValueFactory<ActiveOrder, String>("pair")); activeOrdersRateColumn.setCellValueFactory(new PropertyValueFactory<ActiveOrder, Double>("rate")); activeOrdersTimeColumn.setCellValueFactory( new Callback<TableColumn.CellDataFeatures<ActiveOrder, String>, ObservableValue<String>>() { @Override public ObservableValue<String> call( TableColumn.CellDataFeatures<ActiveOrder, String> activeOrderStringCellDataFeatures) { ActiveOrder activeOrder = activeOrderStringCellDataFeatures.getValue(); DateFormat dateFormat = DateFormat.getDateTimeInstance(); Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(activeOrder.getTimestamp() * 1000); return new SimpleStringProperty(dateFormat.format(calendar.getTime())); } }); activeOrdersTypeColumn.setCellValueFactory(new PropertyValueFactory<ActiveOrder, String>("type")); activeOrdersTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); activeOrdersCancelColumn .setCellFactory(new Callback<TableColumn<ActiveOrder, Boolean>, TableCell<ActiveOrder, Boolean>>() { @Override public TableCell<ActiveOrder, Boolean> call( TableColumn<ActiveOrder, Boolean> activeOrderBooleanTableColumn) { return new ButtonCell<>(activeOrdersTable); } }); activeOrdersCancelColumn.setCellValueFactory( new Callback<TableColumn.CellDataFeatures<ActiveOrder, Boolean>, ObservableValue<Boolean>>() { @Override public ObservableValue<Boolean> call( TableColumn.CellDataFeatures<ActiveOrder, Boolean> activeOrderBooleanCellDataFeatures) { return new SimpleBooleanProperty(true); } }); //Tickers Table MenuItem showOrdersBook = new MenuItem("Show Orders Book"); MenuItem showPublicTrades = new MenuItem("Show Public Trades"); ContextMenu contextMenu = new ContextMenu(showOrdersBook, showPublicTrades); tickersTable.setItems(tickers); tickersTable.setContextMenu(contextMenu); tickersTableBuyColumn.setCellValueFactory(new PropertyValueFactory<Ticker, Double>("buy")); tickersTableFeeColumn.setCellValueFactory(new PropertyValueFactory<Ticker, Double>("fee")); tickersTableSellColumn.setCellValueFactory(new PropertyValueFactory<Ticker, Double>("sell")); tickersTableLastColumn.setCellValueFactory(new PropertyValueFactory<Ticker, Double>("last")); tickersTablePairColumn.setCellValueFactory(new PropertyValueFactory<Ticker, String>("pair")); tickersTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tickersTable.setRowFactory(new Callback<TableView<Ticker>, TableRow<Ticker>>() { @Override public TableRow<Ticker> call(TableView<Ticker> tickerTableView) { return new TableRow<Ticker>() { @Override protected void updateItem(Ticker ticker, boolean b) { super.updateItem(ticker, b); if (!b) { if (tickersData.containsKey(ticker.getPair())) { if (ticker.getLast() < tickersData.get(ticker.getPair()).getLast()) { setStyle("-fx-control-inner-background: rgba(186, 0, 0, 0.5);"); } else if (ticker.getLast() == tickersData.get(ticker.getPair()).getLast()) { setStyle("-fx-control-inner-background: rgba(215, 193, 44, 0.5);"); } else { setStyle("-fx-control-inner-background: rgba(0, 147, 0, 0.5);"); } } } } }; } }); //Menu item to show Orders Book showOrdersBook.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent actionEvent) { Ticker selectedTicker = tickersTable.getSelectionModel().getSelectedItem(); Parent root; try { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(PATH_TO_ORDERS_BOOK_LAYOUT), resources); root = (Parent) fxmlLoader.load(); OrdersBookController ordersBookController = fxmlLoader.getController(); ordersBookController.injectPair(selectedTicker.getPair()); Stage stage = new Stage(); stage.setTitle("Orders Book for " + selectedTicker.getPair().replace("_", "/").toUpperCase()); stage.setScene(new Scene(root)); stage.setResizable(false); stage.show(); } catch (IOException e) { e.printStackTrace(); } } }); //Menu item to show Public Trades showPublicTrades.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent actionEvent) { Ticker selectedTicker = tickersTable.getSelectionModel().getSelectedItem(); Parent root; try { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(PATH_TO_TRADES_LAYOUT), resources); root = (Parent) fxmlLoader.load(); PublicTradesController publicTradesController = fxmlLoader.getController(); publicTradesController.injectPair(selectedTicker.getPair()); Stage stage = new Stage(); stage.setTitle("Public Trades for " + selectedTicker.getPair().replace("_", "/").toUpperCase()); stage.setScene(new Scene(root)); stage.setResizable(false); stage.show(); } catch (IOException e) { e.printStackTrace(); } } }); //Funds Table fundsTableCurrencyColumn.setCellValueFactory(new PropertyValueFactory<Fund, String>("currency")); fundsTableValueColumn.setCellValueFactory(new PropertyValueFactory<Fund, Double>("value")); fundsTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); fundsTable.setItems(fundsData); //Task to load tickers data from server final javafx.concurrent.Service loadTickersService = new javafx.concurrent.Service() { @Override protected Task createTask() { Task<JSONObject> loadTickers = new Task<JSONObject>() { @Override protected JSONObject call() throws Exception { String[] pairsArray = new String[pairs.size()]; pairsArray = pairs.toArray(pairsArray); return App.getPairInfo(pairsArray); } }; loadTickers.setOnFailed(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent workerStateEvent) { logField.appendText(workerStateEvent.getSource().getException().getMessage() + "\r\n"); } }); loadTickers.setOnSucceeded(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent workerStateEvent) { JSONObject jsonObject = (JSONObject) workerStateEvent.getSource().getValue(); //ugly hack to store old values //dump old values to tickersData //TODO think about better solution if (tickers.size() != 0) { for (Ticker x : tickers) { tickersData.put(x.getPair(), x); } } tickers.clear(); for (Iterator iterator = jsonObject.keys(); iterator.hasNext();) { String key = (String) iterator.next(); JSONObject data = jsonObject.getJSONObject(key); Ticker ticker = new Ticker(); ticker.setPair(key); ticker.setUpdated(data.optLong("updated")); ticker.setAvg(data.optDouble("avg")); ticker.setBuy(data.optDouble("buy")); ticker.setSell(data.optDouble("sell")); ticker.setHigh(data.optDouble("high")); ticker.setLast(data.optDouble("last")); ticker.setLow(data.optDouble("low")); ticker.setVol(data.optDouble("vol")); ticker.setVolCur(data.optDouble("vol_cur")); tickers.add(ticker); } } }); return loadTickers; } }; //Update tickers every 15 seconds //TODO better solution is required Timeline timeline = new Timeline(new KeyFrame(Duration.ZERO, new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent actionEvent) { loadTickersService.restart(); } }), new KeyFrame(Duration.seconds(15))); timeline.setCycleCount(Timeline.INDEFINITE); timeline.playFromStart(); } /** * Loads exchange pairs and currencies from XML file, displays error message at the Log field in case of any Exception */ private void loadExchangeConfig() { try { InputStream inputStream = new FileInputStream(EXCHANGE_CONFIG_PATH); DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = builderFactory.newDocumentBuilder(); Document document = builder.parse(inputStream); document.getDocumentElement().normalize(); NodeList nodeList = document.getElementsByTagName("currency"); for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element eElement = (Element) node; currencies.add(eElement.getTextContent()); } } nodeList = document.getElementsByTagName("pair"); for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element eElement = (Element) node; pairs.add(eElement.getTextContent()); } } inputStream.close(); } catch (ParserConfigurationException | SAXException | IOException e) { logField.appendText(e.getMessage() + "\r\n"); } } /** * Gets Active Orders data from server, displays error message at the Log field in case of any Exception */ @FXML private void showActiveOrders() { Task<JSONObject> task = new Task<JSONObject>() { @Override protected JSONObject call() throws Exception { try { return app.getActiveOrders(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return new JSONObject(); } }; task.setOnSucceeded(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent workerStateEvent) { JSONObject jsonObject = (JSONObject) workerStateEvent.getSource().getValue(); if (jsonObject.optInt("success") == 1) { JSONObject data = jsonObject.getJSONObject("return"); for (Iterator iterator = data.keys(); iterator.hasNext();) { String key = (String) iterator.next(); ActiveOrder activeOrder = new ActiveOrder(); activeOrder.setId(Long.parseLong(key)); activeOrder.setAmount(data.optJSONObject(key).optDouble("amount")); activeOrder.setRate(data.optJSONObject(key).optDouble("rate")); activeOrder.setStatus(data.optJSONObject(key).optInt("status")); activeOrder.setTimestamp(data.optJSONObject(key).optLong("timestamp_created")); activeOrder.setPair(data.optJSONObject(key).optString("pair")); activeOrder.setType(data.optJSONObject(key).optString("type")); activeOrders.add(activeOrder); } activeOrdersTable.setItems(activeOrders); } else { logField.appendText(ERROR_TITLE + jsonObject.optString("error", SOMETHING_WENT_WRONG) + "\r\n"); } } }); task.setOnFailed(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent workerStateEvent) { logField.appendText(workerStateEvent.getSource().getException().getMessage() + "\r\n"); } }); Thread thread = new Thread(task); thread.start(); } /** * Helper function to clear Log area */ @FXML private void clearLog() { logField.clear(); } /** * Gets funds data from server, displays error message at the Log field in case of any Exception */ @FXML private void updateFunds() { Task<JSONObject> task = new Task<JSONObject>() { @Override protected JSONObject call() throws Exception { try { return app.getAccountInfo(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return new JSONObject(); } }; task.setOnSucceeded(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent workerStateEvent) { JSONObject jsonObject = (JSONObject) workerStateEvent.getSource().getValue(); //TODO make a check for errors if (jsonObject.optInt("success", 0) == 1) { parseFundsObject(jsonObject.optJSONObject("return").optJSONObject("funds")); } else { logField.appendText(ERROR_TITLE + jsonObject.optString("error", SOMETHING_WENT_WRONG) + "\r\n"); } } }); task.setOnFailed(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent workerStateEvent) { logField.appendText(workerStateEvent.getSource().getException().getMessage() + "\r\n"); } }); Thread thread = new Thread(task); thread.start(); } /** * Reads data from Trading section, sends trade request to server * Displays error message at the Log field in case of any Exception * * @param event Source fired an event (either "Buy" or "Sell" button) */ @FXML private void makeTradeRequest(final ActionEvent event) { Task<JSONObject> task = new Task<JSONObject>() { @Override protected JSONObject call() throws Exception { String type; String idOfSource = ((Button) event.getSource()).getId(); if (buyButton.getId().equals(idOfSource)) { type = "buy"; } else { type = "sell"; } String pair = tradeCurrencyType.getValue().toLowerCase() + "_" + tradePriceCurrencyType.getValue().toLowerCase(); String rate = tradePriceValue.getText(); String amount = tradeAmountValue.getText(); return app.trade(pair, type, rate, amount); } }; task.setOnSucceeded(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent workerStateEvent) { JSONObject jsonObject = (JSONObject) workerStateEvent.getSource().getValue(); if (jsonObject.optInt("success") == 1) { parseFundsObject(jsonObject.optJSONObject("return").optJSONObject("funds")); logField.appendText("Order ID = " + jsonObject.optJSONObject("return").optString("order_id") + " was registered successfully" + "\r\n"); } else { logField.appendText(ERROR_TITLE + jsonObject.optString("error", SOMETHING_WENT_WRONG) + "\r\n"); } } }); task.setOnFailed(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent workerStateEvent) { logField.appendText(workerStateEvent.getSource().getException().getMessage() + "\r\n"); } }); Thread thread = new Thread(task); thread.start(); } /** * Parses JSONObject with funds data inside (key = "funds") * * @param fundsObject Funds JSONObject */ private void parseFundsObject(JSONObject fundsObject) { fundsData.clear(); for (Iterator iterator = fundsObject.keys(); iterator.hasNext();) { String key = (String) iterator.next(); fundsData.add(new Fund(key, fundsObject.optDouble(key))); } FXCollections.sort(fundsData, new Comparator<Fund>() { @Override public int compare(Fund o1, Fund o2) { return o1.getCurrency().compareTo(o2.getCurrency()); } }); } /** * Class: TableCell with Button inside * * @param <S> Type of model * @param <T> Type of data inside cell */ private class ButtonCell<S, T> extends TableCell<S, T> { final Button cellButton = new Button("Cancel"); ButtonCell(final TableView tblView) { cellButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent t) { final int selectedIndex = getTableRow().getIndex(); //delete the selected item in data Task<JSONObject> cancelOrder = new Task<JSONObject>() { @Override protected JSONObject call() throws Exception { return app.cancelOrder(activeOrders.get(selectedIndex).getId()); } }; cancelOrder.setOnSucceeded(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent workerStateEvent) { JSONObject jsonObject = (JSONObject) workerStateEvent.getSource().getValue(); if (jsonObject.optInt("success") == 1) { activeOrders.remove(selectedIndex); parseFundsObject(jsonObject.optJSONObject("return").optJSONObject("funds")); } else { logField.appendText( ERROR_TITLE + jsonObject.optString("error", SOMETHING_WENT_WRONG) + "\r\n"); } } }); cancelOrder.setOnFailed(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent workerStateEvent) { logField.appendText(workerStateEvent.getSource().getException().getMessage() + "\r\n"); } }); Thread thread = new Thread(cancelOrder); thread.run(); } }); } @Override protected void updateItem(T o, boolean b) { super.updateItem(o, b); //if non-empty if (!b) { setGraphic(cellButton); } } } }