diff --git a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/InterceptedResource.java b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/InterceptedResource.java new file mode 100644 index 000000000..28440613a --- /dev/null +++ b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/InterceptedResource.java @@ -0,0 +1,123 @@ +package io.quarkus.ts.http.advanced.reactive; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import jakarta.ws.rs.ConstrainedTo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NameBinding; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.RuntimeType; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Provider; +import jakarta.ws.rs.ext.WriterInterceptor; +import jakarta.ws.rs.ext.WriterInterceptorContext; + +@Path("/intercepted") +public class InterceptedResource { + + /** + * Interceptors write their message to this list, when they are invoked + * It is a bit dumb way, but it is the easier to get indicators if interceptors were invoked to the client + */ + public static List interceptorMessages = new ArrayList<>(); + + @WithWriterInterceptor + @GET + public InterceptedString getInterceptedString() { + return new InterceptedString("foo"); + } + + @GET() + @Path("/messages") + @Produces(MediaType.TEXT_PLAIN) + public String getMessages() { + StringBuilder outputMessage = new StringBuilder(); + for (String string : interceptorMessages) { + outputMessage.append(string); + } + return outputMessage.toString(); + } + + public static class InterceptedString { + public String name; + + public InterceptedString(String name) { + this.name = name; + } + } + + /** + * This annotation binds the providers to only intercept the method in this class. + * Otherwise, they would be global and intercept all endpoints across the entire application. + */ + @NameBinding + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + public @interface WithWriterInterceptor { + + } + + @Provider + public static class InterceptedStringHandler implements MessageBodyWriter { + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return type == InterceptedString.class; + } + + @Override + public void writeTo(InterceptedString interceptedString, Class type, Type genericType, Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException { + entityStream.write((interceptedString.name).getBytes(StandardCharsets.UTF_8)); + } + } + + @Provider + @WithWriterInterceptor + public static class UnconstrainedWriterInterceptor implements WriterInterceptor { + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + InterceptedResource.interceptorMessages.add("Unconstrained interceptor "); + context.proceed(); + } + } + + @Provider + @ConstrainedTo(RuntimeType.CLIENT) + @WithWriterInterceptor + public static class ClientWriterInterceptor implements WriterInterceptor { + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + InterceptedResource.interceptorMessages.add("Client interceptor "); + context.proceed(); + } + } + + @Provider + @ConstrainedTo(RuntimeType.SERVER) + @WithWriterInterceptor + public static class ServerWriterInterceptor implements WriterInterceptor { + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + InterceptedResource.interceptorMessages.add("Server interceptor "); + context.proceed(); + } + } +} diff --git a/http/http-advanced-reactive/src/main/resources/application.properties b/http/http-advanced-reactive/src/main/resources/application.properties index bc40751d2..87a9528cc 100644 --- a/http/http-advanced-reactive/src/main/resources/application.properties +++ b/http/http-advanced-reactive/src/main/resources/application.properties @@ -60,6 +60,8 @@ quarkus.keycloak.policy-enforcer.paths.grpc.path=/api/grpc/* quarkus.keycloak.policy-enforcer.paths.grpc.enforcement-mode=DISABLED quarkus.keycloak.policy-enforcer.paths.client.path=/api/client/* quarkus.keycloak.policy-enforcer.paths.client.enforcement-mode=DISABLED +quarkus.keycloak.policy-enforcer.paths.intercepted.path=/api/intercepted* +quarkus.keycloak.policy-enforcer.paths.intercepted.enforcement-mode=DISABLED quarkus.oidc.client-id=test-application-client quarkus.oidc.credentials.secret=test-application-client-secret # tolerate 1 minute of clock skew between the Keycloak server and the application diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java index 5b01aa253..2452f8585 100644 --- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java @@ -427,6 +427,24 @@ public void constraintsExist() throws JsonProcessingException { Assertions.assertEquals("^[A-Za-z]+$", validation.get("pattern").asText()); } + @Test + @Tag("https://github.com/quarkusio/quarkus/pull/36664") + public void interceptedTest() { + // make server to generate a response so interceptors might intercept it + // ignore response, we will read interceptors result later + getApp().given() + .get(ROOT_PATH + "/intercepted") + .thenReturn(); + + String response = getApp().given() + .get(ROOT_PATH + "/intercepted/messages") + .thenReturn().getBody().asString(); + + Assertions.assertTrue(response.contains("Unconstrained"), "Unconstrained interceptor should be invoked"); + Assertions.assertTrue(response.contains("Server"), "Server interceptor should be invoked"); + Assertions.assertFalse(response.contains("Client"), "Client interceptor should not be invoked"); + } + private void assertAcceptedMediaTypeEqualsResponseBody(String acceptedMediaType) { getApp() .given() diff --git a/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/InterceptedResource.java b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/InterceptedResource.java new file mode 100644 index 000000000..008e04408 --- /dev/null +++ b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/InterceptedResource.java @@ -0,0 +1,123 @@ +package io.quarkus.ts.http.advanced; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import jakarta.ws.rs.ConstrainedTo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NameBinding; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.RuntimeType; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Provider; +import jakarta.ws.rs.ext.WriterInterceptor; +import jakarta.ws.rs.ext.WriterInterceptorContext; + +@Path("/intercepted") +public class InterceptedResource { + + /** + * Interceptors write their message to this list, when they are invoked + * It is a bit dumb way, but it is the easier to get indicators if interceptors were invoked to the client + */ + public static List interceptorMessages = new ArrayList<>(); + + @WithWriterInterceptor + @GET + public InterceptedString getInterceptedString() { + return new InterceptedString("foo"); + } + + @GET() + @Path("/messages") + @Produces(MediaType.TEXT_PLAIN) + public String getMessages() { + StringBuilder outputMessage = new StringBuilder(); + for (String string : interceptorMessages) { + outputMessage.append(string); + } + return outputMessage.toString(); + } + + public static class InterceptedString { + public String name; + + public InterceptedString(String name) { + this.name = name; + } + } + + /** + * This annotation binds the providers to only intercept the method in this class. + * Otherwise, they would be global and intercept all endpoints across the entire application. + */ + @NameBinding + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + public @interface WithWriterInterceptor { + + } + + @Provider + public static class InterceptedStringHandler implements MessageBodyWriter { + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return type == InterceptedString.class; + } + + @Override + public void writeTo(InterceptedString interceptedString, Class type, Type genericType, Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException { + entityStream.write((interceptedString.name).getBytes(StandardCharsets.UTF_8)); + } + } + + @Provider + @WithWriterInterceptor + public static class UnconstrainedWriterInterceptor implements WriterInterceptor { + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + InterceptedResource.interceptorMessages.add("Unconstrained interceptor "); + context.proceed(); + } + } + + @Provider + @ConstrainedTo(RuntimeType.CLIENT) + @WithWriterInterceptor + public static class ClientWriterInterceptor implements WriterInterceptor { + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + InterceptedResource.interceptorMessages.add("Client interceptor "); + context.proceed(); + } + } + + @Provider + @ConstrainedTo(RuntimeType.SERVER) + @WithWriterInterceptor + public static class ServerWriterInterceptor implements WriterInterceptor { + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + InterceptedResource.interceptorMessages.add("Server interceptor "); + context.proceed(); + } + } +} diff --git a/http/http-advanced/src/main/resources/application.properties b/http/http-advanced/src/main/resources/application.properties index 8c2859709..da1dfa3e3 100644 --- a/http/http-advanced/src/main/resources/application.properties +++ b/http/http-advanced/src/main/resources/application.properties @@ -53,6 +53,8 @@ quarkus.keycloak.policy-enforcer.paths.grpc.path=/api/grpc/* quarkus.keycloak.policy-enforcer.paths.grpc.enforcement-mode=DISABLED quarkus.keycloak.policy-enforcer.paths.client.path=/api/client/* quarkus.keycloak.policy-enforcer.paths.client.enforcement-mode=DISABLED +quarkus.keycloak.policy-enforcer.paths.intercepted.path=/api/intercepted* +quarkus.keycloak.policy-enforcer.paths.intercepted.enforcement-mode=DISABLED quarkus.oidc.client-id=test-application-client quarkus.oidc.credentials.secret=test-application-client-secret # tolerate 1 minute of clock skew between the Keycloak server and the application diff --git a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/BaseHttpAdvancedIT.java b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/BaseHttpAdvancedIT.java index 994232eba..d28e33aef 100644 --- a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/BaseHttpAdvancedIT.java +++ b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/BaseHttpAdvancedIT.java @@ -255,6 +255,24 @@ public void sseConnectionTest() { assertTrue(response.contains("event: test234 test"), "SSE failed, unknown bug. Response: " + response); } + @Test + @Tag("https://github.com/quarkusio/quarkus/pull/36664") + public void interceptedTest() { + // make server to generate a response so interceptors might intercept it + // ignore response, we will read interceptors result later + getApp().given() + .get(ROOT_PATH + "/intercepted") + .thenReturn(); + + String response = getApp().given() + .get(ROOT_PATH + "/intercepted/messages") + .thenReturn().getBody().asString(); + + Assertions.assertTrue(response.contains("Unconstrained"), "Unconstrained interceptor should be invoked"); + Assertions.assertTrue(response.contains("Server"), "Server interceptor should be invoked"); + Assertions.assertFalse(response.contains("Client"), "Client interceptor should not be invoked"); + } + protected Protocol getProtocol() { return Protocol.HTTPS; }