Skip to content

Commit

Permalink
Improve AnnotationUtil
Browse files Browse the repository at this point in the history
Motivation:
Follow-up work after line#1560.

Modifications:
- `AnnotationUtil` will lookup meta-annotations of a meta-annotation to support the following case:
```
@ProducesJson
@produces("text/plain")
@interface MyPostServiceSpecifications {}
```
  - `@ProducesJson` will also be found with this PR, which wasn't found before.
- Add `ElementType.ANNOTATION_TYPE` to `Target` annotation for `@Order` and `@StatusCode` so that a user can use them as a meta-annotation for his or her custom annotation.
  • Loading branch information
hyangtack committed Jan 31, 2019
1 parent bfee616 commit ccb8ce2
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -196,21 +196,29 @@ static <T extends Annotation> List<T> find(AnnotatedElement element, Class<T> an
for (final AnnotatedElement e : resolveTargetElements(element, findOptions)) {
for (final Annotation annotation : e.getDeclaredAnnotations()) {
if (findOptions.contains(FindOption.LOOKUP_META_ANNOTATIONS)) {
final Annotation[] metaAnnotations = annotation.annotationType().getDeclaredAnnotations();
for (final Annotation metaAnnotation : metaAnnotations) {
// If the metaAnnotation is "repeatable", we can try to find the annotation
// from the container annotation which is defined by "@Repeatable" annotation.
// There is no way to know whether the metaAnnotation is a container of
// repeatable annotations.
collectAnnotations(builder, metaAnnotation, annotationType, containerType);
}
findMetaAnnotations(builder, annotation, annotationType, containerType);
}
collectAnnotations(builder, annotation, annotationType, containerType);
}
}
return builder.build();
}

private static <T extends Annotation> void findMetaAnnotations(
Builder<T> builder, Annotation annotation,
Class<T> annotationType, Class<? extends Annotation> containerType) {
final Annotation[] metaAnnotations = annotation.annotationType().getDeclaredAnnotations();
for (final Annotation metaAnnotation : metaAnnotations) {
// Lookup meta-annotations of a meta-annotation. Do not go into deeper if the metaAnnotation
// is one of built-in Java meta-annotations in order to avoid stack overflow.
if (!BUILT_IN_META_ANNOTATIONS.contains(metaAnnotation.annotationType())) {
findMetaAnnotations(builder, metaAnnotation, annotationType, containerType);
}

collectAnnotations(builder, metaAnnotation, annotationType, containerType);
}
}

/**
* Returns all annotations which are found from the following.
* <ul>
Expand Down Expand Up @@ -274,12 +282,7 @@ static List<Annotation> getAnnotations(AnnotatedElement element, EnumSet<FindOpt
for (final AnnotatedElement e : resolveTargetElements(element, findOptions)) {
for (final Annotation annotation : e.getDeclaredAnnotations()) {
if (findOptions.contains(FindOption.LOOKUP_META_ANNOTATIONS)) {
final Annotation[] metaAnnotations = annotation.annotationType().getDeclaredAnnotations();
for (final Annotation metaAnnotation : metaAnnotations) {
if (collectingFilter.test(metaAnnotation)) {
builder.add(metaAnnotation);
}
}
getMetaAnnotations(builder, annotation, collectingFilter);
}
if (collectingFilter.test(annotation)) {
builder.add(annotation);
Expand All @@ -289,6 +292,22 @@ static List<Annotation> getAnnotations(AnnotatedElement element, EnumSet<FindOpt
return builder.build();
}

private static void getMetaAnnotations(Builder<Annotation> builder, Annotation annotation,
Predicate<Annotation> collectingFilter) {
final Annotation[] metaAnnotations = annotation.annotationType().getDeclaredAnnotations();
for (final Annotation metaAnnotation : metaAnnotations) {
// Get meta-annotations of a meta-annotation. Do not go into deeper even if one of the built-in Java
// meta-annotations can be accepted by the collectingFilter, in order to avoid stack overflow.
if (!BUILT_IN_META_ANNOTATIONS.contains(metaAnnotation.annotationType())) {
getMetaAnnotations(builder, metaAnnotation, collectingFilter);
}

if (collectingFilter.test(metaAnnotation)) {
builder.add(metaAnnotation);
}
}
}

/**
* Collects the list of {@link AnnotatedElement}s which are to be used to find annotations.
*/
Expand Down Expand Up @@ -346,6 +365,8 @@ private static <T extends Annotation> void collectAnnotations(
return;
}

// If this annotation is a containing annotation of the target annotation,
// try to call "value" method of it in order to get annotations we are finding.
final Method method = Iterables.getFirst(
getMethods(containerType, withName("value"), withParametersCount(0)), null);
if (method == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* Specifies an order which is used to sort the annotated service methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface Order {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@
* The containing annotation type for {@link RequestConverter}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Target({
ElementType.TYPE,
ElementType.METHOD,
ElementType.PARAMETER,
ElementType.CONSTRUCTOR,
ElementType.FIELD
})
public @interface RequestConverters {
/**
* An array of {@link RequestConverter}s.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* {@code @StatusCode(204)} would be applied. Otherwise, {@code @StatusCode(200)} would be applied.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface StatusCode {
/**
* A default HTTP status code of a response produced by an annotated HTTP service.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.function.Function;

import javax.annotation.Nullable;

Expand All @@ -28,56 +29,245 @@
import com.linecorp.armeria.client.HttpClient;
import com.linecorp.armeria.common.AggregatedHttpMessage;
import com.linecorp.armeria.common.HttpData;
import com.linecorp.armeria.common.HttpHeaderNames;
import com.linecorp.armeria.common.HttpHeaders;
import com.linecorp.armeria.common.HttpMethod;
import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.MediaType;
import com.linecorp.armeria.common.RequestContext;
import com.linecorp.armeria.common.logging.LogLevel;
import com.linecorp.armeria.server.DecoratingServiceFunction;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.Service;
import com.linecorp.armeria.server.ServiceRequestContext;
import com.linecorp.armeria.server.SimpleDecoratingService;
import com.linecorp.armeria.server.annotation.Consumes;
import com.linecorp.armeria.server.annotation.ConsumesJson;
import com.linecorp.armeria.server.annotation.Decorator;
import com.linecorp.armeria.server.annotation.DecoratorFactory;
import com.linecorp.armeria.server.annotation.DecoratorFactoryFunction;
import com.linecorp.armeria.server.annotation.ExceptionHandler;
import com.linecorp.armeria.server.annotation.ExceptionHandlerFunction;
import com.linecorp.armeria.server.annotation.Get;
import com.linecorp.armeria.server.annotation.Param;
import com.linecorp.armeria.server.annotation.Order;
import com.linecorp.armeria.server.annotation.Post;
import com.linecorp.armeria.server.annotation.Produces;
import com.linecorp.armeria.server.annotation.ProducesJson;
import com.linecorp.armeria.server.annotation.RequestConverter;
import com.linecorp.armeria.server.annotation.RequestConverterFunction;
import com.linecorp.armeria.server.annotation.ResponseConverter;
import com.linecorp.armeria.server.annotation.ResponseConverterFunction;
import com.linecorp.armeria.server.annotation.StatusCode;
import com.linecorp.armeria.server.annotation.decorator.LoggingDecorator;
import com.linecorp.armeria.testing.server.ServerRule;

import io.netty.util.Attribute;
import io.netty.util.AttributeKey;

public class AnnotatedHttpServiceAnnotationAliasTest {

@RequestConverter(MyRequestConverter.class)
@ResponseConverter(MyResponseConverter.class)
@Consumes("text/plain; charset=utf-8")
@Consumes("application/xml")
@ConsumesJson
@Produces("text/plain; charset=utf-8")
@Produces("application/xml")
@ProducesJson
@ExceptionHandler(MyExceptionHandler1.class)
@ExceptionHandler(MyExceptionHandler2.class)
@LoggingDecorator(requestLogLevel = LogLevel.DEBUG, successfulResponseLogLevel = LogLevel.DEBUG)
@Decorator(MyDecorator1.class)
@Decorator(MyDecorator2.class)
@MyDecorator3
@Order // Just checking whether @Order annotation can be present as a meta-annotation.
@StatusCode(201)
@Retention(RetentionPolicy.RUNTIME)
@interface MyPostServiceSpecifications {}

@RequestConverter(MyRequestConverter.class)
@ResponseConverter(MyResponseConverter.class)
@Produces("text/plain")
@interface MyResponse {}
@ProducesJson
@ExceptionHandler(MyExceptionHandler1.class)
@ExceptionHandler(MyExceptionHandler2.class)
@LoggingDecorator(requestLogLevel = LogLevel.DEBUG, successfulResponseLogLevel = LogLevel.DEBUG)
@Decorator(MyDecorator1.class)
@Decorator(MyDecorator2.class)
@MyDecorator3
@Retention(RetentionPolicy.RUNTIME)
@interface MyGetServiceSpecifications {}

static class MyRequest {
private final String name;

MyRequest(String name) {
this.name = name;
}
}

static class MyRequestConverter implements RequestConverterFunction {
@Nullable
@Override
public Object convertRequest(ServiceRequestContext ctx, AggregatedHttpMessage request,
Class<?> expectedResultType) throws Exception {
if (expectedResultType == MyRequest.class) {
final String decorated = ctx.attr(decoratedFlag).get();
return new MyRequest(request.content().toStringUtf8() + decorated);
}
return RequestConverterFunction.fallthrough();
}
}

static class MyResponseConverter implements ResponseConverterFunction {
@Override
public HttpResponse convertResponse(ServiceRequestContext ctx, HttpHeaders headers,
@Nullable Object result, HttpHeaders trailingHeaders)
throws Exception {
return HttpResponse.of(
headers, HttpData.ofUtf8("Hello, %s!", result), trailingHeaders);
public HttpResponse convertResponse(
ServiceRequestContext ctx, HttpHeaders headers,
@Nullable Object result, HttpHeaders trailingHeaders) throws Exception {
return HttpResponse.of(headers, HttpData.ofUtf8("Hello, %s!", result), trailingHeaders);
}
}

static class MyExceptionHandler1 implements ExceptionHandlerFunction {
@Override
public HttpResponse handleException(RequestContext ctx, HttpRequest req, Throwable cause) {
if (cause instanceof IllegalArgumentException) {
return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, MediaType.PLAIN_TEXT_UTF_8,
"Cause:" + IllegalArgumentException.class.getSimpleName());
}
return ExceptionHandlerFunction.fallthrough();
}
}

static class MyExceptionHandler2 implements ExceptionHandlerFunction {
@Override
public HttpResponse handleException(RequestContext ctx, HttpRequest req, Throwable cause) {
if (cause instanceof IllegalStateException) {
return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, MediaType.PLAIN_TEXT_UTF_8,
"Cause:" + IllegalStateException.class.getSimpleName());
}
return ExceptionHandlerFunction.fallthrough();
}
}

static class MyDecorator1 implements DecoratingServiceFunction<HttpRequest, HttpResponse> {
@Override
public HttpResponse serve(Service<HttpRequest, HttpResponse> delegate,
ServiceRequestContext ctx, HttpRequest req) throws Exception {
appendAttribute(ctx, " (decorated-1)");
return delegate.serve(ctx, req);
}
}

static class MyDecorator2 implements DecoratingServiceFunction<HttpRequest, HttpResponse> {
@Override
public HttpResponse serve(Service<HttpRequest, HttpResponse> delegate,
ServiceRequestContext ctx, HttpRequest req) throws Exception {
appendAttribute(ctx, " (decorated-2)");
return delegate.serve(ctx, req);
}
}

@DecoratorFactory(MyDecorator3Factory.class)
@Retention(RetentionPolicy.RUNTIME)
@interface MyDecorator3 {}

static class MyDecorator3Factory implements DecoratorFactoryFunction<MyDecorator3> {
@Override
public Function<Service<HttpRequest, HttpResponse>,
? extends Service<HttpRequest, HttpResponse>> newDecorator(MyDecorator3 parameter) {
return delegate -> new SimpleDecoratingService<HttpRequest, HttpResponse>(delegate) {
@Override
public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception {
appendAttribute(ctx, " (decorated-3)");
return delegate().serve(ctx, req);
}
};
}
}

static final AttributeKey<String> decoratedFlag =
AttributeKey.valueOf(AnnotatedHttpServiceAnnotationAliasTest.class, "decorated");

private static void appendAttribute(ServiceRequestContext ctx, String value) {
final Attribute<String> attr = ctx.attr(decoratedFlag);
final String v = attr.get();
attr.set((v == null ? "" : v) + value);
}

@ClassRule
public static ServerRule rule = new ServerRule() {
@Override
protected void configure(ServerBuilder sb) throws Exception {
sb.annotatedService(new Object() {
@Get("/hello/{name}")
@MyResponse
public String name(@Param String name) {
return name;
@Post("/hello")
@MyPostServiceSpecifications
public String hello(MyRequest myRequest) {
return myRequest.name;
}

@Get("/exception1")
@MyGetServiceSpecifications
public String exception1() {
throw new IllegalArgumentException("Anticipated!");
}

@Get("/exception2")
@MyGetServiceSpecifications
public String exception2() {
throw new IllegalStateException("Anticipated!");
}
});
}
};

@Test
public void serviceConfiguredWithAlias() {
final AggregatedHttpMessage msg = HttpClient.of(rule.uri("/")).get("/hello/Armeria")
.aggregate().join();
assertThat(msg.status()).isEqualTo(HttpStatus.OK);
assertThat(msg.headers().contentType()).isEqualTo(MediaType.parse("text/plain"));
assertThat(msg.content().toStringUtf8()).isEqualTo("Hello, Armeria!");
public void metaAnnotations() {
final AggregatedHttpMessage msg =
HttpClient.of(rule.uri("/"))
.execute(HttpHeaders.of(HttpMethod.POST, "/hello")
.contentType(MediaType.PLAIN_TEXT_UTF_8)
.add(HttpHeaderNames.ACCEPT, "text/*"),
HttpData.ofUtf8("Armeria"))
.aggregate().join();
assertThat(msg.status()).isEqualTo(HttpStatus.CREATED);
assertThat(msg.headers().contentType()).isEqualTo(MediaType.PLAIN_TEXT_UTF_8);
assertThat(msg.content().toStringUtf8())
.isEqualTo("Hello, Armeria (decorated-1) (decorated-2) (decorated-3)!");
}

@Test
public void metaOfMetaAnnotation_ProducesJson() {
final AggregatedHttpMessage msg =
HttpClient.of(rule.uri("/"))
.execute(HttpHeaders.of(HttpMethod.POST, "/hello")
.contentType(MediaType.PLAIN_TEXT_UTF_8)
.add(HttpHeaderNames.ACCEPT,
"application/json; charset=utf-8"),
HttpData.ofUtf8("Armeria"))
.aggregate().join();
assertThat(msg.status()).isEqualTo(HttpStatus.CREATED);
assertThat(msg.headers().contentType()).isEqualTo(MediaType.JSON_UTF_8);
assertThat(msg.content().toStringUtf8())
.isEqualTo("Hello, Armeria (decorated-1) (decorated-2) (decorated-3)!");
}

@Test
public void exception1() {
final AggregatedHttpMessage msg =
HttpClient.of(rule.uri("/")).get("/exception1").aggregate().join();
assertThat(msg.status()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(msg.content().toStringUtf8())
.isEqualTo("Cause:" + IllegalArgumentException.class.getSimpleName());
}

@Test
public void exception2() {
final AggregatedHttpMessage msg =
HttpClient.of(rule.uri("/")).get("/exception2").aggregate().join();
assertThat(msg.status()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(msg.content().toStringUtf8())
.isEqualTo("Cause:" + IllegalStateException.class.getSimpleName());
}
}

0 comments on commit ccb8ce2

Please sign in to comment.