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);
}
When using an HTTP authentication header in the form of Bearer [token]
, the token can be automatically injected when
using an AuthHeader
parameter:
public interface MyService {
@Handle(method = HttpMethod.GET, path = "/path")
void myEndpoint(AuthHeader authHeader);
}
Similarly, you can access a bearer token from a cookie when using the @Handle.Cookie
annotation together with the
BearerToken
type which also sets the respective auth state:
public interface MyService {
@Handle(method = HttpMethod.GET, path = "/path")
void myEndpoint(@Handle.Cookie("AUTH_TOKEN") 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
valueOf
that accepts a singleString
argument. - A public constructor that accepts a single
String
argument. - A public static method named
of
that accepts a singleString
argument. - A public static method named
fromString
that accepts a singleString
argument. - A public static method named
create
that accepts a singleString
argument.
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
UndertowRuntime
and/orEndpoint
arguments.
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;
// ...
}
}