diff --git a/duniter4j-client/pom.xml b/duniter4j-client/pom.xml index 4a32116a5a1f76eef0813080e32a0b8766615051..94dbf2284d8dfa1ed19ccc67a10b4bcc1595b9e3 100644 --- a/duniter4j-client/pom.xml +++ b/duniter4j-client/pom.xml @@ -15,13 +15,14 @@ <properties> <jTextUtilsVersion>0.3.3</jTextUtilsVersion> <opencsvVersion>2.3</opencsvVersion> + <jansiVersion>1.15</jansiVersion> <!-- i18n configuration --> <i18n.bundleOutputName>duniter4j-client-i18n</i18n.bundleOutputName> <i18n.bundleCsvFile>${i18n.bundleOutputName}.csv</i18n.bundleCsvFile> <maven.jar.main.class> - fr.duniter.client.Main + org.duniter.client.Main </maven.jar.main.class> <bundlePrefix>duniter4j-client-${project.version}</bundlePrefix> @@ -79,10 +80,11 @@ <version>${opencsvVersion}</version> </dependency> + <dependency> - <groupId>org.jline</groupId> - <artifactId>jline</artifactId> - <version>3.2.0</version> + <groupId>org.fusesource.jansi</groupId> + <artifactId>jansi</artifactId> + <version>${jansiVersion}</version> </dependency> </dependencies> diff --git a/duniter4j-client/src/main/assembly/min/duniter4j.sh b/duniter4j-client/src/main/assembly/min/duniter4j.sh index 344b7600978c3d01a7e60a0b07d288b614201109..4ddc4538646a981abccbe62782343c39302d7ccc 100644 --- a/duniter4j-client/src/main/assembly/min/duniter4j.sh +++ b/duniter4j-client/src/main/assembly/min/duniter4j.sh @@ -22,7 +22,7 @@ export JAR="$JARDIR/${project.build.finalName}.${project.packaging}" export I18N_DIR="$APPDIR/i18n" # Retrieve the JAVA installation -if [ "~$JAVA_HOME" -eq "~" ]; then +if [ "$JAVA_HOME~" == "~" ]; then export JAVA_HOME="$APPDIR/jre" export JAVA_COMMAND="$JAVA_HOME/bin/java" diff --git a/duniter4j-client/src/main/filtered-resources/log4j.properties b/duniter4j-client/src/main/filtered-resources/log4j.properties index 998366881a6063fc16080ec9508af9c2b2333b2b..135fd9c4a4d3509072c55b024bace0abee7421fa 100644 --- a/duniter4j-client/src/main/filtered-resources/log4j.properties +++ b/duniter4j-client/src/main/filtered-resources/log4j.properties @@ -18,7 +18,7 @@ log4j.appender.file.layout.ConversionPattern=%d{ISO8601} %5p %c - %m%n # Duniter4j levels log4j.logger.org.duniter=INFO -log4j.logger.org.duniter.core.beans=WARN +log4j.logger.org.duniter.core=WARN # Avoid warning on leaf not found (Duniter issue) log4j.logger.org.duniter.core.client.service.local.NetworkServiceImpl=ERROR diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/utils/ClearableConsole.java b/duniter4j-client/src/main/java/fr/duniter/client/actions/utils/ClearableConsole.java deleted file mode 100644 index 168bdc6f35870c1b504854c66bf49c3b58c17efb..0000000000000000000000000000000000000000 --- a/duniter4j-client/src/main/java/fr/duniter/client/actions/utils/ClearableConsole.java +++ /dev/null @@ -1,214 +0,0 @@ -package fr.duniter.client.actions.utils; - -/* - * #%L - * Duniter4j :: Client - * %% - * Copyright (C) 2014 - 2017 EIS - * %% - * 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/gpl-3.0.html>. - * #L% - */ - -import com.beust.jcommander.internal.Maps; -import org.duniter.core.util.Preconditions; - -import java.io.PrintStream; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Created by blavenie on 28/03/17. - */ -public class ClearableConsole extends PrintStream { - - public interface Color { - int black = 30; - int red = 31; - int green = 32; - int brown = 33; - int blue = 34; - int magenta = 35; - int cyan = 36; - int lightgray = 37; - } - - private int rowsCount = 0; - - private Map<Pattern, Integer> regexColors = Maps.newHashMap(); - - public ClearableConsole() { - super(System.out, true); - } - - public ClearableConsole(PrintStream delegate) { - super(delegate, true); - } - - public void clearConsole() { - if (rowsCount == 0) { - super.print("\033[H\033[2J"); - super.flush(); - } - else { - moveCursor(rowsCount); - } - rowsCount = 0; - } - - public ClearableConsole putRegexColor(String regex, int color) { - Preconditions.checkArgument(color >= 30 && color <= 37); - - regexColors.put(Pattern.compile(regex), color); - return this; - } - - public ClearableConsole removeRegex(String regex) { - - regexColors.remove(Pattern.compile(regex)); - return this; - } - - public void moveCursor(int nbLinesUp) { - for (int i = 1; i <= nbLinesUp; i++) { - super.print("\033[1A"); // Move up - super.print("\033[2K"); // Erase line content - } - - super.flush(); - } - - @Override - public void println(boolean x) { - super.println(x); - rowsCount++; - } - - @Override - public void println(char x) { - super.println(x); - rowsCount++; - } - - @Override - public void println(int x) { - super.println(x); - rowsCount++; - } - - @Override - public void println(long x) { - super.println(x); - rowsCount++; - } - - @Override - public void println(float x) { - super.println(x); - rowsCount++; - } - - @Override - public void println(double x) { - super.println(x); - rowsCount++; - } - - @Override - public void println(char[] x) { - super.println(x); - rowsCount++; - } - - @Override - public void println(String x) { - - super.println(x); - rowsCount++; - } - - @Override - public void println(Object x) { - super.println(x); - rowsCount++; - } - - @Override - public void print(char c) { - super.print(c); - } - - @Override - public void print(char[] x) { - super.print(x); - for (int i=0; i< x.length; i++) { - if (x[i] == '\n') { - rowsCount++; - } - } - } - - @Override - public void print(String s) { - - for (Pattern pattern: regexColors.keySet()) { - Matcher matcher = pattern.matcher(s); - if (matcher.find()) { - StringBuilder sb = new StringBuilder(); - if (matcher.start() > 0) { - sb.append(s.substring(0, matcher.start())); - } - sb.append(String.format("\033[0;%sm", regexColors.get(pattern))); - sb.append(s.substring(matcher.start(), matcher.end())); - sb.append("\033[0m"); - if (matcher.end() < s.length()) { - sb.append(s.substring(matcher.end())); - } - s = sb.toString(); - } - } - super.print(s); - for (int i=0; i< s.length(); i++) { - if (s.charAt(i) == '\n') { - rowsCount++; - } - } - } - - @Override - public void print(Object obj) { - String s = String.valueOf(obj); - print(s); - } - - @Override - public PrintStream append(CharSequence csq, int start, int end) { - for (int i=start; i<=end; i++) { - if (csq.charAt(i) == '\n') { - rowsCount++; - } - } - return super.append(csq, start, end); - } - - @Override - public PrintStream append(char c) { - if (c == '\n') { - rowsCount++; - } - return super.append(c); - } -} diff --git a/duniter4j-client/src/main/java/fr/duniter/client/Main.java b/duniter4j-client/src/main/java/org/duniter/client/Main.java similarity index 96% rename from duniter4j-client/src/main/java/fr/duniter/client/Main.java rename to duniter4j-client/src/main/java/org/duniter/client/Main.java index ff48c43f7aebeeb8dcc8686f64f9cda1553a88cd..c7b1c7258314d38bffd860888e06d61e1f7f2cbf 100644 --- a/duniter4j-client/src/main/java/fr/duniter/client/Main.java +++ b/duniter4j-client/src/main/java/org/duniter/client/Main.java @@ -1,4 +1,4 @@ -package fr.duniter.client; +package org.duniter.client; /* * #%L @@ -27,15 +27,13 @@ import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameters; import com.google.common.collect.Lists; -import fr.duniter.client.actions.NetworkAction; -import fr.duniter.client.actions.TransactionAction; +import org.duniter.client.actions.NetworkAction; +import org.duniter.client.actions.TransactionAction; import org.apache.commons.io.FileUtils; import org.duniter.core.client.config.Configuration; import org.duniter.core.client.config.ConfigurationOption; import org.duniter.core.client.service.ServiceLocator; import org.duniter.core.util.StringUtils; -import org.jline.terminal.Terminal; -import org.jline.terminal.TerminalBuilder; import org.nuiton.i18n.I18n; import org.nuiton.i18n.init.DefaultI18nInitializer; import org.nuiton.i18n.init.UserI18nInitializer; diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/AbstractAction.java b/duniter4j-client/src/main/java/org/duniter/client/actions/AbstractAction.java similarity index 97% rename from duniter4j-client/src/main/java/fr/duniter/client/actions/AbstractAction.java rename to duniter4j-client/src/main/java/org/duniter/client/actions/AbstractAction.java index 796f52d8fbdac15f521f412d5a6b617c8d70eee6..35cee1f07fc4a9a14b8e487a87d761494fbbe0d1 100644 --- a/duniter4j-client/src/main/java/fr/duniter/client/actions/AbstractAction.java +++ b/duniter4j-client/src/main/java/org/duniter/client/actions/AbstractAction.java @@ -1,4 +1,4 @@ -package fr.duniter.client.actions; +package org.duniter.client.actions; /* * #%L diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/NetworkAction.java b/duniter4j-client/src/main/java/org/duniter/client/actions/NetworkAction.java similarity index 62% rename from duniter4j-client/src/main/java/fr/duniter/client/actions/NetworkAction.java rename to duniter4j-client/src/main/java/org/duniter/client/actions/NetworkAction.java index 171326abf2c5716957b6224cfc19edcf50f445cc..b2c64093006593e6e1c1dc035264018078c19738 100644 --- a/duniter4j-client/src/main/java/fr/duniter/client/actions/NetworkAction.java +++ b/duniter4j-client/src/main/java/org/duniter/client/actions/NetworkAction.java @@ -1,4 +1,4 @@ -package fr.duniter.client.actions; +package org.duniter.client.actions; /* * #%L @@ -25,24 +25,22 @@ package fr.duniter.client.actions; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.beust.jcommander.ParametersDelegate; -import com.beust.jcommander.internal.Lists; import dnl.utils.text.table.TextTable; -import fr.duniter.client.actions.params.PeerParameters; -import fr.duniter.client.actions.utils.ClearableConsole; -import fr.duniter.client.actions.utils.Formatters; +import org.duniter.client.actions.params.PeerParameters; +import org.duniter.client.actions.utils.RegexAnsiConsole; +import org.duniter.client.actions.utils.Formatters; import org.apache.commons.io.IOUtils; import org.duniter.core.client.model.local.Peer; import org.duniter.core.client.service.ServiceLocator; -import org.duniter.core.client.service.bma.BlockchainRemoteService; import org.duniter.core.client.service.local.NetworkService; import org.duniter.core.util.CollectionUtils; import org.duniter.core.util.FileUtils; +import org.fusesource.jansi.Ansi; import org.nuiton.i18n.I18n; import java.io.*; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.stream.Collectors; @@ -62,10 +60,9 @@ public class NetworkAction extends AbstractAction { @Parameter(names = "--output", description = "Output file (CSV format)", descriptionKey = "duniter4j.client.network.params.output") private File outputFile = null; - private ClearableConsole console; + private RegexAnsiConsole console; private DateFormat dateFormat; - private List<String> knownBlocks = Lists.newArrayList(); public NetworkAction() { super(); @@ -74,81 +71,40 @@ public class NetworkAction extends AbstractAction { @Override public void run() { + peerParameters.parse(); final Peer mainPeer = peerParameters.getPeer(); checkOutputFileIfNotNull(); // make sure the file (if any) is writable dateFormat = SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, I18n.getDefaultLocale()); - console = new ClearableConsole(System.out) - .putRegexColor(I18n.t("duniter4j.client.network.ssl"), ClearableConsole.Color.green) - .putRegexColor(I18n.t("duniter4j.client.network.mirror"), ClearableConsole.Color.lightgray); - + console = new RegexAnsiConsole(); System.setOut(console); log.info(I18n.t("duniter4j.client.network.loadingPeers")); - List<Peer> peers = loadPeers(mainPeer); - - showPeersTable(peers, true/*autoRefresh*/); - - if (autoRefresh) { - BlockchainRemoteService bcService = ServiceLocator.instance().getBlockchainRemoteService(); - peers.stream().forEach(peer -> { - String buid = peer.getStats().getBlockNumber() + "-" + peer.getStats().getBlockHash(); - if (!knownBlocks.contains(buid)) { - knownBlocks.add(buid); - } - }); + NetworkService service = ServiceLocator.instance().getNetworkService(); - // Start listening for new peer... - bcService.addPeerListener(mainPeer, message -> updatePeers(mainPeer, knownBlocks)); - // Start listening for new block... - bcService.addBlockListener(mainPeer, message -> updatePeers(mainPeer, knownBlocks)); + if (!autoRefresh) { + List<Peer> peers = service.getPeers(mainPeer); + showPeersTable(peers, false); + } + else { + service.addPeersChangeListener(mainPeer, peers -> showPeersTable(peers, true)); try { while(true) { Thread.sleep(10000); // 10 s - } } catch (InterruptedException e) { e.printStackTrace(); } } - // TODO: DEV only - /* else { - try { - int blockCount = 1500; - while(true) { - Thread.sleep(2000); // 2 s - - List<Peer> updatedPeers = new ArrayList<>(); - - for (int i=0; i<5; i++) { - Peer peer = Peer.newBuilder().setHost("p1").setPort(80) - .build(); - peer.getStats().setBlockNumber(blockCount); - updatedPeers.add(peer); - } - updatedPeers.addAll(peers); - - showPeersTable(updatedPeers, true); - blockCount++; - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - }*/ - } /* -- protected methods -- */ - public List<Peer> loadPeers(Peer mainPeer) { - NetworkService service = ServiceLocator.instance().getNetworkService(); - return service.getPeers(mainPeer); - } public void showPeersTable(List<Peer> peers, boolean clearConsole) { @@ -163,18 +119,27 @@ public class NetworkAction extends AbstractAction { } Peer mainConsensusPeer = peers.get(0); - if (mainConsensusPeer.getStats().isMainConsensus()) { - Long mediantTime = mainConsensusPeer.getStats().getMedianTime(); - if (mediantTime != null) { - console.println(I18n.t("duniter4j.client.network.medianTime", - dateFormat.format(new Date(mediantTime * 1000)))); - } - - knownBlocks.stream().forEach(buid -> { - console.putRegexColor(Formatters.formatBuid(buid), ClearableConsole.Color.lightgray); - }); - - console.putRegexColor(formatBuid(mainConsensusPeer.getStats()), ClearableConsole.Color.green); + Peer.Stats mainConsensusStats = mainConsensusPeer.getStats(); + if (mainConsensusStats.isMainConsensus()) { + Long mediantTime = mainConsensusStats.getMedianTime(); + String mainBuid = formatBuid(mainConsensusStats); + + console.resetFgRegexps() + .fgRegexp(I18n.t("duniter4j.client.network.ssl"), Ansi.Color.MAGENTA) + .fgRegexp(I18n.t("duniter4j.client.network.mirror"), Ansi.Color.CYAN) + .fgRegexp(mainBuid, Ansi.Color.GREEN); + + peers.stream() + .filter(peer -> peer.getStats().isForkConsensus()) + .map(peer -> formatBuid(peer.getStats())) + .forEach(forkConsensusBuid -> console.fgRegexp(Formatters.formatBuid(forkConsensusBuid), Ansi.Color.YELLOW)); + + // Log blockchain info + console.println("\t" + I18n.t("duniter4j.client.network.header", + mainBuid, + dateFormat.format(new Date(mediantTime * 1000)), + mainConsensusStats.getConsensusPct() + )); } String[] columnNames = { @@ -208,7 +173,6 @@ public class NetworkAction extends AbstractAction { rows[i++] = row; } - TextTable tt = new TextTable(columnNames, rows); // Write result to filCSV @@ -256,25 +220,8 @@ public class NetworkAction extends AbstractAction { } } - protected void updatePeers(Peer mainPeer, List<String> knownBlocks) { - List<Peer> updatedPeers = loadPeers(mainPeer); - - int knowBlockSize = knownBlocks.size(); - updatedPeers.stream().forEach(peer -> { - String buid = peer.getStats().getBlockNumber() + "-" + peer.getStats().getBlockHash(); - if (!knownBlocks.contains(buid)) { - knownBlocks.add(buid); - } - }); - - // new block received: refresh console - if (knowBlockSize < knownBlocks.size()) { - showPeersTable(updatedPeers, true); - } - } - protected void clearConsole() { - console.clearConsole(); + console.eraseScreen(); } protected String formatBuid(Peer.Stats stats) { diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/TransactionAction.java b/duniter4j-client/src/main/java/org/duniter/client/actions/TransactionAction.java similarity index 97% rename from duniter4j-client/src/main/java/fr/duniter/client/actions/TransactionAction.java rename to duniter4j-client/src/main/java/org/duniter/client/actions/TransactionAction.java index 77c3f121b078ea1b2bf9bd655f5f702f77e5dad1..d8ee63e11057b7ec20b8d6832dd8c1a8a0bf3c59 100644 --- a/duniter4j-client/src/main/java/fr/duniter/client/actions/TransactionAction.java +++ b/duniter4j-client/src/main/java/org/duniter/client/actions/TransactionAction.java @@ -1,4 +1,4 @@ -package fr.duniter.client.actions; +package org.duniter.client.actions; /* * #%L @@ -28,9 +28,9 @@ import com.beust.jcommander.Parameters; import com.beust.jcommander.ParametersDelegate; import com.beust.jcommander.validators.PositiveInteger; import com.google.common.collect.ImmutableList; -import fr.duniter.client.actions.params.AuthParameters; -import fr.duniter.client.actions.params.PeerParameters; -import fr.duniter.client.actions.utils.Formatters; +import org.duniter.client.actions.params.AuthParameters; +import org.duniter.client.actions.params.PeerParameters; +import org.duniter.client.actions.utils.Formatters; import org.duniter.core.client.model.bma.EndpointApi; import org.duniter.core.client.model.local.Currency; import org.duniter.core.client.model.local.Peer; diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/params/AuthParameters.java b/duniter4j-client/src/main/java/org/duniter/client/actions/params/AuthParameters.java similarity index 98% rename from duniter4j-client/src/main/java/fr/duniter/client/actions/params/AuthParameters.java rename to duniter4j-client/src/main/java/org/duniter/client/actions/params/AuthParameters.java index a8311da002f40673737cb5e652a5b8541c05b07b..0991711981397d2f71fadbad20fb81e06eb09c4d 100644 --- a/duniter4j-client/src/main/java/fr/duniter/client/actions/params/AuthParameters.java +++ b/duniter4j-client/src/main/java/org/duniter/client/actions/params/AuthParameters.java @@ -1,4 +1,4 @@ -package fr.duniter.client.actions.params; +package org.duniter.client.actions.params; /* * #%L diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/params/PeerParameters.java b/duniter4j-client/src/main/java/org/duniter/client/actions/params/PeerParameters.java similarity index 98% rename from duniter4j-client/src/main/java/fr/duniter/client/actions/params/PeerParameters.java rename to duniter4j-client/src/main/java/org/duniter/client/actions/params/PeerParameters.java index 4f0b12a291fdf3b934ebca503c456307088d854e..665915442bc9f2a1a5661b761b39f3c684015dcd 100644 --- a/duniter4j-client/src/main/java/fr/duniter/client/actions/params/PeerParameters.java +++ b/duniter4j-client/src/main/java/org/duniter/client/actions/params/PeerParameters.java @@ -1,4 +1,4 @@ -package fr.duniter.client.actions.params; +package org.duniter.client.actions.params; /* * #%L diff --git a/duniter4j-client/src/main/java/fr/duniter/client/actions/utils/Formatters.java b/duniter4j-client/src/main/java/org/duniter/client/actions/utils/Formatters.java similarity index 98% rename from duniter4j-client/src/main/java/fr/duniter/client/actions/utils/Formatters.java rename to duniter4j-client/src/main/java/org/duniter/client/actions/utils/Formatters.java index 57ef912edfec1c8682f67536e27b296b52ccff5c..55530b33ef61753b1dd35c2fff0be82e69e6db38 100644 --- a/duniter4j-client/src/main/java/fr/duniter/client/actions/utils/Formatters.java +++ b/duniter4j-client/src/main/java/org/duniter/client/actions/utils/Formatters.java @@ -1,4 +1,4 @@ -package fr.duniter.client.actions.utils; +package org.duniter.client.actions.utils; /* * #%L diff --git a/duniter4j-client/src/main/java/org/duniter/client/actions/utils/RegexAnsiConsole.java b/duniter4j-client/src/main/java/org/duniter/client/actions/utils/RegexAnsiConsole.java new file mode 100644 index 0000000000000000000000000000000000000000..f5098565d45983d5b3dc5320d19560af3a50a0c6 --- /dev/null +++ b/duniter4j-client/src/main/java/org/duniter/client/actions/utils/RegexAnsiConsole.java @@ -0,0 +1,224 @@ +package org.duniter.client.actions.utils; + +/* + * #%L + * Duniter4j :: Client + * %% + * Copyright (C) 2014 - 2017 EIS + * %% + * 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/gpl-3.0.html>. + * #L% + */ + +import com.beust.jcommander.internal.Maps; +import org.fusesource.jansi.Ansi; +import org.fusesource.jansi.AnsiConsole; + +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.fusesource.jansi.Ansi.Erase; +import static org.fusesource.jansi.Ansi.ansi; + +/** + * Created by blavenie on 28/03/17. + */ +public class RegexAnsiConsole extends PrintStream { + + private Long rowsCount = 0l; + private Map<Pattern, Ansi.Color> fgRegexps = Maps.newHashMap(); + + public RegexAnsiConsole(OutputStream delegate) { + super(AnsiConsole.wrapOutputStream(delegate), true); + } + + public RegexAnsiConsole() { + super(AnsiConsole.out(), true); + } + + public RegexAnsiConsole eraseScreen() { + Ansi ansi = ansi(); + synchronized (rowsCount) { + // If first call to erase: clean screen then save cursor position + if (rowsCount == 0) { + ansi.eraseScreen() + .cursor(0,0) + .saveCursorPosition(); + } + else { + // Try to erase writtent lines + for (int i = 1; i <= rowsCount; i++) { + ansi.cursorUp(1).eraseLine(Erase.ALL); + } + + // Reset rowcount + rowsCount = 0l; + + // Make sure to go back to saved cursor point + // Then clean screen again + ansi.restoreCursorPosition() + .eraseScreen(); + } + + } + + super.print(ansi); + return this; + } + + public RegexAnsiConsole resetFgRegexps() { + fgRegexps.clear(); + return this; + } + + public RegexAnsiConsole fgRegexp(String regex, Ansi.Color color) { + fgRegexps.put(Pattern.compile(regex), color); + return this; + } + + @Override + public void print(String s) { + + for (Pattern pattern: fgRegexps.keySet()) { + Matcher matcher = pattern.matcher(s); + if (matcher.find()) { + Ansi.Color fgColor = fgRegexps.get(pattern); + Ansi ansi = ansi(); + if (matcher.start() > 0) { + ansi.a(s.substring(0, matcher.start())); + } + + ansi.fg(fgColor) + .a(s.substring(matcher.start(), matcher.end())) + .reset(); + if (matcher.end() < s.length()) { + ansi.a(s.substring(matcher.end())); + } + s = ansi.toString(); + } + } + + long newLineCount = 0; + for (int i=0; i<s.length(); i++) { + if (s.charAt(i) == '\n') newLineCount++; + } + incRowCount(newLineCount); + + super.print(s); + } + + @Override + public void println(boolean x) { + super.println(x); + incRowCount(); + } + + @Override + public void println(char x) { + super.println(x); + incRowCount(); + } + + @Override + public void println(int x) { + super.println(x); + incRowCount(); + } + + @Override + public void println(long x) { + super.println(x); + incRowCount(); + } + + @Override + public void println(float x) { + super.println(x); + incRowCount(); + } + + @Override + public void println(double x) { + super.println(x); + incRowCount(); + } + + @Override + public void println(char[] x) { + super.println(x); + incRowCount(); + } + + @Override + public void println(String x) { + + super.println(x); + incRowCount(); + } + + @Override + public void println(Object x) { + super.println(x); + incRowCount(); + } + + @Override + public void print(char c) { + super.print(c); + if (c == '\n') incRowCount(); + } + + @Override + public void print(char[] x) { + print(new String(x)); + } + + @Override + public void print(Object obj) { + print(String.valueOf(obj)); + } + + @Override + public PrintStream append(CharSequence csq, int start, int end) { + long nbNewLine = csq.chars().filter(c -> c == '\n').count(); + incRowCount(nbNewLine); + return super.append(csq, start, end); + } + + @Override + public PrintStream append(char c) { + if (c == '\n') { + incRowCount(); + } + return super.append(c); + } + + /* -- protected methods -- */ + + protected void incRowCount() { + synchronized (rowsCount) { + rowsCount++; + } + } + + protected void incRowCount(long increment) { + synchronized (rowsCount) { + rowsCount += increment; + } + } +} diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpService.java index a6fc7530368e6589067cca844b9608923c256c21..b1a02dee360510d05168ca6f7faa9ef50fa9893f 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpService.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpService.java @@ -27,7 +27,10 @@ import org.duniter.core.beans.Service; import org.duniter.core.client.model.local.Peer; import org.duniter.core.client.service.exception.PeerConnectionException; import org.apache.http.client.methods.HttpUriRequest; +import org.duniter.core.util.websocket.WebsocketClientEndpoint; +import java.io.IOException; +import java.io.InputStream; import java.net.URI; /** @@ -52,4 +55,12 @@ public interface HttpService extends Service { String getPath(String... absolutePath); URIBuilder getURIBuilder(URI baseUri, String... path); + + WebsocketClientEndpoint getWebsocketClientEndpoint(Peer peer, String path, boolean autoReconnect); + + <T> T readValue(String json, Class<T> clazz) throws IOException; + + <T> T readValue(byte[] json, Class<T> clazz) throws IOException; + + <T> T readValue(InputStream json, Class<T> clazz) throws IOException; } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpServiceImpl.java index eaa8ce61df003f3f22a3b760b88d2a7c1f8e01d9..772a3de3dd8d465193f243e8dffb79e77ffb2ec2 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpServiceImpl.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/HttpServiceImpl.java @@ -45,6 +45,7 @@ import org.duniter.core.client.service.exception.*; import org.duniter.core.exception.TechnicalException; import org.duniter.core.util.StringUtils; import org.duniter.core.util.cache.SimpleCache; +import org.duniter.core.util.websocket.WebsocketClientEndpoint; import org.nuiton.i18n.I18n; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,7 +54,11 @@ import java.io.*; import java.net.ConnectException; import java.net.SocketTimeoutException; import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceConfigurationError; /** * Created by eis on 05/02/15. @@ -71,6 +76,7 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean protected SimpleCache<Integer, RequestConfig> requestConfigCache; protected SimpleCache<Integer, HttpClient> httpClientCache; + protected Map<URI, WebsocketClientEndpoint> wsEndPoints = new HashMap<>(); public HttpServiceImpl() { super(); @@ -144,6 +150,13 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean public void close() throws IOException { httpClientCache.clear(); requestConfigCache.clear(); + + if (wsEndPoints.size() != 0) { + for (WebsocketClientEndpoint clientEndPoint: wsEndPoints.values()) { + clientEndPoint.close(); + } + wsEndPoints.clear(); + } } public <T> T executeRequest(HttpUriRequest request, Class<? extends T> resultClass) { @@ -359,7 +372,7 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean // deserialize using gson else { try { - result = objectMapper.readValue(content, ResultClass); + result = readValue(content, ResultClass); } catch (Exception e) { throw new TechnicalException(I18n.t("duniter4j.client.core.invalidResponse"), e); @@ -430,4 +443,41 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean // silent is gold } } + + public WebsocketClientEndpoint getWebsocketClientEndpoint(Peer peer, String path, boolean autoReconnect) { + + try { + URI wsBlockURI = new URI(String.format("%s://%s:%s%s", + peer.isUseSsl() ? "wss" : "ws", + peer.getHost(), + peer.getPort(), + path)); + + // Get the websocket, or open new one if not exists + WebsocketClientEndpoint wsClientEndPoint = wsEndPoints.get(wsBlockURI); + if (wsClientEndPoint == null || wsClientEndPoint.isClosed()) { + log.info(String.format("Starting to listen on [%s]...", wsBlockURI.toString())); + wsClientEndPoint = new WebsocketClientEndpoint(wsBlockURI, autoReconnect); + wsEndPoints.put(wsBlockURI, wsClientEndPoint); + } + + return wsClientEndPoint; + + } catch (URISyntaxException | ServiceConfigurationError ex) { + throw new TechnicalException(String.format("Could not create URI need for web socket [%s]: %s", path, ex.getMessage())); + } + + } + + public <T> T readValue(String json, Class<T> clazz) throws IOException { + return objectMapper.readValue(json, clazz); + } + + public <T> T readValue(byte[] json, Class<T> clazz) throws IOException { + return objectMapper.readValue(json, clazz); + } + + public <T> T readValue(InputStream json, Class<T> clazz) throws IOException { + return objectMapper.readValue(json, clazz); + } } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BaseRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BaseRemoteServiceImpl.java index d0d2e07375440ac61c5572d571c8011621d5dc8e..b655d2ee0ebf17e4b29709bf22ceb80d8987427a 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BaseRemoteServiceImpl.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BaseRemoteServiceImpl.java @@ -31,11 +31,13 @@ import org.duniter.core.client.service.local.PeerService; import org.duniter.core.client.service.ServiceLocator; import org.apache.http.client.methods.HttpUriRequest; import org.duniter.core.exception.TechnicalException; +import org.duniter.core.util.websocket.WebsocketClientEndpoint; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.ServiceConfigurationError; /** * Created by eis on 05/02/15. @@ -91,4 +93,16 @@ public abstract class BaseRemoteServiceImpl implements Service, InitializingBean throw new TechnicalException(e); } } + + public WebsocketClientEndpoint getWebsocketClientEndpoint(Peer peer, String path, boolean autoReconnect) { + return httpService.getWebsocketClientEndpoint(peer, path, autoReconnect); + } + + public <T> T readValue(String json, Class<T> clazz) throws IOException { + return httpService.readValue(json, clazz); + } + + public <T> T readValue(byte[] json, Class<T> clazz) throws IOException { + return httpService.readValue(json, clazz); + } } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteService.java index 8e92845eb8810efaeea6b24bc6570f56061d5db0..2231f3f2561fedfda4a6248b23d21254e4a8b521 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteService.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteService.java @@ -226,20 +226,12 @@ public interface BlockchainRemoteService extends Service { * Listening new block event * @param currencyId * @param listener + * @param autoReconnect * @return */ - WebsocketClientEndpoint addBlockListener(long currencyId, WebsocketClientEndpoint.MessageListener listener); + WebsocketClientEndpoint addBlockListener(long currencyId, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect); - WebsocketClientEndpoint addBlockListener(Peer peer, WebsocketClientEndpoint.MessageListener listener); + WebsocketClientEndpoint addBlockListener(Peer peer, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect); - /** - * Listening new peer event - * @param currencyId - * @param listener - * @return - */ - WebsocketClientEndpoint addPeerListener(long currencyId, WebsocketClientEndpoint.MessageListener listener); - - WebsocketClientEndpoint addPeerListener(Peer peer, WebsocketClientEndpoint.MessageListener listener); } \ No newline at end of file diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceImpl.java index 25c8b7a16c4cd5abab17afa43401eec92a69a157..22c7f92e4ac677b1a4fbd763a4480cf44bd87600 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceImpl.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceImpl.java @@ -566,17 +566,17 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement } @Override - public WebsocketClientEndpoint addBlockListener(long currencyId, WebsocketClientEndpoint.MessageListener listener) { - return addBlockListener(peerService.getActivePeerByCurrencyId(currencyId), listener); + public WebsocketClientEndpoint addBlockListener(long currencyId, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect) { + return addBlockListener(peerService.getActivePeerByCurrencyId(currencyId), listener, autoReconnect); } @Override - public WebsocketClientEndpoint addBlockListener(Peer peer, WebsocketClientEndpoint.MessageListener listener) { + public WebsocketClientEndpoint addBlockListener(Peer peer, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect) { Preconditions.checkNotNull(peer); Preconditions.checkNotNull(listener); // Get (or create) the websocket endpoint - WebsocketClientEndpoint wsClientEndPoint = getWebsocketClientEndpoint(peer, URL_WS_BLOCK); + WebsocketClientEndpoint wsClientEndPoint = getWebsocketClientEndpoint(peer, URL_WS_BLOCK, autoReconnect); // add listener wsClientEndPoint.registerListener(listener); @@ -584,24 +584,6 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement return wsClientEndPoint; } - @Override - public WebsocketClientEndpoint addPeerListener(long currencyId, WebsocketClientEndpoint.MessageListener listener) { - return addBlockListener(peerService.getActivePeerByCurrencyId(currencyId), listener); - } - - @Override - public WebsocketClientEndpoint addPeerListener(Peer peer, WebsocketClientEndpoint.MessageListener listener) { - Preconditions.checkNotNull(peer); - Preconditions.checkNotNull(listener); - - // Get (or create) the websocket endpoint - WebsocketClientEndpoint wsClientEndPoint = getWebsocketClientEndpoint(peer, URL_WS_PEER); - - // add listener - wsClientEndPoint.registerListener(listener); - - return wsClientEndPoint; - } /* -- Internal methods -- */ @@ -820,29 +802,4 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement return Long.parseLong(dividendStr); } - - public WebsocketClientEndpoint getWebsocketClientEndpoint(Peer peer, String path) { - - try { - URI wsBlockURI = new URI(String.format("%s://%s:%s%s", - peer.isUseSsl() ? "wss" : "ws", - peer.getHost(), - peer.getPort(), - path)); - - // Get the websocket, or open new one if not exists - WebsocketClientEndpoint wsClientEndPoint = wsEndPoints.get(wsBlockURI); - if (wsClientEndPoint == null || wsClientEndPoint.isClosed()) { - log.info(String.format("Starting to listen on [%s]...", wsBlockURI.toString())); - wsClientEndPoint = new WebsocketClientEndpoint(wsBlockURI); - wsEndPoints.put(wsBlockURI, wsClientEndPoint); - } - - return wsClientEndPoint; - - } catch (URISyntaxException | ServiceConfigurationError ex) { - throw new TechnicalException(String.format("Could not create URI need for web socket [%s]: %s", path, ex.getMessage())); - } - - } } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteService.java index 262dd501a4848d698d5a1be067a8bc2468cf0a1a..b8e1a0fc784258a9ee22d061019043fc4723cce2 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteService.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteService.java @@ -46,5 +46,7 @@ public interface NetworkRemoteService extends Service { List<Peer> findPeers(Peer peer, String status, EndpointApi endpointApi, Integer currentBlockNumber, String currentBlockHash); + WebsocketClientEndpoint addPeerListener(long currencyId, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect); + WebsocketClientEndpoint addPeerListener(Peer peer, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect); } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteServiceImpl.java index 80a3c4256af7d2272f03e8f8ffdb4960656326d6..5d8b1aef38e1b490ee3209656c8353f6e78f1a3b 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteServiceImpl.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteServiceImpl.java @@ -64,7 +64,6 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N protected ObjectMapper objectMapper; - private Map<URI, WebsocketClientEndpoint> wsEndPoints = new HashMap<>(); public NetworkRemoteServiceImpl() { super(); @@ -77,18 +76,6 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N return result; } - @Override - public void close() throws IOException { - super.close(); - - if (wsEndPoints.size() != 0) { - for (WebsocketClientEndpoint clientEndPoint: wsEndPoints.values()) { - clientEndPoint.close(); - } - wsEndPoints.clear(); - } - } - @Override public List<Peer> getPeers(Peer peer) { return findPeers(peer, null, null, null, null); @@ -164,6 +151,13 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N return result; } + + @Override + public WebsocketClientEndpoint addPeerListener(long currencyId, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect) { + Peer peer = peerService.getActivePeerByCurrencyId(currencyId); + return addPeerListener(peer, listener, autoReconnect); + } + @Override public WebsocketClientEndpoint addPeerListener(Peer peer, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect) { Preconditions.checkNotNull(peer); @@ -214,28 +208,5 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N return hash; } - public WebsocketClientEndpoint getWebsocketClientEndpoint(Peer peer, String path, boolean autoReconnect) { - - try { - URI wsBlockURI = new URI(String.format("%s://%s:%s%s", - peer.isUseSsl() ? "wss" : "ws", - peer.getHost(), - peer.getPort(), - path)); - - // Get the websocket, or open new one if not exists - WebsocketClientEndpoint wsClientEndPoint = wsEndPoints.get(wsBlockURI); - if (wsClientEndPoint == null || wsClientEndPoint.isClosed()) { - log.info(String.format("Starting to listen on [%s]...", wsBlockURI.toString())); - wsClientEndPoint = new WebsocketClientEndpoint(wsBlockURI, autoReconnect); - wsEndPoints.put(wsBlockURI, wsClientEndPoint); - } - - return wsClientEndPoint; - - } catch (URISyntaxException | ServiceConfigurationError ex) { - throw new TechnicalException(String.format("Could not create URI need for web socket [%s]: %s", path, ex.getMessage())); - } - } } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java index 0cc7f547f6c694039bb59b01b950c484edfc2345..f084cfd4aca7c690f92cef452b45b3d99b89a973 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java @@ -37,6 +37,10 @@ import java.util.function.Predicate; */ public interface NetworkService extends Service { + interface PeersChangeListener { + void onChanged(List<Peer> peers); + } + class Sort { public SortType sortType; public boolean sortAsc; @@ -75,5 +79,11 @@ public interface NetworkService extends Service { Comparator<Peer> peerComparator(Sort sort); + void addPeersChangeListener(final Peer mainPeer, final PeersChangeListener listener); + + void addPeersChangeListener(final Peer mainPeer, final PeersChangeListener listener, + final Filter filter, final Sort sort, final boolean autoreconnect, + final ExecutorService executor); + } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java index 2834bb298e43936e11df39de184d47c51f995477..7aacd7313a7f0c7ec208c0aea3b1ef5af040c231 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java @@ -23,14 +23,14 @@ package org.duniter.core.client.service.local; */ import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; -import org.duniter.core.client.model.bma.Constants; -import org.duniter.core.client.model.bma.EndpointApi; -import org.duniter.core.client.model.bma.NetworkPeering; -import org.duniter.core.client.model.bma.NetworkPeers; +import org.duniter.core.client.model.bma.*; +import org.duniter.core.client.model.bma.jackson.JacksonUtils; import org.duniter.core.client.model.local.Peer; import org.duniter.core.client.service.ServiceLocator; import org.duniter.core.client.service.bma.BaseRemoteServiceImpl; +import org.duniter.core.client.service.bma.BlockchainRemoteService; import org.duniter.core.client.service.bma.NetworkRemoteService; import org.duniter.core.client.service.bma.WotRemoteService; import org.duniter.core.client.service.exception.HttpConnectException; @@ -45,6 +45,7 @@ import org.duniter.core.util.websocket.WebsocketClientEndpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.util.*; import java.util.concurrent.*; import java.util.function.Function; @@ -65,17 +66,35 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network private NetworkRemoteService networkRemoteService; private CryptoService cryptoService; private WotRemoteService wotRemoteService; + private BlockchainRemoteService blockchainRemoteService; + + protected class Locker { + private boolean lock = false; + public boolean isLocked() { + return this.lock; + } + public void lock() { + Preconditions.checkArgument(!this.lock); + this.lock = !this.lock; + } + public void unlock() { + Preconditions.checkArgument(this.lock); + this.lock = !this.lock; + } + } public NetworkServiceImpl() { } public NetworkServiceImpl(NetworkRemoteService networkRemoteService, WotRemoteService wotRemoteService, - CryptoService cryptoService) { + CryptoService cryptoService, + BlockchainRemoteService blockchainRemoteService) { this(); this.networkRemoteService = networkRemoteService; this.wotRemoteService = wotRemoteService; this.cryptoService = cryptoService; + this.blockchainRemoteService = blockchainRemoteService; } @Override @@ -84,6 +103,7 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network this.networkRemoteService = ServiceLocator.instance().getNetworkRemoteService(); this.wotRemoteService = ServiceLocator.instance().getWotRemoteService(); this.cryptoService = ServiceLocator.instance().getCryptoService(); + this.blockchainRemoteService = ServiceLocator.instance().getBlockchainRemoteService(); } @Override @@ -146,37 +166,41 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network new CompletableFuture[] {peersFuture, memberUidsFuture}) .thenApply(v -> { final Map<String, String> memberUids = memberUidsFuture.join(); - return peersFuture.join().stream().map(peer -> - CompletableFuture.supplyAsync(() -> getVersion(peer), pool) - .thenApply(this::getCurrentBlock) - .exceptionally(throwable -> { - peer.getStats().setStatus(Peer.PeerStatus.DOWN); - if(!(throwable instanceof HttpConnectException)) { - Throwable cause = throwable.getCause() != null ? throwable.getCause() : throwable; - peer.getStats().setError(cause.getMessage()); - if (log.isDebugEnabled()) { - if (log.isTraceEnabled()) { - log.debug(String.format("[%s] is DOWN: %s", peer, cause.getMessage()), cause); - } - else log.debug(String.format("[%s] is DOWN: %s", peer, cause.getMessage())); - } - } - else if (log.isTraceEnabled()) log.debug(String.format("[%s] is DOWN", peer)); - return peer; - }) - .thenApply(apeer -> { - String uid = StringUtils.isNotBlank(peer.getPubkey()) ? memberUids.get(peer.getPubkey()) : null; - peer.getStats().setUid(uid); - if (peer.getStats().isReacheable() && StringUtils.isNotBlank(uid)) { - getHardship(peer); - } - return apeer; - }) - .exceptionally(throwable -> { - peer.getStats().setHardshipLevel(0); - return peer; - }) - ).collect(Collectors.toList()); + return peersFuture.join().stream() + .map(peer -> asyncRefreshPeer(peer, memberUids, pool)) + .collect(Collectors.toList()); + }); + } + + public CompletableFuture<Peer> asyncRefreshPeer(final Peer peer, final Map<String, String> memberUids, final ExecutorService pool) { + return CompletableFuture.supplyAsync(() -> getVersion(peer), pool) + .thenApply(this::getCurrentBlock) + .exceptionally(throwable -> { + peer.getStats().setStatus(Peer.PeerStatus.DOWN); + if(!(throwable instanceof HttpConnectException)) { + Throwable cause = throwable.getCause() != null ? throwable.getCause() : throwable; + peer.getStats().setError(cause.getMessage()); + if (log.isDebugEnabled()) { + if (log.isTraceEnabled()) { + log.debug(String.format("[%s] is DOWN: %s", peer, cause.getMessage()), cause); + } + else log.debug(String.format("[%s] is DOWN: %s", peer, cause.getMessage())); + } + } + else if (log.isTraceEnabled()) log.debug(String.format("[%s] is DOWN", peer)); + return peer; + }) + .thenApply(apeer -> { + String uid = StringUtils.isNotBlank(peer.getPubkey()) ? memberUids.get(peer.getPubkey()) : null; + peer.getStats().setUid(uid); + if (peer.getStats().isReacheable() && StringUtils.isNotBlank(uid)) { + getHardship(peer); + } + return apeer; + }) + .exceptionally(throwable -> { + peer.getStats().setHardshipLevel(0); + return peer; }); } @@ -223,6 +247,135 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network return peers; } + public void addPeersChangeListener(final Peer mainPeer, final PeersChangeListener listener) { + + // Default filter + Filter filterDef = new Filter(); + filterDef.filterType = null; + filterDef.filterStatus = Peer.PeerStatus.UP; + filterDef.filterEndpoints = ImmutableList.of(EndpointApi.BASIC_MERKLED_API.name(), EndpointApi.BMAS.name()); + + // Default sort + Sort sortDef = new Sort(); + sortDef.sortType = null; + + addPeersChangeListener(mainPeer, listener, filterDef, sortDef, true, null); + + } + + public void addPeersChangeListener(final Peer mainPeer, final PeersChangeListener listener, + final Filter filter, final Sort sort, final boolean autoreconnect, + final ExecutorService executor) { + + + final Locker threadLock = new Locker(); + final List<Peer> result = new ArrayList<>(); + final List<String> knownBlocks = new ArrayList<>(); + final Map<String, Peer> knownPeers = new HashMap<>(); + + final Predicate<Peer> peerFilter = peerFilter(filter); + final Comparator<Peer> peerComparator = peerComparator(sort); + final ExecutorService pool = (executor != null) ? executor : ForkJoinPool.commonPool(); + + // Manage new block event + blockchainRemoteService.addBlockListener(mainPeer, json -> { + if (threadLock.isLocked()) return; + synchronized (threadLock) { + threadLock.lock(); + } + try { + BlockchainBlock block = readValue(json, BlockchainBlock.class); + String blockBuid = buid(block); + boolean isNewBlock = (blockBuid != null && !knownBlocks.contains(blockBuid)); + + // If new block + wait 5s for network propagation + if (isNewBlock && waitSafe(5000)) { + List<Peer> updatedPeers = getPeers(mainPeer, filter, sort); + + knownPeers.clear(); + updatedPeers.stream().forEach(peer -> { + String buid = buid(peer.getStats()); + if (!knownBlocks.contains(buid)) { + knownBlocks.add(buid); + } + knownPeers.put(peer.toString(), peer); + }); + + result.clear(); + result.addAll(updatedPeers); + listener.onChanged(result); + } + } catch(IOException e) { + log.error("Could not parse peer received by WS: " + e.getMessage(), e); + } + finally { + synchronized (threadLock) { + threadLock.unlock(); + } + } + }, autoreconnect); + + // Manage new peer event + networkRemoteService.addPeerListener(mainPeer, json -> { + if (threadLock.isLocked()) return; + synchronized (threadLock) { + threadLock.lock(); + } + + try { + NetworkPeers.Peer bmaPeer = readValue(json, NetworkPeers.Peer.class); + final List<Peer> newPeers = new ArrayList<>(); + addEndpointsAsPeers(bmaPeer, newPeers, null); + + CompletableFuture<List<CompletableFuture<Peer>>> jobs = + CompletableFuture.supplyAsync(() -> wotRemoteService.getMembersUids(mainPeer), pool) + .thenApply(memberUids -> + newPeers.stream().map(peer -> + asyncRefreshPeer(peer, memberUids, pool)) + .collect(Collectors.toList()) + ); + jobs.thenCompose(refreshedPeersFuture -> CompletableFutures.allOfToList(refreshedPeersFuture, refreshedPeer -> { + boolean exists = knownPeers.containsKey(refreshedPeer.toString()); + boolean include = peerFilter.test(refreshedPeer); + if (!include && exists) { + Peer removedPeer = knownPeers.remove(refreshedPeer.toString()); + result.remove(removedPeer); + } + else if (include && exists) { + result.remove(knownPeers.get(refreshedPeer.toString())); + } + return include; + })) + .thenApply(addedPeers -> { + result.addAll(addedPeers); + fillPeerStatsConsensus(result); + result.sort(peerComparator); + + result.stream().forEach(peer -> { + String buid = buid(peer.getStats()); + if (!knownBlocks.contains(buid)) { + knownBlocks.add(buid); + } + knownPeers.put(peer.toString(), peer); + }); + + listener.onChanged(result); + return result; + }); + + } catch(IOException e) { + log.error("Could not parse peer received by WS: " + e.getMessage(), e); + } + finally { + synchronized (threadLock) { + threadLock.unlock(); + } + } + + }, autoreconnect); + } + + /* -- protected methods -- */ protected List<Peer> loadPeerLeafs(Peer peer) { @@ -266,21 +419,7 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network String leaf = leaves.get(i); try { NetworkPeers.Peer peer = networkRemoteService.getPeerLeaf(requestedPeer, leaf); - - if (CollectionUtils.isNotEmpty(peer.getEndpoints())) { - for (NetworkPeering.Endpoint ep: peer.getEndpoints()) { - if (ep != null && ep.getApi() != null) { - Peer peerEp = Peer.newBuilder() - .setCurrency(peer.getCurrency()) - .setHash(leaf) - .setPubkey(peer.getPubkey()) - .setEndpoint(ep) - .build(); - result.add(peerEp); - } - } - } - + addEndpointsAsPeers(peer, result, leaf); } catch(HttpNotFoundException e) { log.warn("Peer not found for leaf=" + leaf); @@ -289,6 +428,22 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network } } + protected void addEndpointsAsPeers(NetworkPeers.Peer peer, List<Peer> result, String hash) { + if (CollectionUtils.isNotEmpty(peer.getEndpoints())) { + for (NetworkPeering.Endpoint ep: peer.getEndpoints()) { + if (ep != null && ep.getApi() != null) { + Peer peerEp = Peer.newBuilder() + .setCurrency(peer.getCurrency()) + .setHash(hash) + .setPubkey(peer.getPubkey()) + .setEndpoint(ep) + .build(); + result.add(peerEp); + } + } + } + } + protected boolean applyPeerFilter(Peer peer, Filter filter) { @@ -463,4 +618,18 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network ? stats.getBlockNumber() + "-" + stats.getBlockHash() : null; } + + protected String buid(BlockchainBlock block) { + if (block == null || block.getNumber() == null || block.getHash() == null) return null; + return block.getNumber() + "-" + block.getHash(); + } + + protected boolean waitSafe(long duration) { + try { + Thread.sleep(duration); + return true; + } catch (InterruptedException e) { + return false; + } + } } diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceTest.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceTest.java index 33ee0035fd0ca25d5d2ea0b87c946949590222cd..855fad2054b24ce985afa3a9c0a8181d83ec0765 100644 --- a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceTest.java +++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/BlockchainRemoteServiceTest.java @@ -146,7 +146,7 @@ public class BlockchainRemoteServiceTest { isWebSocketNewBlockReceived = false; - service.addBlockListener(createTestPeer(), (message) -> { + /*service.addBlockListener(createTestPeer(), (message) -> { try { BlockchainBlock block = JacksonUtils.newObjectMapper().readValue(message, BlockchainBlock.class); log.debug("Received block #" + block.getNumber()); @@ -155,7 +155,7 @@ public class BlockchainRemoteServiceTest { catch (IOException e) { Assert.fail(e.getMessage()); } - }); + });*/ int count = 0; while(!isWebSocketNewBlockReceived) {