Java tutorial
package org.duniter.core.client.service; /* * #%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 com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.utils.URIBuilder; import org.apache.http.config.SocketConfig; import org.apache.http.conn.ConnectTimeoutException; import org.apache.http.entity.ContentType; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.util.EntityUtils; import org.duniter.core.beans.InitializingBean; import org.duniter.core.client.config.Configuration; import org.duniter.core.client.config.ConfigurationOption; 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.bma.BmaTechnicalException; import org.duniter.core.client.service.exception.*; import org.duniter.core.exception.TechnicalException; import org.duniter.core.util.ObjectUtils; import org.duniter.core.util.StringUtils; import org.duniter.core.util.cache.SimpleCache; import org.duniter.core.util.websocket.WebsocketClientEndpoint; import org.nuiton.i18n.I18n; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.net.ConnectException; import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.ServiceConfigurationError; /** * Created by eis on 05/02/15. */ public class HttpServiceImpl implements HttpService, Closeable, InitializingBean { private static final Logger log = LoggerFactory.getLogger(HttpServiceImpl.class); public static final String URL_PEER_ALIVE = "/blockchain/parameters"; private PoolingHttpClientConnectionManager connectionManager; protected ObjectMapper objectMapper; protected Peer defaultPeer; private boolean debug; protected Joiner pathJoiner = Joiner.on('/'); protected SimpleCache<Integer, RequestConfig> requestConfigCache; protected SimpleCache<Integer, HttpClient> httpClientCache; protected Map<URI, WebsocketClientEndpoint> wsEndPoints = new HashMap<>(); public HttpServiceImpl() { super(); this.debug = log.isDebugEnabled(); } @Override public void afterPropertiesSet() throws Exception { // Initialize caches initCaches(); this.objectMapper = JacksonUtils.newObjectMapper(); } /** * Initialize caches */ protected void initCaches() { Configuration config = Configuration.instance(); int cacheTimeInMillis = config.getNetworkCacheTimeInMillis(); final int defaultTimeout = config.getNetworkTimeout() > 0 ? config.getNetworkTimeout() : Integer.parseInt(ConfigurationOption.NETWORK_TIMEOUT.getDefaultValue()); requestConfigCache = new SimpleCache<Integer, RequestConfig>(cacheTimeInMillis * 100) { @Override public RequestConfig load(Integer timeout) { // Use config default timeout, if 0 if (timeout <= 0) timeout = defaultTimeout; return createRequestConfig(timeout); } }; httpClientCache = new SimpleCache<Integer, HttpClient>(cacheTimeInMillis * 100) { @Override public HttpClient load(Integer timeout) { return createHttpClient(timeout); } }; httpClientCache.registerRemoveListener(item -> { log.debug("Closing HttpClient..."); closeQuietly(item); }); } public void connect(Peer peer) throws PeerConnectionException { if (peer == null) { throw new IllegalArgumentException("argument 'peer' must not be null"); } if (peer == defaultPeer) { return; } HttpGet httpGet = new HttpGet(getPath(peer, URL_PEER_ALIVE)); boolean isPeerAlive; try { isPeerAlive = executeRequest(httpClientCache.get(0/*=default timeout*/), httpGet); } catch (TechnicalException e) { this.defaultPeer = null; throw new PeerConnectionException(e); } if (!isPeerAlive) { this.defaultPeer = null; throw new PeerConnectionException("Unable to connect to peer: " + peer.toString()); } this.defaultPeer = peer; } public boolean isConnected() { return this.defaultPeer != null; } @Override public void close() throws IOException { httpClientCache.clear(); requestConfigCache.clear(); if (wsEndPoints.size() != 0) { for (WebsocketClientEndpoint clientEndPoint : wsEndPoints.values()) { clientEndPoint.close(); } wsEndPoints.clear(); } connectionManager.close(); } public <T> T executeRequest(HttpUriRequest request, Class<? extends T> resultClass) { return executeRequest(httpClientCache.get(0), request, resultClass); } public <T> T executeRequest(HttpUriRequest request, Class<? extends T> resultClass, Class<?> errorClass) { //return executeRequest(httpClientCache.get(0), request, resultClass, errorClass); return executeRequest(createHttpClient(0), request, resultClass, errorClass); } public <T> T executeRequest(String absolutePath, Class<? extends T> resultClass) { HttpGet httpGet = new HttpGet(getPath(absolutePath)); return executeRequest(httpClientCache.get(0), httpGet, resultClass); } public <T> T executeRequest(Peer peer, String absolutePath, Class<? extends T> resultClass) { HttpGet httpGet = new HttpGet(peer.getUrl() + absolutePath); return executeRequest(httpClientCache.get(0), httpGet, resultClass); } public String getPath(Peer peer, String... absolutePath) { String path = "/" + pathJoiner.skipNulls().join(absolutePath); return peer.getUrl() + path.replaceAll("//+", "/"); } public String getPath(String... absolutePath) { checkDefaultPeer(); String pathToAppend = pathJoiner.skipNulls().join(absolutePath); String result = pathJoiner.join(defaultPeer.getUrl(), pathToAppend); return result; } public URIBuilder getURIBuilder(URI baseUri, String... path) { String pathToAppend = pathJoiner.skipNulls().join(path); int customQueryStartIndex = pathToAppend.indexOf('?'); String customQuery = null; if (customQueryStartIndex != -1) { customQuery = pathToAppend.substring(customQueryStartIndex + 1); pathToAppend = pathToAppend.substring(0, customQueryStartIndex); } URIBuilder builder = new URIBuilder(baseUri); builder.setPath(baseUri.getPath() + pathToAppend); if (StringUtils.isNotBlank(customQuery)) { builder.setCustomQuery(customQuery); } return builder; } /* -- Internal methods -- */ protected void checkDefaultPeer() { if (defaultPeer == null) { throw new IllegalStateException("No peer to connect"); } } protected PoolingHttpClientConnectionManager createConnectionManager(int maxTotalConnections, int maxConnectionsPerRoute, int timeout) { PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(maxTotalConnections); connectionManager.setDefaultMaxPerRoute(maxConnectionsPerRoute); connectionManager.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(timeout).build()); return connectionManager; } protected HttpClient createHttpClient(int timeout) { if (connectionManager == null) { Configuration config = Configuration.instance(); connectionManager = createConnectionManager(config.getNetworkMaxTotalConnections(), config.getNetworkMaxConnectionsPerRoute(), config.getNetworkTimeout()); } return HttpClients.custom().setConnectionManager(connectionManager) .setDefaultRequestConfig(requestConfigCache.get(timeout)).build(); } protected RequestConfig createRequestConfig(int timeout) { return RequestConfig.custom().setSocketTimeout(timeout).setConnectTimeout(timeout).setMaxRedirects(1) .setCookieSpec(CookieSpecs.IGNORE_COOKIES).build(); } protected <T> T executeRequest(HttpClient httpClient, HttpUriRequest request, Class<? extends T> resultClass) { return executeRequest(httpClient, request, resultClass, Error.class); } @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); if (debug) { log.debug("Received response : " + response.getStatusLine()); } switch (response.getStatusLine().getStatusCode()) { case HttpStatus.SC_OK: { if (resultClass == null || resultClass.equals(HttpResponse.class)) { result = (T) response; } else { result = (T) parseResponse(request, response, resultClass); EntityUtils.consume(response.getEntity()); } break; } case HttpStatus.SC_UNAUTHORIZED: case HttpStatus.SC_FORBIDDEN: throw new HttpUnauthorizeException(I18n.t("duniter4j.client.authentication")); case HttpStatus.SC_NOT_FOUND: throw new HttpNotFoundException(I18n.t("duniter4j.client.notFound", request.toString())); case HttpStatus.SC_BAD_REQUEST: try { Object errorResponse = parseResponse(request, response, errorClass); if (errorResponse instanceof Error) { throw new HttpBadRequestException((Error) errorResponse); } else { throw new HttpBadRequestException(errorResponse.toString()); } } 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: String defaultMessage = I18n.t("duniter4j.client.status", request.toString(), response.getStatusLine().toString()); if (isContentType(response, ContentType.APPLICATION_JSON)) { JsonNode node = objectMapper.readTree(response.getEntity().getContent()); if (node.hasNonNull("ucode")) { throw new BmaTechnicalException(node.get("ucode").asInt(), node.get("message").asText(defaultMessage)); } } throw new TechnicalException(defaultMessage); } } catch (ConnectException e) { throw new HttpConnectException(I18n.t("duniter4j.client.core.connect", request.toString()), e); } catch (SocketTimeoutException | ConnectTimeoutException e) { throw new HttpTimeoutException(I18n.t("duniter4j.client.core.timeout"), e); } catch (TechnicalException e) { throw e; } catch (Throwable e) { throw new TechnicalException(e.getMessage(), e); } finally { // Close is need if (response instanceof CloseableHttpResponse) { try { ((CloseableHttpResponse) response).close(); } catch (IOException e) { // Silent is gold } } } // 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; } protected Object parseResponse(HttpUriRequest request, HttpResponse response, Class<?> ResultClass) throws IOException { Object result = null; boolean isStreamContent = ResultClass == null || ResultClass.equals(InputStream.class); boolean isStringContent = !isStreamContent && ResultClass != null && ResultClass.equals(String.class); InputStream content = response.getEntity().getContent(); // If should return an inputstream if (isStreamContent) { result = content; // must be close by caller } // If should return String else if (isStringContent) { try { String stringContent = getContentAsString(content); // Add a debug before returning the result if (log.isDebugEnabled()) { log.debug("Parsing response:\n" + stringContent); } return stringContent; } finally { if (content != null) { content.close(); } } } // deserialize Json else { try { result = readValue(content, ResultClass); } catch (Exception e) { String requestPath = request.getURI().toString(); // Check if content-type error ContentType contentType = ContentType.getOrDefault(response.getEntity()); String actualMimeType = contentType.getMimeType(); if (!ObjectUtils.equals(ContentType.APPLICATION_JSON.getMimeType(), actualMimeType)) { throw new TechnicalException(I18n.t("duniter4j.client.core.invalidResponseContentType", requestPath, ContentType.APPLICATION_JSON.getMimeType(), actualMimeType)); } // throw a generic error throw new TechnicalException(I18n.t("duniter4j.client.core.invalidResponse", requestPath), e); } finally { if (content != null) { content.close(); } } } if (result == null) { throw new TechnicalException( I18n.t("duniter4j.client.core.emptyResponse", request.getURI().toString())); } return result; } private boolean isContentType(HttpResponse response, ContentType expectedContentType) { if (response.getEntity() == null) return false; ContentType contentType = ContentType.getOrDefault(response.getEntity()); String actualMimeType = contentType.getMimeType(); return ObjectUtils.equals(expectedContentType.getMimeType(), actualMimeType); } protected String getContentAsString(InputStream content) throws IOException { Reader reader = new InputStreamReader(content, StandardCharsets.UTF_8); StringBuilder result = new StringBuilder(); char[] buf = new char[64]; int len = 0; while ((len = reader.read(buf)) != -1) { result.append(buf, 0, len); } return result.toString(); } protected boolean executeRequest(HttpClient httpClient, HttpUriRequest request) { if (log.isDebugEnabled()) { log.debug("Executing request : " + request.getRequestLine()); } try { HttpResponse response = httpClient.execute(request); switch (response.getStatusLine().getStatusCode()) { case HttpStatus.SC_OK: { response.getEntity().consumeContent(); return true; } case HttpStatus.SC_UNAUTHORIZED: case HttpStatus.SC_FORBIDDEN: throw new TechnicalException(I18n.t("duniter4j.client.authentication")); default: throw new TechnicalException( I18n.t("duniter4j.client.status", response.getStatusLine().toString())); } } catch (ConnectException e) { throw new TechnicalException(I18n.t("duniter4j.client.core.connect"), e); } catch (IOException e) { throw new TechnicalException(e.getMessage(), e); } } public static void closeQuietly(HttpClient httpClient) { try { if (httpClient instanceof CloseableHttpClient) { ((CloseableHttpClient) httpClient).close(); } else if (httpClient instanceof Closeable) { ((Closeable) httpClient).close(); } } catch (IOException e) { // silent is gold } } public WebsocketClientEndpoint getWebsocketClientEndpoint(Peer peer, String path, boolean autoReconnect) { try { URI wsBlockURI = new URI(String.format("%s://%s:%s%s", peer.isUseSsl() ? "wss" : "ws", peer.getHost(), peer.getPort(), path)); // Get the websocket, or open new one if not exists WebsocketClientEndpoint wsClientEndPoint = wsEndPoints.get(wsBlockURI); if (wsClientEndPoint == null || wsClientEndPoint.isClosed()) { log.info(String.format("Starting to listen on [%s]...", wsBlockURI.toString())); wsClientEndPoint = new WebsocketClientEndpoint(wsBlockURI, autoReconnect); wsEndPoints.put(wsBlockURI, wsClientEndPoint); } return wsClientEndPoint; } catch (URISyntaxException | ServiceConfigurationError ex) { throw new TechnicalException( String.format("Could not create URI need for web socket [%s]: %s", path, ex.getMessage())); } } public <T> T readValue(String json, Class<T> clazz) throws IOException { return objectMapper.readValue(json, clazz); } public <T> T readValue(byte[] json, Class<T> clazz) throws IOException { return objectMapper.readValue(json, clazz); } public <T> T readValue(InputStream json, Class<T> clazz) throws IOException { return objectMapper.readValue(new InputStreamReader(json, Charsets.UTF_8.name()), clazz); } }