September 21, 2019

HTTP Client in Java 8 HttpURLConnection

The standard HTTP Client in Java 8 is HttpURLConnection, but working with it is not so intuitively, so I have written a small helper class, that does:

  • All HTTP method: GET, POST, PUT, DELETE, etc.
  • Sets HTTP Request Headers
  • Sets HTTP POST Body
  • Read response
  • Read response Body
  • Read response Headers

package se.magnuskkarlsson.example.httpurlconnection;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HttpURLConnectionBuilder {

    // See javax.ws.rs.core.HttpHeaders

    // Body type in request
    public static final String CONTENT_TYPE = "Content-Type: application/json";

    // Accepted response by client
    public static final String ACCEPT = "Accept";

    // See javax.ws.rs.core.MediaType

    // Media Type 'application/json'
    public final static String APPLICATION_JSON = "application/json";

    private URL url;
    private String method;
    private Map<String, String> headers = new HashMap<String, String>();
    private String body;

    public HttpURLConnectionBuilder() {
    }

    // ----------------------- Logic Methods -----------------------

    public HttpURLConnectionBuilder get(URL url) {
        return method(url, "GET");
    }

    public HttpURLConnectionBuilder post(URL url) {
        return method(url, "POST");
    }

    public HttpURLConnectionBuilder put(URL url) {
        return method(url, "PUT");
    }

    public HttpURLConnectionBuilder delete(URL url) {
        return method(url, "DELETE");
    }

    public HttpURLConnectionBuilder method(URL url, String method) {
        this.url = url;
        this.method = method;
        return this;
    }

    public HttpURLConnectionBuilder header(String name, String value) {
        this.headers.put(name, value);
        return this;
    }

    public HttpURLConnectionBuilder body(String body) {
        this.body = body;
        return this;
    }

    public HttpURLConnectionResponse send() throws IOException {
        HttpURLConnection conn = (HttpURLConnection) this.url.openConnection();

        // Set Request Method
        conn.setRequestMethod(this.method);

        // Set Request Headers
        for (String name : this.headers.keySet()) {
            conn.setRequestProperty(name, this.headers.get(name));
        }

        // Trace Log Request
        System.out.println(conn.getRequestMethod() + " " + conn.getURL());
        for (String key : conn.getRequestProperties().keySet()) {
            System.out.println(
                    "  >> " + key + ": " + conn.getRequestProperty(key));
        }

        // Set Request Body
        if (this.body != null) {
            conn.setDoOutput(true);
            try (OutputStreamWriter output = new OutputStreamWriter(
                    new BufferedOutputStream(conn.getOutputStream()),
                    StandardCharsets.UTF_8)) {

                output.write(this.body);
            }
        }

        // Send Request and Get Response Code
        int responseCode = conn.getResponseCode();

        // Get Response Body
        InputStream responseStream = null;
        if (responseCode >= 200 && responseCode <= 299) {

            responseStream = conn.getInputStream();
        } else {

            responseStream = conn.getErrorStream();
        }
        String responseBody = null;
        try (BufferedReader in = new BufferedReader(new InputStreamReader(
                responseStream, StandardCharsets.UTF_8))) {

            StringBuilder builder = new StringBuilder();
            for (String line = null; (line = in.readLine()) != null;) {
                builder.append(line);
            }
            responseBody = builder.toString();
        }

        // Get Response Headers
        Map<String, List<String>> responseHeaders = conn.getHeaderFields();

        // Trace Log Response
        System.out.println("<< " + conn.getHeaderField(null));
        for (String key : conn.getHeaderFields().keySet()) {
            if (key != null) {
                System.out.println(
                        "  << " + key + ": " + conn.getHeaderField(key));
            }
        }

        return new HttpURLConnectionResponse(responseCode, responseBody,
                responseHeaders);
    }

    // ----------------------- Helper Methods -----------------------

    // ----------------------- Get and Set Methods -----------------------

}

And the response class is a simple immutable POJO.


package se.magnuskkarlsson.example.httpurlconnection;

import java.util.List;
import java.util.Map;

public class HttpURLConnectionResponse {

    private final int statusCode;
    private final String body;
    private final Map<String, List<String>> headers;

    public HttpURLConnectionResponse(int statusCode, String body,
            Map<String, List<String>> headers) {

        this.statusCode = statusCode;
        this.body = body;
        this.headers = headers;
    }

    // ----------------------- Logic Methods -----------------------

    // ----------------------- Helper Methods -----------------------

    // ----------------------- Get and Set Methods -----------------------

    public int getStatusCode() {
        return statusCode;
    }

    public String getBody() {
        return body;
    }

    public Map<String, List<String>> getHeaders() {
        return headers;
    }

}

Now lets test it, with a simple REST service.


package se.magnuskkarlsson.example.httpurlconnection;

import java.util.logging.Logger;

import javax.json.Json;
import javax.json.JsonObject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;

@Path("/persons")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class PersonResource {

    private final Logger log = Logger.getLogger(PersonResource.class.getName());

    // ----------------------- Logic Methods -----------------------

    // Common Response Codes For All Requests
    // 200 OK Request was successful
    // 401 Unauthorized User not authenticated
    // 403 Forbidden User not authorized for endpoint request, often due to
    // missing security privileges

    // Common Error Response Codes For GET
    // 404 Not Found Resource not found
    // 400 Bad Request Filter parameters provided were not valid Recommended
    // Response Codes

    @GET
    @Path("/{personId}")
    public Response getById(@PathParam("personId") String personId) {
        if ("404".equals(personId)) {

            JsonObject json = createProblemDetails(null, "Not Found", 404,
                    "Person with ID " + personId + " does not exists.", null);
            return Response.status(Response.Status.NOT_FOUND).entity(json)
                    .build();
        }
        JsonObject json = Json.createObjectBuilder().add("personId", personId)
                .add("name", "Magnus").build();
        return Response.ok(json).build();
    }

    // Common Response Codes For POST
    // 200 OK A custom action or processing request was successful
    // 201 Created Created resource successfully
    // 202 Accepted Long-running request has been accepted
    // 400 Bad Request Fields were not valid or required fields were missing

    @POST
    public Response create(String name, @Context UriInfo uriInfo) {
        log.info("name=" + name);
        UriBuilder builder = uriInfo.getAbsolutePathBuilder();
        builder.path("32");
        // CREATED 201
        return Response.created(builder.build()).build();
    }

    // Common Error Response Codes For PUT or PATCH
    // 200 OK Updated resource successfully
    // 400 Bad Request Fields were not valid or required fields were missing
    // 404 Not Found Resource not found

    // Common Error Response Codes For DELETE
    // 204 No Content Resource deleted
    // 404 Not Found Resource not found

    // ----------------------- Helper Methods -----------------------

    // Problem Details for HTTP APIs https://tools.ietf.org/html/rfc7807
    protected JsonObject createProblemDetails(String type, String title,
            int status, String detail, String instance) {
        type = (type != null) ? type : "about:blank";
        instance = (instance != null) ? instance : "";
        return Json.createObjectBuilder().add("type", type).add("title", title)
                .add("status", status).add("detail", detail)
                .add("instance", instance).build();
    }

    // ----------------------- Get and Set Methods -----------------------

}

And now lets write a System Test for your REST service.


package se.magnuskkarlsson.example.httpurlconnection;

import static org.hamcrest.CoreMatchers.is;
import static se.magnuskkarlsson.example.httpurlconnection.HttpURLConnectionBuilder.*;

import java.net.URL;

import org.hamcrest.CoreMatchers;
import org.junit.Assert;
import org.junit.Test;

public class HttpURLConnectionST {

    @Test
    public void test() throws Exception {
        URL url = new URL(
                "http://localhost:8080/example-java8-httpurlconnection/rest/persons/12");

        HttpURLConnectionResponse response = new HttpURLConnectionBuilder()
                .get(url) //
                .header(CONTENT_TYPE, APPLICATION_JSON) //
                .header(ACCEPT, APPLICATION_JSON) //
                .send();

        Assert.assertThat(response.getStatusCode(), is(200));
        Assert.assertThat(response.getBody(),
                is("{\"personId\":\"12\",\"name\":\"Magnus\"}"));
    }

    @Test
    public void testNonExisting() throws Exception {
        URL url = new URL(
                "http://localhost:8080/example-java8-httpurlconnection/rest/persons/404");

        HttpURLConnectionResponse response = new HttpURLConnectionBuilder()
                .get(url) //
                .header(CONTENT_TYPE, APPLICATION_JSON) //
                .header(ACCEPT, APPLICATION_JSON) //
                .send();

        Assert.assertThat(response.getStatusCode(), is(404));
        Assert.assertThat(response.getBody(),
                CoreMatchers.containsString("\"title\":\"Not Found\""));
    }

    @Test
    public void testPost() throws Exception {
        URL url = new URL(
                "http://localhost:8080/example-java8-httpurlconnection/rest/persons");

        HttpURLConnectionResponse response = new HttpURLConnectionBuilder()
                .post(url) //
                .header(CONTENT_TYPE, APPLICATION_JSON) //
                .header(ACCEPT, APPLICATION_JSON) //
                .body("{\"name\":\"FOO\"}") //
                .send();

        Assert.assertThat(response.getStatusCode(), is(201));
        Assert.assertThat(response.getBody(), is(""));
        Assert.assertThat(response.getHeaders().get("Location").get(0),
                CoreMatchers.notNullValue());
    }

}

No comments: