package io.bitsquare.trade.statistics; import com.google.inject.Inject; import com.google.inject.name.Named; import io.bitsquare.app.AppOptionKeys; import io.bitsquare.common.util.Utilities; import io.bitsquare.locale.CurrencyTuple; import io.bitsquare.locale.CurrencyUtil; import io.bitsquare.p2p.P2PService; import io.bitsquare.p2p.storage.HashMapChangedListener; import io.bitsquare.p2p.storage.payload.StoragePayload; import io.bitsquare.p2p.storage.storageentry.ProtectedStorageEntry; import io.bitsquare.storage.PlainTextWrapper; import io.bitsquare.storage.Storage; import javafx.collections.FXCollections; import javafx.collections.ObservableSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.stream.Collectors; public class TradeStatisticsManager { private static final Logger log = LoggerFactory.getLogger(TradeStatisticsManager.class); private final Storage<HashSet<TradeStatistics>> statisticsStorage; private Storage<PlainTextWrapper> fiatCurrencyListJsonStorage; private Storage<PlainTextWrapper> cryptoCurrencyListJsonStorage; private Storage<PlainTextWrapper> statisticsJsonStorage; private boolean dumpStatistics; private ObservableSet<TradeStatistics> observableTradeStatisticsSet = FXCollections.observableSet(); private HashSet<TradeStatistics> tradeStatisticsSet = new HashSet<>(); @Inject public TradeStatisticsManager(Storage<HashSet<TradeStatistics>> statisticsStorage, Storage<PlainTextWrapper> fiatCurrencyListJsonStorage, Storage<PlainTextWrapper> cryptoCurrencyListJsonStorage, Storage<PlainTextWrapper> statisticsJsonStorage, P2PService p2PService, @Named(AppOptionKeys.DUMP_STATISTICS) boolean dumpStatistics) { this.statisticsStorage = statisticsStorage; this.fiatCurrencyListJsonStorage = fiatCurrencyListJsonStorage; this.cryptoCurrencyListJsonStorage = cryptoCurrencyListJsonStorage; this.statisticsJsonStorage = statisticsJsonStorage; this.dumpStatistics = dumpStatistics; statisticsStorage.setNumMaxBackupFiles(1); init(p2PService); } private void init(P2PService p2PService) { if (dumpStatistics) { this.statisticsJsonStorage.initWithFileName("trade_statistics.json"); this.fiatCurrencyListJsonStorage.initWithFileName("fiat_currency_list.json"); ArrayList<CurrencyTuple> fiatCurrencyList = new ArrayList<>(CurrencyUtil.getAllSortedFiatCurrencies().stream() .map(e -> new CurrencyTuple(e.getCode(), e.getName(), 8)) .collect(Collectors.toList())); fiatCurrencyListJsonStorage.queueUpForSave(new PlainTextWrapper(Utilities.objectToJson(fiatCurrencyList)), 2000); this.cryptoCurrencyListJsonStorage.initWithFileName("crypto_currency_list.json"); ArrayList<CurrencyTuple> cryptoCurrencyList = new ArrayList<>(CurrencyUtil.getAllSortedCryptoCurrencies().stream() .map(e -> new CurrencyTuple(e.getCode(), e.getName(), 8)) .collect(Collectors.toList())); cryptoCurrencyList.add(0, new CurrencyTuple("BTC", "Bitcoin", 8)); cryptoCurrencyListJsonStorage.queueUpForSave(new PlainTextWrapper(Utilities.objectToJson(cryptoCurrencyList)), 2000); } HashSet<TradeStatistics> persisted = statisticsStorage.initAndGetPersistedWithFileName("TradeStatistics"); if (persisted != null) persisted.stream().forEach(e -> add(e, false)); p2PService.addHashSetChangedListener(new HashMapChangedListener() { @Override public void onAdded(ProtectedStorageEntry data) { final StoragePayload storagePayload = data.getStoragePayload(); if (storagePayload instanceof TradeStatistics) add((TradeStatistics) storagePayload, true); } @Override public void onRemoved(ProtectedStorageEntry data) { // We don't remove items } }); // At startup the P2PDataStorage inits earlier, otherwise we ge the listener called. p2PService.getP2PDataStorage().getMap().values().forEach(e -> { final StoragePayload storagePayload = e.getStoragePayload(); if (storagePayload instanceof TradeStatistics) add((TradeStatistics) storagePayload, false); }); } public void add(TradeStatistics tradeStatistics, boolean storeLocally) { if (!tradeStatisticsSet.contains(tradeStatistics)) { boolean itemAlreadyAdded = tradeStatisticsSet.stream().filter(e -> (e.getOfferId().equals(tradeStatistics.getOfferId()))).findAny().isPresent(); if (!itemAlreadyAdded) { tradeStatisticsSet.add(tradeStatistics); observableTradeStatisticsSet.add(tradeStatistics); if (storeLocally) statisticsStorage.queueUpForSave(new HashSet<>(tradeStatisticsSet), 2000); dump(); } else { log.debug("We have already an item with the same offer ID. That might happen if both the offerer and the taker published the tradeStatistics"); } } } public ObservableSet<TradeStatistics> getObservableTradeStatisticsSet() { return observableTradeStatisticsSet; } private void dump() { if (dumpStatistics) { // We store the statistics as json so it is easy for further processing (e.g. for web based services) // TODO This is just a quick solution for storing to one file. // 1 statistic entry has 500 bytes as json. // Need a more scalable solution later when we get more volume. // The flag will only be activated by dedicated nodes, so it should not be too critical for the moment, but needs to // get improved. Maybe a LevelDB like DB...? Could be impl. in a headless version only. List<TradeStatisticsForJson> list = tradeStatisticsSet.stream().map(TradeStatisticsForJson::new).collect(Collectors.toList()); list.sort((o1, o2) -> (o1.tradeDate < o2.tradeDate ? 1 : (o1.tradeDate == o2.tradeDate ? 0 : -1))); TradeStatisticsForJson[] array = new TradeStatisticsForJson[list.size()]; list.toArray(array); statisticsJsonStorage.queueUpForSave(new PlainTextWrapper(Utilities.objectToJson(array)), 5000); } } }