Skip to content

Latest commit

 

History

History
232 lines (172 loc) · 9.32 KB

File metadata and controls

232 lines (172 loc) · 9.32 KB

conjure-undertow-processor generated undertow services

conjure-undertow-processor generates Undertow handler for services that can not easily be represented using Conjure.

Motivation

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.

Getting Started

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.

Features

Check out the ExampleService for a concrete example and the Handle class for a list of available annotations.

Path and Query Parameters

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);
}

Header and Cookie Values

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);
}

Accessing the Request Context or Server Exchange

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);
}

Globbed Path Parameters

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"]

Using Async Handlers

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();
}

Multipart Form Data

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.

Using Custom Serializer or Deserializers

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:

  1. A public static method named valueOf that accepts a single String argument.
  2. A public constructor that accepts a single String argument.
  3. A public static method named of that accepts a single String argument.
  4. A public static method named fromString that accepts a single String argument.
  5. A public static method named create that accepts a single String 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/or Endpoint 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;
        // ...
    }
}