From 0c8c44f008db187fb55d470833d6dcf0d0f812b3 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 14 Apr 2023 13:08:05 +0200 Subject: [PATCH] Support for conditional routing using expressions (#9094) Adds a new `@RequestCondition` annotation which can make the route execution logic conditional. --- .../server/stack/FullHttpStackBenchmark.java | 1 - .../parser/ast/access/ElementMethodCall.java | 18 ++--- .../parser/ast/access/EnvironmentAccess.java | 2 +- .../ast/conditional/TernaryExpression.java | 4 +- .../parser/ast/literal/StringLiteral.java | 2 +- .../http/server/tck/tests/ExpressionTest.java | 70 +++++++++++++++++++ .../http/annotation/CustomHttpMethod.java | 8 +-- .../io/micronaut/http/annotation/Delete.java | 4 +- .../io/micronaut/http/annotation/Get.java | 4 +- .../io/micronaut/http/annotation/Head.java | 4 +- .../io/micronaut/http/annotation/Options.java | 5 +- .../io/micronaut/http/annotation/Patch.java | 4 +- .../io/micronaut/http/annotation/Post.java | 4 +- .../io/micronaut/http/annotation/Put.java | 4 +- .../http/annotation/RouteCondition.java | 51 ++++++++++++++ .../io/micronaut/http/annotation/Trace.java | 4 +- .../expression/RequestConditionContext.java | 57 +++++++++++++++ .../micronaut/http/filter/FilterRunner.java | 2 +- .../ContextMethodCallsExpressionsSpec.groovy | 2 + ...ontextPropertyAccessExpressionsSpec.groovy | 32 +++++++++ .../web/router/DefaultRouteBuilder.java | 9 +++ .../guide/config/evaluatedExpressions.adoc | 4 +- 22 files changed, 261 insertions(+), 34 deletions(-) create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ExpressionTest.java create mode 100644 http/src/main/java/io/micronaut/http/annotation/RouteCondition.java create mode 100644 http/src/main/java/io/micronaut/http/expression/RequestConditionContext.java diff --git a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java index 4914ea83f4e..48c723611db 100644 --- a/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java +++ b/benchmarks/src/jmh/java/io/micronaut/http/server/stack/FullHttpStackBenchmark.java @@ -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(); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java index ba5dc064f22..93f0a3fecd0 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java @@ -118,25 +118,27 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { @Override protected CandidateMethod resolveUsedMethod(ExpressionVisitorContext ctx) { List 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 methodQuery = buildMethodQuery(); - List candidateMethods = - ctx.visitorContext() - .getClassElement(calleeType.getClassName()) - .stream() - .flatMap(element -> element.getEnclosedElements(methodQuery).stream()) + List 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(); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/EnvironmentAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/EnvironmentAccess.java index ce9cbd48607..8d5ba39e3ee 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/EnvironmentAccess.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/EnvironmentAccess.java @@ -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 diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java index f43bbdc4766..630f99a5857 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java @@ -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)); } /** diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java index 7c6cc6ac57b..19318eaa308 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java @@ -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 diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ExpressionTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ExpressionTest.java new file mode 100644 index 00000000000..55f45928fe6 --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ExpressionTest.java @@ -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"; + } + } +} diff --git a/http/src/main/java/io/micronaut/http/annotation/CustomHttpMethod.java b/http/src/main/java/io/micronaut/http/annotation/CustomHttpMethod.java index 8e1971aecf2..4b69110ff07 100644 --- a/http/src/main/java/io/micronaut/http/annotation/CustomHttpMethod.java +++ b/http/src/main/java/io/micronaut/http/annotation/CustomHttpMethod.java @@ -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 */ diff --git a/http/src/main/java/io/micronaut/http/annotation/Delete.java b/http/src/main/java/io/micronaut/http/annotation/Delete.java index 655407cd1e0..80d334b8ea8 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Delete.java +++ b/http/src/main/java/io/micronaut/http/annotation/Delete.java @@ -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; @@ -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}. * diff --git a/http/src/main/java/io/micronaut/http/annotation/Get.java b/http/src/main/java/io/micronaut/http/annotation/Get.java index 69ffda905d5..3e0f2e1b792 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Get.java +++ b/http/src/main/java/io/micronaut/http/annotation/Get.java @@ -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; @@ -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}. * diff --git a/http/src/main/java/io/micronaut/http/annotation/Head.java b/http/src/main/java/io/micronaut/http/annotation/Head.java index 972ea6718c6..31a4d1bacc5 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Head.java +++ b/http/src/main/java/io/micronaut/http/annotation/Head.java @@ -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; @@ -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}. * diff --git a/http/src/main/java/io/micronaut/http/annotation/Options.java b/http/src/main/java/io/micronaut/http/annotation/Options.java index 9db08f6a5fa..7a4408b8960 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Options.java +++ b/http/src/main/java/io/micronaut/http/annotation/Options.java @@ -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; @@ -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}. @@ -67,4 +67,5 @@ */ @AliasFor(annotation = Consumes.class, member = "value") String[] consumes() default {}; + } diff --git a/http/src/main/java/io/micronaut/http/annotation/Patch.java b/http/src/main/java/io/micronaut/http/annotation/Patch.java index d79d98dd857..9d193096da9 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Patch.java +++ b/http/src/main/java/io/micronaut/http/annotation/Patch.java @@ -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; @@ -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}. * diff --git a/http/src/main/java/io/micronaut/http/annotation/Post.java b/http/src/main/java/io/micronaut/http/annotation/Post.java index 26ff5390a93..ce55880c631 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Post.java +++ b/http/src/main/java/io/micronaut/http/annotation/Post.java @@ -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; @@ -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}. * diff --git a/http/src/main/java/io/micronaut/http/annotation/Put.java b/http/src/main/java/io/micronaut/http/annotation/Put.java index 86d856a15b0..b3338ec0f11 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Put.java +++ b/http/src/main/java/io/micronaut/http/annotation/Put.java @@ -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; @@ -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}. * diff --git a/http/src/main/java/io/micronaut/http/annotation/RouteCondition.java b/http/src/main/java/io/micronaut/http/annotation/RouteCondition.java new file mode 100644 index 00000000000..205afd0a89c --- /dev/null +++ b/http/src/main/java/io/micronaut/http/annotation/RouteCondition.java @@ -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. + * + *

Within the scope of the expression a {@code request} variable is available that references the {@link io.micronaut.http.HttpRequest}.

+ * + *

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.

+ * + *

Note that this annotation only applies to the server and is ignored when placed on declarative HTTP client routes.

+ * + * @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(); +} diff --git a/http/src/main/java/io/micronaut/http/annotation/Trace.java b/http/src/main/java/io/micronaut/http/annotation/Trace.java index a2296c2ea63..f173be76226 100644 --- a/http/src/main/java/io/micronaut/http/annotation/Trace.java +++ b/http/src/main/java/io/micronaut/http/annotation/Trace.java @@ -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; @@ -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}. * diff --git a/http/src/main/java/io/micronaut/http/expression/RequestConditionContext.java b/http/src/main/java/io/micronaut/http/expression/RequestConditionContext.java new file mode 100644 index 00000000000..a725d471343 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/expression/RequestConditionContext.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017-2022 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.expression; + +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.context.ServerRequestContext; +import jakarta.inject.Singleton; + +/** + * An expression evaluation context for use with HTTP annotations and the {@code condition} member. + * + *

This context allows access to the current request via a {@code request} object which is an instance of {@link HttpRequest}.

+ * + * @see HttpRequest + * @author graemerocher + * @since 4.0.0 + */ +@Singleton +@Experimental +public final class RequestConditionContext { + + /** + * Default constructor. + */ + @Internal + RequestConditionContext() { + } + + /** + * @return The request object. + */ + @SuppressWarnings("java:S1452") + public @NonNull HttpRequest getRequest() { + return currentRequest(); + } + + private static HttpRequest currentRequest() { + return ServerRequestContext.currentRequest() + .orElseThrow(() -> new IllegalStateException("No request present in evaluation context")); + } +} diff --git a/http/src/main/java/io/micronaut/http/filter/FilterRunner.java b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java index c5a8e2b0435..3f627a3ff93 100644 --- a/http/src/main/java/io/micronaut/http/filter/FilterRunner.java +++ b/http/src/main/java/io/micronaut/http/filter/FilterRunner.java @@ -787,8 +787,8 @@ public ExecutionFlow processResult(Publisher> pub private static sealed class ReactiveContinuationImpl implements FilterContinuation>>, InternalFilterContinuation>> { - private final Function> downstream; protected FilterContext filterContext; + private final Function> downstream; private ReactiveContinuationImpl(Function> downstream, FilterContext filterContext) { diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy index 45183ca9862..de8f07bd533 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy @@ -1,9 +1,11 @@ package io.micronaut.expressions import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec +import spock.lang.Ignore class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec{ + @Ignore("Already tested in Java and fails intermittently due to a Groovy classloading bug") void "test context method calls"() { given: Object expr1 = evaluateAgainstContext("#{ #getIntValue() }", diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy index bfde85a583c..fe621a66901 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -236,6 +236,38 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS expr == "test" } + void "test multi-level context property access safe navigation with optionals - method call"() { + given: + Object expr = evaluateAgainstContext("#{ foo?.bar?.getName() }", + """ + import java.util.Optional; + + @jakarta.inject.Singleton + class Context { + public Optional getFoo() { + return Optional.of(new Foo()); + } + } + + class Foo { + private Bar bar = new Bar(); + public Optional getBar() { + return Optional.ofNullable(bar); + } + } + + class Bar { + private String name = "test"; + public String getName() { + return name; + } + } + """) + + expect: + expr == "test" + } + void "test multi-level context property access non-safe navigation"() { when: diff --git a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java index c375de9d422..801db858f65 100644 --- a/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/DefaultRouteBuilder.java @@ -21,6 +21,7 @@ import io.micronaut.context.env.Environment; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataResolver; +import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; @@ -30,6 +31,7 @@ import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.RouteCondition; import io.micronaut.http.filter.GenericHttpFilter; import io.micronaut.http.filter.HttpFilter; import io.micronaut.http.uri.UriMatchTemplate; @@ -37,6 +39,7 @@ import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.MethodExecutionHandle; import io.micronaut.inject.MethodReference; +import io.micronaut.inject.annotation.EvaluatedAnnotationValue; import io.micronaut.scheduling.executor.ExecutorSelector; import io.micronaut.scheduling.executor.ThreadSelection; import io.micronaut.web.router.exceptions.RoutingException; @@ -872,6 +875,12 @@ final class DefaultUriRoute extends AbstractRoute implements UriRoute { this.uriMatchTemplate = uriTemplate; this.httpMethodName = httpMethodName; this.executorSelector = new RouteExecutorSelector(); + if (targetMethod.isPresent(RouteCondition.class, AnnotationMetadata.VALUE_MEMBER)) { + AnnotationValue annotation = targetMethod.getAnnotation(RouteCondition.class); + if (annotation instanceof EvaluatedAnnotationValue) { + where(request -> annotation.booleanValue().orElse(false)); + } + } } @Override diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index 4bcd1fe46ba..e96234da39f 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -41,7 +41,7 @@ set of available features. Even though the complexity of expression is only limi constructs, it is in general not recommended to place complex logic inside an expression as there are usually better ways to achieve the same result. -== Example Use Case +== Using Expressions in Micronaut framework Expressions can be used anywhere throughout the Micronaut framework and associated modules, but as an example, you can use them to implement simple scheduled job control, for example: @@ -50,6 +50,8 @@ snippet::io.micronaut.docs.expressions.ExampleJob[title="Job Control with Expres <1> Here the `condition` member of the ann:scheduling.annotation.Scheduled[] annotation is used to only execute the job if a pre-condition is met. <2> The `condition` invokes a method of a parameter which is another bean called `ExampleJobControl` which can be used to pause and resume execution of the job as desired. +TIP: You can also use expressions to perform conditional routing using the ann:http.annotation.RouteCondition[] annotation. + == Evaluated Expression Language Reference The Evaluated Expressions syntax supports the following functionality: