All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ontotext.s4.client.HttpClient Maven / Gradle / Ivy

/*
 * S4 Java client library
 * Copyright (c) 2014, Ontotext AD, All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.
 */
package com.ontotext.s4.client;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.zip.GZIPInputStream;

import javax.xml.bind.DatatypeConverter;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;

/**
 * Client responsible for communication with the S4 API. Handles
 * authentication, and serialization and deserialization of JSON request
 * and response bodies.
 * 
 */
public class HttpClient {

  private final ObjectMapper MAPPER = new ObjectMapper().disable(
          DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
          .setInjectableValues(
                  new InjectableValues.Std().addValue(HttpClient.class, this));

  private final XmlMapper XML_MAPPER = (XmlMapper)new XmlMapper().disable(
          DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
          .setInjectableValues(
                  new InjectableValues.Std().addValue(HttpClient.class, this));

  /**
   * The standard base URI for the S4 API.
   */
  public static final URL DEFAULT_BASE_URL;
  static {
    try {
      DEFAULT_BASE_URL = new URL("https://text.s4.ontotext.com/");
    } catch(MalformedURLException e) {
      // can't happen
      throw new ExceptionInInitializerError(e);
    }
  }

  /**
   * The HTTP basic authentication header that will be appended to all
   * requests.
   */
  private String authorizationHeader;

  /**
   * The base URL that will be used to resolve any relative request
   * URIs.
   */
  private URL baseUrl;

  /**
   * Create a client that uses the {@link #DEFAULT_BASE_URL default base
   * URL}.
   * 
   * @param apiKeyId API key identifier for authentication
   * @param apiPassword API key password
   */
  public HttpClient(String apiKeyId, String apiPassword) {
    this(DEFAULT_BASE_URL, apiKeyId, apiPassword);
  }

  /**
   * Create a client using a specified base URL (for advanced use only -
   * the default URL will work for all normal cases).
   * 
   * @param url API base URL
   * @param apiKeyId API key identifier for authentication
   * @param apiPassword API key password
   */
  public HttpClient(URL url, String apiKeyId, String apiPassword) {
    baseUrl = url;
    try {
      // HTTP header is "Basic base64(username:password)"
      authorizationHeader =
              "Basic "
                      + DatatypeConverter
                              .printBase64Binary((apiKeyId + ":" + apiPassword)
                                      .getBytes("UTF-8"));
    } catch(UnsupportedEncodingException e) {
      // should never happen
      throw new RuntimeException("JVM claims not to support UTF-8 encoding...",
              e);
    }
  }

  public URL getBaseUrl() {
    return baseUrl;
  }

  /**
   * Make an API request and parse the JSON response into a new object.
   * 
   * @param target the URL to request (relative URLs will resolve
   *          against the {@link #getBaseUrl() base URL}).
   * @param method the request method (GET, POST, DELETE, etc.)
   * @param responseType the Java type corresponding to a successful
   *          response message for this URL
   * @param requestBody the object that should be serialized to JSON as
   *          the request body. If null no request body is
   *          sent
   * @param extraHeaders any additional HTTP headers, specified as an
   *          alternating sequence of header names and values
   * @return for a successful response, the deserialized response body,
   *         or null for a 201 response
   * @throws HttpClientException if an exception occurs during
   *           processing, or the server returns a 4xx or 5xx error
   *           response (in which case the response JSON message will be
   *           available as a {@link JsonNode} in the exception).
   */
  public  T request(String target, String method,
          TypeReference responseType, Object requestBody,
          String... extraHeaders) throws HttpClientException {
    try {
      HttpURLConnection connection =
              sendRequest(target, method, requestBody, extraHeaders);
      return readResponseOrError(connection, responseType);
    } catch(IOException e) {
      throw new HttpClientException(e);
    }
  }

  /**
   * Make an API request and return the raw data from the response as an
   * InputStream.
   * 
   * @param target the URL to request (relative URLs will resolve
   *          against the {@link #getBaseUrl() base URL}).
   * @param method the request method (GET, POST, DELETE, etc.)
   * @param requestBody the object that should be serialized to JSON as
   *          the request body. If null no request body is
   *          sent
   * @param extraHeaders any additional HTTP headers, specified as an
   *          alternating sequence of header names and values
   * @return for a successful response, the response stream, or
   *         null for a 201 response
   * @throws HttpClientException if an exception occurs during
   *           processing, or the server returns a 4xx or 5xx error
   *           response (in which case the response JSON message will be
   *           available as a {@link JsonNode} in the exception).
   */
  public InputStream requestForStream(String target, String method,
          Object requestBody, String... extraHeaders)
          throws HttpClientException {
    try {
      HttpURLConnection connection =
              sendRequest(target, method, requestBody, extraHeaders);
      int responseCode = connection.getResponseCode();
      if(responseCode == HttpURLConnection.HTTP_NO_CONTENT) {
        // successful response with no content
        return null;
      } else if(responseCode >= 400) {
        readError(connection);
        return null; // not reachable, readError always throws exception
      } else if(responseCode >= 300) {
        // redirect - all redirects we care about from the S4
        // APIs are 303. We have to follow them manually to make
        // authentication work properly.
        String location = connection.getHeaderField("Location");
        // consume body
        InputStream stream = connection.getInputStream();
        IOUtils.copy(stream, new NullOutputStream());
        IOUtils.closeQuietly(stream);
        // follow the redirect
        return requestForStream(location, method, requestBody, extraHeaders);
      } else {
        return connection.getInputStream();
      }
    } catch(IOException e) {
      throw new HttpClientException(e);
    }

  }

  /**
   * Make an API request and parse the JSON response, using the response
   * to update the state of an existing object.
   * 
   * @param target the URL to request (relative URLs will resolve
   *          against the {@link #getBaseUrl() base URL}).
   * @param method the request method (GET, POST, DELETE, etc.)
   * @param responseObject the Java object to update from a successful
   *          response message for this URL
   * @param requestBody the object that should be serialized to JSON as
   *          the request body. If null no request body is
   *          sent
   * @param extraHeaders any additional HTTP headers, specified as an
   *          alternating sequence of header names and values
   * @throws HttpClientException if an exception occurs during
   *           processing, or the server returns a 4xx or 5xx error
   *           response (in which case the response JSON message will be
   *           available as a {@link JsonNode} in the exception).
   */
  public void requestForUpdate(String target, String method,
          Object responseObject, Object requestBody, String... extraHeaders)
          throws HttpClientException {
    try {
      HttpURLConnection connection =
              sendRequest(target, method, requestBody, extraHeaders);
      readResponseOrErrorForUpdate(connection, responseObject);
    } catch(IOException e) {
      throw new HttpClientException(e);
    }
  }

  /**
   * Handles the sending side of an HTTP request, returning a connection
   * from which the response (or error) can be read.
   */
  private HttpURLConnection sendRequest(String target, String method,
          Object requestBody, String... extraHeaders) throws IOException {
    URL requestUrl = new URL(baseUrl, target);
    HttpURLConnection connection =
            (HttpURLConnection)requestUrl.openConnection();
    connection.setRequestMethod(method);
    connection.setInstanceFollowRedirects(false);
    connection.setRequestProperty("Authorization", authorizationHeader);
    boolean sentAccept = false;
    if(extraHeaders != null) {
      for(int i = 0; i < extraHeaders.length; i++) {
        if("Accept".equals(extraHeaders[i])) sentAccept = true;
        connection.setRequestProperty(extraHeaders[i], extraHeaders[++i]);
      }
    }
    if(!sentAccept)
      connection.setRequestProperty("Accept", "application/json");
    if(requestBody != null) {
      connection.setDoOutput(true);
      connection.setRequestProperty("Content-Type", "application/json");
      OutputStream out = connection.getOutputStream();
      try {
        MAPPER.writeValue(out, requestBody);
      } finally {
        out.close();
      }
    }
    return connection;
  }

  /**
   * Read a response or error message from the given connection,
   * handling any 303 redirect responses.
   */
  private  T readResponseOrError(HttpURLConnection connection,
          TypeReference responseType) throws HttpClientException {
    return readResponseOrError(connection, responseType, true);
  }

  /**
   * Read a response or error message from the given connection,
   * handling any 303 redirect responses if followRedirects
   * is true.
   */
  private  T readResponseOrError(HttpURLConnection connection,
          TypeReference responseType, boolean followRedirects)
          throws HttpClientException {
    InputStream stream = null;
    try {
      int responseCode = connection.getResponseCode();
      if(responseCode == HttpURLConnection.HTTP_NO_CONTENT) {
        // successful response with no content
        return null;
      }
      String encoding = connection.getContentEncoding();
      if("gzip".equalsIgnoreCase(encoding)) {
        stream = new GZIPInputStream(connection.getInputStream());
      } else {
        stream = connection.getInputStream();
      }

      if(responseCode < 300 || responseCode >= 400 || !followRedirects) {
        try {
          return MAPPER.readValue(stream, responseType);
        } finally {
          stream.close();
        }
      } else {
        // redirect - all redirects we care about from the S4
        // APIs are 303. We have to follow them manually to make
        // authentication work properly.
        String location = connection.getHeaderField("Location");
        // consume body
        IOUtils.copy(stream, new NullOutputStream());
        IOUtils.closeQuietly(stream);
        // follow the redirect
        return get(location, responseType);
      }
    } catch(Exception e) {
      readError(connection);
      return null; // unreachable, as readError always throws exception
    }
  }

  /**
   * Read a response or error message from the given connection, and
   * update the state of the given object.
   */
  private void readResponseOrErrorForUpdate(HttpURLConnection connection,
          Object responseObject) throws HttpClientException {
    InputStream stream = null;
    try {
      if(connection.getResponseCode() == HttpURLConnection.HTTP_NO_CONTENT) {
        // successful response with no content
        return;
      }
      stream = connection.getInputStream();
      try {
        MAPPER.readerForUpdating(responseObject).readValue(stream);
      } finally {
        stream.close();
      }
    } catch(Exception e) {
      readError(connection);
    }
  }

  /**
   * Read an error response from the given connection and throw a
   * suitable {@link HttpClientException}. This method always throws an
   * exception, it will never return normally.
   */
  private void readError(HttpURLConnection connection)
          throws HttpClientException {
    InputStream stream;
    try {
      String encoding = connection.getContentEncoding();
      if("gzip".equalsIgnoreCase(encoding)) {
        stream = new GZIPInputStream(connection.getInputStream());
      } else {
        stream = connection.getInputStream();
      }

      InputStreamReader reader = new InputStreamReader(stream, "UTF-8");

      try {
        JsonNode errorNode = null;
        if(connection.getContentType().contains("json")) {
          errorNode = MAPPER.readTree(stream);
        } else if(connection.getContentType().contains("xml")) {
          errorNode = XML_MAPPER.readTree(stream);
        }

        throw new HttpClientException("Server returned response code "
                + connection.getResponseCode(), errorNode);

      } finally {
        reader.close();
      }
    } catch(HttpClientException e2) {
      throw e2;
    } catch(Exception e2) {
      throw new HttpClientException("Error communicating with server", e2);
    }
  }

  /**
   * Perform an HTTP GET request, parsing the JSON response to create a
   * new object.
   * 
   * @param target the URL to request (relative URLs will resolve
   *          against the {@link #getBaseUrl() base URL}).
   * @param responseType the Java type corresponding to a successful
   *          response message for this URL
   * @return for a successful response, the deserialized response body,
   *         or null for a 201 response
   * @throws HttpClientException if an exception occurs during
   *           processing, or the server returns a 4xx or 5xx error
   *           response (in which case the response JSON message will be
   *           available as a {@link JsonNode} in the exception).
   */
  public  T get(String target, TypeReference responseType)
          throws HttpClientException {
    return request(target, "GET", responseType, null);
  }

  /**
   * Perform an HTTP GET request, parsing the JSON response to update
   * the state of an existing object.
   * 
   * @param target the URL to request (relative URLs will resolve
   *          against the {@link #getBaseUrl() base URL}).
   * @param responseObject the Java object to update from a successful
   *          response message for this URL
   * @throws HttpClientException if an exception occurs during
   *           processing, or the server returns a 4xx or 5xx error
   *           response (in which case the response JSON message will be
   *           available as a {@link JsonNode} in the exception).
   */
  public void getForUpdate(String target, Object responseObject)
          throws HttpClientException {
    requestForUpdate(target, "GET", responseObject, null);
  }

  /**
   * Perform an HTTP POST request, parsing the JSON response to create a
   * new object.
   * 
   * @param target the URL to request (relative URLs will resolve
   *          against the {@link #getBaseUrl() base URL}).
   * @param responseType the Java type corresponding to a successful
   *          response message for this URL
   * @param requestBody the object that should be serialized to JSON as
   *          the request body. POST requests require a request body, so
   *          this parameter must not be null
   * @return for a successful response, the deserialized response body,
   *         or null for a 201 response
   * @throws HttpClientException if an exception occurs during
   *           processing, or the server returns a 4xx or 5xx error
   *           response (in which case the response JSON message will be
   *           available as a {@link JsonNode} in the exception).
   */
  public  T post(String target, TypeReference responseType,
          Object requestBody) throws HttpClientException {
    return request(target, "POST", responseType, requestBody);
  }

  /**
   * Perform an HTTP POST request, parsing the JSON response to update
   * the state of an existing object.
   * 
   * @param target the URL to request (relative URLs will resolve
   *          against the {@link #getBaseUrl() base URL}).
   * @param responseObject the Java object to update from a successful
   *          response message for this URL
   * @param requestBody the object that should be serialized to JSON as
   *          the request body. POST requests require a request body, so
   *          this parameter must not be null
   * @throws HttpClientException if an exception occurs during
   *           processing, or the server returns a 4xx or 5xx error
   *           response (in which case the response JSON message will be
   *           available as a {@link JsonNode} in the exception).
   */
  public void postForUpdate(String target, Object responseObject,
          Object requestBody) throws HttpClientException {
    requestForUpdate(target, "POST", responseObject, requestBody);
  }

  /**
   * Perform an HTTP DELETE request for the given resource.
   * 
   * @param target the URL to request (relative URLs will resolve
   *          against the {@link #getBaseUrl() base URL}).
   * @throws HttpClientException if an exception occurs during
   *           processing, or the server returns a 4xx or 5xx error
   *           response (in which case the response JSON message will be
   *           available as a {@link JsonNode} in the exception).
   */
  public void delete(String target) throws HttpClientException {
    request(target, "DELETE", new TypeReference() {
    }, null);
  }

  /**
   * Perform an HTTP GET request on a URL whose response is expected to
   * be a 3xx redirection, and return the target redirection URL.
   * 
   * @param source the URL to request (relative URLs will resolve
   *          against the {@link #getBaseUrl() base URL}).
   * @return the URL returned by the "Location" header of the
   *         redirection response.
   * @throws HttpClientException if an exception occurs during
   *           processing, or the server returns a 4xx or 5xx error
   *           response (in which case the response JSON message will be
   *           available as a {@link JsonNode} in the exception), or if
   *           the response was not a 3xx redirection.
   */
  public URL getRedirect(URL source) throws HttpClientException {
    try {
      HttpURLConnection connection = (HttpURLConnection)source.openConnection();
      connection.setRequestMethod("GET");
      connection.setRequestProperty("Authorization", authorizationHeader);
      connection.setRequestProperty("Accept", "application/json");
      connection.setInstanceFollowRedirects(false);
      int responseCode = connection.getResponseCode();
      // make sure we read any response content
      readResponseOrError(connection, new TypeReference() {
      }, false);
      if(responseCode >= 300 && responseCode < 400) {
        // it was a redirect
        String redirectUrl = connection.getHeaderField("Location");
        return new URL(redirectUrl);
      } else {
        throw new HttpClientException("Expected redirect but got "
                + responseCode);
      }
    } catch(IOException e) {
      throw new HttpClientException(e);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy