io.ucoin.ucoinj.core.client.service.bma.BlockchainRemoteServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for io.ucoin.ucoinj.core.client.service.bma.BlockchainRemoteServiceImpl.java

Source

package io.ucoin.ucoinj.core.client.service.bma;

/*
 * #%L
 * UCoin Java :: Core Client API
 * %%
 * Copyright (C) 2014 - 2016 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 io.ucoin.ucoinj.core.client.config.Configuration;
import io.ucoin.ucoinj.core.client.model.bma.gson.JsonArrayParser;
import io.ucoin.ucoinj.core.client.model.local.Currency;
import io.ucoin.ucoinj.core.client.model.local.Identity;
import io.ucoin.ucoinj.core.client.model.local.Peer;
import io.ucoin.ucoinj.core.client.model.local.Wallet;
import io.ucoin.ucoinj.core.client.model.bma.BlockchainBlock;
import io.ucoin.ucoinj.core.client.model.bma.BlockchainMemberships;
import io.ucoin.ucoinj.core.client.model.bma.BlockchainParameters;
import io.ucoin.ucoinj.core.client.service.ServiceLocator;
import io.ucoin.ucoinj.core.client.service.exception.HttpBadRequestException;
import io.ucoin.ucoinj.core.client.service.exception.PubkeyAlreadyUsedException;
import io.ucoin.ucoinj.core.client.service.exception.UidAlreadyUsedException;
import io.ucoin.ucoinj.core.client.service.exception.UidMatchAnotherPubkeyException;
import io.ucoin.ucoinj.core.exception.TechnicalException;
import io.ucoin.ucoinj.core.service.CryptoService;
import io.ucoin.ucoinj.core.util.ObjectUtils;
import io.ucoin.ucoinj.core.util.StringUtils;
import io.ucoin.ucoinj.core.util.cache.Cache;
import io.ucoin.ucoinj.core.util.cache.SimpleCache;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class BlockchainRemoteServiceImpl extends BaseRemoteServiceImpl implements BlockchainRemoteService {

    private static final Logger log = LoggerFactory.getLogger(BlockchainRemoteServiceImpl.class);

    private static final String JSON_DIVIDEND_ATTR = "\"dividend\":";

    public static final String URL_BASE = "/blockchain";

    public static final String URL_PARAMETERS = URL_BASE + "/parameters";

    public static final String URL_BLOCK = URL_BASE + "/block/%s";

    public static final String URL_BLOCKS_FROM = URL_BASE + "/blocks/%s/%s";

    public static final String URL_BLOCK_CURRENT = URL_BASE + "/current";

    public static final String URL_BLOCK_WITH_UD = URL_BASE + "/with/ud";

    public static final String URL_MEMBERSHIP = URL_BASE + "/membership";

    public static final String URL_MEMBERSHIP_SEARCH = URL_BASE + "/memberships/%s";

    private NetworkRemoteService networkRemoteService;

    private Configuration config;

    // Cache need for wallet refresh : iteration on wallet should not
    // execute a download of the current block
    private Cache<Long, BlockchainBlock> mCurrentBlockCache;

    // Cache on blockchain parameters
    private Cache<Long, BlockchainParameters> mParametersCache;

    public BlockchainRemoteServiceImpl() {
        super();
    }

    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        networkRemoteService = ServiceLocator.instance().getNetworkRemoteService();
        config = Configuration.instance();

        // Initialize caches
        initCaches();
    }

    @Override
    public BlockchainParameters getParameters(long currencyId, boolean useCache) {
        if (!useCache) {
            return getParameters(currencyId);
        } else {
            return mParametersCache.get(currencyId);
        }
    }

    @Override
    public BlockchainParameters getParameters(long currencyId) {
        // get blockchain parameter
        BlockchainParameters result = executeRequest(currencyId, URL_PARAMETERS, BlockchainParameters.class);
        return result;
    }

    @Override
    public BlockchainParameters getParameters(Peer peer) {
        // get blockchain parameter
        BlockchainParameters result = executeRequest(peer, URL_PARAMETERS, BlockchainParameters.class);
        return result;
    }

    @Override
    public BlockchainBlock getBlock(long currencyId, long number) {
        // get blockchain parameter
        String path = String.format(URL_BLOCK, number);
        BlockchainBlock result = executeRequest(currencyId, path, BlockchainBlock.class);
        return result;
    }

    @Override
    public Long getBlockDividend(long currencyId, long number) {
        // get blockchain parameter
        String path = String.format(URL_BLOCK, number);
        String json = executeRequest(currencyId, path, String.class);
        return getDividendFromBlockJson(json);
    }

    @Override
    public BlockchainBlock getBlock(Peer peer, int number) {
        // get blockchain parameter
        String path = String.format(URL_BLOCK, number);
        BlockchainBlock result = executeRequest(peer, path, BlockchainBlock.class);
        return result;
    }

    @Override
    public String getBlockAsJson(Peer peer, int number) {
        // get blockchain parameter
        String path = String.format(URL_BLOCK, number);
        return executeRequest(peer, path, String.class);
    }

    @Override
    public String[] getBlocksAsJson(Peer peer, int count, int from) {
        // get blockchain parameter
        String path = String.format(URL_BLOCKS_FROM, count, from);
        String jsonBlocksStr = executeRequest(peer, path, String.class);

        // Parse only array content, but deserialize array item
        JsonArrayParser parser = new JsonArrayParser();
        return parser.getValuesAsArray(jsonBlocksStr);
    }

    /**
     * Retrieve the current block (with short cache)
     *
     * @return
     */
    public BlockchainBlock getCurrentBlock(long currencyId, boolean useCache) {
        if (!useCache) {
            return getCurrentBlock(currencyId);
        } else {
            return mCurrentBlockCache.get(currencyId);
        }
    }

    @Override
    public BlockchainBlock getCurrentBlock(long currencyId) {
        // get blockchain parameter
        BlockchainBlock result = executeRequest(currencyId, URL_BLOCK_CURRENT, BlockchainBlock.class);
        return result;
    }

    @Override
    public BlockchainBlock getCurrentBlock(Peer peer) {
        // get blockchain parameter
        BlockchainBlock result = executeRequest(peer, URL_BLOCK_CURRENT, BlockchainBlock.class);
        return result;
    }

    @Override
    public Currency getCurrencyFromPeer(Peer peer) {
        BlockchainParameters parameter = getParameters(peer);
        BlockchainBlock firstBlock = getBlock(peer, 0);
        BlockchainBlock lastBlock = getCurrentBlock(peer);

        Currency result = new Currency();
        result.setCurrencyName(parameter.getCurrency());
        result.setFirstBlockSignature(firstBlock.getSignature());
        result.setMembersCount(lastBlock.getMembersCount());
        result.setLastUD(parameter.getUd0());

        return result;
    }

    @Override
    public BlockchainParameters getBlockchainParametersFromPeer(Peer peer) {
        return getParameters(peer);
    }

    @Override
    public long getLastUD(long currencyId) {
        // get block number with UD
        String blocksWithUdResponse = executeRequest(currencyId, URL_BLOCK_WITH_UD, String.class);
        Integer blockNumber = getLastBlockNumberFromJson(blocksWithUdResponse);

        // If no result (this could happen when no UD has been send
        if (blockNumber == null) {
            // get the first UD from currency parameter
            BlockchainParameters parameter = getParameters(currencyId);
            return parameter.getUd0();
        }

        // Get the UD from the last block with UD
        Long lastUD = getBlockDividend(currencyId, blockNumber);

        // Check not null (should never append)
        if (lastUD == null) {
            throw new TechnicalException("Unable to get last UD from server");
        }
        return lastUD.longValue();
    }

    @Override
    public long getLastUD(Peer peer) {
        // get block number with UD
        String blocksWithUdResponse = executeRequest(peer, URL_BLOCK_WITH_UD, String.class);
        Integer blockNumber = getLastBlockNumberFromJson(blocksWithUdResponse);

        // If no result (this could happen when no UD has been send
        if (blockNumber == null) {
            // get the first UD from currency parameter
            BlockchainParameters parameter = getParameters(peer);
            return parameter.getUd0();
        }

        // Get the UD from the last block with UD
        String path = String.format(URL_BLOCK, blockNumber);
        String json = executeRequest(peer, path, String.class);
        Long lastUD = getDividendFromBlockJson(json);

        // Check not null (should never append)
        if (lastUD == null) {
            throw new TechnicalException("Unable to get last UD from server");
        }
        return lastUD.longValue();
    }

    /**
     * Check is a identity is not already used by a existing member
     *
     * @param peer
     * @param identity
     * @throws UidAlreadyUsedException    if UID already used by another member
     * @throws PubkeyAlreadyUsedException if pubkey already used by another member
     */
    public void checkNotMemberIdentity(Peer peer, Identity identity)
            throws UidAlreadyUsedException, PubkeyAlreadyUsedException {
        ObjectUtils.checkNotNull(peer);
        ObjectUtils.checkNotNull(identity);
        ObjectUtils.checkArgument(StringUtils.isNotBlank(identity.getUid()));
        ObjectUtils.checkArgument(StringUtils.isNotBlank(identity.getPubkey()));

        // Read membership data from the UID
        BlockchainMemberships result = getMembershipByPubkeyOrUid(peer, identity.getUid());

        // uid already used by another member
        if (result != null) {
            throw new UidAlreadyUsedException(
                    String.format("User identifier '%s' is already used by another member", identity.getUid()));
        }

        result = getMembershipByPubkeyOrUid(peer, identity.getPubkey());

        // pubkey already used by another member
        if (result != null) {
            throw new PubkeyAlreadyUsedException(
                    String.format("Pubkey key '%s' is already used by another member", identity.getPubkey()));
        }
    }

    /**
     * Check is a wallet is a member, and load its attribute isMember and certTimestamp
     *
     * @param wallet
     * @throws UidMatchAnotherPubkeyException is uid already used by another pubkey
     */
    public void loadAndCheckMembership(Peer peer, Wallet wallet) throws UidMatchAnotherPubkeyException {
        ObjectUtils.checkNotNull(wallet);

        // Load membership data
        loadMembership(null, peer, wallet.getIdentity(), true);

        // Something wrong on pubkey : uid already used by another pubkey !
        if (wallet.getIdentity().getIsMember() == null) {
            throw new UidMatchAnotherPubkeyException(wallet.getPubKeyHash());
        }
    }

    /**
     * Load identity attribute isMember and timestamp
     *
     * @param identity
     */
    public void loadMembership(long currencyId, Identity identity, boolean checkLookupForNonMember) {
        loadMembership(currencyId, null, identity, checkLookupForNonMember);
    }

    public BlockchainMemberships getMembershipByUid(long currencyId, String uid) {
        ObjectUtils.checkArgument(StringUtils.isNotBlank(uid));

        BlockchainMemberships result = getMembershipByPubkeyOrUid(currencyId, uid);
        if (result == null || !uid.equals(result.getUid())) {
            return null;
        }
        return result;
    }

    public BlockchainMemberships getMembershipByPublicKey(long currencyId, String pubkey) {
        ObjectUtils.checkArgument(StringUtils.isNotBlank(pubkey));

        BlockchainMemberships result = getMembershipByPubkeyOrUid(currencyId, pubkey);
        if (result == null || !pubkey.equals(result.getPubkey())) {
            return null;
        }
        return result;
    }

    /**
     * Request to integrate the wot
     */
    public void requestMembership(Wallet wallet) {
        ObjectUtils.checkNotNull(wallet);
        ObjectUtils.checkNotNull(wallet.getCurrencyId());

        BlockchainBlock block = getCurrentBlock(wallet.getCurrencyId());

        // Compute membership document
        String membership = getMembership(wallet, block, true /*sideIn*/);

        if (log.isDebugEnabled()) {
            log.debug(String.format("Will send membership document: \n------\n%s------", membership));
        }

        List<NameValuePair> urlParameters = new ArrayList<NameValuePair>();
        urlParameters.add(new BasicNameValuePair("membership", membership));

        HttpPost httpPost = new HttpPost(getPath(wallet.getCurrencyId(), URL_MEMBERSHIP));
        try {
            httpPost.setEntity(new UrlEncodedFormEntity(urlParameters));
        } catch (UnsupportedEncodingException e) {
            throw new TechnicalException(e);
        }

        String membershipResult = executeRequest(httpPost, String.class);
        if (log.isDebugEnabled()) {
            log.debug("received from /tx/process: " + membershipResult);
        }

        executeRequest(httpPost, String.class);
    }

    public BlockchainMemberships getMembershipByPubkeyOrUid(long currencyId, String uidOrPubkey) {
        String path = String.format(URL_MEMBERSHIP_SEARCH, uidOrPubkey);

        // search blockchain membership
        try {
            BlockchainMemberships result = executeRequest(currencyId, path, BlockchainMemberships.class);
            return result;
        } catch (HttpBadRequestException e) {
            log.debug("No member matching this pubkey or uid: " + uidOrPubkey);
            return null;
        }
    }

    public BlockchainMemberships getMembershipByPubkeyOrUid(Peer peer, String uidOrPubkey) {
        String path = String.format(URL_MEMBERSHIP_SEARCH, uidOrPubkey);

        // search blockchain membership
        try {
            BlockchainMemberships result = executeRequest(peer, path, BlockchainMemberships.class);
            return result;
        } catch (HttpBadRequestException e) {
            log.debug("No member matching this pubkey or uid: " + uidOrPubkey);
            return null;
        }
    }

    public String getMembership(Wallet wallet, BlockchainBlock block, boolean sideIn) {

        // Create the member ship document
        String membership = getMembership(wallet.getUid(), wallet.getPubKeyHash(), wallet.getCurrency(),
                block.getNumber(), block.getHash(), sideIn, wallet.getCertTimestamp());

        // Add signature
        CryptoService cryptoService = ServiceLocator.instance().getCryptoService();
        String signature = cryptoService.sign(membership, wallet.getSecKey());

        return new StringBuilder().append(membership).append(signature).append('\n').toString();
    }

    /**
     * Get UD, by block number
     *
     * @param currencyId
     * @param startOffset
     * @return
     */
    public Map<Integer, Long> getUDs(long currencyId, long startOffset) {
        log.debug(String.format("Getting block's UD from block [%s]", startOffset));

        int[] blockNumbersWithUD = getBlocksWithUD(currencyId);

        Map<Integer, Long> result = new LinkedHashMap<Integer, Long>();

        //         Insert the UD0 (if need)
        //        if (startOffset <= 0) {
        //            BlockchainParameters parameters = getParameters(currencyId, true/*with cache*/);
        //            result.put(0, parameters.getUd0());
        //        }

        boolean previousBlockInsert = false;
        if (blockNumbersWithUD != null && blockNumbersWithUD.length != 0) {
            Integer previousBlockNumberWithUd = null;
            for (Integer blockNumber : blockNumbersWithUD) {
                if (blockNumber >= startOffset) {
                    if (!previousBlockInsert) {
                        Long previousUd = getParameters(currencyId, true/*with cache*/).getUd0();
                        Integer previousBlockNumber = 0;
                        if (previousBlockNumberWithUd != null) {
                            previousUd = getBlockDividend(currencyId, previousBlockNumberWithUd);
                            if (previousUd == null) {
                                throw new TechnicalException(String.format(
                                        "Unable to get UD from server block [%s]", previousBlockNumberWithUd));
                            }
                            previousBlockNumber = previousBlockNumberWithUd;
                        }
                        result.put(previousBlockNumber, previousUd);
                        previousBlockInsert = true;
                    }
                    Long ud = getBlockDividend(currencyId, blockNumber);
                    // Check not null (should never append)
                    if (ud == null) {
                        throw new TechnicalException(
                                String.format("Unable to get UD from server block [%s]", blockNumber));
                    }
                    result.put(blockNumber, ud);
                } else {
                    previousBlockNumberWithUd = blockNumber;
                }
            }
        } else {
            result.put(0, getParameters(currencyId, true/*with cache*/).getUd0());
        }

        return result;
    }

    /* -- Internal methods -- */

    /**
     * Initialize caches
     */
    protected void initCaches() {
        int cacheTimeInMillis = config.getNetworkCacheTimeInMillis();

        mCurrentBlockCache = new SimpleCache<Long, BlockchainBlock>(cacheTimeInMillis) {
            @Override
            public BlockchainBlock load(Long currencyId) {
                return getCurrentBlock(currencyId);
            }
        };

        mParametersCache = new SimpleCache<Long, BlockchainParameters>(/*eternal cache*/) {
            @Override
            public BlockchainParameters load(Long currencyId) {
                return getParameters(currencyId);
            }
        };
    }

    protected void loadMembership(Long currencyId, Peer peer, Identity identity, boolean checkLookupForNonMember) {
        ObjectUtils.checkNotNull(identity);
        ObjectUtils.checkArgument(StringUtils.isNotBlank(identity.getUid()));
        ObjectUtils.checkArgument(StringUtils.isNotBlank(identity.getPubkey()));
        ObjectUtils.checkArgument(peer != null || currencyId != null);

        // Read membership data from the UID
        BlockchainMemberships result = peer != null ? getMembershipByPubkeyOrUid(peer, identity.getUid())
                : getMembershipByPubkeyOrUid(currencyId, identity.getUid());

        // uid not used = not was member
        if (result == null) {
            identity.setMember(false);

            if (checkLookupForNonMember) {
                WotRemoteService wotService = ServiceLocator.instance().getWotRemoteService();
                Identity lookupIdentity = peer != null
                        ? wotService.getIdentity(peer, identity.getUid(), identity.getPubkey())
                        : wotService.getIdentity(currencyId, identity.getUid(), identity.getPubkey());

                // Self certification exists, update the cert timestamp
                if (lookupIdentity != null) {
                    identity.setTimestamp(lookupIdentity.getTimestamp());
                }

                // Self certification not exists: make sure the cert time is cleaning
                else {
                    identity.setTimestamp(-1);
                }
            }
        }

        // UID and pubkey is a member: fine
        else if (identity.getPubkey().equals(result.getPubkey())) {
            identity.setMember(true);
            identity.setTimestamp(result.getSigDate());
        }

        // Something wrong on pubkey : uid already used by anither pubkey !
        else {
            identity.setMember(null);
        }

    }

    private int[] getBlocksWithUD(long currencyId) {
        log.debug("Getting blocks with UD");

        String json = executeRequest(currencyId, URL_BLOCK_WITH_UD, String.class);

        int startIndex = json.indexOf("[");
        int endIndex = json.lastIndexOf(']');

        if (startIndex == -1 || endIndex == -1) {
            return null;
        }

        String blockNumbersStr = json.substring(startIndex + 1, endIndex).trim();

        if (StringUtils.isBlank(blockNumbersStr)) {
            return null;
        }

        String[] blockNumbers = blockNumbersStr.split(",");
        int[] result = new int[blockNumbers.length];
        try {
            int i = 0;
            for (String blockNumber : blockNumbers) {
                result[i++] = Integer.parseInt(blockNumber.trim());
            }
        } catch (NumberFormatException e) {
            if (log.isDebugEnabled()) {
                log.debug(String.format("Bad format of the response '%s'.", URL_BLOCK_WITH_UD));
            }
            throw new TechnicalException("Unable to read block with UD numbers: " + e.getMessage(), e);
        }

        return result;
    }

    private String getMembership(String uid, String publicKey, String currency, long blockNumber, String blockHash,
            boolean sideIn, long certificationTime) {
        StringBuilder result = new StringBuilder().append("Version: 1\n").append("Type: Membership\n")
                .append("Currency: ").append(currency).append('\n').append("Issuer: ").append(publicKey)
                .append('\n').append("Block: ").append(blockNumber).append('-').append(blockHash).append('\n')
                .append("Membership: ").append(sideIn ? "IN" : "OUT").append('\n').append("UserID: ").append(uid)
                .append('\n').append("CertTS: ").append(certificationTime).append('\n');

        return result.toString();
    }

    private Integer getLastBlockNumberFromJson(final String json) {

        int startIndex = json.lastIndexOf(',');
        int endIndex = json.lastIndexOf(']');
        if (startIndex == -1 || endIndex == -1) {
            return null;
        }

        String blockNumberStr = json.substring(startIndex + 1, endIndex).trim();
        try {
            return Integer.parseInt(blockNumberStr);
        } catch (NumberFormatException e) {
            if (log.isDebugEnabled()) {
                log.debug("Could not parse JSON (block numbers)");
            }
            throw new TechnicalException("Could not parse server response");
        }
    }

    protected Long getDividendFromBlockJson(String blockJson) {

        int startIndex = blockJson.indexOf(JSON_DIVIDEND_ATTR);
        if (startIndex == -1) {
            return null;
        }
        startIndex += JSON_DIVIDEND_ATTR.length();
        int endIndex = blockJson.indexOf(',', startIndex);
        if (endIndex == -1) {
            return null;
        }

        String dividendStr = blockJson.substring(startIndex, endIndex).trim();
        if (dividendStr.length() == 0 || "null".equals(dividendStr)) {
            return null;
        }

        return Long.parseLong(dividendStr);
    }

}