From 6efbdb26094468ba224df3d96e079c77fe2e08c9 Mon Sep 17 00:00:00 2001 From: Josep Prat Date: Fri, 17 Mar 2017 14:57:42 +0100 Subject: [PATCH] Custom (Java) Header Support #761 (#888) * Custom Header Support #761 Issue: #761 triggered in PR #844 First attempt to offer Custom Header Support Add docs and javadoc * Rename Companion to Factory * apply feedback --- .../model/headers/ModeledCustomHeader.java | 47 +++++++ .../headers/ModeledCustomHeaderFactory.java | 48 +++++++ .../paradox/java/http/common/http-model.md | 22 ++++ .../http/javadsl/CustomHeaderExampleTest.java | 122 ++++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 akka-http-core/src/main/java/akka/http/javadsl/model/headers/ModeledCustomHeader.java create mode 100644 akka-http-core/src/main/java/akka/http/javadsl/model/headers/ModeledCustomHeaderFactory.java create mode 100644 docs/src/test/java/docs/http/javadsl/CustomHeaderExampleTest.java diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/ModeledCustomHeader.java b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/ModeledCustomHeader.java new file mode 100644 index 00000000000..3258d7a1b48 --- /dev/null +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/ModeledCustomHeader.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2009-2017 Lightbend Inc. + */ + +package akka.http.javadsl.model.headers; + +import akka.http.impl.util.Rendering; +import akka.util.Helpers; + +/** + * Support class for building user-defined custom headers defined by implementing `name` and `value`. + * By implementing a {@link ModeledCustomHeader} along with {@link ModeledCustomHeaderFactory} instead of {@link CustomHeader} directly, + * convenience methods that allow parsing this user-defined header from {@link akka.http.javadsl.model.HttpHeader} are + * available to use. + */ +public abstract class ModeledCustomHeader extends CustomHeader { + + private final String name; + private final String value; + + protected ModeledCustomHeader(final String name, final String value) { + super(); + this.name = name; + this.value = value; + } + + @Override + public String name() { + return name; + } + + @Override + public String lowercaseName() { + return Helpers.toRootLowerCase(name); + } + + @Override + public String value() { + return value; + } + + @Override + @SuppressWarnings("unchecked") + public R render(R r) { + return (R) r.$tilde$tilde(name).$tilde$tilde(':').$tilde$tilde(' ').$tilde$tilde(value); + } +} diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/ModeledCustomHeaderFactory.java b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/ModeledCustomHeaderFactory.java new file mode 100644 index 00000000000..0ad6fd1e3c1 --- /dev/null +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/ModeledCustomHeaderFactory.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2009-2017 Lightbend Inc. + */ + +package akka.http.javadsl.model.headers; + +import akka.http.javadsl.model.HttpHeader; +import akka.util.Helpers; + +import java.util.Optional; + +/** + * Companion class for the {@link ModeledCustomHeader} class. It offers methods to create {@link ModeledCustomHeader} + * from {@link String} or {@link HttpHeader}. + */ +public abstract class ModeledCustomHeaderFactory { + + public abstract String name(); + + public String lowercaseName() { + return Helpers.toRootLowerCase(name()); + } + + /** + * Parses the value checking that the format is correct. + * It may throw if value is not correct + */ + protected abstract H parse(final String value); + + /** + * Creates a new {@code ModeledCustomHeader} from the value checking that the format is correct. + * It may throw if value is not correct + */ + public H create(final String value) { + return parse(value); + } + + /** + * Transforms an {@code HttpHeader} to this {@code ModeledCustomHeader} if the name and value are correct. + * It may throw in case of malformed headers + */ + public Optional from(final HttpHeader header) { + if (header.lowercaseName().equals(lowercaseName())) { + return Optional.of(parse(header.value())); + } + return Optional.empty(); + } +} diff --git a/docs/src/main/paradox/java/http/common/http-model.md b/docs/src/main/paradox/java/http/common/http-model.md index 6c081aa3158..d108848893c 100644 --- a/docs/src/main/paradox/java/http/common/http-model.md +++ b/docs/src/main/paradox/java/http/common/http-model.md @@ -279,6 +279,28 @@ The actual logic for determining whether to close the connection is quite involv request's method, protocol and potential `Connection` header as well as the response's protocol, entity and potential `Connection` header. See @github[this test](/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala#L422) for a full table of what happens when. + +## Custom Headers + +Sometimes you may need to model a custom header type which is not part of HTTP and still be able to use it +as convenient as is possible with the built-in types. + +Because of the number of ways one may interact with headers (i.e. try to convert a `CustomHeader` to a `RawHeader` +or the other way around etc), a couple of helper classes for custom Header types are provided by Akka HTTP. +Thanks to extending `ModeledCustomHeader` instead of the plain `CustomHeader` the following methods are at your disposal: + +@@snip [CustomHeaderExampleTest.java](../../../../../test/java/docs/http/javadsl/CustomHeaderExampleTest.java) { #modeled-api-key-custom-header } + +Which allows the this modeled custom header to be used and created in the following ways: + +@@snip [CustomHeaderExampleTest.java](../../../../../test/java/docs/http/javadsl/CustomHeaderExampleTest.java) { #conversion-creation-custom-header } + +Including usage within the header directives like in the following @ref[headerValuePF](../routing-dsl/directives/header-directives/headerValuePF.md#headervaluepf) example: + +@@snip [CustomHeaderExampleTest.java](../../../../../test/java/docs/http/javadsl/CustomHeaderExampleTest.java) { #header-value-pf } + +One can also directly extend `CustomHeader` which requires less boilerplate, however that has the downside of +having to deal with converting `HttpHeader` instances to your custom one. For only rendering such header however it would be enough. ## Parsing / Rendering diff --git a/docs/src/test/java/docs/http/javadsl/CustomHeaderExampleTest.java b/docs/src/test/java/docs/http/javadsl/CustomHeaderExampleTest.java new file mode 100644 index 00000000000..ce5f14c8b65 --- /dev/null +++ b/docs/src/test/java/docs/http/javadsl/CustomHeaderExampleTest.java @@ -0,0 +1,122 @@ +/** + * Copyright (C) 2009-2017 Lightbend Inc. + */ + +package docs.http.javadsl; + +import akka.http.javadsl.model.HttpHeader; +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.StatusCodes; +import akka.http.javadsl.model.headers.ModeledCustomHeader; +import akka.http.javadsl.model.headers.ModeledCustomHeaderFactory; +import akka.http.javadsl.model.headers.RawHeader; +import akka.http.javadsl.server.Route; +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.japi.JavaPartialFunction; +import org.junit.Test; +import scala.PartialFunction; + +import java.util.Optional; + +import static org.junit.Assert.*; + +public class CustomHeaderExampleTest extends JUnitRouteTest { + //#modeled-api-key-custom-header + public static class ApiTokenHeader extends ModeledCustomHeader { + + ApiTokenHeader(String name, String value) { + super(name, value); + } + + public boolean renderInResponses() { + return false; + } + + public boolean renderInRequests() { + return false; + } + + } + + static class ApiTokenHeaderFactory extends ModeledCustomHeaderFactory { + + public String name() { + return "apiKey"; + } + + @Override + public ApiTokenHeader parse(String value) { + return new ApiTokenHeader(name(), value); + } + + } + //#modeled-api-key-custom-header + + @Test + public void showCreation() { + //#conversion-creation-custom-header + final ApiTokenHeaderFactory apiTokenHeaderFactory = new ApiTokenHeaderFactory(); + final ApiTokenHeader token = apiTokenHeaderFactory.create("token"); + assertEquals("token", token.value()); + + final HttpHeader header = apiTokenHeaderFactory.create("token"); + assertEquals("apiKey", header.name()); + assertEquals("token", header.value()); + + final Optional fromRaw = apiTokenHeaderFactory + .from(RawHeader.create("apiKey", "token")); + assertTrue("Expected a header", fromRaw.isPresent()); + assertEquals("apiKey", fromRaw.get().name()); + assertEquals("token", fromRaw.get().value()); + + // will match, header keys are case insensitive + final Optional fromRawUpper = apiTokenHeaderFactory + .from(RawHeader.create("APIKEY", "token")); + assertTrue("Expected a header", fromRawUpper.isPresent()); + assertEquals("apiKey", fromRawUpper.get().name()); + assertEquals("token", fromRawUpper.get().value()); + + // won't match, different header name + final Optional wrong = apiTokenHeaderFactory + .from(RawHeader.create("different", "token")); + assertFalse(wrong.isPresent()); + //#conversion-creation-custom-header + } + + @Test + public void testExtraction() { + //#header-value-pf + final ApiTokenHeaderFactory apiTokenHeaderFactory = new ApiTokenHeaderFactory(); + final PartialFunction extractFromCustomHeader = + new JavaPartialFunction() { + + @Override + public String apply(HttpHeader header, boolean isCheck) throws Exception { + if (isCheck) + return null; + return apiTokenHeaderFactory.from(header) + .map(apiTokenHeader -> "extracted> " + apiTokenHeader) + .orElseGet(() -> "raw> " + header); + } + }; + + final Route route = headerValuePF(extractFromCustomHeader, this::complete); + + testRoute(route) + .run(HttpRequest.GET("/").addHeader(RawHeader.create("apiKey", "TheKey"))) + .assertStatusCode(StatusCodes.OK) + .assertEntity("extracted> apiKey: TheKey"); + + testRoute(route) + .run(HttpRequest.GET("/").addHeader(RawHeader.create("somethingElse", "TheKey"))) + .assertStatusCode(StatusCodes.OK) + .assertEntity("raw> somethingElse: TheKey"); + + testRoute(route) + .run(HttpRequest.GET("/").addHeader(apiTokenHeaderFactory.create("TheKey"))) + .assertStatusCode(StatusCodes.OK) + .assertEntity("extracted> apiKey: TheKey"); + //#header-value-pf + } + +}