Java tutorial
/* * S4 Java client library * Copyright 2016 Ontotext AD * * 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. */ 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, * 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 uee) { // should never happen throw new RuntimeException("JVM claims not to support UTF-8 encoding...", uee); } } 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 <code>null</code>, 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 <code>null</code> for a 201 response * @throws HttpClientException if an exception occurs during processing, * or the server returns a 4xx or 5xx error response */ public <T> T request(String target, String method, TypeReference<T> 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 <code>null</code> 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 <code>null</code> for a 201 response * @throws HttpClientException if an exception occurs during processing, * or the server returns a 4xx or 5xx error response */ 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. * @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> T readResponseOrError(HttpURLConnection connection, TypeReference<T> responseType) throws HttpClientException { return readResponseOrError(connection, responseType, true); } /** * Read a response or error message from the given connection, handling any 303 redirect responses * if <code>followRedirects</code> is true. */ private <T> T readResponseOrError(HttpURLConnection connection, TypeReference<T> 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 <code>null</code> 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> T get(String target, TypeReference<T> 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 <code>null</code> * @return for a successful response, the deserialized response body, or <code>null</code> 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> T post(String target, TypeReference<T> 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 <code>null</code> * @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<JsonNode>() { }, 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<JsonNode>() { }, 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); } } }