From 9666345699b95439d938ea6f17f92f7f8921dd78 Mon Sep 17 00:00:00 2001 From: blavenie <benoit.lavenier@e-is.pro> Date: Fri, 24 Mar 2017 13:28:48 +0100 Subject: [PATCH] - Start peer indexation from Duniter network - new mavn submodule, for command line tool --- duniter4j-cmd/lib/j-text-utils-0.3.3.jar | Bin 0 -> 18856 bytes duniter4j-cmd/lib/j-text-utils-0.3.3.pom | 65 ++ duniter4j-cmd/pom.xml | 156 +++++ .../src/main/java/fr/duniter/cmd/Main.java | 149 +++++ .../fr/duniter/cmd/actions/NetworkAction.java | 80 +++ .../duniter/cmd/actions/SentMoneyAction.java | 76 +++ .../cmd/actions/params/WalletParameters.java | 14 + .../duniter/cmd/actions/utils/Formatters.java | 34 ++ .../services/org.duniter.core.beans.Bean | 13 + .../src/main/resources/duniter4j-cmd.config | 5 + .../src/main/resources/log4j.properties | 27 + .../core/client/config/Configuration.java | 12 +- .../client/config/ConfigurationOption.java | 6 +- .../core/client/model/bma/Constants.java | 9 + ...EndpointProtocol.java => EndpointApi.java} | 5 +- .../core/client/model/bma/NetworkPeering.java | 25 +- .../core/client/model/bma/NetworkPeers.java | 4 +- .../core/client/model/bma/Protocol.java | 4 +- .../core/client/model/bma/TxSource.java | 2 +- .../model/bma/gson/EndpointAdapter.java | 18 +- .../model/bma/gson/MultimapTypeAdapter.java | 2 +- .../bma/jackson/EndpointDeserializer.java | 121 +++- .../model/elasticsearch/DeleteRecord.java | 2 +- .../duniter/core/client/model/local/Peer.java | 445 ++++++++++++-- .../core/client/service/HttpServiceImpl.java | 33 +- .../core/client/service/ServiceLocator.java | 12 +- .../service/bma/BlockchainRemoteService.java | 18 +- .../bma/BlockchainRemoteServiceImpl.java | 48 +- .../service/bma/NetworkRemoteService.java | 12 +- .../service/bma/NetworkRemoteServiceImpl.java | 148 ++++- .../bma/TransactionRemoteServiceImpl.java | 4 +- .../client/service/bma/WotRemoteService.java | 5 + .../service/bma/WotRemoteServiceImpl.java | 32 + .../CurrencyRegistryRemoteServiceImpl.java | 4 +- .../client/service/local/CurrencyService.java | 2 +- .../service/local/CurrencyServiceImpl.java | 2 +- .../client/service/local/NetworkService.java | 57 ++ .../service/local/NetworkServiceImpl.java | 444 ++++++++++++++ .../client/service/local/PeerServiceImpl.java | 2 +- .../org/duniter/core/client/TestResource.java | 5 +- .../core/client/service/HttpServiceTest.java | 15 +- .../bma/BlockchainRemoteServiceTest.java | 11 +- .../service/bma/NetworkRemoteServiceTest.java | 13 +- .../bma/TransactionRemoteServiceTest.java | 9 +- .../service/bma/WotRemoteServiceTest.java | 9 +- .../service/local/NetworkServiceTest.java | 72 +++ .../services/org.duniter.core.beans.Bean | 1 + .../duniter4j-core-client-test.properties | 4 +- .../src/test/resources/log4j.properties | 18 +- duniter4j-core-shared/pom.xml | 10 + .../org/duniter/core/beans/BeanFactory.java | 1 - .../util/concurrent/CompletableFutures.java | 38 ++ .../core/util/http/InetAddressUtils.java | 27 + .../websocket/WebsocketClientEndpoint.java | 23 + .../org/duniter/elasticsearch/PluginInit.java | 6 + .../duniter/elasticsearch/PluginSettings.java | 5 +- .../service/BlockchainService.java | 40 +- .../elasticsearch/service/NetworkService.java | 571 ++++++++++++++++++ .../elasticsearch/service/ServiceLocator.java | 7 +- .../elasticsearch/service/ServiceModule.java | 3 +- .../elasticsearch/threadpool/ThreadPool.java | 5 +- .../services/org.duniter.core.beans.Bean | 1 + .../i18n/duniter4j-es-core_en_GB.properties | 5 + .../i18n/duniter4j-es-core_fr_FR.properties | 5 + .../service/CitiesRegistryService.java | 2 +- pom.xml | 3 +- 66 files changed, 2756 insertions(+), 250 deletions(-) create mode 100644 duniter4j-cmd/lib/j-text-utils-0.3.3.jar create mode 100644 duniter4j-cmd/lib/j-text-utils-0.3.3.pom create mode 100644 duniter4j-cmd/pom.xml create mode 100644 duniter4j-cmd/src/main/java/fr/duniter/cmd/Main.java create mode 100644 duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/NetworkAction.java create mode 100644 duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/SentMoneyAction.java create mode 100644 duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/params/WalletParameters.java create mode 100644 duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/utils/Formatters.java create mode 100644 duniter4j-cmd/src/main/resources/META-INF/services/org.duniter.core.beans.Bean create mode 100644 duniter4j-cmd/src/main/resources/duniter4j-cmd.config create mode 100644 duniter4j-cmd/src/main/resources/log4j.properties rename duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/{EndpointProtocol.java => EndpointApi.java} (92%) create mode 100644 duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java create mode 100644 duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java create mode 100644 duniter4j-core-client/src/test/java/org/duniter/core/client/service/local/NetworkServiceTest.java create mode 100644 duniter4j-core-shared/src/main/java/org/duniter/core/util/concurrent/CompletableFutures.java create mode 100644 duniter4j-core-shared/src/main/java/org/duniter/core/util/http/InetAddressUtils.java create mode 100644 duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/NetworkService.java diff --git a/duniter4j-cmd/lib/j-text-utils-0.3.3.jar b/duniter4j-cmd/lib/j-text-utils-0.3.3.jar new file mode 100644 index 0000000000000000000000000000000000000000..42a9c6fa4ce334d113eba5af341d8a06b1a651e7 GIT binary patch literal 18856 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 V2@dfC1^4v_?v?!n^8NVs{{Wxw$6EjZ literal 0 HcmV?d00001 diff --git a/duniter4j-cmd/lib/j-text-utils-0.3.3.pom b/duniter4j-cmd/lib/j-text-utils-0.3.3.pom new file mode 100644 index 00000000..1a68c2db --- /dev/null +++ b/duniter4j-cmd/lib/j-text-utils-0.3.3.pom @@ -0,0 +1,65 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>dnl.utils</groupId> + <artifactId>j-text-utils</artifactId> + <packaging>jar</packaging> + <version>0.3.3</version> + <name>Java Text Utilities</name> + <url>http://code.google.com/p/j-text-utils</url> + + <dependencies> + <dependency> + <groupId>commons-lang</groupId> + <artifactId>commons-lang</artifactId> + <version>2.4</version> + </dependency> + <dependency> + <groupId>net.sf.opencsv</groupId> + <artifactId>opencsv</artifactId> + <version>2.3</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <version>14.0.1</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.7</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <extensions> + <extension> + <groupId>org.jvnet.wagon-svn</groupId> + <artifactId>wagon-svn</artifactId> + <version>1.9</version> + </extension> + </extensions> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <source>1.7</source> + <target>1.7</target> + </configuration> + </plugin> + </plugins> + </build> + + <distributionManagement> + <repository> + <uniqueVersion>false</uniqueVersion> + <id>googlecode</id> + <url>svn:https://j-text-utils.googlecode.com/svn/trunk/repo/</url> + </repository> + </distributionManagement> + +</project> diff --git a/duniter4j-cmd/pom.xml b/duniter4j-cmd/pom.xml new file mode 100644 index 00000000..0491353b --- /dev/null +++ b/duniter4j-cmd/pom.xml @@ -0,0 +1,156 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <artifactId>duniter4j</artifactId> + <groupId>org.duniter</groupId> + <version>0.9.2-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>duniter4j-cmd</artifactId> + + <properties> + <jTextUtilsVersion>0.3.3</jTextUtilsVersion> + </properties> + + <repositories> + <repository> + <id>d-maven</id> + <url>https://github.com/neilpanchal/j-text-utils/tree/master/repo</url> + </repository> + </repositories> + + <dependencies> + <dependency> + <groupId>org.duniter</groupId> + <artifactId>duniter4j-core-client</artifactId> + <version>${project.version}</version> + </dependency> + + <!-- logging --> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-log4j12</artifactId> + </dependency> + <dependency> + <groupId>log4j</groupId> + <artifactId>log4j</artifactId> + </dependency> + + + <dependency> + <groupId>com.beust</groupId> + <artifactId>jcommander</artifactId> + <version>1.60</version> + </dependency> + + <dependency> + <groupId>dnl.utils</groupId> + <artifactId>j-text-utils</artifactId> + <version>${jTextUtilsVersion}</version> + </dependency> + <dependency> + <groupId>commons-lang</groupId> + <artifactId>commons-lang</artifactId> + <version>2.6</version> + </dependency> + + </dependencies> + + <profiles> + <profile> + <id>install-missing-libs</id> + <activation> + <file> + <missing>${settings.localRepository}/dnl/utils/j-text-utils/${jTextUtilsVersion}/j-text-utils-${jTextUtilsVersion}.jar</missing> + </file> + </activation> + <build> + <plugins> + <plugin> + <artifactId>maven-install-plugin</artifactId> + <version>2.5.2</version> + <executions> + <execution> + <id>installing j-text-utils.jar</id> + <phase>initialize</phase> + <goals> + <goal>install-file</goal> + </goals> + <configuration> + <groupId>dnl.utils</groupId> + <artifactId>j-text-utils</artifactId> + <version>${jTextUtilsVersion}</version> + <packaging>jar</packaging> + <file>${project.basedir}/lib/j-text-utils-${jTextUtilsVersion}.jar</file> + </configuration> + </execution> + </executions> + </plugin> + + <plugin> + <artifactId>maven-antrun-plugin</artifactId> + <executions> + <execution> + <id>enforce-dependencies-exists</id> + <phase>generate-sources</phase> + <goals> + <goal>run</goal> + </goals> + <configuration> + <target> + + <condition property="displayMessage"> + <and> + <not><available file="${project.basedir}/.maven/install.log" /></not> + <!-- do not failed here if performRelease --> + <isfalse value="${performRelease}" /> + </and> + </condition> + <property name="installSuccessMessage">* + ************************************************************************* + * + * IMPORTANT: + * + * Missing lib dependencies successfully installed on [${settings.localRepository}] + * You should now re-run the build. + * This message will NOT appear again + * + ************************************************************************* + </property> + + <echo file="${project.basedir}/.maven/install.log">${installSuccessMessage}</echo> + + <fail message="${installSuccessMessage}" > + <condition> + <istrue value="${displayMessage}"/> + </condition> + </fail> + </target> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + + <profile> + <id>use-installed-libs</id> + <activation> + <file> + <exists>${settings.localRepository}/dnl/utils/j-text-utils/${jTextUtilsVersion}/j-text-utils-${jTextUtilsVersion}.jar</exists> + </file> + </activation> + <dependencies> + <dependency> + <groupId>dnl.utils</groupId> + <artifactId>j-text-utils</artifactId> + <version>${jTextUtilsVersion}</version> + </dependency> + </dependencies> + </profile> + </profiles> +</project> \ No newline at end of file diff --git a/duniter4j-cmd/src/main/java/fr/duniter/cmd/Main.java b/duniter4j-cmd/src/main/java/fr/duniter/cmd/Main.java new file mode 100644 index 00000000..7208a3dc --- /dev/null +++ b/duniter4j-cmd/src/main/java/fr/duniter/cmd/Main.java @@ -0,0 +1,149 @@ +package fr.duniter.cmd; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParameterException; +import com.google.common.collect.Lists; +import fr.duniter.cmd.actions.NetworkAction; +import fr.duniter.cmd.actions.SentMoneyAction; +import org.apache.commons.io.FileUtils; +import org.duniter.core.client.config.Configuration; +import org.duniter.core.client.service.ServiceLocator; +import org.duniter.core.util.StringUtils; +import org.nuiton.i18n.I18n; +import org.nuiton.i18n.init.DefaultI18nInitializer; +import org.nuiton.i18n.init.UserI18nInitializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +/** + * Created by blavenie on 22/03/17. + */ +public class Main { + + @Parameter(names = "-debug", description = "Debug mode", arity = 1) + private boolean debug = false; + + @Parameter(names = "--help", help = true) + private boolean help; + + @Parameter(names = "-config", description = "Configuration file path") + private String configFilename = "duniter-cmd.config"; + + public static void main(String ... args) { + Main main = new Main(); + main.run(args); + } + + protected void run(String ... args) { + + Map<String, Runnable> actions = new HashMap<>(); + actions.put("network", new NetworkAction()); + actions.put("send", new SentMoneyAction()); + + // Parsing args + JCommander jc = new JCommander(this); + actions.entrySet().stream().forEach(entry -> jc.addCommand(entry.getKey(), entry.getValue())); + try { + jc.parse(args); + } + catch(ParameterException e) { + System.err.println(e.getMessage()); + System.err.println("Try --help for usage"); + //jc.usage(); + System.exit(-1); + } + + // Usage, if help or no command + String actionName = jc.getParsedCommand(); + if (StringUtils.isBlank(actionName)) { + jc.usage(); + // Return error code, if not help + if (!help) System.exit(-1); + return; + } + + // Set log level + // TODO + + // Init configuration + initConfiguration(configFilename); + + // Init i18n + try { + initI18n(); + } catch(IOException e) { + System.out.println("Unable to initialize translations"); + System.exit(-1); + } + + // Set a default account id, then load cache + ServiceLocator.instance().getDataContext().setAccountId(0); + + // Initialize service locator + ServiceLocator.instance().init(); + + Runnable action = actions.get(actionName); + action.run(); + } + + + protected String getI18nBundleName() { + return "duniter4j-core-client-i18n"; + } + + /* -- -- */ + + /** + * Convenience methods that could be override to initialize other configuration + * + * @param configFilename + * @param configArgs + */ + protected void initConfiguration(String configFilename) { + String[] configArgs = getConfigArgs(); + Configuration config = new Configuration(configFilename, configArgs); + Configuration.setInstance(config); + } + + protected void initI18n() throws IOException { + Configuration config = Configuration.instance(); + + // --------------------------------------------------------------------// + // init i18n + // --------------------------------------------------------------------// + File i18nDirectory = new File(config.getDataDirectory(), "i18n"); + if (i18nDirectory.exists()) { + // clean i18n cache + FileUtils.cleanDirectory(i18nDirectory); + } + + FileUtils.forceMkdir(i18nDirectory); + + if (debug) { + System.out.println("I18N directory: " + i18nDirectory); + } + + Locale i18nLocale = config.getI18nLocale(); + + if (debug) { + System.out.println(String.format("Starts i18n with locale [%s] at [%s]", + i18nLocale, i18nDirectory)); + } + I18n.init(new UserI18nInitializer( + i18nDirectory, new DefaultI18nInitializer(getI18nBundleName())), + i18nLocale); + } + + protected String[] getConfigArgs() { + List<String> configArgs = Lists.newArrayList(); + /*configArgs.addAll(Lists.newArrayList( + "--option", ConfigurationOption.BASEDIR.getKey(), getResourceDirectory().getAbsolutePath()));*/ + return configArgs.toArray(new String[configArgs.size()]); + } + +} diff --git a/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/NetworkAction.java b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/NetworkAction.java new file mode 100644 index 00000000..ac10b861 --- /dev/null +++ b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/NetworkAction.java @@ -0,0 +1,80 @@ +package fr.duniter.cmd.actions; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import dnl.utils.text.table.TextTable; +import fr.duniter.cmd.actions.utils.Formatters; +import org.duniter.core.client.model.local.Peer; +import org.duniter.core.client.service.ServiceLocator; +import org.duniter.core.client.service.local.NetworkService; +import org.duniter.core.util.CollectionUtils; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Created by blavenie on 22/03/17. + */ +@Parameters(commandDescription = "Display network peers") +public class NetworkAction implements Runnable { + + @Parameter(names = "-host", description = "Duniter host") + private String host = "g1.duniter.org"; + + @Parameter(names = "-port", description = "Duniter port") + private int port = 10901; + + @Override + public void run() { + NetworkService service = ServiceLocator.instance().getNetworkService(); + Peer mainPeer = Peer.newBuilder().setHost(host).setPort(port).build(); + + List<Peer> peers = service.getPeers(mainPeer); + + if (CollectionUtils.isEmpty(peers)) { + System.out.println("No peers found"); + } + else { + + String[] columnNames = { + "Uid", + "Pubkey", + "Address", + "Status", + "API", + "Version", + "Difficulty", + "Block #"}; + + List<Object[]> data = peers.stream().map(peer -> { + boolean isUp = peer.getStats().getStatus() == Peer.PeerStatus.UP; + return new Object[] { + Formatters.formatUid(peer.getStats().getUid()), + Formatters.formatPubkey(peer.getPubkey()), + peer.getHost() + ":" + peer.getPort(), + peer.getStats().getStatus().name(), + isUp && peer.isUseSsl() ? "SSL" : null, + isUp ? peer.getStats().getVersion() : null, + isUp ? peer.getStats().getHardshipLevel() : "Mirror", + isUp ? peer.getStats().getBlockNumber() : null + }; + }) + .collect(Collectors.toList()); + + Object[][] rows = new Object[data.size()][]; + int i = 0; + for (Object[] row : data) { + rows[i++] = row; + } + + + TextTable tt = new TextTable(columnNames, rows); + // this adds the numbering on the left + tt.setAddRowNumbering(true); + tt.printTable(); + } + + } + + +} diff --git a/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/SentMoneyAction.java b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/SentMoneyAction.java new file mode 100644 index 00000000..9b64dcf8 --- /dev/null +++ b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/SentMoneyAction.java @@ -0,0 +1,76 @@ +package fr.duniter.cmd.actions; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParametersDelegate; +import fr.duniter.cmd.actions.params.WalletParameters; +import fr.duniter.cmd.actions.utils.Formatters; +import org.duniter.core.client.config.Configuration; +import org.duniter.core.client.model.bma.BlockchainParameters; +import org.duniter.core.client.model.local.Currency; +import org.duniter.core.client.model.local.Peer; +import org.duniter.core.client.model.local.Wallet; +import org.duniter.core.client.service.ServiceLocator; +import org.duniter.core.client.service.bma.BlockchainRemoteService; +import org.duniter.core.client.service.bma.TransactionRemoteService; +import org.duniter.core.service.CryptoService; +import org.duniter.core.util.crypto.CryptoUtils; +import org.duniter.core.util.crypto.KeyPair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Created by blavenie on 22/03/17. + */ +public class SentMoneyAction implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(SentMoneyAction.class); + + @ParametersDelegate + private WalletParameters walletParams = new WalletParameters(); + + @Parameter(names = "--amount", description = "Amount", required = true) + public int amount; + + @Parameter(names = "--dest", description = "Destination pubkey", required = true) + public String destPubkey; + + @Parameter(names = "--comment", description = "TX Comment") + public String comment; + + @Override + public void run() { + + CryptoService cryptoService = ServiceLocator.instance().getCryptoService(); + TransactionRemoteService txService = ServiceLocator.instance().getTransactionRemoteService(); + Configuration config = Configuration.instance(); + + Peer peer = Peer.newBuilder().setHost(config.getNodeHost()) + .setPort(config.getNodePort()) + .build(); + + Currency currency = ServiceLocator.instance().getBlockchainRemoteService().getCurrencyFromPeer(peer); + ServiceLocator.instance().getCurrencyService().save(currency); + peer.setCurrencyId(currency.getId()); + peer.setCurrency(currency.getCurrencyName()); + ServiceLocator.instance().getPeerService().save(peer); + + // Compute keypair and wallet + KeyPair keypair = cryptoService.getKeyPair(walletParams.salt, walletParams.password); + Wallet wallet = new Wallet( + currency.getCurrencyName(), + null, + keypair.getPubKey(), + keypair.getSecKey()); + wallet.setCurrencyId(currency.getId()); + + System.out.println("Connected to wallet: " + wallet.getPubKeyHash()); + + txService.transfer(wallet, destPubkey, amount, comment); + + + System.out.println(String.format("Successfully sent [%d %s] to [%s]", + amount, + Formatters.currencySymbol(currency.getCurrencyName()), + Formatters.formatPubkey(destPubkey))); + } +} diff --git a/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/params/WalletParameters.java b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/params/WalletParameters.java new file mode 100644 index 00000000..2694cafc --- /dev/null +++ b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/params/WalletParameters.java @@ -0,0 +1,14 @@ +package fr.duniter.cmd.actions.params; + +import com.beust.jcommander.Parameter; + +/** + * Created by blavenie on 22/03/17. + */ +public class WalletParameters { + @Parameter(names = "--salt", description = "Salt (to generate the keypair)", required = true) + public String salt; + + @Parameter(names = "--passwd", description = "Password (to generate the keypair)", required = true) + public String password; +} diff --git a/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/utils/Formatters.java b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/utils/Formatters.java new file mode 100644 index 00000000..0f8cdc9b --- /dev/null +++ b/duniter4j-cmd/src/main/java/fr/duniter/cmd/actions/utils/Formatters.java @@ -0,0 +1,34 @@ +package fr.duniter.cmd.actions.utils; + +/** + * Created by blavenie on 24/03/17. + */ +public class Formatters { + + public static String formatPubkey(String pubkey) { + if (pubkey != null && pubkey.length() > 8) { + return pubkey.substring(0, 8); + } + return pubkey; + } + + public static String formatUid(String uid) { + if (uid != null && uid.length() > 20) { + return uid.substring(0, 19); + } + return uid; + } + + public static String currencySymbol(String currencyName) { + String[] parts = currencyName.split("-_"); + if (parts.length < 2) { + if (currencyName.length() <= 3) { + return currencyName.toUpperCase(); + } + else { + return currencyName.toUpperCase().substring(0,1); + } + } + return currencySymbol(parts[0]) + currencySymbol(parts[1]); + } +} diff --git a/duniter4j-cmd/src/main/resources/META-INF/services/org.duniter.core.beans.Bean b/duniter4j-cmd/src/main/resources/META-INF/services/org.duniter.core.beans.Bean new file mode 100644 index 00000000..bbf9fb6b --- /dev/null +++ b/duniter4j-cmd/src/main/resources/META-INF/services/org.duniter.core.beans.Bean @@ -0,0 +1,13 @@ +org.duniter.core.client.service.bma.BlockchainRemoteServiceImpl +org.duniter.core.client.service.bma.NetworkRemoteServiceImpl +org.duniter.core.client.service.bma.WotRemoteServiceImpl +org.duniter.core.client.service.bma.TransactionRemoteServiceImpl +org.duniter.core.client.service.elasticsearch.CurrencyRegistryRemoteServiceImpl +org.duniter.core.service.Ed25519CryptoServiceImpl +org.duniter.core.client.service.HttpServiceImpl +org.duniter.core.client.service.DataContext +org.duniter.core.client.service.local.PeerServiceImpl +org.duniter.core.client.service.local.CurrencyServiceImpl +org.duniter.core.client.service.local.NetworkServiceImpl +org.duniter.core.client.dao.mem.MemoryCurrencyDaoImpl +org.duniter.core.client.dao.mem.MemoryPeerDaoImpl \ No newline at end of file diff --git a/duniter4j-cmd/src/main/resources/duniter4j-cmd.config b/duniter4j-cmd/src/main/resources/duniter4j-cmd.config new file mode 100644 index 00000000..d9f4f75b --- /dev/null +++ b/duniter4j-cmd/src/main/resources/duniter4j-cmd.config @@ -0,0 +1,5 @@ +duniter4j.node.host=192.168.0.5 +duniter4j.node.port=10901 + +duniter4j.node.elasticsearch.host=localhost +duniter4j.node.elasticsearch.port=9200 diff --git a/duniter4j-cmd/src/main/resources/log4j.properties b/duniter4j-cmd/src/main/resources/log4j.properties new file mode 100644 index 00000000..f31940a1 --- /dev/null +++ b/duniter4j-cmd/src/main/resources/log4j.properties @@ -0,0 +1,27 @@ +### +# Global logging configuration +log4j.rootLogger=ERROR, stdout, file + +# Console output +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{ISO8601} %5p - %m%n + +# duniter4j levels +log4j.logger.org.duniter=INFO +log4j.logger.org.duniter.cmd=INFO +#log4j.logger.org.duniter.core.client.service=DEBUG +log4j.logger.org.duniter.core.client.service.local=DEBUG +#log4j.logger.org.duniter.core.client.service.bma=DEBUG +log4j.logger.org.duniter.core.beans=WARN +#log4j.logger.org.duniter.core.client.service=TRACE + +log4j.appender.file=org.apache.log4j.RollingFileAppender +log4j.appender.file.file=ucoin-client.log +log4j.appender.file.MaxFileSize=10MB +log4j.appender.file.MaxBackupIndex=4 + +log4j.appender.file.layout=org.apache.log4j.PatternLayout +log4j.appender.file.layout.ConversionPattern=%d{ISO8601} %5p %c - %m%n + + diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/config/Configuration.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/config/Configuration.java index e6d06adb..dfc6b619 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/config/Configuration.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/config/Configuration.java @@ -84,14 +84,14 @@ public class Configuration { this.applicationConfig.setEncoding(Charsets.UTF_8.name()); this.applicationConfig.setConfigFileName(file); - // get all config providers + // get allOfToList config providers Set<ApplicationConfigProvider> providers = ApplicationConfigHelper.getProviders(null, null, null, true); - // load all default options + // load allOfToList default options ApplicationConfigHelper.loadAllDefaultOption(applicationConfig, providers); @@ -106,7 +106,7 @@ public class Configuration { // Override application version initVersion(applicationConfig); - // get all transient and final option keys + // get allOfToList transient and final option keys Set<String> optionToSkip = ApplicationConfigHelper.getTransientOptionKeys(providers); @@ -233,11 +233,7 @@ public class Configuration { public String getNodeCurrency() { return applicationConfig.getOption(ConfigurationOption.NODE_CURRENCY.getKey()); } - - public String getNodeProtocol() { - return applicationConfig.getOption(ConfigurationOption.NODE_PROTOCOL.getKey()); - } - + public String getNodeHost() { return applicationConfig.getOption(ConfigurationOption.NODE_HOST.getKey()); } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/config/ConfigurationOption.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/config/ConfigurationOption.java index 41d80093..606253e0 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/config/ConfigurationOption.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/config/ConfigurationOption.java @@ -143,14 +143,14 @@ public enum ConfigurationOption implements ConfigOptionDef { NODE_HOST( "duniter4j.node.host", n("duniter4j.config.option.node.host.description"), - "cgeek.fr", + "g1.duniter.org", String.class, false), NODE_PORT( "duniter4j.node.port", n("duniter4j.config.option.node.port.description"), - "9330", + "10901", Integer.class, false), @@ -164,7 +164,7 @@ public enum ConfigurationOption implements ConfigOptionDef { NETWORK_TIMEOUT( "duniter4j.network.timeout", n("duniter4j.config.option.network.timeout.description"), - "100000", // = 10 s + "20000", // = 2 s Integer.class, false), diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Constants.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Constants.java index 1df3da70..959794ae 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Constants.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Constants.java @@ -32,4 +32,13 @@ public interface Constants { String CURRENCY_NAME = "[A-Za-z0-9_-]"; String PUBKEY = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}"; } + + interface HttpStatus { + int SC_TOO_MANY_REQUESTS = 429; + } + + interface Config { + int TOO_MANY_REQUEST_RETRY_TIME = 500; // 500 ms + int MAX_SAME_REQUEST_COUNT = 5; // 5 requests before to get 429 error + } } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/EndpointProtocol.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/EndpointApi.java similarity index 92% rename from duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/EndpointProtocol.java rename to duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/EndpointApi.java index 1f4b2571..3c9965ff 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/EndpointProtocol.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/EndpointApi.java @@ -23,7 +23,10 @@ package org.duniter.core.client.model.bma; */ -public enum EndpointProtocol { +public enum EndpointApi { BASIC_MERKLED_API, + BMAS, + ES_CORE_API, + ES_USER_API, UNDEFINED } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeering.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeering.java index 1d24f9c5..560a70e0 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeering.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeering.java @@ -35,6 +35,7 @@ public class NetworkPeering implements Serializable { private String block; private String signature; + private String raw; private String pubkey; @@ -112,26 +113,26 @@ public class NetworkPeering implements Serializable { } public static class Endpoint implements Serializable { - public EndpointProtocol protocol; - public String url; + public EndpointApi api; + public String dns; public String ipv4; public String ipv6; public Integer port; - public EndpointProtocol getProtocol() { - return protocol; + public EndpointApi getApi() { + return api; } - public void setProtocol(EndpointProtocol protocol) { - this.protocol = protocol; + public void setApi(EndpointApi api) { + this.api = api; } - public String getUrl() { - return url; + public String getDns() { + return dns; } - public void setUrl(String url) { - this.url = url; + public void setDns(String dns) { + this.dns = dns; } public String getIpv4() { @@ -160,8 +161,8 @@ public class NetworkPeering implements Serializable { @Override public String toString() { - String s = "protocol=" + protocol.name() + "\n" + - "url=" + url + "\n" + + String s = "api=" + api.name() + "\n" + + "dns=" + dns + "\n" + "ipv4=" + ipv4 + "\n" + "ipv6=" + ipv6 + "\n" + "port=" + port + "\n"; diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeers.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeers.java index 90cbf2cf..c413492d 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeers.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkPeers.java @@ -148,7 +148,9 @@ public class NetworkPeers implements Serializable { "status=" + status + "\n" + "block=" + block + "\n"; for(NetworkPeering.Endpoint endpoint: endpoints) { - s += endpoint.toString() + "\n"; + if (endpoint != null) { + s += endpoint.toString() + "\n"; + } } return s; } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Protocol.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Protocol.java index df822dd0..04c77d8f 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Protocol.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Protocol.java @@ -27,9 +27,9 @@ package org.duniter.core.client.model.bma; */ public interface Protocol { - String VERSION = "2"; + String VERSION = "10"; - String TX_VERSION = "3"; + String TX_VERSION = "10"; String TYPE_IDENTITY = "Identity"; diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/TxSource.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/TxSource.java index 36e4901a..4c0a05e0 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/TxSource.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/TxSource.java @@ -89,7 +89,7 @@ public class TxSource { } /** - * Source type : <ul> + * Source sortType : <ul> * <li><code>D</code> : Universal Dividend</li> * <li><code>T</code> : Transaction</li> * </ul> diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/EndpointAdapter.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/EndpointAdapter.java index 2c8d35b9..35896bd9 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/EndpointAdapter.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/EndpointAdapter.java @@ -25,9 +25,9 @@ package org.duniter.core.client.model.bma.gson; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; -import org.duniter.core.client.model.bma.EndpointProtocol; +import org.duniter.core.client.model.bma.EndpointApi; import org.duniter.core.client.model.bma.NetworkPeering; -import org.apache.http.conn.util.InetAddressUtils; +import org.duniter.core.util.http.InetAddressUtils; import java.io.IOException; import java.util.ArrayList; @@ -51,19 +51,19 @@ public class EndpointAdapter extends TypeAdapter<NetworkPeering.Endpoint> { endpoint.ipv4 = word; } else if (InetAddressUtils.isIPv6Address(word)) { endpoint.ipv6 = word; - } else if (word.startsWith("http")) { - endpoint.url = word; + } else if (word.trim().length() > 0) { + endpoint.dns = word; } else { try { - endpoint.protocol = EndpointProtocol.valueOf(word); + endpoint.api = EndpointApi.valueOf(word); } catch (IllegalArgumentException e) { // skip this part } } } - if (endpoint.protocol == null) { - endpoint.protocol = EndpointProtocol.UNDEFINED; + if (endpoint.api == null) { + endpoint.api = EndpointApi.UNDEFINED; } return endpoint; @@ -74,8 +74,8 @@ public class EndpointAdapter extends TypeAdapter<NetworkPeering.Endpoint> { writer.nullValue(); return; } - writer.value(endpoint.protocol.name() + " " + - endpoint.url + " " + + writer.value(endpoint.api.name() + " " + + endpoint.dns + " " + endpoint.ipv4 + " " + endpoint.ipv6 + " " + endpoint.port); diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/MultimapTypeAdapter.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/MultimapTypeAdapter.java index d8b5a9b2..fbd94e04 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/MultimapTypeAdapter.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/gson/MultimapTypeAdapter.java @@ -58,7 +58,7 @@ public class MultimapTypeAdapter implements JsonSerializer<Multimap>, JsonDeseri Preconditions.checkArgument(multimapType instanceof ParameterizedType); final ParameterizedType paramType = (ParameterizedType)multimapType; final Type[] typeArguments = paramType.getActualTypeArguments(); - Preconditions.checkArgument(2 == typeArguments.length, "Type must contain exactly 2 type arguments."); + Preconditions.checkArgument(2 == typeArguments.length, "Type must contain exactly 2 sortType arguments."); final ParameterizedTypeImpl valueType = new ParameterizedTypeImpl(Collection.class, null, typeArguments[1]); final ParameterizedTypeImpl mapType = new ParameterizedTypeImpl(Map.class, null, typeArguments[0], valueType); diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/EndpointDeserializer.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/EndpointDeserializer.java index 2681d5df..7a877fe6 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/EndpointDeserializer.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/EndpointDeserializer.java @@ -3,45 +3,124 @@ package org.duniter.core.client.model.bma.jackson; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; -import org.apache.http.conn.util.InetAddressUtils; -import org.duniter.core.client.model.bma.EndpointProtocol; +import org.duniter.core.client.model.bma.EndpointApi; import org.duniter.core.client.model.bma.NetworkPeering; +import org.duniter.core.util.StringUtils; +import org.duniter.core.util.http.InetAddressUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Created by blavenie on 07/12/16. */ public class EndpointDeserializer extends JsonDeserializer<NetworkPeering.Endpoint> { + + private static final Logger log = LoggerFactory.getLogger(EndpointDeserializer.class); + + public static final String EP_END_REGEXP = "(?:[ ]+([a-z0-9-_]+[.][a-z0-9-_.]*))?(?:[ ]+([0-9.]+))?(?:[ ]+([0-9a-f:]+))?(?:[ ]+([0-9]+))$"; + public static final String BMA_API_REGEXP = "^BASIC_MERKLED_API" + EP_END_REGEXP; + public static final String BMAS_API_REGEXP = "^BMAS" + EP_END_REGEXP; + public static final String OTHER_API_REGEXP = "^([A-Z_-]+)" + EP_END_REGEXP; + + private Pattern bmaPattern; + private Pattern bmasPattern; + private Pattern otherApiPattern; + + public EndpointDeserializer() { + bmaPattern = Pattern.compile(BMA_API_REGEXP); + bmasPattern = Pattern.compile(BMAS_API_REGEXP); + otherApiPattern = Pattern.compile(OTHER_API_REGEXP); + } + @Override public NetworkPeering.Endpoint deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { String ept = jp.getText(); - ArrayList<String> parts = new ArrayList<>(Arrays.asList(ept.split(" "))); + NetworkPeering.Endpoint endpoint = new NetworkPeering.Endpoint(); - endpoint.port = Integer.parseInt(parts.remove(parts.size() - 1)); - for (String word : parts) { - if (InetAddressUtils.isIPv4Address(word)) { - endpoint.ipv4 = word; - } else if (InetAddressUtils.isIPv6Address(word)) { - endpoint.ipv6 = word; - } else if (word.startsWith("http")) { - endpoint.url = word; - } else { - try { - endpoint.protocol = EndpointProtocol.valueOf(word); - } catch (IllegalArgumentException e) { - // skip this part + + // BMA API + Matcher mather = bmaPattern.matcher(ept); + if (mather.matches()) { + endpoint.api = EndpointApi.BASIC_MERKLED_API; + + for(int i=1; i<=mather.groupCount(); i++) { + String word = mather.group(i); + + if (StringUtils.isNotBlank(word)) { + if (InetAddressUtils.isIPv4Address(word)) { + endpoint.ipv4 = word; + } else if (InetAddressUtils.isIPv6Address(word)) { + endpoint.ipv6 = word; + } else if (i == mather.groupCount() && word.matches("\\d+")){ + endpoint.port = Integer.parseInt(word); + } else { + endpoint.dns = word; + } + } + } + + return endpoint; + } + + // BMAS API + mather = bmasPattern.matcher(ept); + if (mather.matches()) { + endpoint.api = EndpointApi.BMAS; + + for(int i=1; i<=mather.groupCount(); i++) { + String word = mather.group(i); + + if (StringUtils.isNotBlank(word)) { + if (InetAddressUtils.isIPv4Address(word)) { + endpoint.ipv4 = word; + } else if (InetAddressUtils.isIPv6Address(word)) { + endpoint.ipv6 = word; + } else if (i == mather.groupCount() && word.matches("\\d+")){ + endpoint.port = Integer.parseInt(word); + } else { + endpoint.dns = word; + } } } + + return endpoint; } - if (endpoint.protocol == null) { - endpoint.protocol = EndpointProtocol.UNDEFINED; + // Other API + mather = otherApiPattern.matcher(ept); + if (mather.matches()) { + try { + endpoint.api = EndpointApi.valueOf(mather.group(1)); + } catch(Exception e) { + log.warn("Unable to deserialize endpoint: unknown api [" + mather.group(1) + "]"); + // not known API: skip + return null; + } + + for(int i=2; i<=mather.groupCount(); i++) { + String word = mather.group(i); + + if (StringUtils.isNotBlank(word)) { + if (InetAddressUtils.isIPv4Address(word)) { + endpoint.ipv4 = word; + } else if (InetAddressUtils.isIPv6Address(word)) { + endpoint.ipv6 = word; + } else if (i == mather.groupCount() && word.matches("\\d+")){ + endpoint.port = Integer.parseInt(word); + } else { + endpoint.dns = word; + } + } + } + + return endpoint; } - return endpoint; + return null; } } \ No newline at end of file diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/DeleteRecord.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/DeleteRecord.java index c09fe0d1..d87291d7 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/DeleteRecord.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/DeleteRecord.java @@ -28,7 +28,7 @@ package org.duniter.core.client.model.elasticsearch; public class DeleteRecord extends Record { public static final String PROPERTY_INDEX="index"; - public static final String PROPERTY_TYPE="type"; + public static final String PROPERTY_TYPE="sortType"; public static final String PROPERTY_ID="id"; private String index; diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/local/Peer.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/local/Peer.java index 5b292b87..02eb05cc 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/local/Peer.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/local/Peer.java @@ -23,107 +23,460 @@ package org.duniter.core.client.model.local; */ +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.duniter.core.client.model.bma.EndpointApi; +import org.duniter.core.client.model.bma.NetworkPeering; +import org.duniter.core.util.Preconditions; +import org.duniter.core.util.StringUtils; +import org.duniter.core.util.http.InetAddressUtils; + import java.io.Serializable; +import java.util.StringJoiner; public class Peer implements LocalEntity, Serializable { + + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + + private String api; + private String dns; + private String ipv4; + private String ipv6; + private Integer port; + private Boolean useSsl; + private String pubkey; + private String hash; + private String currency; + + public Builder() { + + } + + public Builder setApi(String api) { + this.api = api; + return this; + } + + public Builder setDns(String dns) { + this.dns = dns; + return this; + } + + public Builder setIpv4(String ipv4) { + this.ipv4 = ipv4; + return this; + } + + public Builder setIpv6(String ipv6) { + this.ipv6 = ipv6; + return this; + } + + public Builder setPort(int port) { + this.port = port; + return this; + } + + public Builder setUseSsl(boolean useSsl) { + this.useSsl = useSsl; + return this; + } + + public Builder setCurrency(String currency) { + this.currency = currency; + return this; + } + + public Builder setPubkey(String pubkey) { + this.pubkey = pubkey; + return this; + } + + public Builder setHash(String hash) { + this.hash = hash; + return this; + } + + public Builder setHost(String host) { + Preconditions.checkNotNull(host); + if (InetAddressUtils.isIPv4Address(host)) { + this.ipv4 = host; + } + else if (InetAddressUtils.isIPv6Address(host)) { + this.ipv6 = host; + } + else { + this.dns = host; + } + return this; + } + + public Builder setEndpoint(NetworkPeering.Endpoint source) { + Preconditions.checkNotNull(source); + if (source.api != null) { + setApi(source.api.name()); + } + if (StringUtils.isNotBlank(source.dns)) { + setDns(source.dns); + } + if (StringUtils.isNotBlank(source.ipv4)) { + setIpv4(source.ipv4); + } + if (StringUtils.isNotBlank(source.ipv6)) { + setIpv6(source.ipv6); + } + if (StringUtils.isNotBlank(source.ipv6)) { + setHost(source.ipv6); + } + if (source.port != null) { + setPort(source.port); + } + return this; + } + + public Peer build() { + int port = this.port != null ? this.port : 80; + boolean useSsl = this.useSsl != null ? this.useSsl : + (port == 443 || this.api == EndpointApi.BMAS.name()); + String api = this.api != null ? this.api : EndpointApi.BASIC_MERKLED_API.name(); + Peer ep = new Peer(api, dns, ipv4, ipv6, port, useSsl); + if (StringUtils.isNotBlank(this.currency)) { + ep.setCurrency(this.currency); + } + if (StringUtils.isNotBlank(this.pubkey)) { + ep.setPubkey(this.pubkey); + } + if (StringUtils.isNotBlank(this.hash)) { + ep.setHash(this.hash); + } + return ep; + } + + } + + + // Local entity attribute (only used for local DB) private Long id; private Long currencyId; + + private String api; + private String dns; + private String ipv4; + private String ipv6; + + private String url; private String host; + private String pubkey; + + private String hash; + private String currency; + + private Stats stats = new Stats(); + private int port; private boolean useSsl; - private String url; // computed public Peer() { // default constructor, need for de-serialization } - public Peer(String host, int port) { - this.host = host; - this.port = port; - this.useSsl = (port == 443); - this.url = computeUrl(this.host, this.port, this.useSsl); + /** + * @deprecated Use Builder instead + * @param host Can be a ipv4, ipv6 or a dns + * @param port any port, or null (default: 80) + */ + @Deprecated + public Peer(String host, Integer port) { + this.api = EndpointApi.BASIC_MERKLED_API.name(); + if (InetAddressUtils.isIPv4Address(host)) { + this.ipv4 = host; + } + if (InetAddressUtils.isIPv6Address(host)) { + this.ipv6 = host; + } + else { + this.dns = host; + } + this.port = port != null ? port : 80; + this.useSsl = (port == 443 || this.api == EndpointApi.BMAS.name()); + init(); } - public Peer(String host, int port, boolean useSsl) { - this.host = host; + public Peer(String api, String dns, String ipv4, String ipv6, int port, boolean useSsl) { + this.api = api; + this.dns = StringUtils.isNotBlank(dns) ? dns : null; + this.ipv4 = StringUtils.isNotBlank(ipv4) ? ipv4 : null; + this.ipv6 = StringUtils.isNotBlank(ipv6) ? ipv6 : null; this.port = port; this.useSsl = useSsl; - this.url = computeUrl(this.host, this.port, this.useSsl); + init(); } - public String getHost() { - return host; - } - - public int getPort() { - return port; - } - - public String getUrl() { - return url; + protected void init() { + // If SSL: prefer DNS name (should be defined in SSL certificate) + // else (if define) use ipv4 (if NOT local IP) + // else (if define) use dns + // else (if define) use ipv6 + this.host = ((port == 443 || useSsl) && dns != null) ? dns : + (ipv4 != null && InetAddressUtils.isNotLocalIPv4Address(ipv4) ? ipv4 : + (dns != null ? dns : + (ipv6 != null ? "[" + ipv6 + "]" : ""))); + String protocol = (port == 443 || useSsl) ? "https" : "http"; + this.url = protocol + "://" + this.host + (port != 80 ? (":" + port) : ""); } + @JsonIgnore public Long getId() { return id; } + @JsonIgnore public void setId(Long id) { this.id = id; } + @JsonIgnore public Long getCurrencyId() { return currencyId; } + @JsonIgnore public void setCurrencyId(Long currencyId) { this.currencyId = currencyId; } - public void setPort(int port) { - this.port = port; - if (port == 443) { - this.useSsl = true; - } - this.url = computeUrl(this.host, this.port, this.useSsl); + public String getApi() { + return api; } - public void setHost(String host) { - this.host = host; - this.url = computeUrl(this.host, this.port, this.useSsl); + public String getDns() { + return dns; + } + + public void setDns(String dns) { + this.dns = dns; + init(); + } + + public String getIpv4() { + return ipv4; + } + + public void setIpv4(String ipv4) { + this.ipv4 = ipv4; + init(); + } + + public String getIpv6() { + return ipv6; + } + + public void setIpv6(String ipv6) { + this.ipv6 = ipv6; + init(); + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + init(); } public boolean isUseSsl() { - return this.useSsl; + return useSsl; } public void setUseSsl(boolean useSsl) { this.useSsl = useSsl; - this.url = computeUrl(this.host, this.port, this.useSsl); + init(); } - public String toString() { - return new StringBuilder().append(host) - .append(":") - .append(port) - .append(useSsl ? "[+SSL]" : "") - .toString(); + public String getPubkey() { + return pubkey; + } + + public void setPubkey(String pubkey) { + this.pubkey = pubkey; + } + + @JsonIgnore + public String getHost() { + return this.host; // computed in init() + } + + @JsonIgnore + public String getUrl() { + return this.url; // computed in init() + } + + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + + @JsonIgnore + public Stats getStats() { + return stats; } - @Override - public boolean equals(Object o) { - if (o == null) { - return false; + @JsonIgnore + public void setStats(Stats stats) { + this.stats = stats; + } + + public String toString() { + StringJoiner joiner = new StringJoiner(" "); + if (api != null) { + joiner.add(api); + } + if (dns != null) { + joiner.add(dns); + } + if (ipv4 != null) { + joiner.add(ipv4); } - if (id != null && o instanceof Peer) { - return id.equals(((Peer)o).getId()); + if (ipv6 != null) { + joiner.add(ipv6); } - return super.equals(o); + if (port != 80) { + joiner.add(String.valueOf(port)); + } + return joiner.toString(); + } + + public enum PeerStatus { + UP, + DOWN, + ERROR } - /* -- Internal methods -- */ + public static class Stats { + private String version; + private PeerStatus status = PeerStatus.UP; // default + private Integer blockNumber; + private String blockHash; + private String error; + private Long medianTime; + private Integer hardshipLevel; + private boolean isMainConsensus = false; + private boolean isForkConsensus = false; + private Double consensusPct = 0d; + private String uid; + + public Stats() { + + } + + public PeerStatus getStatus() { + return status; + } + + @JsonIgnore + public boolean isReacheable() { + return status != null && status == PeerStatus.UP; + } + + public void setStatus(PeerStatus status) { + this.status = status; + } + + public String getError() { + return error; + } - protected String computeUrl(String host, int port, boolean useSsl) { - return String.format("%s://%s:%s", (useSsl ? "https" : "http"), host, port); + public void setError(String error) { + this.error = error; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Integer getBlockNumber() { + return blockNumber; + } + + public void setBlockNumber(Integer blockNumber) { + this.blockNumber = blockNumber; + } + + public String getBlockHash() { + return blockHash; + } + + public void setBlockHash(String blockHash) { + this.blockHash = blockHash; + } + + public Long getMedianTime() { + return medianTime; + } + + public void setMedianTime(Long medianTime) { + this.medianTime = medianTime; + } + + public boolean isMainConsensus() { + return isMainConsensus; + } + + public void setMainConsensus(boolean mainConsensus) { + this.isMainConsensus = mainConsensus; + } + + public boolean isForkConsensus() { + return isForkConsensus; + } + + public void setForkConsensus(boolean forkConsensus) { + this.isForkConsensus = forkConsensus; + } + + public Double getConsensusPct() { + return consensusPct; + } + + public void setConsensusPct(Double consensusPct) { + this.consensusPct = consensusPct; + } + + public Integer getHardshipLevel() { + return hardshipLevel; + } + + public void setHardshipLevel(Integer hardshipLevel) { + this.hardshipLevel = hardshipLevel; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } } } 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 ac46d141..eaa8ce61 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 @@ -24,7 +24,6 @@ package org.duniter.core.client.service; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Joiner; -import org.apache.commons.io.IOUtils; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; @@ -38,13 +37,13 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.duniter.core.beans.InitializingBean; import org.duniter.core.client.config.Configuration; +import org.duniter.core.client.model.bma.Constants; import org.duniter.core.client.model.bma.Error; import org.duniter.core.client.model.bma.jackson.JacksonUtils; import org.duniter.core.client.model.local.Peer; 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.Cache; import org.duniter.core.util.cache.SimpleCache; import org.nuiton.i18n.I18n; import org.slf4j.Logger; @@ -93,6 +92,7 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean protected void initCaches() { Configuration config = Configuration.instance(); int cacheTimeInMillis = config.getNetworkCacheTimeInMillis(); + int defaultTimeout = config.getNetworkTimeout(); requestConfigCache = new SimpleCache<Integer, RequestConfig>(cacheTimeInMillis) { @Override @@ -162,7 +162,7 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean } public <T> T executeRequest(Peer peer, String absolutePath, Class<? extends T> resultClass) { - HttpGet httpGet = new HttpGet(getPath(peer, absolutePath)); + HttpGet httpGet = new HttpGet(peer.getUrl() + absolutePath); return executeRequest(httpClientCache.get(0), httpGet, resultClass); } @@ -228,12 +228,17 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean @SuppressWarnings("unchecked") protected <T> T executeRequest(HttpClient httpClient, HttpUriRequest request, Class<? extends T> resultClass, Class<?> errorClass) { + return executeRequest(httpClient, request, resultClass, errorClass, 5); + } + + protected <T> T executeRequest(HttpClient httpClient, HttpUriRequest request, Class<? extends T> resultClass, Class<?> errorClass, int retryCount) { T result = null; if (debug) { log.debug("Executing request : " + request.getRequestLine()); } + boolean retry = false; HttpResponse response = null; try { response = httpClient.execute(request); @@ -271,6 +276,11 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean catch(IOException e) { throw new HttpBadRequestException(I18n.t("duniter4j.client.status", response.getStatusLine().toString())); } + + case HttpStatus.SC_SERVICE_UNAVAILABLE: + case Constants.HttpStatus.SC_TOO_MANY_REQUESTS: + retry = true; + break; default: throw new TechnicalException(I18n.t("duniter4j.client.status", request.toString(), response.getStatusLine().toString())); } @@ -296,6 +306,23 @@ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean } } + // HTTP requests limit exceed, retry when possible + if (retry) { + if (retryCount > 0) { + log.debug(String.format("Service unavailable: waiting [%s ms] before retrying...", Constants.Config.TOO_MANY_REQUEST_RETRY_TIME)); + try { + Thread.sleep(Constants.Config.TOO_MANY_REQUEST_RETRY_TIME); + } catch (InterruptedException e) { + throw new TechnicalException(I18n.t("duniter4j.client.status", request.toString(), response.getStatusLine().toString())); + } + // iterate + return executeRequest(httpClient, request, resultClass, errorClass, retryCount - 1); + } + else { + throw new TechnicalException(I18n.t("duniter4j.client.status", request.toString(), response.getStatusLine().toString())); + } + } + return result; } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/ServiceLocator.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/ServiceLocator.java index e0f14a35..38107551 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/ServiceLocator.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/ServiceLocator.java @@ -25,9 +25,13 @@ package org.duniter.core.client.service; import org.duniter.core.beans.Bean; import org.duniter.core.beans.BeanFactory; -import org.duniter.core.client.service.bma.*; +import org.duniter.core.client.service.bma.BlockchainRemoteService; +import org.duniter.core.client.service.bma.NetworkRemoteService; +import org.duniter.core.client.service.bma.TransactionRemoteService; +import org.duniter.core.client.service.bma.WotRemoteService; import org.duniter.core.client.service.elasticsearch.CurrencyRegistryRemoteService; import org.duniter.core.client.service.local.CurrencyService; +import org.duniter.core.client.service.local.NetworkService; import org.duniter.core.client.service.local.PeerService; import org.duniter.core.service.CryptoService; import org.duniter.core.service.MailService; @@ -140,10 +144,14 @@ public class ServiceLocator implements Closeable { return getBean(CurrencyRegistryRemoteService.class); } - public MailService getMaiLService() { + public MailService getMailService() { return getBean(MailService.class); } + public NetworkService getNetworkService() { + return getBean(NetworkService.class); + } + public <S extends Bean> S getBean(Class<S> clazz) { if (beanFactory == null) { initBeanFactory(); 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 8fb4bf2b..8e92845e 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 @@ -76,7 +76,7 @@ public interface BlockchainRemoteService extends Service { /** * Retrieve the dividend of a block, by id (from 0 to current). - * Usefull method to avoid to deserialize all the block + * Usefull method to avoid to deserialize allOfToList the block * * @param currencyId * @param number @@ -222,8 +222,24 @@ public interface BlockchainRemoteService extends Service { */ Map<Integer, Long> getUDs(long currencyId, long startOffset); + /** + * Listening new block event + * @param currencyId + * @param listener + * @return + */ WebsocketClientEndpoint addBlockListener(long currencyId, WebsocketClientEndpoint.MessageListener listener); WebsocketClientEndpoint addBlockListener(Peer peer, WebsocketClientEndpoint.MessageListener listener); + /** + * 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 d431342e..25c8b7a1 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 @@ -22,6 +22,7 @@ package org.duniter.core.client.service.bma; * #L% */ +import com.fasterxml.jackson.databind.ObjectMapper; import org.duniter.core.util.Preconditions; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; @@ -76,6 +77,11 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement public static final String URL_MEMBERSHIP_SEARCH = URL_BASE + "/memberships/%s"; + public static final String URL_WS_BLOCK = "/ws/block"; + + public static final String URL_WS_PEER = "/ws/peer"; + + private ObjectMapper objectMapper; private NetworkRemoteService networkRemoteService; @@ -88,7 +94,7 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement // Cache on blockchain parameters private Cache<Long, BlockchainParameters> mParametersCache; - private Map<URI, WebsocketClientEndpoint> blockWsEndPoints = new HashMap<>(); + private Map<URI, WebsocketClientEndpoint> wsEndPoints = new HashMap<>(); public BlockchainRemoteServiceImpl() { super(); @@ -108,11 +114,11 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement public void close() throws IOException { super.close(); - if (blockWsEndPoints.size() != 0) { - for (WebsocketClientEndpoint clientEndPoint: blockWsEndPoints.values()) { + if (wsEndPoints.size() != 0) { + for (WebsocketClientEndpoint clientEndPoint: wsEndPoints.values()) { clientEndPoint.close(); } - blockWsEndPoints.clear(); + wsEndPoints.clear(); } } @@ -561,8 +567,7 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement @Override public WebsocketClientEndpoint addBlockListener(long currencyId, WebsocketClientEndpoint.MessageListener listener) { - Peer peer = peerService.getActivePeerByCurrencyId(currencyId); - return addBlockListener(peer, listener); + return addBlockListener(peerService.getActivePeerByCurrencyId(currencyId), listener); } @Override @@ -570,8 +575,27 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement Preconditions.checkNotNull(peer); Preconditions.checkNotNull(listener); - // Get the websocket endpoint - WebsocketClientEndpoint wsClientEndPoint = getWebsocketClientEndpoint(peer, "/ws/block"); + // Get (or create) the websocket endpoint + WebsocketClientEndpoint wsClientEndPoint = getWebsocketClientEndpoint(peer, URL_WS_BLOCK); + + // add listener + wsClientEndPoint.registerListener(listener); + + 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); @@ -807,17 +831,17 @@ public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implement path)); // Get the websocket, or open new one if not exists - WebsocketClientEndpoint wsClientEndPoint = blockWsEndPoints.get(wsBlockURI); + WebsocketClientEndpoint wsClientEndPoint = wsEndPoints.get(wsBlockURI); if (wsClientEndPoint == null || wsClientEndPoint.isClosed()) { - log.info(String.format("Starting to listen block from [%s]...", wsBlockURI.toString())); + log.info(String.format("Starting to listen on [%s]...", wsBlockURI.toString())); wsClientEndPoint = new WebsocketClientEndpoint(wsBlockURI); - blockWsEndPoints.put(wsBlockURI, wsClientEndPoint); + wsEndPoints.put(wsBlockURI, wsClientEndPoint); } return wsClientEndPoint; } catch (URISyntaxException | ServiceConfigurationError ex) { - throw new TechnicalException("could not create URI need for web socket on block: " + ex.getMessage()); + 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 9e5da699..262dd501 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 @@ -23,9 +23,11 @@ package org.duniter.core.client.service.bma; */ import org.duniter.core.beans.Service; -import org.duniter.core.client.model.bma.EndpointProtocol; +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.local.Peer; +import org.duniter.core.util.websocket.WebsocketClientEndpoint; import java.util.List; @@ -38,5 +40,11 @@ public interface NetworkRemoteService extends Service { List<Peer> getPeers(Peer peer); - List<Peer> findPeers(Peer peer, String status, EndpointProtocol endpointProtocol, Integer currentBlockNumber, String currentBlockHash); + List<String> getPeersLeaves(Peer peer); + + NetworkPeers.Peer getPeerLeaf(Peer peer, String leaf); + + List<Peer> findPeers(Peer peer, String status, EndpointApi endpointApi, Integer currentBlockNumber, String currentBlockHash); + + 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 48902b02..3815be7e 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 @@ -22,22 +22,31 @@ package org.duniter.core.client.service.bma; * #L% */ -import java.util.ArrayList; -import java.util.List; - -import org.duniter.core.client.model.bma.EndpointProtocol; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +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.jackson.JacksonUtils; import org.duniter.core.client.model.local.Peer; -import org.duniter.core.util.ObjectUtils; +import org.duniter.core.exception.TechnicalException; import org.duniter.core.util.Preconditions; import org.duniter.core.util.StringUtils; +import org.duniter.core.util.websocket.WebsocketClientEndpoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Created by eis on 05/02/15. */ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements NetworkRemoteService{ + private static final Logger log = LoggerFactory.getLogger(NetworkRemoteServiceImpl.class); public static final String URL_BASE = "/network"; @@ -47,10 +56,20 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N public static final String URL_PEERING_PEERS = URL_PEERING + "/peers"; - public static final String URL_PEERING_PEERS_LEAF = URL_PEERING + "/peers?leaf="; + public static final String URL_PEERING_PEERS_LEAVES = URL_PEERING_PEERS + "?leaves=true"; + + public static final String URL_PEERING_PEERS_LEAF = URL_PEERING_PEERS + "?leaf="; + + public static final String URL_WS_PEER = "/ws/peer"; + + protected ObjectMapper objectMapper; + + private Map<URI, WebsocketClientEndpoint> wsEndPoints = new HashMap<>(); public NetworkRemoteServiceImpl() { super(); + + objectMapper = JacksonUtils.newObjectMapper(); } public NetworkPeering getPeering(Peer peer) { @@ -58,13 +77,60 @@ 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); } @Override - public List<Peer> findPeers(Peer peer, String status, EndpointProtocol endpointProtocol, Integer currentBlockNumber, String currentBlockHash) { + public List<String> getPeersLeaves(Peer peer) { + Preconditions.checkNotNull(peer); + + List<String> result = new ArrayList<>(); + JsonNode jsonNode= httpService.executeRequest(peer, URL_PEERING_PEERS_LEAVES, JsonNode.class); + jsonNode.get("leaves").forEach(jsonNode1 -> { + result.add(jsonNode1.asText()); + }); + return result; + } + + @Override + public NetworkPeers.Peer getPeerLeaf(Peer peer, String leaf) { + Preconditions.checkNotNull(peer); + JsonNode jsonNode = httpService.executeRequest(peer, URL_PEERING_PEERS_LEAF + leaf, JsonNode.class); + NetworkPeers.Peer result = null; + + try { + + if (jsonNode.has("leaf")) { + jsonNode = jsonNode.get("leaf"); + if (jsonNode.has("value")) { + jsonNode = jsonNode.get("value"); + String json = objectMapper.writeValueAsString(jsonNode); + result = objectMapper.readValue(json, NetworkPeers.Peer.class); + } + } + } catch(IOException e) { + throw new TechnicalException(e); + } + + return result; + } + + @Override + public List<Peer> findPeers(Peer peer, String status, EndpointApi endpointApi, Integer currentBlockNumber, String currentBlockHash) { Preconditions.checkNotNull(peer); List<Peer> result = new ArrayList<Peer>(); @@ -80,13 +146,15 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N for (NetworkPeering.Endpoint endpoint : remotePeer.endpoints) { - match = endpointProtocol == null || endpointProtocol == endpoint.protocol; + match = endpointApi == null || endpointApi == endpoint.api; - if (match) { - Peer childPeer = toPeer(endpoint); - if (childPeer != null) { - result.add(childPeer); - } + if (match && endpoint != null) { + Peer childPeer = Peer.newBuilder() + .setCurrency(remotePeer.getCurrency()) + .setPubkey(remotePeer.getPubkey()) + .setEndpoint(endpoint) + .build(); + result.add(childPeer); } } @@ -96,25 +164,22 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N return result; } - /* -- Internal methods -- */ + @Override + public WebsocketClientEndpoint addPeerListener(Peer peer, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect) { + Preconditions.checkNotNull(peer); + Preconditions.checkNotNull(listener); - protected Peer toPeer(NetworkPeering.Endpoint source) { - Peer target = new Peer(); - if (StringUtils.isNotBlank(source.ipv4)) { - target.setHost(source.ipv4); - } else if (StringUtils.isNotBlank(source.ipv6)) { - target.setHost(source.ipv6); - } else if (StringUtils.isNotBlank(source.url)) { - target.setHost(source.url); - } else { - target = null; - } - if (target != null && source.port != null) { - target.setPort(source.port); - } - return target; + // Get (or create) the websocket endpoint + WebsocketClientEndpoint wsClientEndPoint = getWebsocketClientEndpoint(peer, URL_WS_PEER, autoReconnect); + + // add listener + wsClientEndPoint.registerListener(listener); + + return wsClientEndPoint; } + /* -- Internal methods -- */ + protected Integer parseBlockNumber(NetworkPeers.Peer remotePeer) { Preconditions.checkNotNull(remotePeer); @@ -148,4 +213,29 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N String hash = remotePeer.block.substring(index+1); 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/bma/TransactionRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/TransactionRemoteServiceImpl.java index b927f4e5..a6ad175c 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/TransactionRemoteServiceImpl.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/TransactionRemoteServiceImpl.java @@ -308,7 +308,9 @@ public class TransactionRemoteServiceImpl extends BaseRemoteServiceImpl implemen } // Comment - sb.append("Comment: ").append(comments).append('\n'); + sb.append("Comment: "); + if (comments != null) sb.append(comments); + sb.append('\n'); return sb.toString(); } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteService.java index 09fbe9c2..0b8aca4c 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteService.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteService.java @@ -32,6 +32,7 @@ import org.duniter.core.client.model.local.Wallet; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Set; public interface WotRemoteService extends Service { @@ -64,6 +65,10 @@ public interface WotRemoteService extends Service { String getSignedIdentity(String currency, byte[] pubKey, byte[] secKey, String uid, String blockUid); + Map<String, String> getMembersUids(long currencyId); + + Map<String, String> getMembersUids(Peer peer); + void sendIdentity(long currencyId, byte[] pubKey, byte[] secKey, String uid, String blockUid); void sendIdentity(Peer peer, String currency, byte[] pubKey, byte[] secKey, String uid, String blockUid); diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteServiceImpl.java index 3e42c346..863e6e07 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteServiceImpl.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/WotRemoteServiceImpl.java @@ -22,6 +22,7 @@ package org.duniter.core.client.service.bma; * #L% */ +import com.fasterxml.jackson.databind.JsonNode; import org.duniter.core.client.model.ModelUtils; import org.duniter.core.client.model.bma.*; import org.duniter.core.client.model.local.Certification; @@ -54,6 +55,8 @@ public class WotRemoteServiceImpl extends BaseRemoteServiceImpl implements WotRe public static final String URL_ADD = URL_BASE + "/add"; + public static final String URL_MEMBERS = URL_BASE + "/members"; + public static final String URL_LOOKUP = URL_BASE + "/lookup/%s"; public static final String URL_REQUIREMENT = URL_BASE+"/requirements/%s"; @@ -119,6 +122,35 @@ public class WotRemoteServiceImpl extends BaseRemoteServiceImpl implements WotRe } + public Map<String, String> getMembersUids(long currencyId) { + // get /wot/members + JsonNode json = executeRequest(currencyId, URL_MEMBERS, JsonNode.class); + + if (json == null || !json.has("results")) return null; + + Map<String, String> result = new HashMap<>(); + + json.get("results").forEach(entry -> { + result.put(entry.get("pubkey").asText(), entry.get("uid").asText()); + }); + return result; + } + + public Map<String, String> getMembersUids(Peer peer) { + // get /wot/members + JsonNode json = executeRequest(peer, URL_MEMBERS, JsonNode.class); + + if (json == null || !json.has("results")) return null; + + Map<String, String> result = new HashMap<>(); + + json.get("results").forEach(entry -> { + result.put(entry.get("pubkey").asText(), entry.get("uid").asText()); + }); + return result; + } + + public void getRequirments(long currencyId, String pubKey) { if (log.isDebugEnabled()) { log.debug(String.format("Try to find user requirements on [%s]", pubKey)); diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/elasticsearch/CurrencyRegistryRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/elasticsearch/CurrencyRegistryRemoteServiceImpl.java index b2ab891f..47c81eef 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/elasticsearch/CurrencyRegistryRemoteServiceImpl.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/elasticsearch/CurrencyRegistryRemoteServiceImpl.java @@ -58,7 +58,7 @@ public class CurrencyRegistryRemoteServiceImpl extends BaseRemoteServiceImpl imp public void afterPropertiesSet() { super.afterPropertiesSet(); config = Configuration.instance(); - peer = new Peer(config.getNodeElasticSearchHost(), config.getNodeElasticSearchPort()); + peer = Peer.newBuilder().setHost(config.getNodeElasticSearchHost()).setPort(config.getNodeElasticSearchPort()).build(); } @Override @@ -97,7 +97,7 @@ public class CurrencyRegistryRemoteServiceImpl extends BaseRemoteServiceImpl imp @Override public List<String> getAllCurrencyNames() { if (log.isDebugEnabled()) { - log.debug("Getting all currency names..."); + log.debug("Getting allOfToList currency names..."); } // get currency diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyService.java index 3dbdd246..f9cbe365 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyService.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyService.java @@ -67,7 +67,7 @@ public interface CurrencyService extends Service { int getCurrencyCount(); /** - * Fill all cache need for currencies + * Fill allOfToList cache need for currencies * @param context */ void loadCache(long accountId); diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyServiceImpl.java index 7ffc39c3..91b9be1f 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyServiceImpl.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/CurrencyServiceImpl.java @@ -167,7 +167,7 @@ public class CurrencyServiceImpl implements CurrencyService, InitializingBean { /** - * Fill all cache need for currencies + * Fill allOfToList cache need for currencies * @param accountId */ public void loadCache(long accountId) { 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 new file mode 100644 index 00000000..501b5a87 --- /dev/null +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java @@ -0,0 +1,57 @@ +package org.duniter.core.client.service.local; + +import org.duniter.core.beans.Service; +import org.duniter.core.client.model.local.Peer; + +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.function.Predicate; + +/** + * Created by blavenie on 20/03/17. + */ +public interface NetworkService extends Service { + + class Sort { + public SortType sortType; + public boolean sortAsc; + } + + class Filter { + public FilterType filterType; + public Peer.PeerStatus filterStatus; + public Boolean filterSsl; + public List<String> filterEndpoints; + } + + + enum SortType { + UID, + PUBKEY, + API, + HARDSHIP, + BLOCK_NUMBER + } + + enum FilterType { + MEMBER, // Only members peers + MIRROR // Only mirror peers + } + + List<Peer> getPeers(Peer mainPeer); + + List<Peer> getPeers(Peer mainPeer, Filter filter, Sort sort); + + CompletableFuture<List<CompletableFuture<Peer>>> asyncGetPeers(Peer mainPeer, ExecutorService pool) throws ExecutionException, InterruptedException; + + List<Peer> fillPeerStatsConsensus(final List<Peer> peers); + + Predicate<Peer> peerFilter(Filter filter); + + Comparator<Peer> peerComparator(Sort sort); + + +} 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 new file mode 100644 index 00000000..6d0d6679 --- /dev/null +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java @@ -0,0 +1,444 @@ +package org.duniter.core.client.service.local; + +import com.fasterxml.jackson.databind.JsonNode; +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.local.Peer; +import org.duniter.core.client.service.ServiceLocator; +import org.duniter.core.client.service.bma.BaseRemoteServiceImpl; +import org.duniter.core.client.service.bma.NetworkRemoteService; +import org.duniter.core.client.service.bma.WotRemoteService; +import org.duniter.core.client.service.exception.HttpConnectException; +import org.duniter.core.client.service.exception.HttpNotFoundException; +import org.duniter.core.exception.TechnicalException; +import org.duniter.core.service.CryptoService; +import org.duniter.core.util.CollectionUtils; +import org.duniter.core.util.Preconditions; +import org.duniter.core.util.StringUtils; +import org.duniter.core.util.concurrent.CompletableFutures; +import org.duniter.core.util.websocket.WebsocketClientEndpoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Created by blavenie on 20/03/17. + */ +public class NetworkServiceImpl extends BaseRemoteServiceImpl implements NetworkService { + + private static final Logger log = LoggerFactory.getLogger(NetworkServiceImpl.class); + + private final static String BMA_URL_STATUS = "/node/summary"; + private final static String BMA_URL_BLOCKCHAIN_CURRENT = "/blockchain/current"; + private final static String BMA_URL_BLOCKCHAIN_HARDSHIP = "/blockchain/hardship/"; + + private NetworkRemoteService networkRemoteService; + private CryptoService cryptoService; + private WotRemoteService wotRemoteService; + + public NetworkServiceImpl() { + } + + public NetworkServiceImpl(NetworkRemoteService networkRemoteService, + WotRemoteService wotRemoteService, + CryptoService cryptoService) { + this(); + this.networkRemoteService = networkRemoteService; + this.wotRemoteService = wotRemoteService; + this.cryptoService = cryptoService; + } + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + this.networkRemoteService = ServiceLocator.instance().getNetworkRemoteService(); + this.wotRemoteService = ServiceLocator.instance().getWotRemoteService(); + this.cryptoService = ServiceLocator.instance().getCryptoService(); + } + + @Override + public List<Peer> getPeers(Peer firstPeer) { + + // 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; + + return getPeers(firstPeer, filterDef, sortDef); + } + + @Override + public List<Peer> getPeers(Peer firstPeer, Filter filter, Sort sort) { + + try { + return asyncGetPeers(firstPeer, null) + .thenCompose(CompletableFutures::allOfToList) + .thenApply(this::fillPeerStatsConsensus) + .thenApply(peers -> peers.stream() + // filter, then sort + .filter(peerFilter(filter)) + .sorted(peerComparator(sort)) + .collect(Collectors.toList())) + .thenApply(this::logPeers) + .get(); + } catch (InterruptedException | ExecutionException e) { + throw new TechnicalException("Error while loading peers: " + e.getMessage(), e); + } + } + + @Override + public Predicate<Peer> peerFilter(final Filter filter) { + return peer -> applyPeerFilter(peer, filter); + } + + @Override + public Comparator<Peer> peerComparator(final Sort sort) { + return Comparator.comparing(peer -> computePeerStatsScore(peer, sort), (score1, score2) -> score2.compareTo(score1)); + } + + @Override + public CompletableFuture<List<CompletableFuture<Peer>>> asyncGetPeers(Peer mainPeer, ExecutorService executor) throws ExecutionException, InterruptedException { + Preconditions.checkNotNull(mainPeer); + + log.debug("Loading network peers..."); + + final ExecutorService pool = (executor != null) ? executor : ForkJoinPool.commonPool(); + + CompletableFuture<List<Peer>> peersFuture = CompletableFuture.supplyAsync(() -> loadPeerLeafs(mainPeer), pool); + CompletableFuture<Map<String, String>> memberUidsFuture = CompletableFuture.supplyAsync(() -> wotRemoteService.getMembersUids(mainPeer), pool); + + return CompletableFuture.allOf( + 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()); + }); + } + + public List<Peer> fillPeerStatsConsensus(final List<Peer> peers) { + + final Map<String,Long> peerCountByBuid = peers.stream() + .filter(peer -> peer.getStats().getStatus() == Peer.PeerStatus.UP) + .map(this::buid) + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + + // Compute main consensus buid + Optional<Map.Entry<String, Long>> maxPeerCountEntry = peerCountByBuid.entrySet().stream() + .sorted(Comparator.comparing(Map.Entry::getValue, (l1, l2) -> l2.compareTo(l1))) + .findFirst(); + + final String mainBuid = maxPeerCountEntry.isPresent() ? maxPeerCountEntry.get().getKey() : null;; + + // Compute total of UP peers + final Long peersUpTotal = peerCountByBuid.values().stream().mapToLong(Long::longValue).sum(); + + // Compute pct by buid + final Map<String, Double> buidsPct = peerCountByBuid.keySet().stream() + .collect(Collectors.toMap( + buid -> buid, + buid -> (peerCountByBuid.get(buid).doubleValue() * 100 / peersUpTotal))); + + // Set consensus stats + peers.forEach(peer -> { + Peer.Stats stats = peer.getStats(); + String buid = buid(stats); + + // Set consensus stats on each peers + if (buid != null) { + boolean isMainConsensus = buid.equals(mainBuid); + stats.setMainConsensus(isMainConsensus); + + boolean isForkConsensus = !isMainConsensus && peerCountByBuid.get(buid) > 1; + stats.setForkConsensus(isForkConsensus); + + stats.setConsensusPct(isMainConsensus || isForkConsensus ? buidsPct.get(buid) : 0d); + } + }); + + return peers; + } + + /* -- protected methods -- */ + + protected List<Peer> loadPeerLeafs(Peer peer) { + List<String> leaves = networkRemoteService.getPeersLeaves(peer); + + if (CollectionUtils.isEmpty(leaves)) return new ArrayList<>(); // should never occur + + // If less than 100 node, get it in ONE call + if (leaves.size() < 100) { + // TODO uncomment on prod + //List<Peer> peers = networkService.getPeers(peer); + //return ImmutableList.of(peers.get(0), peers.get(1), peers.get(2), peers.get(3)); + + //return networkService.getPeers(peer); + } + + // Get it by multiple call on /network/peering?leaf= + List<Peer> result = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(leaves)) { + int offset = 0; + int count = Constants.Config.MAX_SAME_REQUEST_COUNT; + while (offset < leaves.size()) { + if (offset + count > leaves.size()) count = leaves.size() - offset; + loadPeerLeafs(peer, result, leaves, offset, count); + offset += count; + try { + Thread.sleep(1000); // wait 1 s + } catch (InterruptedException e) { + // stop + offset = leaves.size(); + } + } + } + + return result; + } + + protected void loadPeerLeafs(Peer requestedPeer, List<Peer> result, List<String> leaves, int offset, int count) { + + for (int i = offset; i< offset + count; i++) { + 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); + } + } + } + + + } catch(HttpNotFoundException e) { + log.warn("Peer not found for leaf=" + leaf); + // skip + } + } + } + + + protected boolean applyPeerFilter(Peer peer, Filter filter) { + + Peer.Stats stats = peer.getStats(); + + // Filter member or mirror + if (filter.filterType != null && ( + (filter.filterType == FilterType.MEMBER && StringUtils.isBlank(stats.getUid())) + || (filter.filterType == FilterType.MIRROR && StringUtils.isNotBlank(stats.getUid())) + )) { + return false; + } + + // Filter on endpoints + if (CollectionUtils.isNotEmpty(filter.filterEndpoints) + && (StringUtils.isBlank(peer.getApi()) + || !filter.filterEndpoints.contains(peer.getApi()))) { + return false; + } + + // Filter on status + if (filter.filterStatus != null && filter.filterStatus != stats.getStatus()) { + return false; + } + + // Filter on SSL + if (filter.filterSsl != null && filter.filterSsl.booleanValue() != peer.isUseSsl()) { + return false; + } + + return true; + } + + protected Peer getVersion(final Peer peer) { + JsonNode json = executeRequest(peer, BMA_URL_STATUS, JsonNode.class); + // TODO update peer + json = json.get("duniter"); + if (json.isMissingNode()) throw new TechnicalException(String.format("Invalid format of [%s] response", BMA_URL_STATUS)); + json = json.get("version"); + if (json.isMissingNode()) throw new TechnicalException(String.format("No version attribute found in [%s] response", BMA_URL_STATUS)); + String version = json.asText(); + peer.getStats().setVersion(version); + return peer; + } + + protected Peer getCurrentBlock(final Peer peer) { + JsonNode json = executeRequest(peer, BMA_URL_BLOCKCHAIN_CURRENT , JsonNode.class); + + Integer number = json.has("number") ? json.get("number").asInt() : null; + peer.getStats().setBlockNumber(number); + + String hash = json.has("hash") ? json.get("hash").asText() : null; + peer.getStats().setBlockHash(hash); + + Long medianTime = json.has("medianTime") ? json.get("medianTime").asLong() : null; + peer.getStats().setMedianTime(medianTime); + + if (log.isTraceEnabled()) { + log.trace(String.format("[%s] current block [%s-%s]", peer.toString(), number, hash)); + } + + return peer; + } + + protected Peer getHardship(final Peer peer) { + if (StringUtils.isBlank(peer.getPubkey())) return peer; + + JsonNode json = executeRequest(peer, BMA_URL_BLOCKCHAIN_HARDSHIP + peer.getPubkey(), JsonNode.class); + Integer level = json.has("level") ? json.get("level").asInt() : null; + peer.getStats().setHardshipLevel(level); + return peer; + } + + protected String computeUniqueId(Peer peer) { + return cryptoService.hash( + new StringJoiner("|") + .add(peer.getPubkey()) + .add(peer.getDns()) + .add(peer.getIpv4()) + .add(peer.getIpv6()) + .add(String.valueOf(peer.getPort())) + .add(Boolean.toString(peer.isUseSsl())) + .toString()); + } + + protected JsonNode get(final Peer peer, String path) { + return executeRequest(peer, path, JsonNode.class); + } + + /** + * Log allOfToList peers found + */ + protected List<Peer> logPeers(final List<Peer> peers) { + if (!log.isDebugEnabled()) return peers; + + if (CollectionUtils.isEmpty(peers)) { + log.debug("No peers found."); + } + else { + log.debug(String.format("Found %s peers", peers.size())); + if (log.isTraceEnabled()) { + + peers.forEach(peerFound -> { + if (peerFound.getStats().getStatus() == Peer.PeerStatus.DOWN) { + String error = peerFound.getStats().getError(); + log.trace(String.format("Found peer [%s] [%s] %s", + peerFound.toString(), + peerFound.getStats().getStatus().name(), + error != null ? error : "")); + } else { + log.trace(String.format("Found peer [%s] [%s] [v%s] block [%s]", peerFound.toString(), + peerFound.getStats().getStatus().name(), + peerFound.getStats().getVersion(), + peerFound.getStats().getBlockNumber() + )); + + } + }); + } + } + return peers; + } + + protected double computePeerStatsScore(Peer peer, Sort sort) { + double score = 0; + Peer.Stats stats = peer.getStats(); + if (sort != null && sort.sortType != null) { + long specScore = 0; + specScore += (sort.sortType == SortType.UID ? computeScoreAlphaValue(stats.getUid(), 3, sort.sortAsc) : 0); + specScore += (sort.sortType == SortType.PUBKEY ? computeScoreAlphaValue(peer.getPubkey(), 3, sort.sortAsc) : 0); + specScore += (sort.sortType == SortType.API ? + (peer.isUseSsl() ? (sort.sortAsc ? 1 : -1) : + (hasEndPointAPI(peer, EndpointApi.ES_USER_API) ? (sort.sortAsc ? 0.5 : -0.5) : 0)) : 0); + specScore += (sort.sortType == SortType.HARDSHIP ? (stats.getHardshipLevel() != null ? (sort.sortAsc ? (10000-stats.getHardshipLevel()) : stats.getHardshipLevel()): 0) : 0); + specScore += (sort.sortType == SortType.BLOCK_NUMBER ? (stats.getBlockNumber() != null ? (sort.sortAsc ? (1000000000 - stats.getBlockNumber()) : stats.getBlockNumber()) : 0) : 0); + score += (10000000000l * specScore); + } + score += (1000000000 * (stats.getStatus() == Peer.PeerStatus.UP ? 1 : 0)); + score += (100000000 * (stats.isMainConsensus() ? 1 : 0)); + score += (1000000 * (stats.isForkConsensus() ? stats.getConsensusPct() : 0)); + + score += (100 * (stats.getHardshipLevel() != null ? (10000-stats.getHardshipLevel()) : 0)); + score += /* 1 */(peer.getPubkey() != null ? computeScoreAlphaValue(peer.getPubkey(), 2, true) : 0); + + return score; + } + + protected int computeScoreAlphaValue(String value, int nbChars, boolean asc) { + if (StringUtils.isBlank(value)) return 0; + int score = 0; + value = value.toLowerCase(); + if (nbChars > value.length()) { + nbChars = value.length(); + } + score += (int)value.charAt(0); + for (int i=1; i < nbChars; i++) { + score += Math.pow(0.001, i) * value.charAt(i); + } + return asc ? (1000 - score) : score; + } + + protected boolean hasEndPointAPI(Peer peer, EndpointApi api) { + return peer.getApi() != null && peer.getApi().equalsIgnoreCase(api.name()); + } + + protected String buid(Peer peer) { + return buid(peer.getStats()); + } + + protected String buid(Peer.Stats stats) { + return stats.getStatus() == Peer.PeerStatus.UP + ? stats.getBlockNumber() + "-" + stats.getBlockHash() + : null; + } +} diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/PeerServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/PeerServiceImpl.java index d8f36924..f724f157 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/PeerServiceImpl.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/PeerServiceImpl.java @@ -150,7 +150,7 @@ public class PeerServiceImpl implements PeerService, InitializingBean { } /** - * Fill all cache need for currencies + * Fill allOfToList cache need for currencies * @param accountId */ public void loadCache(long accountId) { diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/TestResource.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/TestResource.java index 28d37dbd..1029dadb 100644 --- a/duniter4j-core-client/src/test/java/org/duniter/core/client/TestResource.java +++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/TestResource.java @@ -155,7 +155,10 @@ public class TestResource extends org.duniter.core.test.TestResource { // Set a default account id, then load cache ServiceLocator.instance().getDataContext().setAccountId(0); - Peer peer = new Peer(config.getNodeHost(), config.getNodePort()); + Peer peer = Peer.newBuilder() + .setHost(config.getNodeHost()) + .setPort(config.getNodePort()) + .build(); peer.setCurrencyId(fixtures.getDefaultCurrencyId()); ServiceLocator.instance().getPeerService().save(peer); diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/HttpServiceTest.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/HttpServiceTest.java index 0f96bc8b..62eb7714 100644 --- a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/HttpServiceTest.java +++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/HttpServiceTest.java @@ -25,19 +25,13 @@ package org.duniter.core.client.service; import org.duniter.core.client.TestResource; import org.duniter.core.client.config.Configuration; -import org.duniter.core.client.model.bma.EndpointProtocol; -import org.duniter.core.client.model.bma.NetworkPeering; import org.duniter.core.client.model.local.Peer; -import org.duniter.core.client.service.bma.NetworkRemoteService; -import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.List; - public class HttpServiceTest { private static final Logger log = LoggerFactory.getLogger(HttpServiceTest.class); @@ -64,10 +58,9 @@ public class HttpServiceTest { /* -- internal methods */ protected Peer createTestPeer() { - Peer peer = new Peer( - Configuration.instance().getNodeHost(), - Configuration.instance().getNodePort()); - - return peer; + return Peer.newBuilder() + .setHost(Configuration.instance().getNodeHost()) + .setPort(Configuration.instance().getNodePort()) + .build(); } } 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 fb881047..33ee0035 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 @@ -114,7 +114,7 @@ public class BlockchainRemoteServiceTest { Assert.assertNotNull(result); Assert.assertEquals(10, result.length); - // Make sure all json are valid blocks + // Make sure allOfToList json are valid blocks ObjectMapper objectMapper = JacksonUtils.newObjectMapper(); int number = 0; @@ -192,11 +192,10 @@ public class BlockchainRemoteServiceTest { /* -- Internal methods -- */ protected Peer createTestPeer() { - Peer peer = new Peer( - Configuration.instance().getNodeHost(), - Configuration.instance().getNodePort()); - - return peer; + return Peer.newBuilder() + .setHost(Configuration.instance().getNodeHost()) + .setPort(Configuration.instance().getNodePort()) + .build(); } protected Wallet createTestWallet() { diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/NetworkRemoteServiceTest.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/NetworkRemoteServiceTest.java index 5544aee2..ccd274e8 100644 --- a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/NetworkRemoteServiceTest.java +++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/NetworkRemoteServiceTest.java @@ -25,7 +25,7 @@ package org.duniter.core.client.service.bma; import org.duniter.core.client.TestResource; import org.duniter.core.client.config.Configuration; -import org.duniter.core.client.model.bma.EndpointProtocol; +import org.duniter.core.client.model.bma.EndpointApi; import org.duniter.core.client.model.bma.NetworkPeering; import org.duniter.core.client.model.local.Peer; import org.duniter.core.client.service.ServiceLocator; @@ -69,7 +69,7 @@ public class NetworkRemoteServiceTest { @Test public void findPeers() throws Exception { - List<Peer> result = service.findPeers(peer, null, EndpointProtocol.BASIC_MERKLED_API, null, null); + List<Peer> result = service.findPeers(peer, null, EndpointApi.BASIC_MERKLED_API, null, null); Assert.assertNotNull(result); Assert.assertTrue(result.size() > 0); @@ -87,10 +87,9 @@ public class NetworkRemoteServiceTest { /* -- internal methods */ protected Peer createTestPeer() { - Peer peer = new Peer( - Configuration.instance().getNodeHost(), - Configuration.instance().getNodePort()); - - return peer; + return Peer.newBuilder() + .setHost(Configuration.instance().getNodeHost()) + .setPort(Configuration.instance().getNodePort()) + .build(); } } diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/TransactionRemoteServiceTest.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/TransactionRemoteServiceTest.java index 8d8950c6..3118e7f3 100644 --- a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/TransactionRemoteServiceTest.java +++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/TransactionRemoteServiceTest.java @@ -90,10 +90,9 @@ public class TransactionRemoteServiceTest { } protected Peer createTestPeer() { - Peer peer = new Peer( - Configuration.instance().getNodeHost(), - Configuration.instance().getNodePort()); - - return peer; + return Peer.newBuilder() + .setHost(Configuration.instance().getNodeHost()) + .setPort(Configuration.instance().getNodePort()) + .build(); } } diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/WotRemoteServiceTest.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/WotRemoteServiceTest.java index 5cbbd36e..ca52ee15 100644 --- a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/WotRemoteServiceTest.java +++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/WotRemoteServiceTest.java @@ -165,10 +165,9 @@ public class WotRemoteServiceTest { } protected Peer createTestPeer() { - Peer peer = new Peer( - Configuration.instance().getNodeHost(), - Configuration.instance().getNodePort()); - - return peer; + return Peer.newBuilder() + .setHost(Configuration.instance().getNodeHost()) + .setPort(Configuration.instance().getNodePort()) + .build(); } } diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/local/NetworkServiceTest.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/local/NetworkServiceTest.java new file mode 100644 index 00000000..8106d95f --- /dev/null +++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/local/NetworkServiceTest.java @@ -0,0 +1,72 @@ +package org.duniter.core.client.service.local; + +/* + * #%L + * UCoin Java Client :: Core API + * %% + * Copyright (C) 2014 - 2015 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 org.duniter.core.client.TestResource; +import org.duniter.core.client.config.Configuration; +import org.duniter.core.client.model.local.Peer; +import org.duniter.core.client.service.ServiceLocator; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public class NetworkServiceTest { + + private static final Logger log = LoggerFactory.getLogger(NetworkServiceTest.class); + + @ClassRule + public static final TestResource resource = TestResource.create(); + + private NetworkService service; + private Peer peer; + + @Before + public void setUp() { + peer = createTestPeer(); + service = ServiceLocator.instance().getNetworkService(); + } + + @Test + public void start() throws Exception { + + List<Peer> peers = service.getPeers(peer); + + Assert.assertNotNull(peers); + Assert.assertTrue(peers.size() > 0); + } + + /* -- internal methods */ + + protected Peer createTestPeer() { + return Peer.newBuilder() + .setHost(Configuration.instance().getNodeHost()) + .setPort(Configuration.instance().getNodePort()) + .build(); + } +} diff --git a/duniter4j-core-client/src/test/resources/META-INF/services/org.duniter.core.beans.Bean b/duniter4j-core-client/src/test/resources/META-INF/services/org.duniter.core.beans.Bean index e4fdb2bb..bbf9fb6b 100644 --- a/duniter4j-core-client/src/test/resources/META-INF/services/org.duniter.core.beans.Bean +++ b/duniter4j-core-client/src/test/resources/META-INF/services/org.duniter.core.beans.Bean @@ -8,5 +8,6 @@ org.duniter.core.client.service.HttpServiceImpl org.duniter.core.client.service.DataContext org.duniter.core.client.service.local.PeerServiceImpl org.duniter.core.client.service.local.CurrencyServiceImpl +org.duniter.core.client.service.local.NetworkServiceImpl org.duniter.core.client.dao.mem.MemoryCurrencyDaoImpl org.duniter.core.client.dao.mem.MemoryPeerDaoImpl \ No newline at end of file diff --git a/duniter4j-core-client/src/test/resources/duniter4j-core-client-test.properties b/duniter4j-core-client/src/test/resources/duniter4j-core-client-test.properties index 9e017425..d9f4f75b 100644 --- a/duniter4j-core-client/src/test/resources/duniter4j-core-client-test.properties +++ b/duniter4j-core-client/src/test/resources/duniter4j-core-client-test.properties @@ -1,5 +1,5 @@ -duniter4j.node.host=gtest.duniter.org -duniter4j.node.port=10900 +duniter4j.node.host=192.168.0.5 +duniter4j.node.port=10901 duniter4j.node.elasticsearch.host=localhost duniter4j.node.elasticsearch.port=9200 diff --git a/duniter4j-core-client/src/test/resources/log4j.properties b/duniter4j-core-client/src/test/resources/log4j.properties index a3e37387..e1c60841 100644 --- a/duniter4j-core-client/src/test/resources/log4j.properties +++ b/duniter4j-core-client/src/test/resources/log4j.properties @@ -7,12 +7,7 @@ log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{ISO8601} %5p (%c:%L) - %m%n -# ucoin levels -log4j.logger.org.duniter=INFO -log4j.logger.org.duniter.core.client.service=DEBUG -log4j.logger.org.duniter.core.client.service.bma=DEBUG -log4j.logger.org.duniter.core.beans=WARN - +# File output log4j.appender.file=org.apache.log4j.RollingFileAppender log4j.appender.file.file=ucoin-client.log log4j.appender.file.MaxFileSize=10MB @@ -21,4 +16,15 @@ log4j.appender.file.MaxBackupIndex=4 log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=%d{ISO8601} %5p %c - %m%n +# duniter4j levels +log4j.logger.org.duniter=INFO +log4j.logger.org.duniter.core.client.service=DEBUG +log4j.logger.org.duniter.core.client.service.bma=DEBUG +log4j.logger.org.duniter.core.beans=WARN +log4j.logger.org.duniter.core.client.service=TRACE + +# Framework levels +log4j.logger.org.apache.http=WARN + + diff --git a/duniter4j-core-shared/pom.xml b/duniter4j-core-shared/pom.xml index 7844bdea..f826f638 100644 --- a/duniter4j-core-shared/pom.xml +++ b/duniter4j-core-shared/pom.xml @@ -41,6 +41,16 @@ <artifactId>scrypt</artifactId> </dependency> + <!-- http --> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpcore</artifactId> + </dependency> + <dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> diff --git a/duniter4j-core-shared/src/main/java/org/duniter/core/beans/BeanFactory.java b/duniter4j-core-shared/src/main/java/org/duniter/core/beans/BeanFactory.java index 4d1b4d45..4567cea4 100644 --- a/duniter4j-core-shared/src/main/java/org/duniter/core/beans/BeanFactory.java +++ b/duniter4j-core-shared/src/main/java/org/duniter/core/beans/BeanFactory.java @@ -120,7 +120,6 @@ public class BeanFactory implements Closeable{ throw new TechnicalException(String.format("Unable to create bean with type [%s]: not configured for the service loader [%s]", clazz.getName(), Bean.class.getCanonicalName())); } - @Override public void close() throws IOException { for(Object bean: beansCache.values()) { diff --git a/duniter4j-core-shared/src/main/java/org/duniter/core/util/concurrent/CompletableFutures.java b/duniter4j-core-shared/src/main/java/org/duniter/core/util/concurrent/CompletableFutures.java new file mode 100644 index 00000000..b0b78f91 --- /dev/null +++ b/duniter4j-core-shared/src/main/java/org/duniter/core/util/concurrent/CompletableFutures.java @@ -0,0 +1,38 @@ +package org.duniter.core.util.concurrent; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Helper class on CompletableFuture and concurrent classes + * Created by blavenie on 24/03/17. + */ +public class CompletableFutures { + + private CompletableFutures() { + } + + public static <T> CompletableFuture<List<T>> allOfToList(List<CompletableFuture<T>> futures) { + CompletableFuture<Void> allDoneFuture = + CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])); + return allDoneFuture.thenApply(v -> + futures.stream() + .map(future -> future.join()) + .filter(peer -> peer != null) // skip + .collect(Collectors.toList()) + ); + } + + public static <T> CompletableFuture<List<T>> allOfToList(List<CompletableFuture<T>> futures, Predicate<? super T> filter) { + CompletableFuture<Void> allDoneFuture = + CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])); + return allDoneFuture.thenApply(v -> + futures.stream() + .map(future -> future.join()) + .filter(filter) + .collect(Collectors.toList()) + ); + } +} diff --git a/duniter4j-core-shared/src/main/java/org/duniter/core/util/http/InetAddressUtils.java b/duniter4j-core-shared/src/main/java/org/duniter/core/util/http/InetAddressUtils.java new file mode 100644 index 00000000..b88050ef --- /dev/null +++ b/duniter4j-core-shared/src/main/java/org/duniter/core/util/http/InetAddressUtils.java @@ -0,0 +1,27 @@ +package org.duniter.core.util.http; + +import java.util.regex.Pattern; + +/** + * Created by blavenie on 24/03/17. + */ +public class InetAddressUtils { + + public static final Pattern LOCAL_IP_ADDRESS_PATTERN = Pattern.compile("^127[.]0[.]0.|192[.]168[.]|10[.]0[.]0[.]|172[.]16[.]"); + + private InetAddressUtils() { + } + + public static boolean isNotLocalIPv4Address(String input) { + return org.apache.http.conn.util.InetAddressUtils.isIPv4Address(input) && + !LOCAL_IP_ADDRESS_PATTERN.matcher(input).matches(); + } + + public static boolean isIPv4Address(String input) { + return org.apache.http.conn.util.InetAddressUtils.isIPv4Address(input); + } + + public static boolean isIPv6Address(String input) { + return org.apache.http.conn.util.InetAddressUtils.isIPv6Address(input); + } +} diff --git a/duniter4j-core-shared/src/main/java/org/duniter/core/util/websocket/WebsocketClientEndpoint.java b/duniter4j-core-shared/src/main/java/org/duniter/core/util/websocket/WebsocketClientEndpoint.java index 6a58a068..2f127d8d 100644 --- a/duniter4j-core-shared/src/main/java/org/duniter/core/util/websocket/WebsocketClientEndpoint.java +++ b/duniter4j-core-shared/src/main/java/org/duniter/core/util/websocket/WebsocketClientEndpoint.java @@ -136,6 +136,17 @@ public class WebsocketClientEndpoint implements Closeable { } } + /** + * unregister message listener + * + * @param listener + */ + public void unregisterListener(MessageListener listener) { + synchronized (messageListeners) { + this.messageListeners.remove(listener); + } + } + /** * register connection listener * @@ -147,6 +158,18 @@ public class WebsocketClientEndpoint implements Closeable { } } + /** + * unregister connection listener + * + * @param listener + */ + public void unregisterListener(ConnectionListener listener) { + synchronized (connectionListeners) { + this.connectionListeners.remove(listener); + } + } + + /** * Send a message. * diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginInit.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginInit.java index b8d4bde5..89d27739 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginInit.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginInit.java @@ -27,6 +27,7 @@ import org.duniter.core.client.model.local.Peer; import org.duniter.elasticsearch.rest.security.RestSecurityController; import org.duniter.elasticsearch.service.BlockchainService; import org.duniter.elasticsearch.service.CurrencyService; +import org.duniter.elasticsearch.service.NetworkService; import org.duniter.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.health.ClusterHealthStatus; @@ -135,6 +136,11 @@ public class PluginInit extends AbstractLifecycleComponent<PluginInit> { injector.getInstance(BlockchainService.class) .indexLastBlocks(peer) .listenAndIndexNewBlock(peer); + + // Index peers (and listen if new peer appear) + injector.getInstance(NetworkService.class) + .indexLastPeers(peer)/* + .listenAndIndexNewPeer(peer)*/; } } } diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginSettings.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginSettings.java index 50cb48e4..e95270d3 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginSettings.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/PluginSettings.java @@ -105,7 +105,6 @@ public class PluginSettings extends AbstractLifecycleComponent<PluginSettings> { applicationConfig.setDefaultOption(ConfigurationOption.BASEDIR.getKey(), baseDir); applicationConfig.setDefaultOption(ConfigurationOption.NODE_HOST.getKey(), getNodeBmaHost()); applicationConfig.setDefaultOption(ConfigurationOption.NODE_PORT.getKey(), String.valueOf(getNodeBmaPort())); - applicationConfig.setDefaultOption(ConfigurationOption.NODE_PROTOCOL.getKey(), getNodeBmaUseSsl() ? "https" : "http"); applicationConfig.setDefaultOption(ConfigurationOption.NETWORK_TIMEOUT.getKey(), String.valueOf(getNetworkTimeout())); try { @@ -168,7 +167,7 @@ public class PluginSettings extends AbstractLifecycleComponent<PluginSettings> { } public boolean getNodeBmaUseSsl() { - return settings.getAsBoolean("duniter.useSsl", getNodeBmaPort() == 443); + return settings.getAsBoolean("duniter.useSsl", null); } public boolean isIndexBulkEnable() { @@ -227,7 +226,7 @@ public class PluginSettings extends AbstractLifecycleComponent<PluginSettings> { return null; } - Peer peer = new Peer(getNodeBmaHost(), getNodeBmaPort(), getNodeBmaUseSsl()); + Peer peer = Peer.newBuilder().setHost(getNodeBmaHost()).setPort(getNodeBmaPort()).setUseSsl(getNodeBmaUseSsl()).build(); return peer; } diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/BlockchainService.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/BlockchainService.java index ce82dfc3..8f3192de 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/BlockchainService.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/BlockchainService.java @@ -30,7 +30,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import org.duniter.core.client.model.bma.BlockchainBlock; import org.duniter.core.client.model.bma.BlockchainParameters; -import org.duniter.core.client.model.bma.EndpointProtocol; +import org.duniter.core.client.model.bma.EndpointApi; import org.duniter.core.client.model.local.Peer; import org.duniter.core.util.json.JsonAttributeParser; import org.duniter.core.client.model.bma.jackson.JacksonUtils; @@ -178,7 +178,7 @@ public class BlockchainService extends AbstractService { // Check if index exists createIndexIfNotExists(currencyName, true/*wait cluster health*/); - // Then index all blocks + // Then index allOfToList blocks BlockchainBlock peerCurrentBlock = blockchainRemoteService.getCurrentBlock(peer); if (peerCurrentBlock != null) { @@ -243,7 +243,7 @@ public class BlockchainService extends AbstractService { progressionModel.setStatus(ProgressionModel.Status.SUCCESS); } else { - logger.warn(String.format("[%s] [%s] Could not indexed all blocks. Missing %s blocks.", currencyName, peer, missingBlocks.size())); + logger.warn(String.format("[%s] [%s] Could not indexed allOfToList blocks. Missing %s blocks.", currencyName, peer, missingBlocks.size())); progressionModel.setStatus(ProgressionModel.Status.FAILED); } } @@ -294,7 +294,7 @@ public class BlockchainService extends AbstractService { .build(); createIndexRequestBuilder.setSettings(indexSettings); createIndexRequestBuilder.addMapping(BLOCK_TYPE, createBlockTypeMapping()); - createIndexRequestBuilder.addMapping(PEER_TYPE, createPeerTypeMapping()); + createIndexRequestBuilder.addMapping(PEER_TYPE, NetworkService.createPeerTypeMapping()); createIndexRequestBuilder.execute().actionGet(); } @@ -689,34 +689,6 @@ public class BlockchainService extends AbstractService { } } - public XContentBuilder createPeerTypeMapping() { - try { - XContentBuilder mapping = XContentFactory.jsonBuilder() - .startObject() - .startObject(PEER_TYPE) - .startObject("properties") - - // currency - .startObject("currency") - .field("type", "string") - .endObject() - - // pubkey - .startObject("pubkey") - .field("type", "string") - .field("index", "not_analyzed") - .endObject() - - - .endObject() - .endObject().endObject(); - - return mapping; - } - catch(IOException ioe) { - throw new TechnicalException("Error while getting mapping for block index: " + ioe.getMessage(), ioe); - } - } public BlockchainBlock getBlockByIdStr(String currencyName, String blockId) { @@ -954,9 +926,9 @@ public class BlockchainService extends AbstractService { // Select other peers, in filtering on the same blockchain version // TODO : a activer quand les peers seront bien mis à jour (UP/DOWN, block, hash...) - //List<Peer> otherPeers = networkRemoteService.findPeers(peer, "UP", EndpointProtocol.BASIC_MERKLED_API, + //List<Peer> otherPeers = networkRemoteService.findPeers(peer, "UP", EndpointApi.BASIC_MERKLED_API, // currentBlock.getNumber(), currentBlock.getHash()); - List<Peer> otherPeers = networkRemoteService.findPeers(peer, null, EndpointProtocol.BASIC_MERKLED_API, + List<Peer> otherPeers = networkRemoteService.findPeers(peer, null, EndpointApi.BASIC_MERKLED_API, null, null); for(Peer childPeer: otherPeers) { diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/NetworkService.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/NetworkService.java new file mode 100644 index 00000000..209abd26 --- /dev/null +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/NetworkService.java @@ -0,0 +1,571 @@ +package org.duniter.elasticsearch.service; + +/* + * #%L + * UCoin Java Client :: Core API + * %% + * Copyright (C) 2014 - 2015 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.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import org.duniter.core.client.model.bma.BlockchainParameters; +import org.duniter.core.client.model.bma.EndpointApi; +import org.duniter.core.client.model.bma.jackson.JacksonUtils; +import org.duniter.core.client.model.local.Peer; +import org.duniter.core.client.service.local.NetworkServiceImpl; +import org.duniter.core.exception.TechnicalException; +import org.duniter.core.model.NullProgressionModel; +import org.duniter.core.model.ProgressionModel; +import org.duniter.core.util.CollectionUtils; +import org.duniter.core.util.Preconditions; +import org.duniter.core.util.StringUtils; +import org.duniter.core.util.concurrent.CompletableFutures; +import org.duniter.core.util.json.JsonAttributeParser; +import org.duniter.core.util.json.JsonSyntaxException; +import org.duniter.core.util.websocket.WebsocketClientEndpoint; +import org.duniter.elasticsearch.PluginSettings; +import org.duniter.elasticsearch.exception.DuplicateIndexIdException; +import org.duniter.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.action.ActionFuture; +import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.search.SearchType; +import org.elasticsearch.action.update.UpdateRequestBuilder; +import org.elasticsearch.action.update.UpdateResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.SearchHitField; +import org.elasticsearch.search.highlight.HighlightField; +import org.elasticsearch.search.sort.SortOrder; +import org.nuiton.i18n.I18n; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +/** + * Created by Benoit on 30/03/2015. + */ +public class NetworkService extends AbstractService { + + public static final String PEER_TYPE = "peer"; + + private final ProgressionModel nullProgressionModel = new NullProgressionModel(); + + private org.duniter.core.client.service.bma.BlockchainRemoteService blockchainRemoteService; + private org.duniter.core.client.service.local.NetworkService networkService; + private ThreadPool threadPool; + private List<WebsocketClientEndpoint.ConnectionListener> connectionListeners = new ArrayList<>(); + private final WebsocketClientEndpoint.ConnectionListener dispatchConnectionListener; + + private final JsonAttributeParser blockCurrencyParser = new JsonAttributeParser("currency"); + private final JsonAttributeParser blockHashParser = new JsonAttributeParser("hash"); + + private ObjectMapper objectMapper; + + @Inject + public NetworkService(Client client, PluginSettings settings, ThreadPool threadPool, + final ServiceLocator serviceLocator){ + super("duniter.network", client, settings); + this.objectMapper = JacksonUtils.newObjectMapper(); + this.threadPool = threadPool; + threadPool.scheduleOnStarted(() -> { + this.blockchainRemoteService = serviceLocator.getBlockchainRemoteService(); + this.networkService = serviceLocator.getNetworkService(); + }); + dispatchConnectionListener = new WebsocketClientEndpoint.ConnectionListener() { + @Override + public void onSuccess() { + synchronized (connectionListeners) { + connectionListeners.stream().forEach(connectionListener -> connectionListener.onSuccess()); + } + } + @Override + public void onError(Exception e, long lastTimeUp) { + synchronized (connectionListeners) { + connectionListeners.stream().forEach(connectionListener -> connectionListener.onError(e, lastTimeUp)); + } + } + }; + } + + public void registerConnectionListener(WebsocketClientEndpoint.ConnectionListener listener) { + synchronized (connectionListeners) { + connectionListeners.add(listener); + } + } + + public NetworkService indexLastPeers(Peer peer) { + indexLastPeers(peer, nullProgressionModel); + return this; + } + + public NetworkService indexLastPeers(Peer peer, ProgressionModel progressionModel) { + + try { + // Get the blockchain name from node + BlockchainParameters parameter = blockchainRemoteService.getParameters(peer); + if (parameter == null) { + progressionModel.setStatus(ProgressionModel.Status.FAILED); + logger.error(I18n.t("duniter4j.es.networkService.indexPeers.remoteParametersError", peer)); + return this; + } + String currencyName = parameter.getCurrency(); + + indexPeers(currencyName, peer, progressionModel); + + } catch(Exception e) { + logger.error("Error during indexLastPeers: " + e.getMessage(), e); + progressionModel.setStatus(ProgressionModel.Status.FAILED); + } + + return this; + } + + + public NetworkService indexPeers(String currencyName, Peer firstPeer, ProgressionModel progressionModel) { + progressionModel.setStatus(ProgressionModel.Status.RUNNING); + progressionModel.setTotal(100); + long timeStart = System.currentTimeMillis(); + + try { + + progressionModel.setTask(I18n.t("duniter4j.es.networkService.indexPeers.task", currencyName, firstPeer)); + logger.info(I18n.t("duniter4j.es.networkService.indexPeers.task", currencyName, firstPeer)); + + // Default filter + org.duniter.core.client.service.local.NetworkService.Filter filterDef = new org.duniter.core.client.service.local.NetworkService.Filter(); + filterDef.filterType = null; + filterDef.filterStatus = Peer.PeerStatus.UP; + filterDef.filterEndpoints = ImmutableList.of(EndpointApi.BASIC_MERKLED_API.name(), EndpointApi.BMAS.name()); + + // Default sort + org.duniter.core.client.service.local.NetworkService.Sort sortDef = new org.duniter.core.client.service.local.NetworkService.Sort(); + sortDef.sortType = null; + + try { + networkService.asyncGetPeers(firstPeer, threadPool.scheduler()) + .thenCompose(CompletableFutures::allOfToList) + .thenApply(networkService::fillPeerStatsConsensus) + .thenApply(peers -> peers.stream() + // filter, then sort + .filter(networkService.peerFilter(filterDef)) + .map(peer -> savePeer(peer, false)) + .collect(Collectors.toList())) + .thenApply(peers -> { + logger.info(I18n.t("duniter4j.es.networkService.indexPeers.succeed", currencyName, firstPeer, peers.size(), (System.currentTimeMillis() - timeStart))); + progressionModel.setStatus(ProgressionModel.Status.SUCCESS); + return peers; + }); + } catch (InterruptedException | ExecutionException e) { + throw new TechnicalException("Error while loading peers: " + e.getMessage(), e); + } + } catch(Exception e) { + logger.error("Error during indexBlocksFromNode: " + e.getMessage(), e); + progressionModel.setStatus(ProgressionModel.Status.FAILED); + } + + return this; + } + +/* + public void start(Peer peer, FilterAndSortSpec networkSpec) { + Preconditions.checkNotNull(networkSpec); + this.networkSpec = networkSpec; + start(peer); + } + + public void start(Peer peer) { + Preconditions.checkNotNull(peer); + + log.debug("Starting network crawler..."); + + addListeners(peer); + + this.mainPeer = peer; + + try { + this.peers = loadPeers(this.mainPeer).get(); + } + catch(Exception e) { + throw new TechnicalException("Error during start load peers", e); + } + + isStarted = true; + log.info("Network crawler started"); + } + + public void stop() { + if (!isStarted) return; + log.debug("Stopping network crawler..."); + + removeListeners(); + + this.mainPeer = null; + this.mainPeerWsEp = null; + this.isStarted = false; + + this.executorService.shutdown(); + + log.info("Network crawler stopped"); + }*/ + + /** + * Create or update a peer, depending on its existence and hash + * @param peer + * @param wait wait indexBlocksFromNode end + * @throws DuplicateIndexIdException + */ + public Peer savePeer(final Peer peer, boolean wait) throws DuplicateIndexIdException { + Preconditions.checkNotNull(peer, "peer could not be null") ; + Preconditions.checkNotNull(peer.getCurrency(), "peer attribute 'currency' could not be null"); + //Preconditions.checkNotNull(peer.getHash(), "peer attribute 'hash' could not be null"); + Preconditions.checkNotNull(peer.getPubkey(), "peer attribute 'pubkey' could not be null"); + Preconditions.checkNotNull(peer.getHost(), "peer attribute 'host' could not be null"); + Preconditions.checkNotNull(peer.getApi(), "peer 'api' could not be null"); + + Peer existingPeer = getPeerByHash(peer.getCurrency(), peer.getHash()); + + // Currency not exists, or has changed, so create it + if (existingPeer == null) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Insert new peer [%s]", peer)); + } + + // Index new peer + indexPeer(peer, wait); + } + + // Update existing peer + else { + logger.trace(String.format("Update peer [%s]", peer)); + updatePeer(peer, wait); + } + return peer; + } + + public void indexPeer(Peer peer, boolean wait) { + Preconditions.checkNotNull(peer); + Preconditions.checkArgument(StringUtils.isNotBlank(peer.getCurrency())); + Preconditions.checkNotNull(peer.getHash()); + Preconditions.checkNotNull(peer.getHost()); + Preconditions.checkNotNull(peer.getApi()); + + // Serialize into JSON + // WARN: must use GSON, to have same JSON result (e.g identities and joiners field must be converted into String) + try { + String json = objectMapper.writeValueAsString(peer); + + // Preparing indexBlocksFromNode + IndexRequestBuilder indexRequest = client.prepareIndex(peer.getCurrency(), PEER_TYPE) + .setId(peer.getHash()) + .setSource(json); + + // Execute indexBlocksFromNode + ActionFuture<IndexResponse> futureResponse = indexRequest + .setRefresh(true) + .execute(); + + if (wait) { + futureResponse.actionGet(); + } + } + catch(JsonProcessingException e) { + throw new TechnicalException(e); + } + } + + public void updatePeer(Peer peer, boolean wait) { + Preconditions.checkNotNull(peer); + Preconditions.checkArgument(StringUtils.isNotBlank(peer.getCurrency())); + Preconditions.checkNotNull(peer.getHash()); + Preconditions.checkNotNull(peer.getHost()); + Preconditions.checkNotNull(peer.getApi()); + + // Serialize into JSON + // WARN: must use GSON, to have same JSON result (e.g identities and joiners field must be converted into String) + try { + String json = objectMapper.writeValueAsString(peer); + + // Preparing indexBlocksFromNode + UpdateRequestBuilder updateRequest = client.prepareUpdate(peer.getCurrency(), PEER_TYPE, peer.getHash()) + .setDoc(json); + + // Execute indexBlocksFromNode + ActionFuture<UpdateResponse> futureResponse = updateRequest + .setRefresh(true) + .execute(); + + if (wait) { + futureResponse.actionGet(); + } + } + catch(JsonProcessingException e) { + throw new TechnicalException(e); + } + } + + /** + * + * @param currencyName + * @param number the peer hash + * @param json block as JSON + */ + public NetworkService indexPeerFromJson(String currencyName, int number, byte[] json, boolean refresh, boolean wait) { + Preconditions.checkNotNull(json); + Preconditions.checkArgument(json.length > 0); + + // Preparing indexBlocksFromNode + IndexRequestBuilder indexRequest = client.prepareIndex(currencyName, PEER_TYPE) + .setId(String.valueOf(number)) + .setRefresh(refresh) + .setSource(json); + + // Execute indexBlocksFromNode + if (!wait) { + indexRequest.execute(); + } + else { + indexRequest.execute().actionGet(); + } + + return this; + } + + /** + * Index the given block, as the last (current) block. This will check is a fork has occur, and apply a rollback so. + */ + public void onNetworkChanged() { + logger.info("ES network service -> peers changed: TODO: index new peers"); + } + + /** + * + * @param json block as json + * @param refresh Enable ES update with 'refresh' tag ? + * @param wait need to wait until processed ? + */ + public NetworkService indexPeer(Peer peer, String json, boolean refresh, boolean wait) { + Preconditions.checkNotNull(json); + Preconditions.checkArgument(json.length() > 0); + + String currencyName = blockCurrencyParser.getValueAsString(json); + String hash = blockHashParser.getValueAsString(json); + + logger.info(I18n.t("duniter4j.es.networkService.indexPeer", currencyName, peer)); + if (logger.isTraceEnabled()) { + logger.trace(json); + } + + + // Preparing index + IndexRequestBuilder indexRequest = client.prepareIndex(currencyName, PEER_TYPE) + .setId(hash) + .setRefresh(refresh) + .setSource(json); + + // Execute indexBlocksFromNode + if (!wait) { + indexRequest.execute(); + } + else { + indexRequest.execute().actionGet(); + } + + return this; + } + + public List<Peer> findPeersByHash(String currencyName, String query) { + String[] queryParts = query.split("[\\t ]+"); + + // Prepare request + SearchRequestBuilder searchRequest = client + .prepareSearch(currencyName) + .setTypes(PEER_TYPE) + .setSearchType(SearchType.DFS_QUERY_THEN_FETCH); + + // If only one term, search as prefix + if (queryParts.length == 1) { + searchRequest.setQuery(QueryBuilders.prefixQuery("hash", query)); + } + + // If more than a word, search on terms match + else { + searchRequest.setQuery(QueryBuilders.matchQuery("hash", query)); + } + + // Sort as score/memberCount + searchRequest.addSort("_score", SortOrder.DESC) + .addSort("number", SortOrder.DESC); + + // Highlight matched words + searchRequest.setHighlighterTagsSchema("styled") + .addHighlightedField("hash") + .addFields("hash") + .addFields("*", "_source"); + + // Execute query + SearchResponse searchResponse = searchRequest.execute().actionGet(); + + // Read query result + return toBlocks(searchResponse, true); + } + + /* -- Internal methods -- */ + + public static XContentBuilder createPeerTypeMapping() { + try { + XContentBuilder mapping = XContentFactory.jsonBuilder() + .startObject() + .startObject(PEER_TYPE) + .startObject("properties") + + // currency + .startObject("currency") + .field("sortType", "string") + .endObject() + + // pubkey + .startObject("pubkey") + .field("sortType", "string") + .field("index", "not_analyzed") + .endObject() + + // api + .startObject("api") + .field("sortType", "string") + .field("index", "not_analyzed") + .endObject() + + // uid + .startObject("uid") + .field("sortType", "string") + .endObject() + + // dns + .startObject("dns") + .field("sortType", "string") + .endObject() + + // ipv4 + .startObject("ipv4") + .field("sortType", "string") + .endObject() + + // ipv6 + .startObject("ipv6") + .field("sortType", "string") + .endObject() + + .endObject() + .endObject().endObject(); + + return mapping; + } + catch(IOException ioe) { + throw new TechnicalException("Error while getting mapping for peer index: " + ioe.getMessage(), ioe); + } + } + + public Peer getPeerByHash(String currencyName, String hash) { + + // Prepare request + SearchRequestBuilder searchRequest = client + .prepareSearch(currencyName) + .setTypes(PEER_TYPE) + .setSearchType(SearchType.DFS_QUERY_THEN_FETCH); + + // If more than a word, search on terms match + searchRequest.setQuery(QueryBuilders.matchQuery("_id", hash)); + + // Execute query + try { + SearchResponse searchResponse = searchRequest.execute().actionGet(); + List<Peer> blocks = toBlocks(searchResponse, false); + if (CollectionUtils.isEmpty(blocks)) { + return null; + } + + // Return the unique result + return CollectionUtils.extractSingleton(blocks); + } + catch(JsonSyntaxException e) { + throw new TechnicalException(String.format("Error while getting indexed peer #%s for [%s]", hash, currencyName), e); + } + + } + + protected List<Peer> toBlocks(SearchResponse response, boolean withHighlight) { + // Read query result + List<Peer> result = Lists.newArrayList(); + response.getHits().forEach(searchHit -> { + Peer peer; + if (searchHit.source() != null) { + String jsonString = new String(searchHit.source()); + try { + peer = objectMapper.readValue(jsonString, Peer.class); + } catch(Exception e) { + if (logger.isDebugEnabled()) { + logger.debug("Error while parsing peer from JSON:\n" + jsonString); + } + throw new JsonSyntaxException("Error while read peer from JSON: " + e.getMessage(), e); + } + } + else { + peer = new Peer(); + SearchHitField field = searchHit.getFields().get("hash"); + peer.setHash(field.getValue()); + } + result.add(peer); + + // If possible, use highlights + if (withHighlight) { + Map<String, HighlightField> fields = searchHit.getHighlightFields(); + for (HighlightField field : fields.values()) { + String blockNameHighLight = field.getFragments()[0].string(); + peer.setHash(blockNameHighLight); + } + } + }); + + return result; + } + + + protected void reportIndexPeersProgress(ProgressionModel progressionModel, String currencyName, Peer peer, int offset, int total) { + int pct = offset * 100 / total; + progressionModel.setCurrent(pct); + + progressionModel.setMessage(I18n.t("duniter4j.es.networkService.indexPeers.progress", currencyName, peer, offset, pct)); + if (logger.isInfoEnabled()) { + logger.info(I18n.t("duniter4j.es.networkService.indexPeers.progress", currencyName, peer, offset, pct)); + } + + } + +} diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceLocator.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceLocator.java index 23609da1..b264f91d 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceLocator.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceLocator.java @@ -33,17 +33,15 @@ import org.duniter.core.client.service.DataContext; import org.duniter.core.client.service.HttpService; import org.duniter.core.client.service.HttpServiceImpl; import org.duniter.core.client.service.bma.*; +import org.duniter.core.client.service.local.*; +import org.duniter.core.client.service.local.NetworkService; import org.duniter.core.client.service.local.CurrencyService; -import org.duniter.core.client.service.local.CurrencyServiceImpl; -import org.duniter.core.client.service.local.PeerService; -import org.duniter.core.client.service.local.PeerServiceImpl; import org.duniter.core.exception.TechnicalException; import org.duniter.core.service.CryptoService; import org.duniter.core.service.Ed25519CryptoServiceImpl; import org.duniter.core.service.MailService; import org.duniter.core.service.MailServiceImpl; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.inject.Injector; import org.elasticsearch.common.inject.Singleton; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.ESLoggerFactory; @@ -95,6 +93,7 @@ public class ServiceLocator .bind(MailService.class, MailServiceImpl.class) .bind(PeerService.class, PeerServiceImpl.class) .bind(CurrencyService.class, CurrencyServiceImpl.class) + .bind(NetworkService.class, NetworkServiceImpl.class) .bind(HttpService.class, HttpServiceImpl.class) .bind(CurrencyDao.class, MemoryCurrencyDaoImpl.class) .bind(PeerDao.class, MemoryPeerDaoImpl.class) diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceModule.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceModule.java index fd377ed5..68d7ccbb 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceModule.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/ServiceModule.java @@ -52,8 +52,9 @@ public class ServiceModule extends AbstractModule implements Module { bind(PluginInit.class).asEagerSingleton(); bind(ChangeService.class).asEagerSingleton(); - // indexation services + // blockchain indexation services bind(BlockchainService.class).asEagerSingleton(); + bind(NetworkService.class).asEagerSingleton(); // Duniter Client API beans bindWithLocator(BlockchainRemoteService.class); diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/threadpool/ThreadPool.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/threadpool/ThreadPool.java index 4818cbf5..3025a909 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/threadpool/ThreadPool.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/threadpool/ThreadPool.java @@ -98,7 +98,7 @@ public class ThreadPool extends AbstractLifecycleComponent<ThreadPool> { public void doClose() {} /** - * Schedules an rest when node is started (all services and modules ready) + * Schedules an rest when node is started (allOfToList services and modules ready) * * @param job the rest to execute when node started * @return a ScheduledFuture who's get will return when the task is complete and throw an exception if it is canceled @@ -233,4 +233,7 @@ public class ThreadPool extends AbstractLifecycleComponent<ThreadPool> { return canContinue; } + public ScheduledExecutorService scheduler() { + return delegate.scheduler(); + } } diff --git a/duniter4j-es-core/src/main/resources/META-INF/services/org.duniter.core.beans.Bean b/duniter4j-es-core/src/main/resources/META-INF/services/org.duniter.core.beans.Bean index 0f39d761..1cf6cb3c 100644 --- a/duniter4j-es-core/src/main/resources/META-INF/services/org.duniter.core.beans.Bean +++ b/duniter4j-es-core/src/main/resources/META-INF/services/org.duniter.core.beans.Bean @@ -9,6 +9,7 @@ org.duniter.core.client.service.HttpServiceImpl org.duniter.core.client.service.DataContext org.duniter.core.client.service.local.PeerServiceImpl org.duniter.core.client.service.local.CurrencyServiceImpl +org.duniter.core.client.service.local.NetworkServiceImpl org.duniter.core.client.dao.mem.MemoryCurrencyDaoImpl org.duniter.core.client.dao.mem.MemoryPeerDaoImpl diff --git a/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_en_GB.properties b/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_en_GB.properties index a6067b35..f7bd9849 100644 --- a/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_en_GB.properties +++ b/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_en_GB.properties @@ -34,6 +34,11 @@ duniter4j.config.option.tasks.queueCapacity.description= duniter4j.config.option.temp.directory.description= duniter4j.config.option.version.description= duniter4j.config.parse.error= +duniter4j.es.networkService.indexPeer= +duniter4j.es.networkService.indexPeers.progress= +duniter4j.es.networkService.indexPeers.remoteParametersError= +duniter4j.es.networkService.indexPeers.succeed= +duniter4j.es.networkService.indexPeers.task= duniter4j.executor.task.waitingExecution= duniter4j.job.stopped= duniter4j.job.stopping= diff --git a/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_fr_FR.properties b/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_fr_FR.properties index 7e587779..f8468a74 100644 --- a/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_fr_FR.properties +++ b/duniter4j-es-core/src/main/resources/i18n/duniter4j-es-core_fr_FR.properties @@ -34,6 +34,11 @@ duniter4j.config.option.tasks.queueCapacity.description= duniter4j.config.option.temp.directory.description= duniter4j.config.option.version.description= duniter4j.config.parse.error= +duniter4j.es.networkService.indexPeer=[%s] Indexing peer [%s]... +duniter4j.es.networkService.indexPeers.progress=[%s] [%s] Indexing peers (%s%%)... +duniter4j.es.networkService.indexPeers.remoteParametersError=[%s] Error when calling [/blockchain/parameters]\: %s +duniter4j.es.networkService.indexPeers.succeed=[%s] [%s] All peers indexed\: found [%s] in [%s ms] +duniter4j.es.networkService.indexPeers.task=[%s] [%s] Indexing peers... duniter4j.executor.task.waitingExecution= duniter4j.job.stopped= duniter4j.job.stopping= diff --git a/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/CitiesRegistryService.java b/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/CitiesRegistryService.java index dadcc55a..b4b30dfb 100644 --- a/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/CitiesRegistryService.java +++ b/duniter4j-es-gchange/src/main/java/org/duniter/elasticsearch/gchange/service/CitiesRegistryService.java @@ -105,7 +105,7 @@ public class CitiesRegistryService extends AbstractService { public void initCities() { if (log.isDebugEnabled()) { - log.debug("Initializing all registry cities"); + log.debug("Initializing allOfToList registry cities"); } //File bulkFile = createCitiesBulkFile2(); diff --git a/pom.xml b/pom.xml index 3896935d..c5c5a9db 100644 --- a/pom.xml +++ b/pom.xml @@ -104,6 +104,7 @@ <module>duniter4j-es-user</module> <module>duniter4j-es-gchange</module> <module>duniter4j-es-assembly</module> + <module>duniter4j-cmd</module> </modules> <scm> @@ -408,7 +409,7 @@ <!-- This is need to override the option version, in configuration classes --> <addDefaultImplementationEntries>true</addDefaultImplementationEntries> <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries> - <!-- Main class, configured in sub-modules --> + <!-- fr.duniter.cmd.Main class, configured in sub-modules --> <mainClass>${maven.jar.main.class}</mainClass> </manifest> <manifestEntries> -- GitLab