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

com.github.alexdlaird.http.DefaultHttpClient Maven / Gradle / Ivy

/*
 * Copyright (c) 2023 Alex Laird
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.github.alexdlaird.http;

import com.github.alexdlaird.util.StringUtils;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.github.alexdlaird.util.StringUtils.isNotBlank;

/**
 * A default client for executing JSON-based HTTP requests.
 *
 * 

Basic Usage

*
 * final HttpClient httpClient = new DefaultHttpClient.Builder();
 *
 * final SomePOJORequest postPojo = new MyPOJO("id", "data");
 * final Response<SomePOJOResponse> postResponse = httpClient.post("http://localhost/pojo",
 *                                                                 postPojo,
 *                                                                 SomePOJOResponse.class);
 *
 * final Response<SomePOJOResponse> getResponse = httpClient.get("http://localhost/pojo/id",
 *                                                                 SomePOJOResponse.class);
 *
 * final SomePOJORequest putPojo = new MyPOJO("updated-data");
 * final Response<SomePOJOResponse> postResponse = httpClient.post("http://localhost/pojo/id",
 *                                                                 putPojo,
 *                                                                 SomePOJOResponse.class);
 *
 * final Response<Map> deleteResponse = httpClient.delete("http://localhost/pojo/id");
 * 
*/ public class DefaultHttpClient implements HttpClient { private static final Logger LOGGER = Logger.getLogger(String.valueOf(DefaultHttpClient.class)); private final Gson gson; private final String encoding; private final String contentType; private final int timeout; private DefaultHttpClient(final Builder builder) { this.encoding = builder.encoding; this.contentType = builder.contentType; this.timeout = builder.timeout; this.gson = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create(); } @Override public Response get(final String url, final List parameters, final Map additionalHeaders, final Class clazz) { try { return execute(urlWithParameters(url, parameters), null, "GET", additionalHeaders, clazz); } catch (IOException e) { throw new HttpClientException("HTTP GET error", e); } } @Override public Response post(final String url, final R request, final List parameters, final Map additionalHeaders, final Class clazz) { try { return execute(urlWithParameters(url, parameters), convertRequestToString(request), "POST", additionalHeaders, clazz); } catch (IOException e) { throw new HttpClientException("HTTP POST error", e); } } @Override public Response put(final String url, final R request, final List parameters, final Map additionalHeaders, final Class clazz) { try { return execute(urlWithParameters(url, parameters), convertRequestToString(request), "PUT", additionalHeaders, clazz); } catch (IOException e) { throw new HttpClientException("HTTP PUT error", e); } } @Override public Response delete(final String url, final List parameters, final Map additionalHeaders, final Class clazz) { try { return execute(urlWithParameters(url, parameters), null, "DELETE", additionalHeaders, clazz); } catch (IOException e) { throw new HttpClientException("HTTP DELETE error", e); } } private void appendDefaultsToConnection(final HttpURLConnection httpUrlConnection, final Map additionalHeaders) { httpUrlConnection.setRequestProperty("Content-Type", contentType); if (additionalHeaders != null) { for (final Map.Entry entry : additionalHeaders.entrySet()) { httpUrlConnection.setRequestProperty(entry.getKey(), entry.getValue()); } } } /** * Override this method if you could like to implement a custom URL connection. * * @param url The URL to connect to. * @return A URL connection. * @throws IOException An I/O exception has occurred. */ protected HttpURLConnection createHttpUrlConnection(final String url) throws IOException { return (HttpURLConnection) new URL(url).openConnection(); } private String convertRequestToString(final T request) { if (request != null) { return gson.toJson(request); } else { return null; } } private T convertResponseFromString(final String response, final Class clazz) { if (isNotBlank(response)) { try { return gson.fromJson(response, clazz); } catch (JsonSyntaxException ex) { return null; } } else { return null; } } private String urlWithParameters(final String url, final List parameters) throws UnsupportedEncodingException { final StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(url); if (parameters != null && parameters.size() > 0) { boolean first = true; for (final Parameter parameter : parameters) { if (!first) { stringBuilder.append("&"); } else { stringBuilder.append("?"); first = false; } stringBuilder.append(URLEncoder.encode(parameter.getName(), encoding)); stringBuilder.append("="); stringBuilder.append(URLEncoder.encode(parameter.getValue(), encoding)); } } return stringBuilder.toString(); } private Response execute(final String url, final String body, final String method, final Map additionalHeaders, final Class clazz) throws IOException { HttpURLConnection httpUrlConnection = null; OutputStream outputStream = null; InputStream inputStream = null; try { httpUrlConnection = createHttpUrlConnection(url); httpUrlConnection.setRequestMethod(method); httpUrlConnection.setConnectTimeout(timeout); httpUrlConnection.setReadTimeout(timeout); appendDefaultsToConnection(httpUrlConnection, additionalHeaders); modifyConnection(httpUrlConnection); if (isNotBlank(body)) { httpUrlConnection.setDoOutput(true); httpUrlConnection.connect(); outputStream = httpUrlConnection.getOutputStream(); outputStream.write(body.getBytes(Charset.forName(encoding))); } else { httpUrlConnection.setRequestProperty("Content-Length", "0"); httpUrlConnection.connect(); } inputStream = httpUrlConnection.getInputStream(); final String responseBody = StringUtils.streamToString(inputStream, Charset.forName(encoding)); return new Response<>(httpUrlConnection.getResponseCode(), convertResponseFromString(responseBody, clazz), responseBody, httpUrlConnection.getHeaderFields()); } catch (Exception ex) { String msg = "An unknown error occurred when performing the operation"; int statusCode = -1; String errorResponse = null; if (httpUrlConnection != null) { try { statusCode = httpUrlConnection.getResponseCode(); errorResponse = StringUtils.streamToString(httpUrlConnection.getErrorStream(), Charset.forName(encoding)); msg = "An error occurred when performing the operation (" + httpUrlConnection.getResponseCode() + "): " + errorResponse; } catch (IOException | NullPointerException ignored) { } } throw new HttpClientException(msg, ex, url, statusCode, errorResponse); } finally { if (httpUrlConnection != null) { httpUrlConnection.disconnect(); } try { if (outputStream != null) { outputStream.close(); } if (inputStream != null) { inputStream.close(); } } catch (IOException ex) { LOGGER.log(Level.INFO, "Unable to close connection", ex); } } } /** * Builder for a {@link DefaultHttpClient}, see docs for that class for example usage. */ public static class Builder { private String encoding = "UTF-8"; private String contentType = "application/json"; public int timeout = 4000; /** * Default encoding for requests. */ public Builder withEncoding(final String encoding) { this.encoding = encoding; return this; } /** * Default contentType header for requests. */ public Builder withContentType(final String contentType) { this.contentType = contentType; return this; } /** * Default timeout for requests. */ public Builder withTimeout(final int timeout) { this.timeout = timeout; return this; } public DefaultHttpClient build() { return new DefaultHttpClient(this); } } }