Skip to content

Commit

Permalink
Support for conditional routing using expressions (#9094)
Browse files Browse the repository at this point in the history
Adds a new `@RequestCondition` annotation which can make the route execution logic conditional.
  • Loading branch information
graemerocher authored Apr 14, 2023
1 parent 2004749 commit 0c8c44f
Show file tree
Hide file tree
Showing 22 changed files with 261 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ public static void main(String[] args) throws Exception {
.measurementIterations(30)
.mode(Mode.AverageTime)
.timeUnit(TimeUnit.NANOSECONDS)
.addProfiler(AsyncProfiler.class, "libPath=/home/yawkat/bin/async-profiler-2.9-linux-x64/build/libasyncProfiler.so;output=flamegraph")
.forks(1)
.jvmArgsAppend("-Djmh.executor=CUSTOM", "-Djmh.executor.class=" + JmhFastThreadLocalExecutor.class.getName())
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,25 +118,27 @@ protected void generateBytecode(ExpressionVisitorContext ctx) {
@Override
protected CandidateMethod resolveUsedMethod(ExpressionVisitorContext ctx) {
List<Type> argumentTypes = resolveArgumentTypes(ctx);
Type calleeType = callee.resolveType(ctx);
ClassElement classElement = callee.resolveClassElement(ctx);

if (isNullSafe() && classElement.isAssignable(Optional.class)) {
// safe navigate optional
classElement = classElement.getFirstTypeArgument().orElse(classElement);
}


ElementQuery<MethodElement> methodQuery = buildMethodQuery();
List<CandidateMethod> candidateMethods =
ctx.visitorContext()
.getClassElement(calleeType.getClassName())
.stream()
.flatMap(element -> element.getEnclosedElements(methodQuery).stream())
List<CandidateMethod> candidateMethods = classElement.getEnclosedElements(methodQuery).stream()
.map(method -> toCandidateMethod(ctx, method, argumentTypes))
.filter(method -> method.isMatching(ctx.visitorContext()))
.toList();

if (candidateMethods.isEmpty()) {
throw new ExpressionCompilationException(
"No method [ " + name + stringifyArguments(ctx) + " ] available in class " + calleeType);
"No method [ " + name + stringifyArguments(ctx) + " ] available in class " + classElement.getName());
} else if (candidateMethods.size() > 1) {
throw new ExpressionCompilationException(
"Ambiguous method call. Found " + candidateMethods.size() +
" matching methods: " + candidateMethods + " in class " + calleeType);
" matching methods: " + candidateMethods + " in class " + classElement.getName());
}

return candidateMethods.iterator().next();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ protected void generateBytecode(ExpressionVisitorContext ctx) {
@Override
protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) {
resolveType(ctx);
return STRING_ELEMENT;
return ctx.visitorContext().getClassElement(String.class).orElse(STRING_ELEMENT);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ public void generateBytecode(ExpressionVisitorContext ctx) {

@Override
protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) {
return ClassElement.of(doResolveType(ctx).getClassName());
String className = doResolveType(ctx).getClassName();
return ctx.visitorContext().getClassElement(className)
.orElse(ClassElement.of(className));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public void generateBytecode(ExpressionVisitorContext ctx) {

@Override
protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) {
return STRING_ELEMENT;
return ctx.visitorContext().getClassElement(String.class).orElse(STRING_ELEMENT);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2017-2023 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.http.server.tck.tests;

import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.RouteCondition;
import io.micronaut.http.tck.AssertionUtils;
import io.micronaut.http.tck.HttpResponseAssertion;
import org.junit.jupiter.api.Test;

import java.io.IOException;

import static io.micronaut.http.tck.TestScenario.asserts;

@SuppressWarnings({
"java:S5960", // We're allowed assertions, as these are used in tests only
"checkstyle:MissingJavadocType",
"checkstyle:DesignForExtension"
})
public class ExpressionTest {
public static final String SPEC_NAME = "ExpressionTest";

@Test
void testConditionalGetRequest() throws IOException {
asserts(SPEC_NAME,
HttpRequest.GET("/expr/test"),
(server, request) -> AssertionUtils.assertThrows(server, request,
HttpResponseAssertion.builder()
.status(HttpStatus.NOT_FOUND)
.build()));

asserts(SPEC_NAME,
HttpRequest.GET("/expr/test")
.header(HttpHeaders.AUTHORIZATION, "foo"),
(server, request) -> AssertionUtils.assertDoesNotThrow(server, request,
HttpResponseAssertion.builder()
.status(HttpStatus.OK)
.body("ok")
.build()));
}

@Controller("/expr")
@Requires(property = "spec.name", value = SPEC_NAME)
static class ExpressionController {
@Get(value = "/test")
@RouteCondition("#{request.headers.getFirst('Authorization')?.contains('foo')}")
String testGet() {
return "ok";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@
*/
package io.micronaut.http.annotation;

import io.micronaut.context.annotation.AliasFor;
import io.micronaut.core.async.annotation.SingleResult;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import io.micronaut.context.annotation.AliasFor;
import io.micronaut.core.async.annotation.SingleResult;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* This annotation is designed for non-standard http methods, that you
* can provide by specifying the required {@link #method()} property.
*
*
* @author spirit-1984
* @since 1.3.0
*/
Expand Down
4 changes: 2 additions & 2 deletions http/src/main/java/io/micronaut/http/annotation/Delete.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
*/
package io.micronaut.http.annotation;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import io.micronaut.context.annotation.AliasFor;
import io.micronaut.core.async.annotation.SingleResult;

Expand All @@ -26,6 +24,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#DELETE}.
*
Expand Down
4 changes: 2 additions & 2 deletions http/src/main/java/io/micronaut/http/annotation/Get.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
*/
package io.micronaut.http.annotation;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import io.micronaut.context.annotation.AliasFor;
import io.micronaut.core.async.annotation.SingleResult;

Expand All @@ -26,6 +24,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#GET}.
*
Expand Down
4 changes: 2 additions & 2 deletions http/src/main/java/io/micronaut/http/annotation/Head.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
*/
package io.micronaut.http.annotation;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import io.micronaut.context.annotation.AliasFor;

import java.lang.annotation.Documented;
Expand All @@ -25,6 +23,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#HEAD}.
*
Expand Down
5 changes: 3 additions & 2 deletions http/src/main/java/io/micronaut/http/annotation/Options.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
*/
package io.micronaut.http.annotation;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import io.micronaut.context.annotation.AliasFor;

import java.lang.annotation.Documented;
Expand All @@ -25,6 +23,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Annotation that can be applied to method to signify the method receives a
* {@link io.micronaut.http.HttpMethod#OPTIONS}.
Expand Down Expand Up @@ -67,4 +67,5 @@
*/
@AliasFor(annotation = Consumes.class, member = "value")
String[] consumes() default {};

}
4 changes: 2 additions & 2 deletions http/src/main/java/io/micronaut/http/annotation/Patch.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
*/
package io.micronaut.http.annotation;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import io.micronaut.context.annotation.AliasFor;
import io.micronaut.core.async.annotation.SingleResult;

Expand All @@ -26,6 +24,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#PATCH}.
*
Expand Down
4 changes: 2 additions & 2 deletions http/src/main/java/io/micronaut/http/annotation/Post.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
*/
package io.micronaut.http.annotation;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import io.micronaut.context.annotation.AliasFor;
import io.micronaut.core.async.annotation.SingleResult;

Expand All @@ -26,6 +24,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#POST}.
*
Expand Down
4 changes: 2 additions & 2 deletions http/src/main/java/io/micronaut/http/annotation/Put.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
*/
package io.micronaut.http.annotation;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import io.micronaut.context.annotation.AliasFor;
import io.micronaut.core.async.annotation.SingleResult;

Expand All @@ -26,6 +24,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#PUT}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2017-2023 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.http.annotation;

import io.micronaut.context.annotation.AnnotationExpressionContext;
import io.micronaut.core.annotation.Experimental;
import io.micronaut.http.expression.RequestConditionContext;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Allows defining a condition for this route to match using an expression.
*
* <p>Within the scope of the expression a {@code request} variable is available that references the {@link io.micronaut.http.HttpRequest}.</p>
*
* <p>When added to a method the condition will be evaluated during route matching and if the condition
* does not evaluate to {@code true} the route will not be matched resulting in a {@link io.micronaut.http.HttpStatus#NOT_FOUND} response. </p>
*
* <p>Note that this annotation only applies to the server and is ignored when placed on declarative HTTP client routes.</p>
*
* @see io.micronaut.http.expression.RequestConditionContext
* @since 4.0.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@AnnotationExpressionContext(RequestConditionContext.class)
@Experimental
public @interface RouteCondition {
/**
* An expression that evalutes to {@code true} or {@code false}.
* @return The expression
* @since 4.0.0
*/
String value();
}
4 changes: 2 additions & 2 deletions http/src/main/java/io/micronaut/http/annotation/Trace.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
*/
package io.micronaut.http.annotation;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import io.micronaut.context.annotation.AliasFor;

import java.lang.annotation.Documented;
Expand All @@ -25,6 +23,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Annotation that can be applied to method to signify the method receives a {@link io.micronaut.http.HttpMethod#TRACE}.
*
Expand Down
Loading

0 comments on commit 0c8c44f

Please sign in to comment.