Java tutorial
/* * Copyright (c) 2013, the authors. * * This file is part of 'DXFS'. * * DXFS 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. * * DXFS 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 DXFS. If not, see <http://www.gnu.org/licenses/>. */ package nextflow.fs.dx.api; // Copyright (C) 2013 DNAnexus, Inc. // // This file is part of dx-toolkit (DNAnexus platform client libraries). // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain a // copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. import java.io.IOException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.routing.HttpRoute; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.scheme.SchemeSocketFactory; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.PoolingClientConnectionManager; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Class for making a raw DNAnexus API call via HTTP. */ public class DxHttpClient { private static Logger log = LoggerFactory.getLogger(DxHttpClient.class); private static final int NUM_RETRIES = 5; private final JsonNode securityContext; private final String apiserver; private final HttpClient httpclient; static class Holder { static final DxHttpClient INSTANCE = new DxHttpClient(); } public static DxHttpClient getInstance() { return Holder.INSTANCE; } /** * @return The underlying {@code HttpClient} object */ public HttpClient http() { return httpclient; } /** * Creates the http client using a thread safe {@code PoolingClientConnectionManager} * * See http://hc.apache.org/httpcomponents-client-4.2.x/tutorial/html/connmgmt.html#d5e581 * * @authors Paolo Di Tommaso <paolo.ditommaso@gmail.com> * * @return */ protected DxHttpClient() { log.debug("Creating DxHttpClient object"); try { DxEnv env = DxEnv.getInstance(); securityContext = env.getSecurityContext(); apiserver = env.getApiserverPath(); final String PROT = env.getApiserverProtocol(); final String HOST = env.getApiserverHost(); final int PORT = env.getApiserverPort(); SchemeRegistry schemeRegistry = new SchemeRegistry(); SchemeSocketFactory factory = "https".equals(PROT) ? SSLSocketFactory.getSocketFactory() : PlainSocketFactory.getSocketFactory(); schemeRegistry.register(new Scheme(PROT, PORT, factory)); PoolingClientConnectionManager manager = new PoolingClientConnectionManager(schemeRegistry); manager.setMaxTotal(100); manager.setDefaultMaxPerRoute(10); manager.setMaxPerRoute(new HttpRoute(new HttpHost(HOST, PORT)), 50); httpclient = new DefaultHttpClient(manager); } catch (IOException e) { throw new IllegalStateException(e); } } private String errorMessage(String method, String resource, String errorString, int retryWait, int nextRetryNum, int maxRetries) { String baseError = method + " " + resource + ": " + errorString + "."; if (nextRetryNum <= maxRetries) { return baseError + " Waiting " + retryWait + " seconds before retry " + nextRetryNum + " of " + maxRetries; } else { return baseError; } } /** * Holds either the raw text of a response or a parsed JSON version of it. */ private static class ParsedResponse { public final String responseText; public final JsonNode responseJson; public ParsedResponse(String responseText, JsonNode responseJson) { this.responseText = responseText; this.responseJson = responseJson; } } /** * Issues a request against the specified resource and returns either the * text of the response or the parsed JSON of the response (depending on * whether parseResponse is set). */ private ParsedResponse requestImpl(String resource, String data, boolean parseResponse) throws IOException { HttpPost request = new HttpPost(apiserver + resource); request.setHeader("Content-Type", "application/json"); request.setHeader("Authorization", securityContext.get("auth_token_type").textValue() + " " + securityContext.get("auth_token").textValue()); request.setEntity(new StringEntity(data)); // Retry with exponential backoff int timeout = 1; for (int i = 0; i <= NUM_RETRIES; i++) { HttpResponse response = null; boolean okToRetry = false; try { response = httpclient.execute(request); } catch (ClientProtocolException e) { log.error(errorMessage("POST", resource, e.toString(), timeout, i + 1, NUM_RETRIES)); } catch (IOException e) { log.error(errorMessage("POST", resource, e.toString(), timeout, i + 1, NUM_RETRIES)); } if (response != null) { int statusCode = response.getStatusLine().getStatusCode(); HttpEntity entity = response.getEntity(); if (statusCode == HttpStatus.SC_OK) { // 200 OK byte[] value = EntityUtils.toByteArray(entity); int realLength = value.length; if (entity.getContentLength() >= 0 && realLength != entity.getContentLength()) { String errorStr = "Received response of " + realLength + " bytes but Content-Length was " + entity.getContentLength(); log.error(errorMessage("POST", resource, errorStr, timeout, i + 1, NUM_RETRIES)); } else { if (parseResponse) { JsonNode responseJson = null; try { responseJson = DxJson.parseJson(new String(value, "UTF-8")); } catch (JsonProcessingException e) { if (entity.getContentLength() < 0) { // content-length was not provided, and the // JSON could not be parsed. Retry since // this is a streaming request from the // server that probably just encountered a // transient error. } else { throw e; } } if (responseJson != null) { return new ParsedResponse(null, responseJson); } } else { return new ParsedResponse(new String(value, "UTF-8"), null); } } } else { // Non-200 status codes. // 500 InternalError should get retried. 4xx errors should // be considered not recoverable. if (statusCode < 500) { throw new IOException(EntityUtils.toString(entity)); } else { log.error(errorMessage("POST", resource, EntityUtils.toString(entity), timeout, i + 1, NUM_RETRIES)); } } } if (i < NUM_RETRIES) { try { Thread.sleep(timeout * 1000); } catch (InterruptedException e) { log.debug("Stopped sleep caused by: {}", e.getMessage()); } timeout *= 2; } } throw new IOException("POST " + resource + " failed"); } /** * Issues a request against the specified resource and returns the result * as a String. */ public String request(String resource, String data) throws IOException { return requestImpl(resource, data, false).responseText; } /** * Issues a request against the specified resource and returns the result * as a JSON object. */ public JsonNode request(String resource, JsonNode data) throws IOException { String dataAsString = data.toString(); return requestImpl(resource, dataAsString, true).responseJson; } }