conjure-undertow-processor generates Undertow handler for services that can not easily be represented using
Conjure.
Historically we have supported Jersey as web server framework, but later migrated to Undertow. Beyond a desire not to support multiple frameworks, Jersey is problematic for a variety of other reasons and does not align with our long term technical goals of our Java ecosystem.
Consumers that are still using Jersey should migrate and declare their endpoints using Conjure. However, some
complex services are using features that are tricky to represent or not supported using Conjure. To help these services
migrate away from Jersey, conjure-undertow-processor provides a more flexible way of defining your service interface
while still profiting from code generation.
In your API project, add the following dependencies to your build.gradle file (make sure you are using
gradle-processors):
dependencies {
implementation 'com.palantir.conjure.java:conjure-undertow-annotations'
annotationProcessor 'com.palantir.conjure.java:conjure-undertow-processor'
}Next, define your service interface and annotate it using conjure-undertow-annotations:
public interface MyService {
@Handle(method = HttpMethod.POST, path = "/myEndpoint/{pathParam}")
MyResponse myEndpoint(
AuthHeader authHeader,
@Handle.PathParam String pathParam,
@Handle.QueryParam("queryParam") Optional<String> queryParam,
@Handle.Body MyBody body);
}During compilation, the conjure-undertow-processor will generate a respective MyServiceEndpoints handler that can
be used with Undertow similar to handlers generated by conjure-java.
Check out the ExampleService for a concrete example and the Handle class for a list of available annotations.
Path and query parameters can be defined using the @Handle.PathParam and @Handle.QueryParam annotations:
public interface MyService {
@Handle(method = HttpMethod.GET, path = "/api/{myParam}/{otherParam}")
void myEndpoint(
@Handle.PathParam String myParam,
@Handle.PathParam String otherParam,
@Handle.QueryParam("queryParam") String queryParam,
@Handle.QueryParam("maybeQueryParam") Optional<Boolean> maybeQueryParam);
}To access header fields or cookie values, you can use the @Handle.Header or @Handle.Cookie annotations:
public interface MyService {
@Handle(method = HttpMethod.GET, path = "/path")
void myEndpoint(
@Handle.Header("Foo") String fooHeader,
@Handle.Cookie("MY_COOKIE") Optional<String> cookieValue);
}To access a bearer token provided using the HTTP Authorization header in the form of Bearer [token], you can use an unannotated AuthHeader or Optional<AuthHeader> parameter, which will be deserialized using the AuthorizationExtractor:
public interface MyService {
@Handle(method = HttpMethod.GET, path = "/path")
void myEndpoint(AuthHeader authHeader);
}public interface MyService {
@Handle(method = HttpMethod.GET, path = "/path")
void myEndpoint(Optional<AuthHeader> authHeader);
}Similarly, to access a bearer token provided using a cookie, you can use the @Handle.Cookie annotation on a
BearerToken or Optional<BearerToken> parameter, which will be deserialized using the AuthorizationExtractor:
public interface MyService {
@Handle(method = HttpMethod.GET, path = "/path")
void myEndpoint(@Handle.Cookie("AUTH_TOKEN") BearerToken token);
}public interface MyService {
@Handle(method = HttpMethod.GET, path = "/path")
void myEndpoint(@Handle.Cookie("AUTH_TOKEN") Optional<BearerToken> token);
}If required, you can inject the RequestContext
or the underlying Undertow HttpServerExchange:
public interface MyService {
@Handle(method = HttpMethod.GET, path = "/path")
void myEndpoint(HttpServerExchange exchange, RequestContext context);
}The conjure-undertow-processor only supports a restricted version of globbed or wildcard path parameters. Only
as a catch-all at the end of the path when using the @Handle.PathMultiParam annotation:
public interface MyService {
@Handle(method = HttpMethod.GET, path = "/path/{params}")
void myEndpoint(@Handle.PathMultiParam List<String> params);
}For the above endpoint, the following table shows how various requests are deserialized into wildcard path parameters.
| Request | Params |
|---|---|
GET /path/ |
[""] |
GET /path/foo |
["foo"] |
GET /path/foo/ |
["foo", ""] |
GET /path/foo/bar |
["foo", "bar"] |
GET /path/foo/bar%2Fbaz |
["foo", "bar/baz"] |
The conjure-undertow-processor supports async handlers by wrapping the response in a ListenableFuture.
public interface MyService {
@Handle(method = HttpMethod.GET, path = "/path/async")
ListenableFuture<MyResponse> asyncEndpoint();
@Handle(method = HttpMethod.GET, path = "/path/async-void")
ListenableFuture<Void> asyncVoidEndpoint();
}For endpoints using form data, you can use the @Handle.FormParam annotation.
public interface MyService {
@Handle(method = HttpMethod.POST, path = "/path/form-data")
void myEndpoint(
@Handle.FormParam("fieldA") String fieldA,
@Handle.FormParam(value = "fieldB", decoder = MyTypeDecoder.clas) MyType fieldB);
}Note that file form parameters are currently not supported by this annotation but can be accessed using a @Handle.Body annotation with a custom serializer.
Per default, conjure-undertow-processor supports decoding parameters for all plain Conjure types as well as primitives and types that have one of the following:
- A public static method named
valueOfthat accepts a singleStringargument. - A public constructor that accepts a single
Stringargument. - A public static method named
ofthat accepts a singleStringargument. - A public static method named
fromStringthat accepts a singleStringargument. - A public static method named
createthat accepts a singleStringargument.
In the presence of more than one eligible method or constructor, the first matching element following the ordering above is used.
For other parameter types, you can provide a custom decoder by providing an implementation of
either ParamDecoder or
CollectionParamDecoder that is one of the following:
- An enum with a single value.
- A class with a constructor that accepts no arguments.
- A class with a constructor that accepts some combination of
UndertowRuntimeand/orEndpointarguments.
public interface MyService {
@Handle(method = HttpMethod.GET, path = "/path")
void customParam(
@Handle.QueryParam(value = "query", decoder = MyCollectionParamDecoder.class)
Optional<MyType> queryParam);
enum MyCollectionParamDecoder implements CollectionParamDecoder<Optional<MyType>> {
private final PlainSerDe serde;
MyCollectionParamDecoder(UndertowRuntime runtime) {
this.serde = runtime.plainSerDe();
}
@Override
public Optional<MyType> decode(Collection<String> value) {
return serde.deserializeOptionalComplex(values, MyType::from);
}
}
}Similarly, you can provide your own behavior for serializing and deserializing the request body by providing an
implementation of either SerializerFactory
or DeserializerFactory that is one of the following:
- An enum with a single value.
- A class with a constructor that accepts no arguments.
public interface MyService {
@Handle(method = HttpMethod.POST, path = "/path", produces = MyResponseSerializerFactory.class)
MyResponse customParam(@Handle.Body(MyBodyDeserializerFactory.class) MyBody body);
enum MyBodyDeserializerFactory implements DeserializerFactory<MyBody> {
INSTANCE;
// ...
}
enum MyResponseSerializerFactory implements SerializerFactory<MyResponse> {
INSTANCE;
// ...
}
}