com.ethercamp.harmony.service.PeersService.java Source code

Java tutorial

Introduction

Here is the source code for com.ethercamp.harmony.service.PeersService.java

Source

/*
 * Copyright 2015, 2016 Ether.Camp Inc. (US)
 * This file is part of Ethereum Harmony.
 *
 * Ethereum Harmony 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.
 *
 * Ethereum Harmony 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 Ethereum Harmony.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.ethercamp.harmony.service;

import com.ethercamp.harmony.dto.PeerDTO;
import com.maxmind.geoip.Country;
import com.maxmind.geoip.LookupService;
import lombok.extern.slf4j.Slf4j;
import org.ethereum.facade.Ethereum;
import org.ethereum.listener.EthereumListenerAdapter;
import org.ethereum.net.eth.message.EthMessageCodes;
import org.ethereum.net.message.Message;
import org.ethereum.net.rlpx.Node;
import org.ethereum.net.rlpx.discover.NodeManager;
import org.ethereum.net.rlpx.discover.NodeStatistics;
import org.ethereum.net.server.Channel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.*;

import static java.util.stream.Collectors.*;

/**
 * Service for rendering list of peers and geographic activity in ethereum network.
 *
 * Created by Stan Reshetnyk on 25.07.16.
 */
@Service
@Slf4j(topic = "harmony")
public class PeersService {

    private Optional<LookupService> lookupService = Optional.empty();

    private final Map<String, Locale> localeMap = new HashMap<>();

    @Autowired
    private NodeManager nodeManager;

    @Autowired
    private ClientMessageService clientMessageService;

    @Autowired
    private Ethereum ethereum;

    @Autowired
    private Environment env;

    @PostConstruct
    private void postConstruct() {
        // gather blocks to calculate hash rate
        ethereum.addListener(new EthereumListenerAdapter() {
            @Override
            public void onRecvMessage(Channel channel, Message message) {
                // notify client about new block
                // using PeerDTO as it already has both country fields
                if (message.getCommand() == EthMessageCodes.NEW_BLOCK) {
                    clientMessageService.sendToTopic("/topic/newBlockFrom",
                            createPeerDTO(channel.getPeerId(), channel.getInetSocketAddress().getHostName(), 0l,
                                    0.0, 0, true, null, Optional.of(channel.getEthHandler().getBestKnownBlock())
                                            .map(b -> b.getNumber()).orElse(0L)));
                }
            }
        });

        createGeoDatabase();
    }

    /**
     * Reads discovered and active peers from ethereum and sends to client.
     */
    @Scheduled(fixedRate = 1500)
    private void doSendPeersInfo() {
        // #1 Read discovered nodes. Usually ~150 nodes
        final List<Node> nodes = nodeManager.getTable().getAllNodes().stream().map(n -> n.getNode())
                .collect(toList());

        // #2 Convert active peers to DTO
        final List<PeerDTO> resultPeers = ethereum.getChannelManager().getActivePeers().stream()
                .map(channel -> createPeerDTO(channel.getPeerId(), channel.getNode().getHost(),
                        channel.getNodeStatistics().lastPongReplyTime.get(), channel.getPeerStats().getAvgLatency(),
                        channel.getNodeStatistics().getReputation(), true, channel.getNodeStatistics(),
                        channel.getEthHandler().getBestKnownBlock().getNumber()))
                .collect(toList());

        // #3 Convert discovered peers to DTO and add to result
        nodes.forEach(node -> {
            boolean isPeerAdded = resultPeers.stream()
                    .anyMatch(addedPeer -> addedPeer.getNodeId().equals(node.getHexId()));
            if (!isPeerAdded) {
                NodeStatistics nodeStatistics = nodeManager.getNodeStatistics(node);
                resultPeers
                        .add(createPeerDTO(node.getHexId(), node.getHost(), nodeStatistics.lastPongReplyTime.get(),
                                0.0, nodeStatistics.getReputation(), false, null, 0));
            }
        });

        clientMessageService.sendToTopic("/topic/peers", resultPeers);
    }

    private String getPeerDetails(NodeStatistics nodeStatistics, String country, long maxBlockNumber) {
        final String countryRow = "Country: " + country;

        if (nodeStatistics == null || nodeStatistics.getClientId() == null) {
            return countryRow;
        }

        final String delimiter = "\n";
        final String blockNumber = "Block number: #"
                + NumberFormat.getNumberInstance(Locale.US).format(maxBlockNumber);
        final String clientId = StringUtils.trimWhitespace(nodeStatistics.getClientId());
        final String details = "Details: " + clientId;
        final String supports = "Supported protocols: " + nodeStatistics.capabilities.stream()
                .filter(c -> c != null).map(c -> StringUtils.capitalize(c.getName()) + ": " + c.getVersion())
                .collect(joining(", "));

        final String[] array = clientId.split("/");
        if (array.length >= 4) {
            final String type = "Type: " + array[0];
            final String os = "OS: " + StringUtils.capitalize(array[2]);
            final String version = "Version: " + array[3];

            return String.join(delimiter, type, os, version, countryRow, "", details, supports, blockNumber);
        } else {
            return String.join(delimiter, countryRow, details, supports, blockNumber);
        }
    }

    private PeerDTO createPeerDTO(String peerId, String ip, long lastPing, double avgLatency, int reputation,
            boolean isActive, NodeStatistics nodeStatistics, long maxBlockNumber) {
        // code or ""

        final Optional<Country> country = lookupService.map(service -> service.getCountry(ip));
        final String country2Code = country.map(c -> c.getCode()).orElse("");

        // code or ""
        final String country3Code = iso2CountryCodeToIso3CountryCode(country2Code);

        return new PeerDTO(peerId, ip, country3Code, country2Code, lastPing, avgLatency, reputation, isActive,
                getPeerDetails(nodeStatistics, country.map(Country::getName).orElse("Unknown location"),
                        maxBlockNumber));
    }

    /**
     * Create MaxMind lookup service to find country by IP.
     * IPv6 is not used.
     */
    private void createGeoDatabase() {
        final String[] countries = Locale.getISOCountries();
        final Optional<String> dbFilePath = Optional.ofNullable(env.getProperty("maxmind.file"));

        for (String country : countries) {
            Locale locale = new Locale("", country);
            localeMap.put(locale.getISO3Country().toUpperCase(), locale);
        }
        lookupService = dbFilePath.flatMap(path -> {
            try {
                return Optional.ofNullable(new LookupService(path,
                        LookupService.GEOIP_MEMORY_CACHE | LookupService.GEOIP_CHECK_CACHE));
            } catch (IOException e) {
                log.error("Problem finding maxmind database at " + path + ". " + e.getMessage());
                log.error(
                        "Wasn't able to create maxmind location service. Country information will not be available.");
                return Optional.empty();
            }
        });
    }

    private String iso2CountryCodeToIso3CountryCode(String iso2CountryCode) {
        Locale locale = new Locale("", iso2CountryCode);
        try {
            return locale.getISO3Country();
        } catch (MissingResourceException e) {
            // silent
        }
        return "";
    }

}