Skip to content

Commit

Permalink
Custom (Java) Header Support #761 (#888)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jlprat authored and ktoso committed Mar 17, 2017
1 parent 34c0837 commit 6efbdb2
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright (C) 2009-2017 Lightbend Inc. <http://www.lightbend.com>
*/

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 extends Rendering> R render(R r) {
return (R) r.$tilde$tilde(name).$tilde$tilde(':').$tilde$tilde(' ').$tilde$tilde(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright (C) 2009-2017 Lightbend Inc. <http://www.lightbend.com>
*/

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<H extends ModeledCustomHeader> {

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<H> from(final HttpHeader header) {
if (header.lowercaseName().equals(lowercaseName())) {
return Optional.of(parse(header.value()));
}
return Optional.empty();
}
}
22 changes: 22 additions & 0 deletions docs/src/main/paradox/java/http/common/http-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a id="custom-headers-java"></a>
## 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

Expand Down
122 changes: 122 additions & 0 deletions docs/src/test/java/docs/http/javadsl/CustomHeaderExampleTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Copyright (C) 2009-2017 Lightbend Inc. <http://www.lightbend.com>
*/

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<ApiTokenHeader> {

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<ApiTokenHeader> 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<ApiTokenHeader> 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<ApiTokenHeader> 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<HttpHeader, String> extractFromCustomHeader =
new JavaPartialFunction<HttpHeader, String>() {

@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
}

}

0 comments on commit 6efbdb2

Please sign in to comment.