nextflow.fs.dx.api.DxHttpClient.java Source code

Java tutorial

Introduction

Here is the source code for nextflow.fs.dx.api.DxHttpClient.java

Source

/*
 * 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;
    }
}