org.neo4j.ogm.drivers.http.request.HttpRequest.java Source code

Java tutorial

Introduction

Here is the source code for org.neo4j.ogm.drivers.http.request.HttpRequest.java

Source

/*
 * Copyright (c) 2002-2018 "Neo Technology,"
 * Network Engine for Objects in Lund AB [http://neotechnology.com]
 *
 * This product is licensed to you under the Apache License, Version 2.0 (the "License").
 * You may not use this product except in compliance with the License.
 *
 * This product may include a number of subcomponents with
 * separate copyright notices and license terms. Your use of the source
 * code for these subcomponents is subject to the terms and
 *  conditions of the subcomponent's license, as noted in the LICENSE file.
 */

package org.neo4j.ogm.drivers.http.request;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.NoHttpResponseException;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.neo4j.ogm.config.Credentials;
import org.neo4j.ogm.config.ObjectMapperFactory;
import org.neo4j.ogm.drivers.http.response.GraphModelResponse;
import org.neo4j.ogm.drivers.http.response.GraphRowsModelResponse;
import org.neo4j.ogm.drivers.http.response.RestModelResponse;
import org.neo4j.ogm.drivers.http.response.RowModelResponse;
import org.neo4j.ogm.exception.ConnectionException;
import org.neo4j.ogm.exception.ResultProcessingException;
import org.neo4j.ogm.model.GraphModel;
import org.neo4j.ogm.model.GraphRowListModel;
import org.neo4j.ogm.model.RestModel;
import org.neo4j.ogm.model.RowModel;
import org.neo4j.ogm.request.DefaultRequest;
import org.neo4j.ogm.request.GraphModelRequest;
import org.neo4j.ogm.request.GraphRowListModelRequest;
import org.neo4j.ogm.request.Request;
import org.neo4j.ogm.request.RestModelRequest;
import org.neo4j.ogm.request.RowModelRequest;
import org.neo4j.ogm.request.Statement;
import org.neo4j.ogm.request.Statements;
import org.neo4j.ogm.response.EmptyResponse;
import org.neo4j.ogm.response.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * @author Vince Bickers
 * @author Luanne Misquitta
 * @author Michael J. Simons
 */
public class HttpRequest implements Request {

    private static final Logger LOGGER = LoggerFactory.getLogger(HttpRequest.class);
    private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.objectMapper();
    private static final String JSON_PARSE_ERROR_EXCEPTION_MESSAGE = "Could not parse the servers response as JSON";

    private final String url;
    private final CloseableHttpClient httpClient;
    private final Credentials credentials;
    private final boolean readOnly;

    public HttpRequest(CloseableHttpClient httpClient, String url, Credentials credentials) {
        this(httpClient, url, credentials, false);
    }

    public HttpRequest(CloseableHttpClient httpClient, String url, Credentials credentials, boolean readOnly) {
        this.httpClient = httpClient;
        this.url = url;
        this.credentials = credentials;
        this.readOnly = readOnly;
    }

    @Override
    public Response<GraphModel> execute(GraphModelRequest request) {
        if (request.getStatement().length() == 0) {
            return new EmptyResponse();
        } else {
            String cypher = cypherRequest(request);
            return new GraphModelResponse(executeRequest(cypher));
        }
    }

    @Override
    public Response<RowModel> execute(RowModelRequest request) {
        if (request.getStatement().length() == 0) {
            return new EmptyResponse();
        } else {
            String cypher = cypherRequest(request);
            return new RowModelResponse(executeRequest(cypher));
        }
    }

    @Override
    public Response<RowModel> execute(DefaultRequest query) {
        Statements statements = new Statements(query.getStatements());
        String cypher = cypherRequest(statements);
        return new RowModelResponse(executeRequest(cypher));
    }

    @Override
    public Response<GraphRowListModel> execute(GraphRowListModelRequest request) {
        if (request.getStatement().length() == 0) {
            return new EmptyResponse();
        } else {
            String cypher = cypherRequest(request);
            return new GraphRowsModelResponse(executeRequest(cypher));
        }
    }

    @Override
    public Response<RestModel> execute(RestModelRequest request) {
        if (request.getStatement().length() == 0) {
            return new EmptyResponse();
        } else {
            String cypher = cypherRequest(request);
            return new RestModelResponse(executeRequest(cypher));
        }
    }

    // we use the OBJECT_MAPPER to create the request string from the statement.
    // this driver is the only one that needs to do this, because the request format
    // is different for each type of request - GraphModelRequest/RowModelRequest, etc
    private String cypherRequest(Statement statement) {
        List<Statement> statementList = new ArrayList<>();
        statementList.add(statement);
        try {
            return OBJECT_MAPPER.writeValueAsString(new Statements(statementList));
        } catch (JsonProcessingException jpe) {
            throw new ResultProcessingException("Could not create JSON due to " + jpe.getLocalizedMessage(), jpe);
        }
    }

    private String cypherRequest(Statements statements) {
        try {
            return OBJECT_MAPPER.writeValueAsString(statements);
        } catch (JsonProcessingException jpe) {
            throw new ResultProcessingException("Could not create JSON due to " + jpe.getLocalizedMessage(), jpe);
        }
    }

    private CloseableHttpResponse executeRequest(String cypher) throws HttpRequestException {

        String url = this.url;

        assert (url != null);

        HttpPost request = new HttpPost(url);

        request.setEntity(new StringEntity(cypher, "UTF-8"));
        request.setHeader("X-WRITE", readOnly ? "0" : "1");

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Thread: {}, url: {}, request: {}", Thread.currentThread().getId(), url, cypher);
        }

        return execute(httpClient, request, credentials);
    }

    public static CloseableHttpResponse execute(CloseableHttpClient httpClient, HttpRequestBase request,
            Credentials credentials) throws HttpRequestException {

        LOGGER.debug("Thread: {}, request: {}", Thread.currentThread().getId(), request);

        CloseableHttpResponse response;

        request.setHeader(new BasicHeader(HTTP.CONTENT_TYPE, "application/json;charset=UTF-8"));
        request.setHeader(new BasicHeader(HTTP.USER_AGENT, "neo4j-ogm.java/2.0"));
        request.setHeader(new BasicHeader("Accept", "application/json;charset=UTF-8"));

        HttpAuthorization.authorize(request, credentials);

        // use defaults: 3 retries, 2 second wait between attempts
        RetryOnExceptionStrategy retryStrategy = new RetryOnExceptionStrategy();

        while (retryStrategy.shouldRetry()) {

            try {

                response = httpClient.execute(request);

                StatusLine statusLine = response.getStatusLine();
                HttpEntity responseEntity = response.getEntity();

                if (statusLine.getStatusCode() >= 300) {
                    String responseText = statusLine.getReasonPhrase();
                    if (responseEntity != null) {
                        responseText = parseError(EntityUtils.toString(responseEntity));
                        LOGGER.warn("Thread: {}, response: {}", Thread.currentThread().getId(), responseText);
                    }
                    throw new HttpResponseException(statusLine.getStatusCode(), responseText);
                }
                if (responseEntity == null) {
                    throw new ClientProtocolException("Response contains no content");
                }

                return response; // don't close response yet, it is not consumed!
            }

            // if we didn't get a response at all, try again
            catch (NoHttpResponseException nhre) {
                LOGGER.warn("Thread: {}, No response from server:  Retrying in {} milliseconds, retries left: {}",
                        Thread.currentThread().getId(), retryStrategy.getTimeToWait(),
                        retryStrategy.numberOfTriesLeft);
                retryStrategy.errorOccurred();
            } catch (RetryException re) {
                throw new HttpRequestException(request, re);
            } catch (ClientProtocolException uhe) {
                throw new ConnectionException(request.getURI().toString(), uhe);
            } catch (IOException ioe) {
                throw new HttpRequestException(request, ioe);
            }

            // here we catch any exception we throw above (plus any we didn't throw ourselves),
            // log the problem, close any connection held by the request
            // and then rethrow the exception to the caller.
            catch (Exception exception) {
                LOGGER.warn("Thread: {}, exception: {}", Thread.currentThread().getId(),
                        exception.getCause().getLocalizedMessage());
                request.releaseConnection();
                throw exception;
            }
        }
        throw new RuntimeException("Fatal Exception: Should not have occurred!");
    }

    static class RetryOnExceptionStrategy {

        public static final int DEFAULT_RETRIES = 3;
        public static final long DEFAULT_WAIT_TIME_IN_MILLI = 2000;

        private int numberOfRetries;
        private int numberOfTriesLeft;
        private long timeToWait;

        public RetryOnExceptionStrategy() {
            this(DEFAULT_RETRIES, DEFAULT_WAIT_TIME_IN_MILLI);
        }

        public RetryOnExceptionStrategy(int numberOfRetries, long timeToWait) {
            this.numberOfRetries = numberOfRetries;
            numberOfTriesLeft = numberOfRetries;
            this.timeToWait = timeToWait;
        }

        /**
         * @return true if there are tries left
         */
        public boolean shouldRetry() {
            return numberOfTriesLeft > 0;
        }

        public void errorOccurred() {
            numberOfTriesLeft--;
            if (!shouldRetry()) {
                throw new RetryException("Retry Failed: Total " + numberOfRetries + " attempts made at interval "
                        + getTimeToWait() + "ms");
            }
            waitUntilNextTry();
        }

        public long getTimeToWait() {
            return timeToWait;
        }

        private void waitUntilNextTry() {
            try {
                Thread.sleep(getTimeToWait());
            } catch (InterruptedException ignored) {
            }
        }
    }

    private static String parseError(String responseBody) {
        try {
            final JsonNode responseNode = OBJECT_MAPPER.readTree(responseBody);
            final JsonNode errors = responseNode.findValue("errors");
            if (errors.elements().hasNext()) {
                final JsonNode errorNode = errors.elements().next();
                return errorNode.findValue("message").asText();
            } else {
                return responseBody;
            }
        } catch (JsonParseException e) {
            // Don't return the responseBody here as it is logged in #execute
            // See: https://www.owasp.org/index.php/Log_Injection, returning
            // it above should be rethought as well.
            return JSON_PARSE_ERROR_EXCEPTION_MESSAGE;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    static class RetryException extends RuntimeException {

        public RetryException(String msg) {
            super(msg);
        }
    }
}