diff --git a/README.md b/README.md new file mode 100644 index 0000000..54b3435 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# HTT4J + +## Description + +This is a simple, lightweight and tiny wrapper for Java's HttpURLConnection. It has no external +dependencies and is written for Java 8. + +It comes with a entity mapping system (serialization and deserialization for request and response bodies) +with optional mappings for third party libraries (currently supporting: GSON). + +### Rationale + +Most HTTP client for Java are either built for Java 11+, or have a large amount of dependencies, +which means that in order to use them, one needs to built a fatjar that often end up being huge. +This aims to offer a nicer way to interact with the Java 8 HTTP client, without having to double the +size of the output artifacts. + +## Usage + +### Repository + +HTT4J is available from [IntellectualSites](https://intellectualsites.com)' maven repository: + +```xml + + intellectualsites-snapshots + https://mvn.intellectualsites.com/content/repositories/snapshots + +``` + +```xml + + com.intellectualsites.http + HTTP4J + 1.0-SNAPSHOT + +``` + +### Code + +All requests are done using an instance of `com.intellectualsites.http.HttpClient`: + +```java +HttpClient client = HttpClient.newBuilder() + .withBaseURL("https://your.api.com") + .build(); +``` + +The client also take in a `com.intellectualsites.http.EntityMapper` instance. This +is used to map request & response bodies to Java objects. By default, it includes a mapper +for Java strings. + +```java +EntityMapper entityMapper = EntityMapper.newInstance() + .registerDeserializer(JsonObject.class, GsonMapper.deserializer(JsonObject.class, GSON)); +``` + +The above snippet would create an entity mapper that maps to and from Java strings, and +from HTTP response's to GSON json objects. + +This can then be included in the HTTP client by using `.withEntityMapper(mapper)` to +be used in all requests, or added to individual requests. + +HTTP4J also supports request decorators, that can be used to modify each request. These are +added by using: + +```java +builder.withDecorator(request -> { + request.doSomething(); +}); +``` + +The built client can then be used to make HTTP requests, like such: + +```java +client.post("/some/api").withInput(() -> "Hello World") + .onStatus(200, response -> { + System.out.println("Everything is fine"); + System.out.println("Response: " + response.getResponseEntity(String.class)); + }) + .onStatus(404, response -> System.err.println("Could not find the resource =(")) + .onRemaining(response -> System.err.printf("Got status code: %d\n", response.getStatusCode())) + .onException(Throwable::printStackTrace) + .execute(); +``` + +#### Exception Handling + +HTTP4J will forward all RuntimeExceptions by default, and wrap all other exceptions (that do not +extend RuntimeException) in a RuntimeException. + +By using `onException(exception -> {})` you are able to modify the behaviour. + +#### Examples + +More examples can be found in [HttpClientTest.java](https://github.com/Sauilitired/HTTP4J/blob/master/src/test/java/com/intellectualsites/http/HttpClientTest.java) diff --git a/src/main/java/com/intellectualsites/http/ClientSettings.java b/src/main/java/com/intellectualsites/http/ClientSettings.java index fc27510..7098eda 100644 --- a/src/main/java/com/intellectualsites/http/ClientSettings.java +++ b/src/main/java/com/intellectualsites/http/ClientSettings.java @@ -26,13 +26,18 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; import java.util.Objects; +import java.util.function.Consumer; /** * Settings that change the behaviour of {@link HttpClient} */ final class ClientSettings { + private final Collection> decorators = new LinkedList<>(); private String baseURL; private EntityMapper entityMapper; @@ -60,6 +65,15 @@ final class ClientSettings { return this.entityMapper; } + /** + * Get all registered request decorators + * + * @return Unmodifiable collection of decorators + */ + @NotNull Collection> getRequestDecorators() { + return Collections.unmodifiableCollection(this.decorators); + } + /** * Set the base URL, that is prepended to * the URL of each request @@ -67,7 +81,7 @@ final class ClientSettings { * @param baseURL base URL */ void setBaseURL(@NotNull final String baseURL) { - this.baseURL = Objects.requireNonNull(baseURL); + this.baseURL = Objects.requireNonNull(baseURL, "Base URL may not be null"); } /** @@ -80,4 +94,14 @@ void setEntityMapper(@Nullable final EntityMapper entityMapper) { this.entityMapper = entityMapper; } + /** + * Add a new request decorator. This will have the opportunity + * to decorate every request made by this client + * + * @param decorator Decorator + */ + void addDecorator(@NotNull final Consumer decorator) { + this.decorators.add(Objects.requireNonNull(decorator, "Decorator may not be null")); + } + } diff --git a/src/main/java/com/intellectualsites/http/HttpClient.java b/src/main/java/com/intellectualsites/http/HttpClient.java index 705b8fb4..53affa7 100644 --- a/src/main/java/com/intellectualsites/http/HttpClient.java +++ b/src/main/java/com/intellectualsites/http/HttpClient.java @@ -150,7 +150,7 @@ private Builder() { * @return Builder instance */ @NotNull public Builder withBaseURL(@NotNull final String baseURL) { - Objects.requireNonNull(baseURL); + Objects.requireNonNull(baseURL, "Base URL may not be null"); if (baseURL.endsWith("/")) { this.settings.setBaseURL(baseURL.substring(0, baseURL.length() - 1)); } else { @@ -171,6 +171,18 @@ private Builder() { return this; } + /** + * Add a new request decorator. This will have the opportunity + * to decorate every request made by this client + * + * @param decorator Decorator + * @return Builder instance + */ + @NotNull public Builder withDecorator(@NotNull final Consumer decorator) { + this.settings.addDecorator(Objects.requireNonNull(decorator, "Decorator may not be null")); + return this; + } + /** * Create a new {@link HttpClient} using the * settings specified in the builder @@ -297,6 +309,9 @@ private WrappedRequestBuilder(@NotNull final HttpMethod method, @NotNull String * the method will return {@code null} */ @Nullable public HttpResponse execute() { + for (final Consumer decorator : settings.getRequestDecorators()) { + decorator.accept(this); + } try { final Throwable[] throwables = new Throwable[1]; if (this.exceptionHandler == null) { diff --git a/src/test/java/com/intellectualsites/http/HttpClientTest.java b/src/test/java/com/intellectualsites/http/HttpClientTest.java index 2417483..0ede973 100644 --- a/src/test/java/com/intellectualsites/http/HttpClientTest.java +++ b/src/test/java/com/intellectualsites/http/HttpClientTest.java @@ -49,6 +49,8 @@ public class HttpClientTest { private static final String BASE_BODY = "Unicorns are real!"; private static final String BASE_HEADER_KEY = "X-Test-Header"; private static final String BASE_HEADER_VALUE = "yay"; + private static final String ECHO_HEADER_KEY = "X-Test-Echo"; + private static final String ECHO_HEADER_VALUE = "Wooo!"; private static final String ECHO_CONTENT = UUID.randomUUID().toString(); private static MockServerClient mockServer; @@ -72,7 +74,8 @@ public class HttpClientTest { public static final class EchoCallBack implements ExpectationResponseCallback { @Override public org.mockserver.model.HttpResponse handle(HttpRequest httpRequest) { - return org.mockserver.model.HttpResponse.response(httpRequest.getBodyAsString()); + return org.mockserver.model.HttpResponse.response(httpRequest.getBodyAsString()) + .withHeader(ECHO_HEADER_KEY, httpRequest.getFirstHeader(ECHO_HEADER_KEY)); } } @@ -85,8 +88,11 @@ public static final class EchoCallBack implements ExpectationResponseCallback { @BeforeEach void setupClient() { final EntityMapper mapper = EntityMapper.newInstance() .registerDeserializer(JsonObject.class, GsonMapper.deserializer(JsonObject.class, GSON)); - this.client = - HttpClient.newBuilder().withBaseURL(BASE_PATH).withEntityMapper(mapper).build(); + this.client = HttpClient.newBuilder() + .withBaseURL(BASE_PATH) + .withEntityMapper(mapper) + .withDecorator(request -> request.withHeader(ECHO_HEADER_KEY, ECHO_HEADER_VALUE)) + .build(); } @Test void testSimpleGet() { @@ -105,6 +111,7 @@ public static final class EchoCallBack implements ExpectationResponseCallback { }).execute(); assertNotNull(echoResponse); assertEquals(ECHO_CONTENT, echoResponse.getResponseEntity(String.class)); + assertEquals(ECHO_HEADER_VALUE, echoResponse.getHeaders().getHeader(ECHO_HEADER_KEY)); } @Test void testThrow() {