From f600839ad859fcb667b99eec6b1ded1289b2a1ce Mon Sep 17 00:00:00 2001 From: Sergey Gavrilov Date: Fri, 19 Aug 2022 17:16:39 +0300 Subject: [PATCH 01/35] compile time expressions impl --- .../aop/chain/DefaultInterceptorRegistry.java | 4 + .../aop/chain/MethodInterceptorChain.java | 9 + .../micronaut/aop/writer/AopProxyWriter.java | 6 + .../EvaluatedExpressionWriter.java | 140 +++ ...eanContextExpressionEvaluationContext.java | 102 ++ .../CompositeExpressionEvaluationContext.java | 54 ++ .../context/ExpressionContextFactory.java | 136 +++ .../context/ExpressionContextLoader.java | 94 ++ .../context/ExpressionContextReference.java | 32 + .../context/ExpressionEvaluationContext.java | 56 ++ .../context/ExpressionWithContext.java | 55 ++ .../MethodExpressionEvaluationContext.java | 50 + ...undEvaluatedEvaluatedExpressionParser.java | 149 +++ .../parser/EvaluatedExpressionParser.java | 40 + ...gleEvaluatedEvaluatedExpressionParser.java | 515 ++++++++++ .../parser/ast/ExpressionNode.java | 75 ++ .../parser/ast/access/AbstractMethodCall.java | 199 ++++ .../parser/ast/access/CandidateMethod.java | 245 +++++ .../ast/access/ContextElementAccess.java | 102 ++ .../parser/ast/access/ContextMethodCall.java | 108 +++ .../access/ContextMethodParameterAccess.java | 75 ++ .../parser/ast/access/ElementMethodCall.java | 121 +++ .../parser/ast/access/PropertyAccess.java | 78 ++ .../ast/collection/OneDimensionalArray.java | 87 ++ .../ast/conditional/TernaryExpression.java | 135 +++ .../parser/ast/literal/BoolLiteral.java | 48 + .../parser/ast/literal/DoubleLiteral.java | 48 + .../parser/ast/literal/FloatLiteral.java | 48 + .../parser/ast/literal/IntLiteral.java | 49 + .../parser/ast/literal/LongLiteral.java | 48 + .../parser/ast/literal/NullLiteral.java | 43 + .../parser/ast/literal/StringLiteral.java | 53 + .../ast/operator/binary/AddOperator.java | 155 +++ .../ast/operator/binary/AndOperator.java | 67 ++ .../ast/operator/binary/BinaryOperator.java | 51 + .../ast/operator/binary/DivOperator.java | 58 ++ .../ast/operator/binary/EqOperator.java | 62 ++ .../ast/operator/binary/GtOperator.java | 45 + .../ast/operator/binary/GteOperator.java | 45 + .../operator/binary/InstanceofOperator.java | 68 ++ .../ast/operator/binary/LogicalOperator.java | 49 + .../ast/operator/binary/LtOperator.java | 45 + .../ast/operator/binary/LteOperator.java | 45 + .../ast/operator/binary/MatchesOperator.java | 76 ++ .../ast/operator/binary/MathOperator.java | 68 ++ .../ast/operator/binary/ModOperator.java | 58 ++ .../ast/operator/binary/MulOperator.java | 59 ++ .../ast/operator/binary/NeqOperator.java | 62 ++ .../ast/operator/binary/OrOperator.java | 55 ++ .../ast/operator/binary/PowOperator.java | 88 ++ .../operator/binary/RelationalOperator.java | 111 +++ .../ast/operator/binary/SubOperator.java | 59 ++ .../ast/operator/unary/NegOperator.java | 83 ++ .../ast/operator/unary/NotOperator.java | 74 ++ .../ast/operator/unary/PosOperator.java | 53 + .../ast/operator/unary/UnaryOperator.java | 43 + .../parser/ast/types/TypeIdentifier.java | 93 ++ .../EvaluatedExpressionCompilationUtils.java | 254 +++++ .../parser/ast/util/TypeDescriptors.java | 193 ++++ .../ExpressionCompilationContext.java | 36 + .../ExpressionCompilationException.java | 29 + .../exception/ExpressionParsingException.java | 28 + .../expressions/parser/token/Token.java | 30 + .../expressions/parser/token/TokenType.java | 85 ++ .../expressions/parser/token/Tokenizer.java | 214 +++++ .../util/EvaluatedExpressionsUtils.java | 83 ++ .../AbstractAnnotationMetadataBuilder.java | 49 +- .../annotation/AnnotationMetadataWriter.java | 45 +- .../micronaut/inject/ast/MethodElement.java | 9 + ...edExpressionContextTypeElementVisitor.java | 82 ++ .../ExpressionContextReferenceWriter.java | 91 ++ .../writer/AbstractClassFileWriter.java | 19 + .../inject/writer/BeanDefinitionVisitor.java | 9 + .../inject/writer/BeanDefinitionWriter.java | 103 +- .../ExecutableMethodsDefinitionWriter.java | 35 +- ...icronaut.inject.visitor.TypeElementVisitor | 1 + .../core/annotation/AnnotationMetadata.java | 10 + .../core/annotation/AnnotationValue.java | 14 +- .../annotation/AnnotationValueBuilder.java | 5 +- .../EvaluatedExpressionReference.java | 77 ++ .../core/expression/EvaluatedExpression.java | 53 + .../micronaut/http/uri/UriMatchTemplate.java | 2 +- .../AbstractEvaluatedExpressionsSpec.groovy | 123 +++ .../ast/groovy/InjectTransform.groovy | 10 + .../GroovyAnnotationMetadataBuilder.java | 55 +- ...odehaus.groovy.transform.ASTTransformation | 2 +- ...notationLevelContextExpressionsSpec.groovy | 73 ++ .../ArrayMethodsExpressionsSpec.groovy | 220 +++++ .../CompoundExpressionsSpec.groovy | 58 ++ .../ContextMethodCallsExpressionsSpec.groovy | 142 +++ ...ontextPropertyAccessExpressionsSpec.groovy | 59 ++ .../expressions/LiteralExpressionsSpec.groovy | 96 ++ ...entEvaluationContextExpressionsSpec.groovy | 31 + .../OperatorExpressionsSpec.groovy | 905 ++++++++++++++++++ .../TernaryOperationExpressionsSpec.groovy | 36 + .../TestExpressionsInjectionSpec.groovy | 167 ++++ .../TestExpressionsUsageSpec.groovy | 129 +++ .../TypeIdentifierExpressionsSpec.groovy | 52 + .../AbstractEvaluatedExpressionsSpec.groovy | 124 +++ .../BeanDefinitionInjectProcessor.java | 21 +- .../JavaAnnotationMetadataBuilder.java | 23 +- .../TypeElementVisitorProcessor.java | 3 + .../processing/visitor/JavaMethodElement.java | 23 + ...notationLevelContextExpressionsSpec.groovy | 73 ++ .../ArrayMethodsExpressionsSpec.groovy | 219 +++++ .../CompoundExpressionsSpec.groovy | 56 ++ .../ContextMethodCallsExpressionsSpec.groovy | 142 +++ ...ontextPropertyAccessExpressionsSpec.groovy | 61 ++ .../expressions/LiteralExpressionSpec.groovy | 95 ++ ...entEvaluationContextExpressionsSpec.groovy | 33 + .../expressions/OperatorExpressionSpec.groovy | 904 +++++++++++++++++ .../TernaryOperationExpressionsSpec.groovy | 36 + .../TestExpressionsInjectionSpec.groovy | 168 ++++ .../TestExpressionsUsageSpec.groovy | 129 +++ .../TypeIdentifierExpressionsSpec.groovy | 52 + .../KotlinAnnotationMetadataBuilder.kt | 8 +- .../visitor/BeanIntrospectionSpec.groovy | 4 +- .../AbstractBeanResolutionContext.java | 5 + .../context/AbstractEvaluatedExpression.java | 115 +++ .../AbstractExecutableMethodsDefinition.java | 47 +- .../AbstractInitializableBeanDefinition.java | 239 ++++- ...tInitializableBeanDefinitionReference.java | 3 + .../context/BeanDefinitionAware.java | 33 + .../context/ContextConfigurable.java | 31 + .../context/ExpressionsAwareArgument.java | 89 ++ .../micronaut/context/RequiresCondition.java | 7 + .../EvaluatedExpressionContext.java | 39 + .../micronaut/context/annotation/Value.java | 7 +- .../ExpressionEvaluationException.java | 32 + ...AbstractEnvironmentAnnotationMetadata.java | 5 + .../AnnotationMetadataHierarchy.java | 10 + .../annotation/DefaultAnnotationMetadata.java | 33 + .../EvaluatedAnnotationMetadata.java | 101 ++ .../annotation/EvaluatedAnnotationValue.java | 86 ++ .../EvaluatedConvertibleValuesMap.java | 108 +++ .../MappingAnnotationMetadataDelegate.java | 451 +++++++++ .../annotation/MutableAnnotationMetadata.java | 55 +- 137 files changed, 11976 insertions(+), 84 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/BeanContextExpressionEvaluationContext.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/CompositeExpressionEvaluationContext.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextFactory.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextLoader.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextReference.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContext.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionWithContext.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/MethodExpressionEvaluationContext.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/CompoundEvaluatedEvaluatedExpressionParser.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/EvaluatedExpressionParser.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/CandidateMethod.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AddOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AndOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/DivOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/EqOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GtOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GteOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LogicalOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LtOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LteOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MathOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ModOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MulOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NeqOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/OrOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/PowOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/SubOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NegOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NotOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/PosOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/EvaluatedExpressionCompilationUtils.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionCompilationContext.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionCompilationException.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionParsingException.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/token/Token.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java create mode 100644 core-processor/src/main/java/io/micronaut/inject/beans/visitor/EvaluatedExpressionContextTypeElementVisitor.java create mode 100644 core-processor/src/main/java/io/micronaut/inject/beans/visitor/ExpressionContextReferenceWriter.java create mode 100644 core/src/main/java/io/micronaut/core/annotation/EvaluatedExpressionReference.java create mode 100644 core/src/main/java/io/micronaut/core/expression/EvaluatedExpression.java create mode 100644 inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/LiteralExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/OperatorExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy create mode 100644 inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/LiteralExpressionSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy create mode 100644 inject/src/main/java/io/micronaut/context/AbstractEvaluatedExpression.java create mode 100644 inject/src/main/java/io/micronaut/context/BeanDefinitionAware.java create mode 100644 inject/src/main/java/io/micronaut/context/ContextConfigurable.java create mode 100644 inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java create mode 100644 inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java create mode 100644 inject/src/main/java/io/micronaut/context/exceptions/ExpressionEvaluationException.java create mode 100644 inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java create mode 100644 inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java create mode 100644 inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java create mode 100644 inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java diff --git a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java index 44410c571b7..4f4415bd09d 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java +++ b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java @@ -34,6 +34,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.type.Executable; import io.micronaut.inject.ExecutableMethod; +import io.micronaut.context.ContextConfigurable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -239,6 +240,9 @@ public Interceptor[] resolveConstructorInterceptors( } private static void instrumentAnnotationMetadata(BeanContext beanContext, Object method) { + if (method instanceof ContextConfigurable ctxConfigurable) { + ctxConfigurable.configure(beanContext); + } if (beanContext instanceof ApplicationContext applicationContext && method instanceof EnvironmentConfigurable environmentConfigurable) { // ensure metadata is environment aware if (environmentConfigurable.hasPropertyExpressions()) { diff --git a/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java b/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java index cd792b0a919..4836ce51af1 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java +++ b/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java @@ -31,6 +31,7 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; +import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; import io.micronaut.inject.qualifiers.Qualifiers; import java.lang.reflect.Method; @@ -97,6 +98,14 @@ public MethodInterceptorChain(Interceptor[] interceptors, T target, Execut this.kind = null; } + @Override + public AnnotationMetadata getAnnotationMetadata() { + if (executionHandle.getAnnotationMetadata() instanceof EvaluatedAnnotationMetadata eam) { + return EvaluatedAnnotationMetadata.wrapIfNecessary(eam, originalParameters); + } + return executionHandle.getAnnotationMetadata(); + } + @Override @NonNull public InterceptorKind getKind() { diff --git a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java index a7602010107..d2e72632290 100644 --- a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java +++ b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java @@ -41,6 +41,7 @@ import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.Toggleable; import io.micronaut.core.value.OptionalValues; +import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.ProxyBeanDefinition; @@ -1301,6 +1302,11 @@ public AnnotationMetadata getAnnotationMetadata() { return proxyBeanDefinitionWriter.getAnnotationMetadata(); } + @Override + public Set getEvaluatedExpressions() { + return proxyBeanDefinitionWriter.getEvaluatedExpressions(); + } + @Override public void visitConfigBuilderField(ClassElement type, String field, AnnotationMetadata annotationMetadata, ConfigurationMetadataBuilder metadataBuilder, boolean isInterface) { proxyBeanDefinitionWriter.visitConfigBuilderField(type, field, annotationMetadata, metadataBuilder, isInterface); diff --git a/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java b/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java new file mode 100644 index 00000000000..f6f2ba36194 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java @@ -0,0 +1,140 @@ +/* + * 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.expressions; + +import io.micronaut.context.AbstractEvaluatedExpression; +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.context.ExpressionWithContext; +import io.micronaut.expressions.parser.CompoundEvaluatedEvaluatedExpressionParser; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.expressions.parser.exception.ExpressionParsingException; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.inject.writer.AbstractClassFileWriter; +import io.micronaut.inject.writer.ClassWriterOutputVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; + +import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES; +import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS; + +/** + * Writer for compile-time expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class EvaluatedExpressionWriter extends AbstractClassFileWriter { + private static final Method EVALUATED_EXPRESSIONS_CONSTRUCTOR = + new Method(CONSTRUCTOR_NAME, getConstructorDescriptor(Object.class)); + + private static final Type EVALUATED_EXPRESSION_TYPE = + Type.getType(AbstractEvaluatedExpression.class); + + private final ExpressionWithContext expressionMetadata; + private final VisitorContext visitorContext; + private final Element originatingElement; + + public EvaluatedExpressionWriter(ExpressionWithContext expressionMetadata, + VisitorContext visitorContext, + Element originatingElement) { + this.visitorContext = visitorContext; + this.expressionMetadata = expressionMetadata; + this.originatingElement = originatingElement; + } + + @Override + public void accept(ClassWriterOutputVisitor outputVisitor) throws IOException { + String expressionClassName = expressionMetadata.expressionClassName(); + try (OutputStream outputStream = outputVisitor.visitClass(expressionClassName, + getOriginatingElements())) { + ClassWriter classWriter = generateClassBytes(expressionClassName); + outputStream.write(classWriter.toByteArray()); + } + } + + private ClassWriter generateClassBytes(String expressionClassName) { + ClassWriter classWriter = new ClassWriter(COMPUTE_MAXS | COMPUTE_FRAMES); + + startPublicClass( + classWriter, + getInternalName(expressionClassName), + EVALUATED_EXPRESSION_TYPE); + + GeneratorAdapter cv = startConstructor(classWriter, Object.class); + cv.loadThis(); + cv.loadArg(0); + + cv.invokeConstructor(EVALUATED_EXPRESSION_TYPE, EVALUATED_EXPRESSIONS_CONSTRUCTOR); + // RETURN + cv.visitInsn(RETURN); + // MAXSTACK = 2 + // MAXLOCALS = 1 + cv.visitMaxs(2, 1); + + GeneratorAdapter evaluateMethodVisitor = startProtectedVarargsMethod(classWriter, "doEvaluate", + Object.class.getName(), Object.class.getName() + "[]"); + + ExpressionCompilationContext ctx = new ExpressionCompilationContext( + expressionMetadata.evaluationContext(), + visitorContext, + evaluateMethodVisitor); + + Object annotationValue = expressionMetadata.annotationValue(); + + try { + ExpressionNode ast = new CompoundEvaluatedEvaluatedExpressionParser(annotationValue).parse(); + ast.compile(ctx); + pushBoxPrimitiveIfNecessary(ast.resolveType(ctx), evaluateMethodVisitor); + } catch (ExpressionParsingException | ExpressionCompilationException ex) { + failCompilation(ex, annotationValue); + } + + evaluateMethodVisitor.visitMaxs(2, 1); + evaluateMethodVisitor.returnValue(); + return classWriter; + } + + private void failCompilation(Throwable ex, Object initialAnnotationValue) { + String strRepresentation = null; + + if (initialAnnotationValue instanceof String str) { + strRepresentation = str; + } else if (initialAnnotationValue instanceof String[] strArray) { + strRepresentation = Arrays.toString(strArray); + } + + String message = null; + if (ex instanceof ExpressionParsingException parsingException) { + message = "Failed to parse evaluated expression [" + strRepresentation + "]. " + + "Cause: " + parsingException.getMessage(); + } else if (ex instanceof ExpressionCompilationException compilationException) { + message = "Failed to compile evaluated expression [" + strRepresentation + "]. " + + "Cause: " + compilationException.getMessage(); + } + + visitorContext.fail(message, originatingElement); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/BeanContextExpressionEvaluationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/BeanContextExpressionEvaluationContext.java new file mode 100644 index 00000000000..876d3f5cb1f --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/BeanContextExpressionEvaluationContext.java @@ -0,0 +1,102 @@ +/* + * 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.expressions.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.inject.ast.AnnotationElement; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.ast.PropertyElementQuery; +import io.micronaut.inject.ast.TypedElement; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import static io.micronaut.inject.ast.ElementQuery.ALL_METHODS; +import static java.util.function.Predicate.not; + +/** + * Root expression evaluation context is a context which elements are + * registered in and obtained through bean context. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class BeanContextExpressionEvaluationContext implements ExpressionEvaluationContext { + + private final Set contextTypes = ConcurrentHashMap.newKeySet(); + + public BeanContextExpressionEvaluationContext(ClassElement... contextClasses) { + Arrays.stream(contextClasses) + .filter(classElement -> !(classElement instanceof AnnotationElement)) + .forEach(contextTypes::add); + } + + /** + * @return set of class elements registered in this evaluation context. + */ + public Set getContextTypes() { + return contextTypes; + } + + @Override + public List getMethods(String name) { + return contextTypes.stream() + .map(element -> findMatchingMethods(element, name)) + .flatMap(Collection::stream) + .toList(); + } + + private List findMatchingMethods(ClassElement classElement, String name) { + String propertyName = NameUtils.getPropertyNameForGetter(name, + PropertyElementQuery.of(classElement.getAnnotationMetadata()) + .getReadPrefixes()); + + return Stream.concat( + classElement.getEnclosedElements(ALL_METHODS.onlyAccessible().named(name)).stream(), + getNamedProperties(classElement, propertyName).stream() + .map(PropertyElement::getReadMethod) + .flatMap(Optional::stream)) + .distinct() + .filter(method -> method.getSimpleName().equals(name)) + .toList(); + } + + @Override + public List getTypedElements(String name) { + return contextTypes.stream() + .flatMap(classElement -> getNamedProperties(classElement, name).stream()) + .toList(); + } + + private List getNamedProperties(ClassElement classElement, String name) { + return classElement.getBeanProperties( + PropertyElementQuery.of(classElement.getAnnotationMetadata()) + .includes(Collections.singleton(name))) + .stream() + .filter(not(PropertyElement::isExcluded)) + .toList(); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/CompositeExpressionEvaluationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/CompositeExpressionEvaluationContext.java new file mode 100644 index 00000000000..4d0b7406e42 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/CompositeExpressionEvaluationContext.java @@ -0,0 +1,54 @@ +/* + * 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.expressions.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.TypedElement; + +import java.util.Arrays; +import java.util.List; + +/** + * Class representing multiple expression evaluation contexts. This class is used as a wrapper + * to simplify obtaining context elements in cases when multiple contexts are available at + * compilation stage. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class CompositeExpressionEvaluationContext implements ExpressionEvaluationContext { + private final ExpressionEvaluationContext[] evaluationContexts; + + public CompositeExpressionEvaluationContext(ExpressionEvaluationContext... evaluationContexts) { + this.evaluationContexts = evaluationContexts; + } + + @Override + public List getMethods(String name) { + return Arrays.stream(evaluationContexts) + .flatMap(context -> context.getMethods(name).stream()) + .toList(); + } + + @Override + public List getTypedElements(String name) { + return Arrays.stream(evaluationContexts) + .flatMap(context -> context.getTypedElements(name).stream()) + .toList(); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextFactory.java new file mode 100644 index 00000000000..c8ec24f7dd6 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextFactory.java @@ -0,0 +1,136 @@ +/* + * 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.expressions.context; + +import io.micronaut.context.annotation.EvaluatedExpressionContext; +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.EvaluatedExpressionReference; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.visitor.VisitorContext; + +import java.util.Optional; + +/** + * Factory for producing expression evaluation context. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class ExpressionContextFactory { + private final ExpressionEvaluationContext loadedExpressionEvaluationContext; + private final VisitorContext visitorContext; + + public ExpressionContextFactory(VisitorContext visitorContext) { + this.loadedExpressionEvaluationContext = ExpressionContextLoader.getExpressionContext(); + this.visitorContext = visitorContext; + } + + /** + * Builds expression evaluation context for method. Expression evaluation context + * for method allows referencing method parameter names in evaluated expressions. + * + * @param expression expression reference + * @param methodElement annotated method + * @return evaluation context for method + */ + @NonNull + public ExpressionEvaluationContext buildForMethod(@NonNull EvaluatedExpressionReference expression, + @NonNull MethodElement methodElement) { + ExpressionEvaluationContext evaluationContext = buildEvaluationContext(expression); + return new CompositeExpressionEvaluationContext( + evaluationContext, + new MethodExpressionEvaluationContext(methodElement)); + } + + /** + * Builds expression evaluation context for expression reference. + * + * @param expression expression reference + * @return evaluation context for method + */ + @NonNull + public ExpressionEvaluationContext buildEvaluationContext(EvaluatedExpressionReference expression) { + String annotationName = expression.annotationName(); + String memberName = expression.annotationMember(); + + ClassElement annotation = visitorContext.getClassElement(annotationName).orElse(null); + + ExpressionEvaluationContext evaluationContext = loadedExpressionEvaluationContext; + if (annotation != null) { + evaluationContext = addAnnotationLevelEvaluationContext(evaluationContext, annotation); + evaluationContext = addAnnotationMemberEvaluationContext(evaluationContext, annotation, memberName); + } + + return evaluationContext; + } + + private ExpressionEvaluationContext addAnnotationLevelEvaluationContext( + ExpressionEvaluationContext currentEvaluationContext, + ClassElement annotation) { + + ExpressionEvaluationContext annotationLevelContext = + Optional.ofNullable(annotation.getAnnotation(EvaluatedExpressionContext.class)) + .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER)) + .map(AnnotationClassValue::getName) + .flatMap(visitorContext::getClassElement) + .map(BeanContextExpressionEvaluationContext::new) + .orElse(null); + + if (annotationLevelContext != null) { + return new CompositeExpressionEvaluationContext( + currentEvaluationContext, + annotationLevelContext); + } + + return currentEvaluationContext; + } + + private ExpressionEvaluationContext addAnnotationMemberEvaluationContext( + ExpressionEvaluationContext currentEvaluationContext, + ClassElement annotation, + String annotationMember) { + ElementQuery memberQuery = + ElementQuery.ALL_METHODS + .onlyDeclared() + .annotated(am -> am.hasAnnotation(EvaluatedExpressionContext.class)) + .named(annotationMember); + + ExpressionEvaluationContext annMemberContext = + annotation.getEnclosedElements(memberQuery).stream() + .map(element -> Optional.ofNullable(element.getDeclaredAnnotation(EvaluatedExpressionContext.class))) + .flatMap(Optional::stream) + .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER).stream()) + .map(AnnotationClassValue::getName) + .flatMap(className -> visitorContext.getClassElement(className).stream()) + .findFirst() + .map(BeanContextExpressionEvaluationContext::new) + .orElse(null); + + if (annMemberContext != null) { + return new CompositeExpressionEvaluationContext( + currentEvaluationContext, + annMemberContext); + } + + return currentEvaluationContext; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextLoader.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextLoader.java new file mode 100644 index 00000000000..a4954e61f41 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextLoader.java @@ -0,0 +1,94 @@ +/* + * 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.expressions.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.io.service.SoftServiceLoader; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.visitor.VisitorContext; + +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +/** + * This class is responsible for providing expression evaluation context + * from classes annotated with {@link io.micronaut.context.annotation.EvaluatedExpressionContext}. + * + * Classes from external modules are loaded through {@link ExpressionContextReference} + * provided by those modules. Context classes that are compiled in the same module need to be + * explicitly added to context loader. Once all context classes are added, expression context + * needs to be initialized. After the context is initialized, it can be used by code responsible + * for expressions compilation + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public final class ExpressionContextLoader { + private static final Set LOADED_CONTEXTS = ConcurrentHashMap.newKeySet(); + + private static volatile BeanContextExpressionEvaluationContext expressionContext = new BeanContextExpressionEvaluationContext(); + + static { + SoftServiceLoader.load( + ExpressionContextReference.class, + ExpressionContextLoader.class.getClassLoader()) + .disableFork() + .collectAll() + .stream() + .map(ExpressionContextReference::getType) + .forEach(LOADED_CONTEXTS::add); + } + + /** + * Adds evaluated expression context class element to context loader + * at compilation time. + * + * @param contextClass context class element + * @param visitorContext visitor context + */ + public static void addContextClass(@NonNull ClassElement contextClass, + @NonNull VisitorContext visitorContext) { + ClassElement[] contextElements = + Stream.concat( + Stream.concat(expressionContext.getContextTypes().stream(), Stream.of(contextClass)), + LOADED_CONTEXTS.stream() + .map(visitorContext::getClassElement) + .filter(Optional::isPresent) + .map(Optional::get)) + .toArray(ClassElement[]::new); + + expressionContext = new BeanContextExpressionEvaluationContext(contextElements); + } + + /** + * Resets expression evaluation context. + */ + public static void reset() { + expressionContext = new BeanContextExpressionEvaluationContext(); + } + + /** + * @return expressions evaluation context. + */ + @NonNull + public static ExpressionEvaluationContext getExpressionContext() { + return expressionContext; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextReference.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextReference.java new file mode 100644 index 00000000000..c494d2530ec --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextReference.java @@ -0,0 +1,32 @@ +/* + * 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.expressions.context; + +import io.micronaut.core.annotation.Internal; + +/** + * A reference for expression context class. This reference is generated at compilation + * time and allows compiled modules expose their classes annotated with + * {@link ExpressionEvaluationContext} in such a way that dependant modules can reference + * external contexts at their compilation time. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public abstract class ExpressionContextReference { + public abstract String getType(); +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContext.java new file mode 100644 index 00000000000..e0d91f776cf --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContext.java @@ -0,0 +1,56 @@ +/* + * 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.expressions.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.TypedElement; + +import java.util.List; + +/** + * The context against which expressions are evaluated. + * Context methods, properties and method parameters can be referenced + * in evaluated expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public sealed interface ExpressionEvaluationContext permits BeanContextExpressionEvaluationContext, + MethodExpressionEvaluationContext, + CompositeExpressionEvaluationContext { + /** + * Returns list of methods registered in expression evaluation context + * and matching provided name. + * + * @param name method name to look for + * @return list of matching methods + */ + @NonNull + List getMethods(@NonNull String name); + + /** + * Provides list of typed elements registered in expression evaluation context + * and matching provided name. + * + * @param name element name to look for + * @return list of matching elements + */ + @NonNull + List getTypedElements(@NonNull String name); +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionWithContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionWithContext.java new file mode 100644 index 00000000000..5bc7616225e --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionWithContext.java @@ -0,0 +1,55 @@ +/* + * 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.expressions.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.EvaluatedExpressionReference; + +/** + * Metadata for evaluated expression used at compilation time + * to generate expression class. + * + * @param expressionReference reference to evaluated expression in annotation + * @param evaluationContext the context against which expression will be evaluated + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public record ExpressionWithContext(@NonNull EvaluatedExpressionReference expressionReference, + @NonNull ExpressionEvaluationContext evaluationContext) { + + /** + * Provides initial annotation value treated as evaluated expression. + * + * @return initial annotation value + */ + @NonNull + public Object annotationValue() { + return expressionReference.annotationValue(); + } + + /** + * Provides generated class name for this expression. + * + * @return expression class name + */ + @NonNull + public String expressionClassName() { + return expressionReference.expressionClassName(); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/MethodExpressionEvaluationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/MethodExpressionEvaluationContext.java new file mode 100644 index 00000000000..f7b527df5c4 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/MethodExpressionEvaluationContext.java @@ -0,0 +1,50 @@ +/* + * 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.expressions.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.TypedElement; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Represents expression evaluation context for concrete method. Method's expression + * evaluation context allows referencing method parameters by name in evaluated + * expressions. + * + * @param methodElement method for which evaluation context is built. + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public record MethodExpressionEvaluationContext(@NonNull MethodElement methodElement) implements ExpressionEvaluationContext { + + @Override + public List getTypedElements(String name) { + return Arrays.stream(methodElement.getParameters()) + .filter(parameter -> parameter.getName().equals(name)) + .toList(); + } + + @Override + public List getMethods(String name) { + return Collections.emptyList(); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/CompoundEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/CompoundEvaluatedEvaluatedExpressionParser.java new file mode 100644 index 00000000000..f5aae15d61c --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/CompoundEvaluatedEvaluatedExpressionParser.java @@ -0,0 +1,149 @@ +/* + * 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.expressions.parser; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.literal.StringLiteral; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; +import io.micronaut.expressions.parser.ast.collection.OneDimensionalArray; +import io.micronaut.expressions.parser.ast.operator.binary.AddOperator; +import io.micronaut.expressions.parser.exception.ExpressionParsingException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static io.micronaut.core.expression.EvaluatedExpression.EXPRESSION_PREFIX; + +/** + * This parser is used to split complex expression into multiple + * single expressions if necessary and delegate each atomic expression + * parsing to separate instance of {@link SingleEvaluatedEvaluatedExpressionParser}, + * then combining single expressions parsing results. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class CompoundEvaluatedEvaluatedExpressionParser implements EvaluatedExpressionParser { + + private final Object expression; + + /** + * Instantiates compound expression parser. + * + * @param expression either string or string[] + */ + public CompoundEvaluatedEvaluatedExpressionParser(@NonNull Object expression) { + if (!(expression instanceof String || expression instanceof String[])) { + throw new ExpressionParsingException("Can not parse expression: " + expression); + } + + this.expression = expression; + } + + @Override + public ExpressionNode parse() throws ExpressionParsingException { + // if expression doesn't have prefix, the whole string is treated as expression + if (expression instanceof String str && !str.contains(EXPRESSION_PREFIX)) { + return new SingleEvaluatedEvaluatedExpressionParser(str).parse(); + } + + return parseTemplateExpression(expression); + } + + private ExpressionNode parseTemplateExpression(Object expression) { + if (expression instanceof String str) { + List expressionParts = + splitExpressionParts(str).stream() + .map(this::prepareExpressionPart) + .map(SingleEvaluatedEvaluatedExpressionParser::new) + .map(SingleEvaluatedEvaluatedExpressionParser::parse) + .toList(); + + if (expressionParts.size() == 1) { + return expressionParts.get(0); + } else { + return expressionParts.stream() + .reduce(new StringLiteral(""), AddOperator::new); + } + } else { + List arrayNodes = + Arrays.stream((String[]) expression) + .map(this::parseTemplateExpression) + .toList(); + + return buildArrayOfExpressions(arrayNodes); + } + } + + private ExpressionNode buildArrayOfExpressions(List nodes) { + TypeIdentifier arrayElementType = new TypeIdentifier("Object"); + return new OneDimensionalArray(arrayElementType, nodes); + } + + private List splitExpressionParts(String expression) { + List parts = new ArrayList<>(); + String nextPart = nextPart(expression); + while (!nextPart.isEmpty()) { + parts.add(nextPart); + expression = expression.substring(nextPart.length()); + nextPart = nextPart(expression); + } + + return parts; + } + + private String nextPart(String expression) { + if (expression.startsWith(EXPRESSION_PREFIX)) { + int unbalancedParenthesis = 1; + + StringBuilder expressionPart = new StringBuilder(EXPRESSION_PREFIX); + int pointer = EXPRESSION_PREFIX.length(); + while (unbalancedParenthesis > 0 && pointer < expression.length()) { + char nextChar = expression.charAt(pointer++); + expressionPart.append(nextChar); + if (nextChar == '{') { + unbalancedParenthesis++; + } else if (nextChar == '}') { + unbalancedParenthesis--; + } + } + + if (unbalancedParenthesis > 0) { + throw new ExpressionParsingException("Unbalanced parenthesis in expression: " + expression); + } + + return expressionPart.toString(); + } else { + int substringUntil = expression.contains(EXPRESSION_PREFIX) + ? expression.indexOf(EXPRESSION_PREFIX) + : expression.length(); + return expression.substring(0, substringUntil); + } + } + + private String prepareExpressionPart(String expressionPart) { + if (expressionPart.startsWith(EXPRESSION_PREFIX)) { + return expressionPart.substring(EXPRESSION_PREFIX.length(), + expressionPart.length() - 1); + } else { + return "'" + expressionPart + "'"; + } + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/EvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/EvaluatedExpressionParser.java new file mode 100644 index 00000000000..fe218c32691 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/EvaluatedExpressionParser.java @@ -0,0 +1,40 @@ +/* + * 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.expressions.parser; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.exception.ExpressionParsingException; + +/** + * Interface for evaluated expression parser. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public sealed interface EvaluatedExpressionParser permits SingleEvaluatedEvaluatedExpressionParser, + CompoundEvaluatedEvaluatedExpressionParser { + /** + * Parse expression into AST. + * + * @return expression AST + * @throws ExpressionParsingException when expression violates syntactic rules + */ + @NonNull + ExpressionNode parse() throws ExpressionParsingException; +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java new file mode 100644 index 00000000000..ad72ab932c1 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java @@ -0,0 +1,515 @@ +/* + * 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.expressions.parser; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.access.ContextElementAccess; +import io.micronaut.expressions.parser.ast.access.ContextMethodCall; +import io.micronaut.expressions.parser.ast.access.ElementMethodCall; +import io.micronaut.expressions.parser.ast.access.PropertyAccess; +import io.micronaut.expressions.parser.ast.conditional.TernaryExpression; +import io.micronaut.expressions.parser.ast.literal.BoolLiteral; +import io.micronaut.expressions.parser.ast.literal.DoubleLiteral; +import io.micronaut.expressions.parser.ast.literal.FloatLiteral; +import io.micronaut.expressions.parser.ast.literal.IntLiteral; +import io.micronaut.expressions.parser.ast.literal.LongLiteral; +import io.micronaut.expressions.parser.ast.literal.NullLiteral; +import io.micronaut.expressions.parser.ast.literal.StringLiteral; +import io.micronaut.expressions.parser.ast.operator.binary.InstanceofOperator; +import io.micronaut.expressions.parser.ast.operator.binary.MatchesOperator; +import io.micronaut.expressions.parser.ast.operator.binary.PowOperator; +import io.micronaut.expressions.parser.ast.operator.binary.AndOperator; +import io.micronaut.expressions.parser.ast.operator.binary.OrOperator; +import io.micronaut.expressions.parser.ast.operator.binary.AddOperator; +import io.micronaut.expressions.parser.ast.operator.binary.DivOperator; +import io.micronaut.expressions.parser.ast.operator.binary.ModOperator; +import io.micronaut.expressions.parser.ast.operator.binary.MulOperator; +import io.micronaut.expressions.parser.ast.operator.binary.SubOperator; +import io.micronaut.expressions.parser.ast.operator.binary.EqOperator; +import io.micronaut.expressions.parser.ast.operator.binary.GtOperator; +import io.micronaut.expressions.parser.ast.operator.binary.GteOperator; +import io.micronaut.expressions.parser.ast.operator.binary.LtOperator; +import io.micronaut.expressions.parser.ast.operator.binary.LteOperator; +import io.micronaut.expressions.parser.ast.operator.binary.NeqOperator; +import io.micronaut.expressions.parser.ast.operator.unary.NegOperator; +import io.micronaut.expressions.parser.ast.operator.unary.NotOperator; +import io.micronaut.expressions.parser.ast.operator.unary.PosOperator; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; +import io.micronaut.expressions.parser.exception.ExpressionParsingException; +import io.micronaut.expressions.parser.token.Token; +import io.micronaut.expressions.parser.token.TokenType; +import io.micronaut.expressions.parser.token.Tokenizer; + +import java.util.ArrayList; +import java.util.List; + +import static io.micronaut.expressions.parser.token.TokenType.BOOL; +import static io.micronaut.expressions.parser.token.TokenType.COLON; +import static io.micronaut.expressions.parser.token.TokenType.DECREMENT; +import static io.micronaut.expressions.parser.token.TokenType.DIV; +import static io.micronaut.expressions.parser.token.TokenType.DOT; +import static io.micronaut.expressions.parser.token.TokenType.DOUBLE; +import static io.micronaut.expressions.parser.token.TokenType.EQ; +import static io.micronaut.expressions.parser.token.TokenType.EXPRESSION_CONTEXT_REF; +import static io.micronaut.expressions.parser.token.TokenType.FLOAT; +import static io.micronaut.expressions.parser.token.TokenType.GT; +import static io.micronaut.expressions.parser.token.TokenType.GTE; +import static io.micronaut.expressions.parser.token.TokenType.IDENTIFIER; +import static io.micronaut.expressions.parser.token.TokenType.INCREMENT; +import static io.micronaut.expressions.parser.token.TokenType.INSTANCEOF; +import static io.micronaut.expressions.parser.token.TokenType.INT; +import static io.micronaut.expressions.parser.token.TokenType.LONG; +import static io.micronaut.expressions.parser.token.TokenType.LT; +import static io.micronaut.expressions.parser.token.TokenType.LTE; +import static io.micronaut.expressions.parser.token.TokenType.L_PAREN; +import static io.micronaut.expressions.parser.token.TokenType.L_SQUARE; +import static io.micronaut.expressions.parser.token.TokenType.MATCHES; +import static io.micronaut.expressions.parser.token.TokenType.MINUS; +import static io.micronaut.expressions.parser.token.TokenType.MOD; +import static io.micronaut.expressions.parser.token.TokenType.MUL; +import static io.micronaut.expressions.parser.token.TokenType.NE; +import static io.micronaut.expressions.parser.token.TokenType.NOT; +import static io.micronaut.expressions.parser.token.TokenType.NULL; +import static io.micronaut.expressions.parser.token.TokenType.OR; +import static io.micronaut.expressions.parser.token.TokenType.PLUS; +import static io.micronaut.expressions.parser.token.TokenType.POW; +import static io.micronaut.expressions.parser.token.TokenType.QMARK; +import static io.micronaut.expressions.parser.token.TokenType.R_PAREN; +import static io.micronaut.expressions.parser.token.TokenType.STRING; + +/** + * Parser for building AST for single evaluated expression. + * A single expression is parsed as a whole, + * it cannot contain multiple expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class SingleEvaluatedEvaluatedExpressionParser implements EvaluatedExpressionParser { + private final Tokenizer tokenizer; + private Token lookahead; + + /** + * Instantiates a parser for single passed expression. + * Expression string must not contain expression template wrapper like #{...} + * + * @param expression expression to parse + */ + public SingleEvaluatedEvaluatedExpressionParser(String expression) { + this.tokenizer = new Tokenizer(expression); + this.lookahead = tokenizer.getNextToken(); + } + + @Override + public ExpressionNode parse() throws ExpressionParsingException { + try { + final ExpressionNode expressionNode = expression(); + if (lookahead != null) { + throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); + } + return expressionNode; + } catch (NullPointerException ex) { + throw new ExpressionParsingException("Unexpected end of input"); + } + } + + // Expression + // : TernaryExpression + // ; + private ExpressionNode expression() { + return ternaryExpression(); + } + + // TernaryExpression + // : OrExpression + // | OrExpression '?' Expression ':' Expression + // ; + private ExpressionNode ternaryExpression() { + ExpressionNode orExpression = orExpression(); + if (lookahead != null && lookahead.type() == QMARK) { + eat(QMARK); + ExpressionNode trueExpr = expression(); + eat(COLON); + ExpressionNode falseExpr = expression(); + return new TernaryExpression(orExpression, trueExpr, falseExpr); + } + return orExpression; + } + + // OrExpression + // : AndExpression + // | OrExpression '||' AndExpression -> AndExpression '||' AndExpression '||' AndExpression + // ; + private ExpressionNode orExpression() { + ExpressionNode leftNode = andExpression(); + while (lookahead != null && lookahead.type() == OR) { + eat(OR); + leftNode = new OrOperator(leftNode, andExpression()); + } + return leftNode; + } + + // AndExpression + // : EqualityExpression + // | AndExpression '&&' EqualityExpression + // ; + private ExpressionNode andExpression() { + ExpressionNode leftNode = equalityExpression(); + while (lookahead != null && lookahead.type() == TokenType.AND) { + eat(TokenType.AND); + leftNode = new AndOperator(leftNode, equalityExpression()); + } + return leftNode; + } + + // EqualityExpression + // : RelationalExpression + // | EqualityExpression '==' RelationalExpression + // | EqualityExpression '!=' RelationalExpression + // ; + private ExpressionNode equalityExpression() { + ExpressionNode leftNode = relationalExpression(); + while (lookahead != null && lookahead.type().isOneOf(EQ, NE)) { + TokenType tokenType = lookahead.type(); + eat(tokenType); + if (tokenType == EQ) { + leftNode = new EqOperator(leftNode, relationalExpression()); + } else if (tokenType == NE) { + leftNode = new NeqOperator(leftNode, relationalExpression()); + } + } + return leftNode; + } + + // RelationalExpression + // : AdditiveExpression + // | RelationalExpression RelOperator AdditiveExpression + // | RelationalExpression 'instanceof' TypeIdentifier + // | RelationalExpression 'matches' StringLiteral + // ; + private ExpressionNode relationalExpression() { + ExpressionNode leftNode = additiveExpression(); + while (lookahead != null && (lookahead.type() + .isOneOf(GT, GTE, LT, LTE, INSTANCEOF, MATCHES))) { + TokenType tokenType = lookahead.type(); + eat(lookahead.type()); + leftNode = switch (tokenType) { + case GT -> new GtOperator(leftNode, additiveExpression()); + case LT -> new LtOperator(leftNode, additiveExpression()); + case GTE -> new GteOperator(leftNode, additiveExpression()); + case LTE -> new LteOperator(leftNode, additiveExpression()); + case INSTANCEOF -> new InstanceofOperator(leftNode, typeIdentifier()); + case MATCHES -> new MatchesOperator(leftNode, stringLiteral()); + default -> leftNode; + }; + } + return leftNode; + } + + // AdditiveExpression + // : PowExpression + // | AdditiveExpression '+' PowExpression + // | AdditiveExpression '-' PowExpression + // ; + private ExpressionNode additiveExpression() { + ExpressionNode leftNode = multiplicativeExpression(); + while (lookahead != null && lookahead.type().isOneOf(PLUS, MINUS)) { + TokenType tokenType = lookahead.type(); + eat(tokenType); + if (tokenType == PLUS) { + leftNode = new AddOperator(leftNode, multiplicativeExpression()); + } else if (tokenType == MINUS) { + leftNode = new SubOperator(leftNode, multiplicativeExpression()); + } + } + return leftNode; + } + + // MultiplicativeExpression + // : PowExpression + // | MultiplicativeExpression '*' PowExpression + // | MultiplicativeExpression '/' PowExpression + // | MultiplicativeExpression '%' PowExpression + // ; + private ExpressionNode multiplicativeExpression() { + ExpressionNode leftNode = powExpression(); + while (lookahead != null && lookahead.type().isOneOf(MUL, DIV, MOD)) { + TokenType tokenType = lookahead.type(); + eat(tokenType); + if (tokenType == MUL) { + leftNode = new MulOperator(leftNode, powExpression()); + } else if (tokenType == DIV) { + leftNode = new DivOperator(leftNode, powExpression()); + } else if (tokenType == MOD) { + leftNode = new ModOperator(leftNode, powExpression()); + } + } + return leftNode; + } + + // PowExpression + // : UnaryExpression + // | PowExpression '^' UnaryExpression + // ; + private ExpressionNode powExpression() { + ExpressionNode leftNode = unaryExpression(); + while (lookahead != null && lookahead.type() == POW) { + eat(POW); + leftNode = new PowOperator(leftNode, unaryExpression()); + } + return leftNode; + } + + // UnaryExpression + // : '+' UnaryExpression + // | '-' UnaryExpression + // | '!' UnaryExpression + // | '++' UnaryExpression + // | '--' UnaryExpression + // | PostfixExpression + // ; + private ExpressionNode unaryExpression() { + TokenType tokenType = lookahead.type(); + if (tokenType == PLUS) { + eat(PLUS); + return new PosOperator(unaryExpression()); + } else if (tokenType == MINUS) { + eat(MINUS); + return new NegOperator(unaryExpression()); + } else if (tokenType == NOT) { + eat(NOT); + return new NotOperator(unaryExpression()); + } else if (tokenType == INCREMENT) { + throw new ExpressionParsingException("Prefix increment operation is not supported"); + } else if (tokenType == DECREMENT) { + throw new ExpressionParsingException("Prefix decrement operation is not supported"); + } else { + return postfixExpression(); + } + } + + // PostfixExpression + // : PrimaryExpression + // | PostfixExpression '.' MethodOrPropertyAccess + // | PostfixExpression '++' + // | PostfixExpression '--' + // ; + private ExpressionNode postfixExpression() { + ExpressionNode leftNode = primaryExpression(); + while (lookahead != null && (lookahead.type() + .isOneOf(DOT, L_SQUARE, INCREMENT, DECREMENT))) { + TokenType tokenType = lookahead.type(); + if (tokenType == INCREMENT) { + throw new ExpressionParsingException("Postfix increment operation is not " + + "supported"); + } else if (tokenType == DECREMENT) { + throw new ExpressionParsingException("Postfix decrement operation is not " + + "supported"); + } else if (tokenType == DOT) { + eat(DOT); + leftNode = methodOrPropertyAccess(leftNode); + } else { + throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); + } + } + return leftNode; + } + + // PrimaryExpression + // : ContextAccess + // | TypeIdentifier + // | ParenthesizedExpression + // | Literal + // ; + private ExpressionNode primaryExpression() { + return switch (lookahead.type()) { + case EXPRESSION_CONTEXT_REF -> contextAccess(); + case IDENTIFIER -> typeIdentifier(); + case L_PAREN -> parenthesizedExpression(); + case STRING, INT, LONG, DOUBLE, FLOAT, BOOL, NULL -> literal(); + default -> throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); + }; + } + + // ContextAccess + // : '#' Identifier + // | '#' Identifier MethodArguments + // ; + private ExpressionNode contextAccess() { + eat(EXPRESSION_CONTEXT_REF); + String identifier = identifier(); + + // Ambiguous case when 'T(' can be considered as method call + if (identifier.equals("T") && lookahead != null && lookahead.type() == L_PAREN) { + throw new ExpressionParsingException("Expected identifier, got type reference"); + } + + if (lookahead != null && lookahead.type() == L_PAREN) { + List methodArguments = methodArguments(); + return new ContextMethodCall(identifier, methodArguments); + } + return new ContextElementAccess(identifier); + } + + // MethodOrFieldAccess + // : SimpleIdentifier + // | SimpleIdentifier MethodArguments + // ; + private ExpressionNode methodOrPropertyAccess(ExpressionNode callee) { + String identifier = identifier(); + if (lookahead != null && lookahead.type() == L_PAREN) { + List methodArguments = methodArguments(); + return new ElementMethodCall(callee, identifier, methodArguments); + } + return new PropertyAccess(callee, identifier); + } + + // MethodArguments: + // '(' MethodArgumentsList ')' + // ; + private List methodArguments() { + eat(L_PAREN); + List arguments = new ArrayList<>(); + if (lookahead.type() != R_PAREN) { + arguments = methodArgumentsList(); + } + eat(R_PAREN); + return arguments; + } + + // MethodArgumentsList + // : Expression + // | MethodArgumentsList ',' Expression + // ; + private List methodArgumentsList() { + List arguments = new ArrayList<>(); + if (lookahead.type() != R_PAREN) { + ExpressionNode firstArgument = expression(); + arguments.add(firstArgument); + + while (lookahead.type() != R_PAREN) { + eat(TokenType.COMMA); + arguments.add(expression()); + } + } + return arguments; + } + + // TypeReference + // : 'T(' ChainedIdentifier')' + // ; + private TypeIdentifier typeIdentifier() { + eat(IDENTIFIER); + eat(L_PAREN); + List parts = new ArrayList<>(); + parts.add(identifier()); + while (lookahead != null && lookahead.type() == DOT) { + eat(DOT); + parts.add(identifier()); + } + eat(R_PAREN); + return new TypeIdentifier(String.join(".", parts)); + } + + private String identifier() { + Token token = eat(IDENTIFIER); + return token.value(); + } + + // ParenthesizedExpression + // : '(' Expression ')' + // ; + private ExpressionNode parenthesizedExpression() { + eat(L_PAREN); + ExpressionNode parenthesizedExpression = expression(); + eat(R_PAREN); + return parenthesizedExpression; + } + + // Literal + // : StringLiteral + // | IntLiteral + // | LongLiteral + // | DecimalLiteral + // | FloatLiteral + // | BoolLiteral + // ; + private ExpressionNode literal() { + return switch (lookahead.type()) { + case DOUBLE -> doubleLiteral(); + case FLOAT -> floatLiteral(); + case INT -> intLiteral(); + case STRING -> stringLiteral(); + case LONG -> longLiteral(); + case BOOL -> boolLiteral(); + case NULL -> nullLiteral(); + default -> throw new ExpressionParsingException("Unknown literal type: " + lookahead.type()); + }; + } + + private StringLiteral stringLiteral() { + Token token = eat(STRING); + String value = token.value(); + // removing surrounding quotes + return new StringLiteral(token.value().substring(1, value.length() - 1)); + } + + private DoubleLiteral doubleLiteral() { + Token token = eat(DOUBLE); + return new DoubleLiteral(Double.parseDouble(token.value())); + } + + private FloatLiteral floatLiteral() { + Token token = eat(FLOAT); + return new FloatLiteral(Float.parseFloat(token.value())); + } + + private IntLiteral intLiteral() { + Token token = eat(INT); + return new IntLiteral(Integer.decode(token.value())); + } + + private LongLiteral longLiteral() { + Token token = eat(LONG); + return new LongLiteral(Long.decode(token.value().replaceAll("([lL])", ""))); + } + + private BoolLiteral boolLiteral() { + Token token = eat(BOOL); + return new BoolLiteral(Boolean.parseBoolean(token.value())); + } + + private NullLiteral nullLiteral() { + eat(NULL); + return new NullLiteral(); + } + + private Token eat(TokenType tokenType) { + if (lookahead == null) { + throw new ExpressionParsingException("Unexpected end of input. Expected: '" + tokenType + "'"); + } + + Token token = lookahead; + if (token.type() != tokenType) { + throw new ExpressionParsingException("Unexpected token: " + token.value() + ". Expected: '" + tokenType + "'"); + } + + lookahead = tokenizer.getNextToken(); + return token; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java new file mode 100644 index 00000000000..b20b249f984 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java @@ -0,0 +1,75 @@ +/* + * 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.expressions.parser.ast; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Type; + +/** + * Abstract evaluated expression AST node. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +public abstract class ExpressionNode { + + protected Type nodeType; + + /** + * Compiles this expression AST node against passes compilation context. + * Node compilation includes type resolution and bytecode generation. + * + * @param ctx expression compilation context + */ + public final void compile(@NonNull ExpressionCompilationContext ctx) { + resolveType(ctx); + generateBytecode(ctx); + } + + /** + * Generates bytecode for this AST node. + * + * @param ctx expression compilation context + */ + protected abstract void generateBytecode(@NonNull ExpressionCompilationContext ctx); + + /** + * On resolution stage type information is collected and node validity is checked. Once type + * is resolved, type resolution result is cached. + * + * @param ctx expression compilation context + * + * @return resolved type + */ + @NonNull + public final Type resolveType(@NonNull ExpressionCompilationContext ctx) { + if (nodeType == null) { + nodeType = doResolveType(ctx); + } + return nodeType; + } + + /** + * Resolves expression AST node type. + * + * @param ctx expression compilation context + * + * @return resolved type + */ + @NonNull + protected abstract Type doResolveType(@NonNull ExpressionCompilationContext ctx); +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java new file mode 100644 index 00000000000..20d35b0e4ac --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java @@ -0,0 +1,199 @@ +/* + * 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.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.collection.OneDimensionalArray; +import io.micronaut.expressions.parser.ast.util.TypeDescriptors; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.visitor.VisitorContext; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isPrimitive; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushBoxPrimitiveIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; +import static io.micronaut.inject.processing.JavaModelUtils.getTypeReference; + +/** + * Abstract expression AST node for method calls. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public abstract sealed class AbstractMethodCall extends ExpressionNode permits ContextMethodCall, + ElementMethodCall { + protected final String name; + protected final List arguments; + + protected CandidateMethod usedMethod; + + public AbstractMethodCall(String name, + List arguments) { + this.name = name; + this.arguments = arguments; + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + usedMethod = resolveUsedMethod(ctx); + return usedMethod.getReturnType(); + } + + /** + * Resolves single {@link CandidateMethod} used by this AST node. + * + * @param ctx Expression compilation context + * @return AST node candidate method + * @throws io.micronaut.expressions.parser.exception.ExpressionCompilationException if no + * candidate method can be found or if there is more than one candidate method. + */ + @NonNull + protected abstract CandidateMethod resolveUsedMethod(ExpressionCompilationContext ctx); + + /** + * Builds candidate method for method element. + * + * @param ctx expression compilation context + * @param methodElement method element + * @param argumentTypes types of arguments used for method invocation in expression + * + * @return candidate method + */ + protected CandidateMethod toCandidateMethod(ExpressionCompilationContext ctx, + MethodElement methodElement, + List argumentTypes) { + VisitorContext visitorContext = ctx.visitorContext(); + + List arguments = + argumentTypes.stream() + .map(type -> getRequiredClassElement(type, visitorContext)) + .toList(); + + return new CandidateMethod(methodElement, arguments); + } + + /** + * This method wraps original method arguments into + * array for methods using varargs. + * + * @return list of arguments, including varargs arguments wrapped in array + */ + protected List prepareVarargsArguments() { + List arguments = new ArrayList<>(); + int varargsIndex = usedMethod.getVarargsIndex(); + + List nodesWrappedInArray = new ArrayList<>(); + for (int i = 0; i < this.arguments.size(); i++) { + ExpressionNode argument = this.arguments.get(i); + if (varargsIndex > i) { + arguments.add(argument); + } else { + nodesWrappedInArray.add(argument); + } + } + + ClassElement lastParameter = this.usedMethod.getLastParameter(); + + OneDimensionalArray varargsArray = + new OneDimensionalArray( + new TypeIdentifier(lastParameter.getCanonicalName()), + nodesWrappedInArray); + + arguments.add(varargsArray); + return arguments; + } + + /** + * Resolve types of method invocation arguments. + * + * @param ctx expression evaluation context + * + * @return types of method arguments + */ + protected List resolveArgumentTypes(ExpressionCompilationContext ctx) { + return arguments.stream() + .map(argument -> argument instanceof TypeIdentifier + ? TypeDescriptors.CLASS + : argument.resolveType(ctx)) + .toList(); + } + + /** + * Compiles method arguments. + * + * @param ctx expression evaluation context + */ + protected void compileArguments(ExpressionCompilationContext ctx) { + List arguments = this.arguments; + if (usedMethod.isVarArgs()) { + arguments = prepareVarargsArguments(); + } + + for (int i = 0; i < arguments.size(); i++) { + compileArgument(ctx, i, arguments.get(i)); + } + } + + /** + * Compiles given method argument. + * + * @param ctx expression evaluation context + * @param argumentIndex argument index + * @param argument compiled argument + */ + private void compileArgument(ExpressionCompilationContext ctx, + int argumentIndex, + ExpressionNode argument) { + GeneratorAdapter mv = ctx.methodVisitor(); + if (usedMethod.getParameters().size() > argumentIndex) { + Type parameterType = getTypeReference(usedMethod.getParameters().get(argumentIndex)); + Type argumentType = argument.resolveType(ctx); + + argument.compile(ctx); + if (isPrimitive(parameterType)) { + pushUnboxPrimitiveIfNecessary(argumentType, mv); + } else { + pushBoxPrimitiveIfNecessary(argumentType, mv); + } + } + } + + /** + * Prepares arguments string for logging purposes. + * + * @param ctx expression compilation context + * + * @return arguments string + */ + protected String stringifyArguments(ExpressionCompilationContext ctx) { + return arguments.stream() + .map(argument -> argument.resolveType(ctx)) + .map(Type::getClassName) + .collect(Collectors.joining(", ", "(", ")")); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/CandidateMethod.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/CandidateMethod.java new file mode 100644 index 00000000000..d4942f55b43 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/CandidateMethod.java @@ -0,0 +1,245 @@ +/* + * 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.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.TypedElement; +import io.micronaut.inject.visitor.VisitorContext; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.Method; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.isAssignable; +import static io.micronaut.inject.processing.JavaModelUtils.getTypeReference; + +/** + * Class representing candidate method used in evaluated expression. + * Encapsulates logic determining whether invocation of method in expression + * with concrete arguments matches list of parameters of concrete method. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +final class CandidateMethod { + private final MethodElement methodElement; + private final List parameterTypes; + private final List argumentTypes; + + private int varargsIndex = -1; + + public CandidateMethod(MethodElement methodElement, List argumentTypes) { + this.methodElement = methodElement; + this.argumentTypes = argumentTypes; + this.parameterTypes = Arrays.stream(methodElement.getParameters()) + .map(ParameterElement::getType) + .toList(); + } + + public CandidateMethod(MethodElement methodElement) { + this(methodElement, Collections.emptyList()); + } + + /** + * Whether candidate method is vargars method. + * + * @return true if it is + */ + public boolean isVarArgs() { + return getVarargsIndex() != -1; + } + + /** + * Returns index of varargs parameter. If method has no varargs + * parameter, -1 is returned. + * + * @return varargs index or -1 + */ + public int getVarargsIndex() { + return varargsIndex; + } + + /** + * @return Returns candidate method return type. + */ + @NonNull + public Type getReturnType() { + return getTypeReference(methodElement.getReturnType()); + } + + /** + * @return Type of class that owns candidate method. + */ + @NonNull + public Type getOwningType() { + return getTypeReference(methodElement.getOwningType()); + } + + /** + * @return last parameter of candidate method. + */ + @NonNull + public ClassElement getLastParameter() { + return CollectionUtils.last(parameterTypes); + } + + /** + * @return candidate method descriptor. + */ + @NonNull + public String getDescriptor() { + return toAsmMethod().getDescriptor(); + } + + /** + * @return list of candidate method parameters. + */ + @NonNull + public List getParameters() { + return parameterTypes; + } + + /** + * Checks list of arguments against list of method parameters to decide whether there is + * a match. This check also supports varargs resolution for cases when method is explicitly + * defined as varargs method or when last method parameter is a one-dimensional array. + * + * @param ctx + * @return + */ + public boolean isMatching(VisitorContext ctx) { + int totalParams = parameterTypes.size(); + int totalArguments = argumentTypes.size(); + + if (totalParams == 0) { + return totalArguments == 0; + } else if (totalArguments < totalParams - 1) { + // list of arguments may be shorter than list of parameters only by 1 element and + // only in case last parameter is varargs parameter, otherwise method doesn't match + return false; + } + + ClassElement lastArgument = CollectionUtils.last(argumentTypes); + ClassElement lastParameter = getLastParameter(); + boolean varargsCandidate = methodElement.isVarArgs() || + (lastParameter.isArray() && lastParameter.getArrayDimensions() == 1); + + if (varargsCandidate) { + // maybe just array argument + if (totalArguments == totalParams && isAssignable(lastParameter, lastArgument)) { + return true; + } + + if (isMatchingVarargs(ctx)) { + this.varargsIndex = calculateVarargsIndex(); + return true; + } + + return false; + } + + if (totalArguments != totalParams) { + return false; + } + + for (int i = 0; i < parameterTypes.size(); i++) { + ClassElement argumentType = argumentTypes.get(i); + ClassElement parameterType = parameterTypes.get(i); + + if (!isAssignable(parameterType, argumentType)) { + return false; + } + } + return true; + } + + /** + * Returns {@link Method} representation of this candidate method. + * + * @return asm method + */ + public Method toAsmMethod() { + StringBuilder builder = new StringBuilder(); + builder.append('('); + + for (TypedElement parameterType : parameterTypes) { + builder.append(getTypeReference(parameterType).getDescriptor()); + } + + builder.append(')'); + + builder.append(getTypeReference(methodElement.getReturnType()).getDescriptor()); + return new Method(methodElement.getSimpleName(), builder.toString()); + } + + private boolean isMatchingVarargs(VisitorContext ctx) { + for (int paramIndex = 0; paramIndex < parameterTypes.size(); paramIndex++) { + ClassElement parameterType = parameterTypes.get(paramIndex); + + boolean isLastParameter = paramIndex == parameterTypes.size() - 1; + if (isLastParameter) { + parameterType = getRequiredClassElement(getTypeReference(parameterType).getElementType(), ctx); + + if (argumentTypes.size() < paramIndex) { + // if we got here it means that last parameter is varargs but methods + // arguments list doesn't include an argument for varargs parameter, so + // an empty array is used as varargs argument, which is treated as a match + return true; + } + + // check whether all remaining arguments match parameter type + for (int argIndex = paramIndex; argIndex < argumentTypes.size(); argIndex++) { + ClassElement argumentType = argumentTypes.get(paramIndex); + if (!isAssignable(parameterType, argumentType)) { + return false; + } + } + + return true; + } + + // too little arguments, no match + if (argumentTypes.size() < paramIndex) { + return false; + } + + // no match if argument is not assignable to parameter + if (!isAssignable(parameterType, argumentTypes.get(paramIndex))) { + return false; + } + } + + return false; + } + + private int calculateVarargsIndex() { + return CollectionUtils.last(parameterTypes) == null ? -1 : parameterTypes.size() - 1; + } + + @Override + public String toString() { + return methodElement.getDescription(false); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java new file mode 100644 index 00000000000..b05adfc0ec5 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java @@ -0,0 +1,102 @@ +/* + * 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.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.context.ExpressionEvaluationContext; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.ast.TypedElement; +import org.objectweb.asm.Type; + +import java.util.List; + +import static java.util.Collections.emptyList; + +/** + * Expression AST node used for context element access. + * Either evaluation context method element, property element or method argument can + * be accessed. When method is accessed it is clear at AST building stage, + * but whether property or method argument is accessed is unclear until type resolution against + * evaluation context is executed. This node checks evaluation context to resolve + * concrete node type, instantiates respective node and delegates type resolution + * and bytecode generation to this node + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class ContextElementAccess extends ExpressionNode { + + private final String name; + + private ContextMethodCall contextPropertyMethodCall; + private ContextMethodParameterAccess contextMethodParameterAccess; + + public ContextElementAccess(String name) { + this.name = name; + } + + @Override + protected void generateBytecode(ExpressionCompilationContext ctx) { + if (contextMethodParameterAccess != null) { + contextMethodParameterAccess.compile(ctx); + } else if (contextPropertyMethodCall != null) { + contextPropertyMethodCall.compile(ctx); + } + } + + @Override + public Type doResolveType(ExpressionCompilationContext ctx) { + ExpressionEvaluationContext evaluationContext = ctx.evaluationContext(); + List namedElements = evaluationContext.getTypedElements(name); + + if (namedElements.size() == 0) { + throw new ExpressionCompilationException( + "No element with name [" + name + "] available in evaluation context"); + } else if (namedElements.size() > 1) { + throw new ExpressionCompilationException( + "Ambiguous expression evaluation context reference. Found " + namedElements.size() + + " elements with name [" + name + "]"); + } + + TypedElement element = namedElements.iterator().next(); + if (element instanceof PropertyElement property) { + + String readMethodName = + property.getReadMethod() + .orElseThrow(() -> new ExpressionCompilationException( + "Failed to obtain read method for property [" + name + "]")) + .getName(); + + contextPropertyMethodCall = new ContextMethodCall(readMethodName, emptyList()); + return contextPropertyMethodCall.resolveType(ctx); + + } else if (element instanceof ParameterElement parameter) { + + contextMethodParameterAccess = new ContextMethodParameterAccess(parameter); + return contextMethodParameterAccess.resolveType(ctx); + + } else { + throw new ExpressionCompilationException( + "Unsupported element referenced in expression: [" + element + "]. Only " + + "properties, methods and method parameters can be referenced in expressions"); + } + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java new file mode 100644 index 00000000000..f701056c012 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java @@ -0,0 +1,108 @@ +/* + * 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.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.context.ExpressionEvaluationContext; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.List; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.EVALUATED_EXPRESSION_TYPE; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static org.objectweb.asm.Opcodes.CHECKCAST; + +/** + * Expression node used for invocation of method from expression + * evaluation context. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class ContextMethodCall extends AbstractMethodCall { + + private static final Method GET_BEAN_METHOD = + new Method("getBean", Type.getType(Object.class), + new Type[]{Type.getType(Class.class)}); + + public ContextMethodCall(String name, List arguments) { + super(name, arguments); + } + + @Override + protected CandidateMethod resolveUsedMethod(ExpressionCompilationContext ctx) { + List argumentTypes = resolveArgumentTypes(ctx); + + ExpressionEvaluationContext evaluationContext = ctx.evaluationContext(); + List candidateMethods = + evaluationContext.getMethods(name) + .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 evaluation context"); + } else if (candidateMethods.size() > 1) { + throw new ExpressionCompilationException( + "Ambiguous expression evaluation context reference. Found " + candidateMethods.size() + + " matching methods: " + candidateMethods); + } + + return candidateMethods.iterator().next(); + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Type calleeType = usedMethod.getOwningType(); + + ClassElement calleeClass = getRequiredClassElement(calleeType, ctx.visitorContext()); + + pushGetBeanFromContext(mv, calleeType); + compileArguments(ctx); + if (calleeClass.isInterface()) { + mv.invokeVirtual(calleeType, usedMethod.toAsmMethod()); + } else { + mv.invokeVirtual(calleeType, usedMethod.toAsmMethod()); + } + } + + /** + * Pushing method obtaining bean of provided type from beanContext. + * + * @param mv methodVisitor + * @param beanType required bean typ + */ + private void pushGetBeanFromContext(GeneratorAdapter mv, Type beanType) { + mv.loadThis(); + mv.push(beanType); + + // invoke getBean method + mv.invokeVirtual(EVALUATED_EXPRESSION_TYPE, GET_BEAN_METHOD); + + // cast the return value to the correct type + mv.visitTypeInsn(CHECKCAST, beanType.getInternalName()); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java new file mode 100644 index 00000000000..2990748062c --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java @@ -0,0 +1,75 @@ +/* + * 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.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ParameterElement; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static io.micronaut.inject.processing.JavaModelUtils.getTypeReference; +import static org.objectweb.asm.Opcodes.AALOAD; + +/** + * Expression AST node used for context method parameter access. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +final class ContextMethodParameterAccess extends ExpressionNode { + private final ParameterElement parameterElement; + + private Integer parameterIndex; + + public ContextMethodParameterAccess(ParameterElement parameterElement) { + this.parameterElement = parameterElement; + } + + @Override + protected void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + mv.loadArg(0); + mv.push(parameterIndex); + mv.visitInsn(AALOAD); + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + String parameterName = parameterElement.getName(); + ParameterElement[] methodParameters = parameterElement.getMethodElement().getParameters(); + + Integer paramIndex = null; + for (int i = 0; i < methodParameters.length; i++) { + ParameterElement methodParameter = methodParameters[i]; + if (methodParameter.getName().equals(parameterName)) { + paramIndex = i; + break; + } + } + + if (paramIndex == null) { + throw new ExpressionCompilationException( + "Can not find parameter with name [" + parameterName + "] in method parameters"); + } + + this.parameterIndex = paramIndex; + return getTypeReference(parameterElement.getType()); + } +} 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 new file mode 100644 index 00000000000..2699b6c725a --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ElementMethodCall.java @@ -0,0 +1,121 @@ +/* + * 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.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.visitor.VisitorContext; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.List; + +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static org.objectweb.asm.Opcodes.INVOKESTATIC; + +/** + * Expression AST node used for method invocation. + * This node represents both object method invocation and static method + * invocation + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public sealed class ElementMethodCall extends AbstractMethodCall permits PropertyAccess { + + protected final ExpressionNode callee; + + public ElementMethodCall(ExpressionNode callee, + String name, + List arguments) { + super(name, arguments); + this.callee = callee; + } + + @Override + protected void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + VisitorContext visitorContext = ctx.visitorContext(); + Type calleeType = callee.resolveType(ctx); + Method method = usedMethod.toAsmMethod(); + + ClassElement calleeClass = getRequiredClassElement(calleeType, visitorContext); + + if (callee instanceof TypeIdentifier) { + compileArguments(ctx); + if (calleeClass.isInterface()) { + mv.visitMethodInsn(INVOKESTATIC, calleeType.getInternalName(), name, + usedMethod.getDescriptor(), true); + } else { + mv.invokeStatic(calleeType, method); + } + } else { + callee.compile(ctx); + compileArguments(ctx); + if (calleeClass.isInterface()) { + mv.invokeInterface(calleeType, method); + } else { + mv.invokeVirtual(calleeType, method); + } + } + } + + @Override + protected CandidateMethod resolveUsedMethod(ExpressionCompilationContext ctx) { + List argumentTypes = resolveArgumentTypes(ctx); + Type calleeType = callee.resolveType(ctx); + + ElementQuery methodQuery = buildMethodQuery(); + List candidateMethods = + ctx.visitorContext() + .getClassElement(calleeType.getClassName()) + .stream() + .flatMap(element -> element.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); + } else if (candidateMethods.size() > 1) { + throw new ExpressionCompilationException( + "Ambiguous method call. Found " + candidateMethods.size() + + " matching methods: " + candidateMethods + " in class " + calleeType); + } + + return candidateMethods.iterator().next(); + } + + private ElementQuery buildMethodQuery() { + ElementQuery query = ElementQuery.ALL_METHODS.onlyAccessible() + .named(name); + + if (callee instanceof TypeIdentifier) { + query = query.onlyStatic(); + } + + return query; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java new file mode 100644 index 00000000000..246735f04a4 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java @@ -0,0 +1,78 @@ +/* + * 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.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PropertyElement; +import io.micronaut.inject.ast.PropertyElementQuery; +import org.objectweb.asm.Type; + +import java.util.Collections; +import java.util.List; + +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static java.util.Collections.emptyList; +import static java.util.function.Predicate.not; + +/** + * Expression AST node used for accessing object property. + * Property access is under the hood an invocation of object getter method + * of respective property. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class PropertyAccess extends ElementMethodCall { + public PropertyAccess(ExpressionNode callee, String name) { + super(callee, name, emptyList()); + } + + @Override + protected CandidateMethod resolveUsedMethod(ExpressionCompilationContext ctx) { + Type calleeType = callee.resolveType(ctx); + ClassElement classElement = getRequiredClassElement(calleeType, ctx.visitorContext()); + + List propertyElements = + classElement.getBeanProperties( + PropertyElementQuery.of(classElement.getAnnotationMetadata()) + .includes(Collections.singleton(name))).stream() + .filter(not(PropertyElement::isExcluded)) + .toList(); + + if (propertyElements.size() == 0) { + throw new ExpressionCompilationException( + "Can not find property with name [" + name + "] in class " + calleeType); + } else if (propertyElements.size() > 1) { + throw new ExpressionCompilationException( + "Ambiguous property access. Found " + propertyElements.size() + + " matching properties with name [" + name + "] in class " + calleeType); + } + + PropertyElement property = propertyElements.iterator().next(); + MethodElement methodElement = + property.getReadMethod() + .orElseThrow(() -> new ExpressionCompilationException( + "Can not resolve access method for property [" + name + "] in class " + calleeType)); + + return new CandidateMethod(methodElement); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java new file mode 100644 index 00000000000..0da43b73e26 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java @@ -0,0 +1,87 @@ +/* + * 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.expressions.parser.ast.collection; + +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import java.util.List; + +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushBoxPrimitiveIfNecessary; +import static io.micronaut.inject.processing.JavaModelUtils.getTypeReference; +import static org.objectweb.asm.Opcodes.AASTORE; + +/** + * Expression AST node for array instantiation. This node is not used when + * parsing user's expressions as array instantiation is not supported in + * evaluated expressions. That's why it doesn't support multidimensional arrays, + * and the presence of initializer is assumed. It is designed for concrete use-cases, + * such as wrapping varargs method arguments and building compound evaluated expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +public final class OneDimensionalArray extends ExpressionNode { + private final TypeIdentifier elementTypeIdentifier; + private final List initializer; + + public OneDimensionalArray(TypeIdentifier elementTypeIdentifier, + List initializer) { + this.elementTypeIdentifier = elementTypeIdentifier; + this.initializer = initializer; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + int arraySize = initializer.size(); + + mv.push(arraySize); + mv.newArray(elementTypeIdentifier.resolveType(ctx)); + + if (arraySize > 0) { + mv.dup(); + } + + for (int i = 0; i < arraySize; i++) { + ExpressionNode element = initializer.get(i); + boolean isLastElement = i == arraySize - 1; + mv.push(i); + + Type elementType = element.resolveType(ctx); + element.compile(ctx); + if (!elementTypeIdentifier.isPrimitive()) { + pushBoxPrimitiveIfNecessary(elementType, mv); + mv.visitInsn(AASTORE); + } else { + mv.arrayStore(elementType); + } + if (!isLastElement) { + mv.dup(); + } + } + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + return getTypeReference(getRequiredClassElement(elementTypeIdentifier.resolveType(ctx), ctx.visitorContext()) + .toArray()); + } +} 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 new file mode 100644 index 00000000000..0f7f8b31667 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/TernaryExpression.java @@ -0,0 +1,135 @@ +/* + * 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.expressions.parser.ast.conditional; + +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import org.objectweb.asm.Label; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.OBJECT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.computeNumericOperationTargetType; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isOneOf; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.toUnboxedIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.isAssignable; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushBoxPrimitiveIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushPrimitiveCastIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.commons.GeneratorAdapter.NE; + +/** + * Expression AST node for ternary expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +public final class TernaryExpression extends ExpressionNode { + private final ExpressionNode condition; + private final ExpressionNode trueExpr; + private final ExpressionNode falseExpr; + + public TernaryExpression(ExpressionNode condition, ExpressionNode trueExpr, + ExpressionNode falseExpr) { + this.condition = condition; + this.trueExpr = trueExpr; + this.falseExpr = falseExpr; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Label falseLabel = new Label(); + Label returnLabel = new Label(); + + Type trueType = trueExpr.resolveType(ctx); + Type falseType = falseExpr.resolveType(ctx); + Type numericType = null; + if (isNumeric(trueType) && isNumeric(falseType)) { + numericType = computeNumericOperationTargetType( + toUnboxedIfNecessary(trueType), + toUnboxedIfNecessary(falseType)); + } + + mv.push(true); + Type conditionType = condition.resolveType(ctx); + + condition.compile(ctx); + pushUnboxPrimitiveIfNecessary(conditionType, mv); + + mv.ifCmp(BOOLEAN, NE, falseLabel); + trueExpr.compile(ctx); + if (numericType != null) { + pushPrimitiveCastIfNecessary(trueType, numericType, mv); + } else { + pushBoxPrimitiveIfNecessary(trueType, mv); + } + + mv.visitJumpInsn(GOTO, returnLabel); + + mv.visitLabel(falseLabel); + falseExpr.compile(ctx); + if (numericType != null) { + pushPrimitiveCastIfNecessary(falseType, numericType, mv); + } else { + pushBoxPrimitiveIfNecessary(falseType, mv); + } + + mv.visitLabel(returnLabel); + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + if (!isOneOf(condition.resolveType(ctx), BOOLEAN, BOOLEAN_WRAPPER)) { + throw new ExpressionCompilationException("Invalid ternary operator. Condition should resolve to boolean type"); + } + + Type trueType = trueExpr.resolveType(ctx); + Type falseType = falseExpr.resolveType(ctx); + + if (trueType.equals(falseType)) { + return trueType; + } + + if (isNumeric(trueType) && isNumeric(falseType)) { + return computeNumericOperationTargetType( + toUnboxedIfNecessary(trueType), + toUnboxedIfNecessary(falseType)); + } else if (isNumeric(trueType) || isNumeric(falseType)) { + return OBJECT; + } + + ClassElement trueClassElement = getRequiredClassElement(trueType, ctx.visitorContext()); + ClassElement falseClassElement = getRequiredClassElement(falseType, ctx.visitorContext()); + + if (isAssignable(trueClassElement, falseClassElement)) { + return trueType; + } + + if (isAssignable(falseClassElement, trueClassElement)) { + return falseType; + } + + return OBJECT; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java new file mode 100644 index 00000000000..9d19e0fec81 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java @@ -0,0 +1,48 @@ +/* + * 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.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; + +/** + * Expression AST node for boolean literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class BoolLiteral extends ExpressionNode { + private final boolean value; + + public BoolLiteral(boolean value) { + this.value = value; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + ctx.methodVisitor().push(value); + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + return BOOLEAN; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java new file mode 100644 index 00000000000..63d27eb75d7 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java @@ -0,0 +1,48 @@ +/* + * 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.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; + +/** + * Expression AST node for double literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class DoubleLiteral extends ExpressionNode { + private final double value; + + public DoubleLiteral(double value) { + this.value = value; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + ctx.methodVisitor().push(value); + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + return DOUBLE; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java new file mode 100644 index 00000000000..1d40c9ef564 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java @@ -0,0 +1,48 @@ +/* + * 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.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; + +/** + * Expression AST node for float literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class FloatLiteral extends ExpressionNode { + private final float value; + + public FloatLiteral(float value) { + this.value = value; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + ctx.methodVisitor().push(value); + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + return FLOAT; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java new file mode 100644 index 00000000000..6d5ebbd3fb6 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java @@ -0,0 +1,49 @@ +/* + * 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.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.INT; + +/** + * Expression AST node for integer literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class IntLiteral extends ExpressionNode { + + private final int value; + + public IntLiteral(int value) { + this.value = value; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + ctx.methodVisitor().push(value); + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + return INT; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java new file mode 100644 index 00000000000..ad5399dcd80 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java @@ -0,0 +1,48 @@ +/* + * 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.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; + +/** + * Expression AST node for long literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class LongLiteral extends ExpressionNode { + private final long value; + + public LongLiteral(long value) { + this.value = value; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + ctx.methodVisitor().push(value); + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + return LONG; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java new file mode 100644 index 00000000000..7468c03791e --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java @@ -0,0 +1,43 @@ +/* + * 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.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.OBJECT; +import static org.objectweb.asm.Opcodes.ACONST_NULL; + +/** + * Expression AST node for null literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class NullLiteral extends ExpressionNode { + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + ctx.methodVisitor().visitInsn(ACONST_NULL); + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + return OBJECT; + } +} 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 new file mode 100644 index 00000000000..ec89dbd236f --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/StringLiteral.java @@ -0,0 +1,53 @@ +/* + * 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.expressions.parser.ast.literal; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.STRING; + +/** + * Expression AST node for string literal. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class StringLiteral extends ExpressionNode { + + private final String value; + + public StringLiteral(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + ctx.methodVisitor().push(value); + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + return STRING; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AddOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AddOperator.java new file mode 100644 index 00000000000..e2f10596e97 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AddOperator.java @@ -0,0 +1,155 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.util.TypeDescriptors; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.Map; +import java.util.Optional; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.STRING; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.VOID; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.computeNumericOperationTargetType; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushPrimitiveCastIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; +import static org.objectweb.asm.Opcodes.DADD; +import static org.objectweb.asm.Opcodes.DUP; +import static org.objectweb.asm.Opcodes.FADD; +import static org.objectweb.asm.Opcodes.IADD; +import static org.objectweb.asm.Opcodes.LADD; +import static org.objectweb.asm.Opcodes.NEW; + +/** + * Expression node for binary '+' operator. Works both for math operation and string + * concatenation. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class AddOperator extends BinaryOperator { + + private static final Map ADD_OPERATION_OPCODES = Map.of( + "D", DADD, + "I", IADD, + "F", FADD, + "J", LADD); + + private static final Type STRING_BUILDER_TYPE = Type.getType(StringBuilder.class); + + private static final Method STRING_BUILD_CONSTRUCTOR = + new Method("", VOID, new Type[]{}); + + private static final Method STRING_BUILD_TO_STRING = + new Method("toString", STRING, new Type[]{}); + + public AddOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected Type resolveOperationType(Type leftOperandType, Type rightOperandType) { + if (!(leftOperandType.equals(STRING) + || rightOperandType.equals(STRING) + || (isNumeric(leftOperandType) && isNumeric(rightOperandType)))) { + throw new ExpressionCompilationException( + "'+' operation can only be applied to numeric and string types"); + } + + if (leftOperandType.equals(STRING) + || rightOperandType.equals(STRING)) { + return STRING; + } + + return computeNumericOperationTargetType(leftOperandType, rightOperandType); + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + Type leftType = leftOperand.resolveType(ctx); + Type rightType = rightOperand.resolveType(ctx); + + GeneratorAdapter mv = ctx.methodVisitor(); + if (leftType.equals(STRING) || (rightType.equals(STRING))) { + concatStrings(ctx); + } else { + Type targetType = resolveType(ctx); + + leftOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(leftType, mv); + pushPrimitiveCastIfNecessary(leftType, targetType, mv); + + rightOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(rightType, mv); + pushPrimitiveCastIfNecessary(rightType, targetType, mv); + + int opcode = + Optional.ofNullable(ADD_OPERATION_OPCODES.get(targetType.getDescriptor())) + .orElseThrow(() -> new ExpressionCompilationException( + "Can not apply '+' operation to " + targetType)); + + mv.visitInsn(opcode); + } + } + + private void concatStrings(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + initStringBuilder(mv); + pushOperand(ctx, leftOperand); + pushOperand(ctx, rightOperand); + mv.invokeVirtual(STRING_BUILDER_TYPE, STRING_BUILD_TO_STRING); + } + + private void initStringBuilder(GeneratorAdapter mv) { + mv.visitTypeInsn(NEW, STRING_BUILDER_TYPE.getInternalName()); + mv.visitInsn(DUP); + mv.invokeConstructor(STRING_BUILDER_TYPE, STRING_BUILD_CONSTRUCTOR); + } + + private void pushOperand(ExpressionCompilationContext ctx, ExpressionNode operand) { + GeneratorAdapter mv = ctx.methodVisitor(); + if (operand instanceof AddOperator addOperator) { + Type operatorType = addOperator.resolveType(ctx); + if (operatorType.equals(STRING)) { + pushOperand(ctx, addOperator.leftOperand); + pushOperand(ctx, addOperator.rightOperand); + } else { + addOperator.compile(ctx); + pushAppendMethod(operand.resolveType(ctx), mv); + } + } else if (operand != null) { + operand.compile(ctx); + pushAppendMethod(operand.resolveType(ctx), mv); + } + } + + private void pushAppendMethod(Type operandType, GeneratorAdapter mv) { + Type argumentType = TypeDescriptors.isPrimitive(operandType) + ? operandType + : Type.getType(Object.class); + + Method appendMethod = new Method("append", STRING_BUILDER_TYPE, new Type[]{argumentType}); + mv.invokeVirtual(STRING_BUILDER_TYPE, appendMethod); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AndOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AndOperator.java new file mode 100644 index 00000000000..7fe3109b7fe --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AndOperator.java @@ -0,0 +1,67 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Label; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.IFEQ; + +/** + * Expression AST node for binary && operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class AndOperator extends LogicalOperator { + public AndOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Label falseLabel = new Label(); + Label trueLabel = new Label(); + + pushOperand(ctx, leftOperand, falseLabel); + pushOperand(ctx, rightOperand, falseLabel); + + mv.push(true); + mv.visitJumpInsn(GOTO, trueLabel); + + mv.visitLabel(falseLabel); + mv.push(false); + + mv.visitLabel(trueLabel); + } + + private void pushOperand(ExpressionCompilationContext ctx, ExpressionNode operand, Label falseLabel) { + if (operand instanceof AndOperator andOperator) { + pushOperand(ctx, andOperator.leftOperand, falseLabel); + pushOperand(ctx, andOperator.rightOperand, falseLabel); + } else if (operand != null) { + GeneratorAdapter mv = ctx.methodVisitor(); + operand.compile(ctx); + mv.visitJumpInsn(IFEQ, falseLabel); + } + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java new file mode 100644 index 00000000000..403b9b30fe7 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java @@ -0,0 +1,51 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Type; + +/** + * Abstract expression AST node for binary operators. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +public abstract sealed class BinaryOperator extends ExpressionNode permits LogicalOperator, + RelationalOperator, + PowOperator, + AddOperator, + EqOperator, + MathOperator { + protected final ExpressionNode leftOperand; + protected final ExpressionNode rightOperand; + + public BinaryOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + this.leftOperand = leftOperand; + this.rightOperand = rightOperand; + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + Type leftType = leftOperand.resolveType(ctx); + Type rightType = rightOperand.resolveType(ctx); + return resolveOperationType(leftType, rightType); + } + + protected abstract Type resolveOperationType(Type leftOperandType, + Type rightOperandType); +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/DivOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/DivOperator.java new file mode 100644 index 00000000000..738fe1ef871 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/DivOperator.java @@ -0,0 +1,58 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import java.util.Map; +import java.util.Optional; + +import static org.objectweb.asm.Opcodes.DDIV; +import static org.objectweb.asm.Opcodes.FDIV; +import static org.objectweb.asm.Opcodes.IDIV; +import static org.objectweb.asm.Opcodes.LDIV; + +/** + * Expression node for binary '/' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class DivOperator extends MathOperator { + private static final Map DIV_OPERATION_OPCODES = Map.of( + "D", DDIV, + "I", IDIV, + "F", FDIV, + "J", LDIV); + + public DivOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected int getMathOperationOpcode(ExpressionCompilationContext ctx) { + Type type = resolveType(ctx); + String typeDescriptor = type.getDescriptor(); + return Optional.ofNullable(DIV_OPERATION_OPCODES.get(typeDescriptor)) + .orElseThrow(() -> new ExpressionCompilationException( + "'/' operation can not be applied to " + type)); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/EqOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/EqOperator.java new file mode 100644 index 00000000000..807481218b5 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/EqOperator.java @@ -0,0 +1,62 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.Objects; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.OBJECT; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushBoxPrimitiveIfNecessary; + +/** + * Expression AST node for binary '==' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public sealed class EqOperator extends BinaryOperator permits NeqOperator { + public EqOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Type lefType = leftOperand.resolveType(ctx); + Type rightType = rightOperand.resolveType(ctx); + + leftOperand.compile(ctx); + pushBoxPrimitiveIfNecessary(lefType, mv); + + rightOperand.compile(ctx); + pushBoxPrimitiveIfNecessary(rightType, mv); + + mv.invokeStatic(Type.getType(Objects.class), new Method("equals", BOOLEAN, new Type[]{OBJECT, OBJECT})); + } + + @Override + protected Type resolveOperationType(Type leftOperandType, Type rightOperandType) { + return BOOLEAN; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GtOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GtOperator.java new file mode 100644 index 00000000000..5c855ea07b1 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GtOperator.java @@ -0,0 +1,45 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; + +import static org.objectweb.asm.Opcodes.IFLE; +import static org.objectweb.asm.Opcodes.IF_ICMPLE; + +/** + * Expression AST node for binary '>' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class GtOperator extends RelationalOperator { + public GtOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected Integer intComparisonOpcode() { + return IF_ICMPLE; + } + + @Override + protected Integer nonIntComparisonOpcode() { + return IFLE; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GteOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GteOperator.java new file mode 100644 index 00000000000..1a36cee4ace --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/GteOperator.java @@ -0,0 +1,45 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; + +import static org.objectweb.asm.Opcodes.IFLT; +import static org.objectweb.asm.Opcodes.IF_ICMPLT; + +/** + * Expression AST node for binary '>=' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class GteOperator extends RelationalOperator { + public GteOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected Integer intComparisonOpcode() { + return IF_ICMPLT; + } + + @Override + protected Integer nonIntComparisonOpcode() { + return IFLT; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java new file mode 100644 index 00000000000..42b2262d745 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java @@ -0,0 +1,68 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isPrimitive; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushBoxPrimitiveIfNecessary; +import static org.objectweb.asm.Opcodes.INSTANCEOF; + +/** + * Expression AST node for 'instanceof' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class InstanceofOperator extends ExpressionNode { + private final ExpressionNode operand; + private final TypeIdentifier typeIdentifier; + + public InstanceofOperator(ExpressionNode operand, TypeIdentifier typeIdentifier) { + this.operand = operand; + this.typeIdentifier = typeIdentifier; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + Type targetType = typeIdentifier.resolveType(ctx); + if (isPrimitive(targetType)) { + throw new ExpressionCompilationException( + "'instanceof' operation can not be used with primitive right-hand side type"); + } + + GeneratorAdapter mv = ctx.methodVisitor(); + Type expressionType = operand.resolveType(ctx); + + operand.compile(ctx); + pushBoxPrimitiveIfNecessary(expressionType, mv); + + mv.visitTypeInsn(INSTANCEOF, targetType.getInternalName()); + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + return BOOLEAN; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LogicalOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LogicalOperator.java new file mode 100644 index 00000000000..62a296aaeb5 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LogicalOperator.java @@ -0,0 +1,49 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isBoolean; +import static org.objectweb.asm.Type.BOOLEAN_TYPE; + +/** + * Abstract expression AST node for binary logical operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public abstract sealed class LogicalOperator extends BinaryOperator permits AndOperator, OrOperator { + + public LogicalOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected Type resolveOperationType(Type leftOperandType, + Type rightOperandType) { + if (!isBoolean(leftOperandType) && !isBoolean(rightOperandType)) { + throw new ExpressionCompilationException( + "Logical operation can only be applied to boolean types"); + } + + return BOOLEAN_TYPE; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LtOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LtOperator.java new file mode 100644 index 00000000000..78971a4b699 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LtOperator.java @@ -0,0 +1,45 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; + +import static org.objectweb.asm.Opcodes.IFGE; +import static org.objectweb.asm.Opcodes.IF_ICMPGE; + +/** + * Expression AST node for binary '<' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class LtOperator extends RelationalOperator { + public LtOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected Integer intComparisonOpcode() { + return IF_ICMPGE; + } + + @Override + protected Integer nonIntComparisonOpcode() { + return IFGE; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LteOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LteOperator.java new file mode 100644 index 00000000000..bc21106281c --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/LteOperator.java @@ -0,0 +1,45 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; + +import static org.objectweb.asm.Opcodes.IFGT; +import static org.objectweb.asm.Opcodes.IF_ICMPGT; + +/** + * Expression AST node for binary '<=' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class LteOperator extends RelationalOperator { + public LteOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected Integer intComparisonOpcode() { + return IF_ICMPGT; + } + + @Override + protected Integer nonIntComparisonOpcode() { + return IFGT; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java new file mode 100644 index 00000000000..f4410f71eb1 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java @@ -0,0 +1,76 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.literal.StringLiteral; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.STRING; + +/** + * Expression AST node for regex 'matches' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class MatchesOperator extends ExpressionNode { + + private static final Method MATCHES = new Method("matches", BOOLEAN, new Type[]{STRING}); + + private final ExpressionNode operand; + private final StringLiteral pattern; + + public MatchesOperator(ExpressionNode operand, StringLiteral pattern) { + this.operand = operand; + this.pattern = pattern; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + operand.compile(ctx); + pattern.compile(ctx); + mv.invokeVirtual(STRING, MATCHES); + } + + @Override + protected Type doResolveType(ExpressionCompilationContext ctx) { + if (!operand.resolveType(ctx).equals(STRING)) { + throw new ExpressionCompilationException( + "Operator 'matches' can only be applied to String operand"); + } + + String patternValue = pattern.getValue(); + try { + Pattern.compile(patternValue); + } catch (PatternSyntaxException ex) { + throw new ExpressionCompilationException("Invalid RegEx pattern provided: " + patternValue); + } + + return BOOLEAN; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MathOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MathOperator.java new file mode 100644 index 00000000000..def1804e796 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MathOperator.java @@ -0,0 +1,68 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.computeNumericOperationTargetType; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushPrimitiveCastIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; + +/** + * Abstract expression AST node for binary math operations + * on primitive types. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public abstract sealed class MathOperator extends BinaryOperator permits DivOperator, + ModOperator, + MulOperator, + SubOperator { + public MathOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Type targetType = resolveType(ctx); + + Type leftType = leftOperand.resolveType(ctx); + leftOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(leftType, mv); + pushPrimitiveCastIfNecessary(leftType, targetType, mv); + + Type rightType = rightOperand.resolveType(ctx); + rightOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(rightType, mv); + pushPrimitiveCastIfNecessary(rightType, targetType, mv); + + mv.visitInsn(getMathOperationOpcode(ctx)); + } + + @Override + protected Type resolveOperationType(Type leftOperandType, Type rightOperandType) { + return computeNumericOperationTargetType(leftOperandType, rightOperandType); + } + + protected abstract int getMathOperationOpcode(ExpressionCompilationContext ctx); +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ModOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ModOperator.java new file mode 100644 index 00000000000..0249983c35f --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ModOperator.java @@ -0,0 +1,58 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import java.util.Map; +import java.util.Optional; + +import static org.objectweb.asm.Opcodes.DREM; +import static org.objectweb.asm.Opcodes.FREM; +import static org.objectweb.asm.Opcodes.IREM; +import static org.objectweb.asm.Opcodes.LREM; + +/** + * Expression AST node for binary '/' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class ModOperator extends MathOperator { + private static final Map MOD_OPERATION_OPCODES = Map.of( + "D", DREM, + "I", IREM, + "F", FREM, + "J", LREM); + + public ModOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected int getMathOperationOpcode(ExpressionCompilationContext ctx) { + Type type = resolveType(ctx); + String typeDescriptor = type.getDescriptor(); + return Optional.ofNullable(MOD_OPERATION_OPCODES.get(typeDescriptor)) + .orElseThrow(() -> new ExpressionCompilationException( + "'%' operation can not be applied to " + type)); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MulOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MulOperator.java new file mode 100644 index 00000000000..d63028d2e0a --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MulOperator.java @@ -0,0 +1,59 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import java.util.Map; +import java.util.Optional; + +import static org.objectweb.asm.Opcodes.DMUL; +import static org.objectweb.asm.Opcodes.FMUL; +import static org.objectweb.asm.Opcodes.IMUL; +import static org.objectweb.asm.Opcodes.LMUL; + +/** + * Expression node for binary '*' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class MulOperator extends MathOperator { + + private static final Map MUL_OPERATION_OPCODES = Map.of( + "D", DMUL, + "I", IMUL, + "F", FMUL, + "J", LMUL); + + public MulOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected int getMathOperationOpcode(ExpressionCompilationContext ctx) { + Type type = resolveType(ctx); + String typeDescriptor = type.getDescriptor(); + return Optional.ofNullable(MUL_OPERATION_OPCODES.get(typeDescriptor)) + .orElseThrow(() -> new ExpressionCompilationException( + "'*' operation can not be applied to " + type)); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NeqOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NeqOperator.java new file mode 100644 index 00000000000..b2002195eee --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NeqOperator.java @@ -0,0 +1,62 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Label; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.IFNE; +import static org.objectweb.asm.Type.BOOLEAN_TYPE; + +/** + * Expression AST node for binary '!=' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class NeqOperator extends EqOperator { + public NeqOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + super.generateBytecode(ctx); + + GeneratorAdapter mv = ctx.methodVisitor(); + Label elseLabel = new Label(); + Label endOfCmpLabel = new Label(); + + mv.visitJumpInsn(IFNE, elseLabel); + + mv.push(true); + mv.visitJumpInsn(GOTO, endOfCmpLabel); + mv.visitLabel(elseLabel); + mv.push(false); + mv.visitLabel(endOfCmpLabel); + } + + @Override + protected Type resolveOperationType(Type leftOperandType, Type rightOperandType) { + return BOOLEAN_TYPE; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/OrOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/OrOperator.java new file mode 100644 index 00000000000..deffe47150c --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/OrOperator.java @@ -0,0 +1,55 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Label; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.IFEQ; + +/** + * Expression node for binary '||' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class OrOperator extends LogicalOperator { + public OrOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Label falseLabel = new Label(); + Label returnLabel = new Label(); + + leftOperand.compile(ctx); + mv.visitJumpInsn(IFEQ, falseLabel); + mv.push(true); + mv.visitJumpInsn(GOTO, returnLabel); + + mv.visitLabel(falseLabel); + rightOperand.compile(ctx); + + mv.visitLabel(returnLabel); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/PowOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/PowOperator.java new file mode 100644 index 00000000000..2436900b437 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/PowOperator.java @@ -0,0 +1,88 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isOneOf; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.toUnboxedIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushPrimitiveCastIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; + +/** + * Expression AST node for '^' operator. '^' operator in evaluated + * expressions means power operation + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class PowOperator extends BinaryOperator { + + private static final Type MATH_TYPE = Type.getType(Math.class); + private static final Method POW_METHOD = new Method("pow", DOUBLE, new Type[]{DOUBLE, DOUBLE}); + + public PowOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + + Type leftType = leftOperand.resolveType(ctx); + Type rightType = rightOperand.resolveType(ctx); + + leftOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(leftType, mv); + pushPrimitiveCastIfNecessary(leftType, DOUBLE, mv); + + rightOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(leftType, mv); + pushPrimitiveCastIfNecessary(rightType, DOUBLE, mv); + + mv.invokeStatic(MATH_TYPE, POW_METHOD); + + if (resolveType(ctx) == LONG) { + pushPrimitiveCastIfNecessary(DOUBLE, LONG, mv); + } + } + + @Override + protected Type resolveOperationType(Type leftOperandType, Type rightOperandType) { + if (!isNumeric(leftOperandType) || !isNumeric(rightOperandType)) { + throw new ExpressionCompilationException("Power operation can only be applied to numeric types"); + } + + if (isOneOf(toUnboxedIfNecessary(leftOperandType), DOUBLE, FLOAT) || + isOneOf(toUnboxedIfNecessary(rightOperandType), DOUBLE, FLOAT)) { + return DOUBLE; + } + + // Int power operation result might not fit in int value + return LONG; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java new file mode 100644 index 00000000000..e36de48acf9 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java @@ -0,0 +1,111 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Label; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.computeNumericOperationTargetType; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isOneOf; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushPrimitiveCastIfNecessary; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; +import static org.objectweb.asm.Opcodes.DCMPL; +import static org.objectweb.asm.Opcodes.FCMPL; +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.LCMP; +import static org.objectweb.asm.Type.BOOLEAN_TYPE; + +/** + * Abstract expression AST node for relational operators. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public abstract sealed class RelationalOperator extends BinaryOperator permits GtOperator, + GteOperator, + LtOperator, + LteOperator { + public RelationalOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + this.nodeType = BOOLEAN; + } + + @Override + protected Type resolveOperationType(Type leftOperandType, + Type rightOperandType) { + if (!isNumeric(leftOperandType) || !isNumeric(rightOperandType)) { + throw new ExpressionCompilationException("Relational operation can only be applied to" + + " numeric types"); + } + + return BOOLEAN_TYPE; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + + Type leftType = leftOperand.resolveType(ctx); + Type rightType = rightOperand.resolveType(ctx); + + Type targetType = computeNumericOperationTargetType(leftType, rightType); + + leftOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(leftType, mv); + pushPrimitiveCastIfNecessary(leftType, targetType, mv); + + rightOperand.compile(ctx); + pushUnboxPrimitiveIfNecessary(rightType, mv); + pushPrimitiveCastIfNecessary(rightType, targetType, mv); + + Label elseLabel = new Label(); + Label endOfCmpLabel = new Label(); + + if (isOneOf(targetType, DOUBLE, FLOAT, LONG)) { + String targetDescriptor = targetType.getDescriptor(); + switch (targetDescriptor) { + case "D" -> mv.visitInsn(DCMPL); + case "F" -> mv.visitInsn(FCMPL); + case "J" -> mv.visitInsn(LCMP); + default -> { } + } + mv.visitJumpInsn(nonIntComparisonOpcode(), elseLabel); + } else { + mv.visitJumpInsn(intComparisonOpcode(), elseLabel); + } + + mv.push(true); + mv.visitJumpInsn(GOTO, endOfCmpLabel); + mv.visitLabel(elseLabel); + mv.push(false); + mv.visitLabel(endOfCmpLabel); + } + + protected abstract Integer intComparisonOpcode(); + + protected abstract Integer nonIntComparisonOpcode(); +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/SubOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/SubOperator.java new file mode 100644 index 00000000000..4cdffa92844 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/SubOperator.java @@ -0,0 +1,59 @@ +/* + * 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.expressions.parser.ast.operator.binary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import java.util.Map; +import java.util.Optional; + +import static org.objectweb.asm.Opcodes.DSUB; +import static org.objectweb.asm.Opcodes.FSUB; +import static org.objectweb.asm.Opcodes.ISUB; +import static org.objectweb.asm.Opcodes.LSUB; + +/** + * Expression AST node for binary '-' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class SubOperator extends MathOperator { + + private static final Map SUB_OPERATION_OPCODES = Map.of( + "D", DSUB, + "I", ISUB, + "F", FSUB, + "J", LSUB); + + public SubOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { + super(leftOperand, rightOperand); + } + + @Override + protected int getMathOperationOpcode(ExpressionCompilationContext ctx) { + Type type = resolveType(ctx); + String typeDescriptor = type.getDescriptor(); + return Optional.ofNullable(SUB_OPERATION_OPCODES.get(typeDescriptor)) + .orElseThrow(() -> new ExpressionCompilationException( + "'*' operation can not be applied to " + type)); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NegOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NegOperator.java new file mode 100644 index 00000000000..8afd8c2bfb2 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NegOperator.java @@ -0,0 +1,83 @@ +/* + * 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.expressions.parser.ast.operator.unary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.INT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.INT_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isOneOf; +import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.pushUnboxPrimitiveIfNecessary; +import static org.objectweb.asm.Opcodes.DNEG; +import static org.objectweb.asm.Opcodes.FNEG; +import static org.objectweb.asm.Opcodes.INEG; +import static org.objectweb.asm.Opcodes.LNEG; + +/** + * Expression node for unary '-' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class NegOperator extends UnaryOperator { + public NegOperator(ExpressionNode operand) { + super(operand); + } + + @Override + public Type doResolveType(ExpressionCompilationContext ctx) { + Type nodeType = super.doResolveType(ctx); + if (!isNumeric(nodeType)) { + throw new ExpressionCompilationException( + "Invalid unary '-' operation. Unary '-' can only be applied to numeric types"); + } + return nodeType; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + + operand.compile(ctx); + pushUnboxPrimitiveIfNecessary(operand.resolveType(ctx), mv); + + if (isOneOf(operand.resolveType(ctx), INT, INT_WRAPPER)) { + mv.visitInsn(INEG); + } else if (isOneOf(operand.resolveType(ctx), DOUBLE, DOUBLE_WRAPPER)) { + mv.visitInsn(DNEG); + } else if (isOneOf(operand.resolveType(ctx), FLOAT, FLOAT_WRAPPER)) { + mv.visitInsn(FNEG); + } else if (isOneOf(operand.resolveType(ctx), LONG, LONG_WRAPPER)) { + mv.visitInsn(LNEG); + } else { + throw new ExpressionCompilationException( + "Invalid unary '-' operation. Unary '-' can only be applied to numeric types"); + } + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NotOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NotOperator.java new file mode 100644 index 00000000000..2cdd4cfe839 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NotOperator.java @@ -0,0 +1,74 @@ +/* + * 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.expressions.parser.ast.operator.unary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Label; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isBoolean; +import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.IFNE; + +/** + * Expression node for unary '!' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class NotOperator extends UnaryOperator { + public NotOperator(ExpressionNode operand) { + super(operand); + } + + @Override + public Type doResolveType(ExpressionCompilationContext ctx) { + if (nodeType != null) { + return nodeType; + } + + Type nodeType = super.doResolveType(ctx); + if (!isBoolean(nodeType)) { + throw new ExpressionCompilationException( + "Invalid unary '!' operation. Unary '!' can only be applied to boolean types"); + } + + this.nodeType = nodeType; + return nodeType; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + GeneratorAdapter mv = ctx.methodVisitor(); + Label falseLabel = new Label(); + Label returnLabel = new Label(); + + operand.compile(ctx); + mv.visitJumpInsn(IFNE, falseLabel); + mv.push(true); + mv.visitJumpInsn(GOTO, returnLabel); + + mv.visitLabel(falseLabel); + mv.push(false); + + mv.visitLabel(returnLabel); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/PosOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/PosOperator.java new file mode 100644 index 00000000000..4ddafec21f3 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/PosOperator.java @@ -0,0 +1,53 @@ +/* + * 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.expressions.parser.ast.operator.unary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.isNumeric; + +/** + * Expression node for unary '+' operator. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class PosOperator extends UnaryOperator { + public PosOperator(ExpressionNode operand) { + super(operand); + } + + @Override + public Type doResolveType(ExpressionCompilationContext ctx) { + Type nodeType = super.doResolveType(ctx); + + if (!isNumeric(nodeType)) { + throw new ExpressionCompilationException("Invalid unary '+' operation"); + } + + return nodeType; + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + operand.compile(ctx); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java new file mode 100644 index 00000000000..efa9cc64cdb --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java @@ -0,0 +1,43 @@ +/* + * 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.expressions.parser.ast.operator.unary; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import org.objectweb.asm.Type; + +/** + * Abstract expression node for unary operators. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public abstract sealed class UnaryOperator extends ExpressionNode permits NegOperator, + NotOperator, + PosOperator { + protected final ExpressionNode operand; + + public UnaryOperator(ExpressionNode operand) { + this.operand = operand; + } + + @Override + public Type doResolveType(ExpressionCompilationContext ctx) { + return operand.resolveType(ctx); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java new file mode 100644 index 00000000000..79bff825d72 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java @@ -0,0 +1,93 @@ +/* + * 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.expressions.parser.ast.types; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.util.TypeDescriptors; +import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.processing.JavaModelUtils; +import org.objectweb.asm.Type; + +import java.util.Map; + +/** + * Expression node for type identifier. Bytecode for identifier is not generated + * directly - it is generated by nodes using the identifier. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class TypeIdentifier extends ExpressionNode { + private static final Map PRIMITIVES = Map.of( + "int", TypeDescriptors.INT, + "long", TypeDescriptors.LONG, + "byte", TypeDescriptors.BYTE, + "short", TypeDescriptors.SHORT, + "char", TypeDescriptors.CHAR, + "boolean", TypeDescriptors.BOOLEAN, + "double", TypeDescriptors.DOUBLE, + "float", TypeDescriptors.FLOAT); + + private final String name; + + public TypeIdentifier(String name) { + this.name = name; + } + + public boolean isPrimitive() { + return PRIMITIVES.containsKey(this.toString()); + } + + @Override + public void generateBytecode(ExpressionCompilationContext ctx) { + ctx.methodVisitor().push(resolveType(ctx)); + } + + @Override + public Type doResolveType(ExpressionCompilationContext ctx) { + String name = this.toString(); + if (PRIMITIVES.containsKey(name)) { + return PRIMITIVES.get(name); + } + + Type resolvedType = resolveObjectType(ctx, name); + + // may be java.lang type + if (resolvedType == null && !name.contains(".")) { + resolvedType = resolveObjectType(ctx, "java.lang." + name); + } + + if (resolvedType == null) { + throw new ExpressionCompilationException("Unknown type identifier: " + name); + } + + return resolvedType; + } + + private Type resolveObjectType(ExpressionCompilationContext ctx, String name) { + return ctx.visitorContext().getClassElement(name) + .map(JavaModelUtils::getTypeReference) + .orElse(null); + } + + @Override + public String toString() { + return name; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/EvaluatedExpressionCompilationUtils.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/EvaluatedExpressionCompilationUtils.java new file mode 100644 index 00000000000..3a6df9a282d --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/EvaluatedExpressionCompilationUtils.java @@ -0,0 +1,254 @@ +/* + * 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.expressions.parser.ast.util; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import io.micronaut.inject.visitor.VisitorContext; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.Optional; + +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BYTE; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BYTE_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.CHAR; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.CHAR_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.INT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.INT_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.SHORT; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.SHORT_WRAPPER; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.toBoxedIfNecessary; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.toUnboxedIfNecessary; +import static io.micronaut.inject.processing.JavaModelUtils.getTypeReference; +import static org.objectweb.asm.Opcodes.D2F; +import static org.objectweb.asm.Opcodes.D2I; +import static org.objectweb.asm.Opcodes.D2L; +import static org.objectweb.asm.Opcodes.F2D; +import static org.objectweb.asm.Opcodes.F2I; +import static org.objectweb.asm.Opcodes.F2L; +import static org.objectweb.asm.Opcodes.I2D; +import static org.objectweb.asm.Opcodes.I2F; +import static org.objectweb.asm.Opcodes.I2L; +import static org.objectweb.asm.Opcodes.L2D; +import static org.objectweb.asm.Opcodes.L2F; +import static org.objectweb.asm.Opcodes.L2I; + +/** + * Unility methods for used when compiling evaluated expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class EvaluatedExpressionCompilationUtils { + + /** + * Checks whether the argument class element is assignable to the parameter + * class element. This method also accepts primitive and wrapper elements and + * determines whether argument can be assigned to parameter after boxing or unboxing. + * In case when parameter or argument is an array, array dimensions are also checked. + * + * @param parameter checked parameter + * @param argument checked argument + * @return whether argument is assignable to parameter + */ + public static boolean isAssignable(@NonNull ClassElement parameter, + @NonNull ClassElement argument) { + if (!argument.isAssignable(parameter)) { + Type parameterType = getTypeReference(parameter); + Type argumentType = getTypeReference(argument); + + return toUnboxedIfNecessary(parameterType).equals(toUnboxedIfNecessary(argumentType)) + || toBoxedIfNecessary(parameterType).equals(toBoxedIfNecessary(argumentType)); + } + + if (parameter.getArrayDimensions() > 0 || argument.getArrayDimensions() > 0) { + return parameter.getArrayDimensions() == argument.getArrayDimensions(); + } + + return true; + } + + /** + * Provides {@link ClassElement} for passed type or throws exception + * if class element can not be provided. + * + * @param type Type element for which {@link ClassElement} needs to be obtained. + * This type can also represent a primitive type. In this case it will be + * boxed + * @param visitorContext visitor context + * @return resolved class element + * @throws ExpressionCompilationException if class element can not be obtained + */ + @NonNull + public static ClassElement getRequiredClassElement(Type type, + VisitorContext visitorContext) { + boolean isArrayType = type.getDescriptor().startsWith("["); + if (isArrayType) { + Type elementType = type.getElementType(); + ClassElement classElement = toPrimitiveElement(elementType).orElse(null); + if (classElement == null) { + classElement = getClassElementForName(visitorContext, elementType.getClassName()); + } + + for (int i = 0; i < type.getDimensions(); i++) { + classElement = classElement.toArray(); + } + + return classElement; + } + + + String className = toBoxedIfNecessary(type).getClassName(); + return getClassElementForName(visitorContext, className); + } + + private static ClassElement getClassElementForName(VisitorContext visitorContext, String className) { + return visitorContext.getClassElement(className) + .orElseThrow(() -> new ExpressionCompilationException( + "Can not resolve type information for [" + className + "]")); + } + + /** + * Pushed unboxing instruction if passed type is a primitive wrapper. + * + * @param type type to unbox + * @param mv method visitor + */ + public static void pushUnboxPrimitiveIfNecessary(@NonNull Type type, + @NonNull GeneratorAdapter mv) { + if (type.equals(BOOLEAN_WRAPPER)) { + mv.invokeVirtual(BOOLEAN_WRAPPER, new Method("booleanValue", "()Z")); + } else if (type.equals(INT_WRAPPER)) { + mv.invokeVirtual(INT_WRAPPER, new Method("intValue", "()I")); + } else if (type.equals(DOUBLE_WRAPPER)) { + mv.invokeVirtual(DOUBLE_WRAPPER, new Method("doubleValue", "()D")); + } else if (type.equals(LONG_WRAPPER)) { + mv.invokeVirtual(LONG_WRAPPER, new Method("longValue", "()J")); + } else if (type.equals(FLOAT_WRAPPER)) { + mv.invokeVirtual(FLOAT_WRAPPER, new Method("floatValue", "()F")); + } else if (type.equals(SHORT_WRAPPER)) { + mv.invokeVirtual(SHORT_WRAPPER, new Method("shortValue", "()S")); + } else if (type.equals(CHAR_WRAPPER)) { + mv.invokeVirtual(CHAR_WRAPPER, new Method("charValue", "()C")); + } else if (type.equals(BYTE_WRAPPER)) { + mv.invokeVirtual(BYTE_WRAPPER, new Method("byteValue", "()B")); + } + } + + /** + * Pushed primitive boxing instruction if passed type is a wrapper. + * + * @param type type to box + * @param mv method visitor + */ + public static void pushBoxPrimitiveIfNecessary(@NonNull Type type, + @NonNull GeneratorAdapter mv) { + if (type.equals(BOOLEAN)) { + mv.invokeStatic(BOOLEAN_WRAPPER, new Method("valueOf", BOOLEAN_WRAPPER, new Type[]{BOOLEAN})); + } else if (type.equals(INT)) { + mv.invokeStatic(INT_WRAPPER, new Method("valueOf", INT_WRAPPER, new Type[]{INT})); + } else if (type.equals(DOUBLE)) { + mv.invokeStatic(DOUBLE_WRAPPER, new Method("valueOf", DOUBLE_WRAPPER, new Type[]{DOUBLE})); + } else if (type.equals(LONG)) { + mv.invokeStatic(LONG_WRAPPER, new Method("valueOf", LONG_WRAPPER, new Type[]{LONG})); + } else if (type.equals(FLOAT)) { + mv.invokeStatic(FLOAT_WRAPPER, new Method("valueOf", FLOAT_WRAPPER, new Type[]{FLOAT})); + } else if (type.equals(SHORT)) { + mv.invokeStatic(SHORT_WRAPPER, new Method("valueOf", SHORT_WRAPPER, new Type[]{SHORT})); + } else if (type.equals(CHAR)) { + mv.invokeStatic(CHAR_WRAPPER, new Method("valueOf", CHAR_WRAPPER, new Type[]{CHAR})); + } else if (type.equals(BYTE)) { + mv.invokeStatic(BYTE_WRAPPER, new Method("valueOf", BYTE_WRAPPER, new Type[]{BYTE})); + } + } + + /** + * @param type type to be converted to {@link PrimitiveElement} + * @return optional corresponding primitive element + */ + public static Optional toPrimitiveElement(Type type) { + try { + return Optional.of(PrimitiveElement.valueOf(type.getClassName())); + } catch (IllegalArgumentException ex) { + return Optional.empty(); + } + } + + /** + * This method checks whether passed primitive type needs to be explicitly cast + * to target type. If it is, respective cast instruction is pushed. + * + * @param type type to cast + * @param targetType target type to which the cast is required + * @param mv method visitor + */ + public static void pushPrimitiveCastIfNecessary(@NonNull Type type, + @NonNull Type targetType, + @NonNull GeneratorAdapter mv) { + String typeDescriptor = type.getDescriptor(); + String targetDescriptor = targetType.getDescriptor(); + + switch (targetDescriptor) { + case "J" -> { + switch (typeDescriptor) { + case "I" -> mv.visitInsn(I2L); + case "D" -> mv.visitInsn(D2L); + case "F" -> mv.visitInsn(F2L); + default -> { } + } + } + case "I" -> { + switch (typeDescriptor) { + case "J" -> mv.visitInsn(L2I); + case "D" -> mv.visitInsn(D2I); + case "F" -> mv.visitInsn(F2I); + default -> { } + } + } + case "D" -> { + switch (typeDescriptor) { + case "J" -> mv.visitInsn(L2D); + case "I" -> mv.visitInsn(I2D); + case "F" -> mv.visitInsn(F2D); + default -> { } + } + } + case "F" -> { + switch (typeDescriptor) { + case "J" -> mv.visitInsn(L2F); + case "I" -> mv.visitInsn(I2F); + case "D" -> mv.visitInsn(D2F); + default -> { } + } + } + default -> { } + } + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java new file mode 100644 index 00000000000..c5aa7ed58c8 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java @@ -0,0 +1,193 @@ +/* + * 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.expressions.parser.ast.util; + +import io.micronaut.context.AbstractEvaluatedExpression; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import org.objectweb.asm.Type; + +import java.util.Map; + +import static java.util.stream.Collectors.toMap; + +/** + * Set of constants and utility methods for working with type descriptors + * while compiling evaluated expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class TypeDescriptors { + public static final Type EVALUATED_EXPRESSION_TYPE = Type.getType(AbstractEvaluatedExpression.class); + + public static final Type STRING = Type.getType(String.class); + public static final Type OBJECT = Type.getType(Object.class); + public static final Type CLASS = Type.getType(Class.class); + public static final Type VOID = Type.VOID_TYPE; + + // Primitives + public static final Type DOUBLE = Type.DOUBLE_TYPE; + public static final Type FLOAT = Type.FLOAT_TYPE; + public static final Type INT = Type.INT_TYPE; + public static final Type LONG = Type.LONG_TYPE; + public static final Type BOOLEAN = Type.BOOLEAN_TYPE; + public static final Type CHAR = Type.CHAR_TYPE; + public static final Type SHORT = Type.SHORT_TYPE; + public static final Type BYTE = Type.BYTE_TYPE; + + // Wrappers + public static final Type BOOLEAN_WRAPPER = Type.getType(Boolean.class); + public static final Type INT_WRAPPER = Type.getType(Integer.class); + public static final Type LONG_WRAPPER = Type.getType(Long.class); + public static final Type DOUBLE_WRAPPER = Type.getType(Double.class); + public static final Type FLOAT_WRAPPER = Type.getType(Float.class); + public static final Type SHORT_WRAPPER = Type.getType(Short.class); + public static final Type BYTE_WRAPPER = Type.getType(Byte.class); + public static final Type CHAR_WRAPPER = Type.getType(Character.class); + + public static final Map PRIMITIVE_TO_WRAPPER = Map.of( + BOOLEAN, BOOLEAN_WRAPPER, + INT, INT_WRAPPER, + DOUBLE, DOUBLE_WRAPPER, + LONG, LONG_WRAPPER, + FLOAT, FLOAT_WRAPPER, + SHORT, SHORT_WRAPPER, + CHAR, CHAR_WRAPPER, + BYTE, BYTE_WRAPPER); + + public static final Map WRAPPER_TO_PRIMITIVE = + PRIMITIVE_TO_WRAPPER.entrySet() + .stream() + .collect(toMap(Map.Entry::getValue, Map.Entry::getKey)); + + /** + * Checks if passed type is a primitive. + * + * @param type type to check + * @return true if it is + */ + public static boolean isPrimitive(@NonNull Type type) { + return PRIMITIVE_TO_WRAPPER.containsKey(type); + } + + /** + * Checks if passed type is either boolean primitive or wrapper. + * + * @param type type to check + * @return true if it is + */ + public static boolean isBoolean(@NonNull Type type) { + return isOneOf(type, BOOLEAN, BOOLEAN_WRAPPER); + } + + /** + * Checks if passed type is one of numeric primitives or numeric wrappers. + * + * @param type type to check + * @return true if it is + */ + @NonNull + public static boolean isNumeric(@NonNull Type type) { + return isOneOf(type, + DOUBLE, DOUBLE_WRAPPER, + FLOAT, FLOAT_WRAPPER, + INT, INT_WRAPPER, + LONG, LONG_WRAPPER, + SHORT, SHORT_WRAPPER, + CHAR, CHAR_WRAPPER, + BYTE, BYTE_WRAPPER); + } + + /** + * If passed type is boxed type, returns responsive primitive, otherwise returns + * original passed type. + * + * @param type type to check + * @return unboxed type or original passed type + */ + @NonNull + public static Type toUnboxedIfNecessary(@NonNull Type type) { + if (WRAPPER_TO_PRIMITIVE.containsKey(type)) { + return WRAPPER_TO_PRIMITIVE.get(type); + } + return type; + } + + /** + * If passed type is primitive, returns responsive boxed type, otherwise returns + * original passed type. + * + * @param type type to check + * @return boxed type or original passed type + */ + @NonNull + public static Type toBoxedIfNecessary(@NonNull Type type) { + if (PRIMITIVE_TO_WRAPPER.containsKey(type)) { + return PRIMITIVE_TO_WRAPPER.get(type); + } + return type; + } + + /** + * For two passed types computes result numeric operation type. This method accepts + * both primitive and wrapper types, but returns only primitive type. + * + * @param leftOperandType left operand type + * @param rightOperandType right operand type + * @return numeric operation result type + * @throws ExpressionCompilationException if ony of the passed types is not a numeric type + */ + @NonNull + public static Type computeNumericOperationTargetType(@NonNull Type leftOperandType, + @NonNull Type rightOperandType) { + if (!isNumeric(leftOperandType) || !isNumeric(rightOperandType)) { + throw new ExpressionCompilationException("Numeric operation can only be applied to numeric types"); + } + + if (toUnboxedIfNecessary(leftOperandType).equals(DOUBLE) + || toUnboxedIfNecessary(rightOperandType).equals(DOUBLE)) { + return DOUBLE; + } else if (toUnboxedIfNecessary(leftOperandType).equals(FLOAT) + || toUnboxedIfNecessary(rightOperandType).equals(FLOAT)) { + return FLOAT; + } else if (toUnboxedIfNecessary(leftOperandType).equals(LONG) + || toUnboxedIfNecessary(rightOperandType).equals(LONG)) { + return LONG; + } else { + return INT; + } + } + + /** + * Utility method to check if passed type (first argument) is the same as any of + * compared types (second and following args). + * + * @param type type to check + * @param comparedTypes types against which checked types is compared + * @return true if checked type is amount compared types + */ + public static boolean isOneOf(Type type, Type... comparedTypes) { + for (Type comparedType: comparedTypes) { + if (type.equals(comparedType)) { + return true; + } + } + return false; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionCompilationContext.java b/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionCompilationContext.java new file mode 100644 index 00000000000..7585fb4ba50 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionCompilationContext.java @@ -0,0 +1,36 @@ +/* + * 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.expressions.parser.compilation; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.context.ExpressionEvaluationContext; +import io.micronaut.inject.visitor.VisitorContext; +import org.objectweb.asm.commons.GeneratorAdapter; + +/** + * Context class used for compiling expressions. + * + * @param evaluationContext expression evaluation context + * @param visitorContext visitor context + * @param methodVisitor method visitor for compiled expression class + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public record ExpressionCompilationContext(ExpressionEvaluationContext evaluationContext, + VisitorContext visitorContext, + GeneratorAdapter methodVisitor) { } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionCompilationException.java b/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionCompilationException.java new file mode 100644 index 00000000000..4335b0ccfa4 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionCompilationException.java @@ -0,0 +1,29 @@ +/* + * 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.expressions.parser.exception; + +/** + * Exception throws when problems with expression compilation occur. + * These usually include problems with type resolution. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +public class ExpressionCompilationException extends RuntimeException { + public ExpressionCompilationException(String message) { + super(message); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionParsingException.java b/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionParsingException.java new file mode 100644 index 00000000000..fe0d8b14e97 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionParsingException.java @@ -0,0 +1,28 @@ +/* + * 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.expressions.parser.exception; + +/** + * Exception throws when problems with expression parsing occur. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +public class ExpressionParsingException extends RuntimeException { + public ExpressionParsingException(String message) { + super(message); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Token.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Token.java new file mode 100644 index 00000000000..1e6a5b8de70 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Token.java @@ -0,0 +1,30 @@ +/* + * 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.expressions.parser.token; + +import io.micronaut.core.annotation.Internal; + +/** + * Parsed token with value and type. + * + * @param type token type + * @param value token string value + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public record Token(TokenType type, String value) { } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java new file mode 100644 index 00000000000..14b2c440b3f --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java @@ -0,0 +1,85 @@ +/* + * 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.expressions.parser.token; + +import io.micronaut.core.annotation.Internal; + +/** + * List of supported token types. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public enum TokenType { + WHITESPACE, + IDENTIFIER, + + EXPRESSION_CONTEXT_REF, + DOT, + COMMA, + COLON, + L_PAREN, + R_PAREN, + L_CURLY, + R_CURLY, + L_SQUARE, + R_SQUARE, + QMARK, + NOT, + + // MATH OPERATORS + POW, + PLUS, + MINUS, + MUL, + DIV, + MOD, + INCREMENT, + DECREMENT, + + // LITERALS + DOUBLE, + FLOAT, + INT, + LONG, + STRING, + BOOL, + NULL, + + // LOGICAL OPERATORS + OR, + AND, + + // RELATIONAL OPERATORS + EQ, + NE, + GT, + GTE, + LT, + LTE, + INSTANCEOF, + MATCHES; + + public boolean isOneOf(TokenType... others) { + for (TokenType comparedType: others) { + if (comparedType == this) { + return true; + } + } + return false; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java new file mode 100644 index 00000000000..cb619707b7c --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java @@ -0,0 +1,214 @@ +/* + * 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.expressions.parser.token; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.expressions.parser.exception.ExpressionParsingException; + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static io.micronaut.expressions.parser.token.TokenType.AND; +import static io.micronaut.expressions.parser.token.TokenType.BOOL; +import static io.micronaut.expressions.parser.token.TokenType.COLON; +import static io.micronaut.expressions.parser.token.TokenType.COMMA; +import static io.micronaut.expressions.parser.token.TokenType.DECREMENT; +import static io.micronaut.expressions.parser.token.TokenType.DIV; +import static io.micronaut.expressions.parser.token.TokenType.DOT; +import static io.micronaut.expressions.parser.token.TokenType.DOUBLE; +import static io.micronaut.expressions.parser.token.TokenType.EQ; +import static io.micronaut.expressions.parser.token.TokenType.EXPRESSION_CONTEXT_REF; +import static io.micronaut.expressions.parser.token.TokenType.FLOAT; +import static io.micronaut.expressions.parser.token.TokenType.GT; +import static io.micronaut.expressions.parser.token.TokenType.GTE; +import static io.micronaut.expressions.parser.token.TokenType.IDENTIFIER; +import static io.micronaut.expressions.parser.token.TokenType.INCREMENT; +import static io.micronaut.expressions.parser.token.TokenType.INSTANCEOF; +import static io.micronaut.expressions.parser.token.TokenType.INT; +import static io.micronaut.expressions.parser.token.TokenType.LONG; +import static io.micronaut.expressions.parser.token.TokenType.LT; +import static io.micronaut.expressions.parser.token.TokenType.LTE; +import static io.micronaut.expressions.parser.token.TokenType.L_CURLY; +import static io.micronaut.expressions.parser.token.TokenType.L_PAREN; +import static io.micronaut.expressions.parser.token.TokenType.L_SQUARE; +import static io.micronaut.expressions.parser.token.TokenType.MATCHES; +import static io.micronaut.expressions.parser.token.TokenType.MINUS; +import static io.micronaut.expressions.parser.token.TokenType.MOD; +import static io.micronaut.expressions.parser.token.TokenType.MUL; +import static io.micronaut.expressions.parser.token.TokenType.NE; +import static io.micronaut.expressions.parser.token.TokenType.NOT; +import static io.micronaut.expressions.parser.token.TokenType.NULL; +import static io.micronaut.expressions.parser.token.TokenType.OR; +import static io.micronaut.expressions.parser.token.TokenType.PLUS; +import static io.micronaut.expressions.parser.token.TokenType.POW; +import static io.micronaut.expressions.parser.token.TokenType.QMARK; +import static io.micronaut.expressions.parser.token.TokenType.R_CURLY; +import static io.micronaut.expressions.parser.token.TokenType.R_PAREN; +import static io.micronaut.expressions.parser.token.TokenType.R_SQUARE; +import static io.micronaut.expressions.parser.token.TokenType.STRING; +import static io.micronaut.expressions.parser.token.TokenType.WHITESPACE; + +/** + * Tokenizer for parsing evaluated expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class Tokenizer { + + private static final Map TOKENS = CollectionUtils.mapOf( + // WHITESPACES + "^\\s+", WHITESPACE, + + // BRACES + "^\\{", L_CURLY, + "^}", R_CURLY, + "^\\[", L_SQUARE, + "^]", R_SQUARE, + "^\\(", L_PAREN, + "^\\)", R_PAREN, + + // KEYWORDS + "^instanceof\\b", INSTANCEOF, + "^matches\\b", MATCHES, + + // LITERALS + "^null\\b", NULL, // NULL + "^(true|false)\\b", BOOL, // BOOLEAN + "^'[^']*'", STRING, // STRING + // FLOAT + "^\\d+\\.\\d*((e|E)(\\+|-)?\\d+)?(f|F)", FLOAT, + "^\\.\\d+((e|E)(\\+|-)?\\d+)?(f|F)", FLOAT, + "^\\d+((e|E)(\\+|-)?\\d+)?(f|F)", FLOAT, + // DOUBLE + "^\\d+\\.\\d*((e|E)(\\+|-)?\\d+)?(d|D)?", DOUBLE, + "^\\.\\d+((e|E)(\\+|-)?\\d+)?(d|D)?", DOUBLE, + "^\\d+((e|E)(\\+|-)?\\d+)(d|D)?", DOUBLE, + "^\\d+((e|E)(\\+|-)?\\d+)?(d|D)", DOUBLE, + // LONG + "^0(x|X)[0-9a-fA-F]+(l|L)", LONG, + "^\\d+(l|L)", LONG, + // INT + "^0(x|X)[0-9a-fA-F]+", INT, + "^\\d+", INT, + + // SYMBOLS + "^#", EXPRESSION_CONTEXT_REF, + "^\\?", QMARK, + "^\\.", DOT, + "^,", COMMA, + "^\\:", COLON, + + // RELATIONAL OPERATORS + "^==", EQ, + "^!=", NE, + "^>=", GTE, + "^>", GT, + "^<=", LTE, + "^<", LT, + + // LOGICAL OPERATORS + "^!", NOT, + "^&&", AND, + "^and\\b", AND, + "^\\|\\|", OR, + "^or\\b", OR, + + // MATH OPERATORS + "^\\+\\+", INCREMENT, + "^\\+", PLUS, + "^\\-\\-", DECREMENT, + "^\\-", MINUS, + "^\\*", MUL, + "^/", DIV, + "^div\\b", DIV, + "^%", MOD, + "^mod\\b", MOD, + "^\\^", POW, + + // IDENTIFIERS + "^\\w+", IDENTIFIER + ); + + private static final List PATTERNS = + TOKENS.entrySet() + .stream() + .map(entry -> TokenPattern.of(entry.getKey(), entry.getValue())) + .toList(); + + private final int length; + private final String expression; + + private int cursor; + private String remaining; + + public Tokenizer(String expression) { + this.expression = expression; + this.remaining = expression; + this.cursor = 0; + this.length = expression.length(); + } + + @Nullable + public Token getNextToken() { + if (!hasMoreTokens()) { + return null; + } + + remaining = expression.substring(cursor); + for (TokenPattern pattern: PATTERNS) { + Token token = pattern.matches(remaining); + if (token == null) { + continue; + } + + cursor += token.value().length(); + + if (token.type() == WHITESPACE) { + return getNextToken(); + } + + return token; + } + + throw new ExpressionParsingException("Unexpected token: " + remaining); + } + + private boolean hasMoreTokens() { + return cursor < length; + } + + private record TokenPattern(Pattern pattern, TokenType tokenType) { + public static TokenPattern of(String pattern, TokenType tokenType) { + return new TokenPattern(Pattern.compile(pattern), tokenType); + } + + @Nullable + public Token matches(String value) { + Matcher matcher = pattern.matcher(value); + if (!matcher.find()) { + return null; + } + + return new Token(tokenType, matcher.group()); + } + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java b/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java new file mode 100644 index 00000000000..9d08dcb9314 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java @@ -0,0 +1,83 @@ +/* + * 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.expressions.util; + +import io.micronaut.core.annotation.EvaluatedExpressionReference; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +/** + * Utility class for working with annotation metadata containing + * evaluated expressions. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +public final class EvaluatedExpressionsUtils { + + /** + * Finds evaluated expression references in provided annotation metadata, + * including nested annotation values. + * + * @param annotationMetadata metadata to search references in + * @return collection of expression references + */ + public static Collection findEvaluatedExpressionReferences(AnnotationMetadata annotationMetadata) { + return Stream.concat( + annotationMetadata.getAnnotationNames().stream(), + annotationMetadata.getStereotypeAnnotationNames().stream()) + .map(annotationMetadata::getAnnotation) + .flatMap(annotation -> getNestedAnnotationValues(annotation).stream()) + .flatMap(av -> av.getValues().values().stream()) + .filter(value -> value instanceof EvaluatedExpressionReference) + .map(value -> (EvaluatedExpressionReference) value) + .distinct() + .toList(); + } + + private static Collection> getNestedAnnotationValues(Object value) { + List> result = new ArrayList<>(); + if (value instanceof AnnotationValue annotationValue) { + for (Object nestedValue: annotationValue.getValues().values()) { + result.addAll(getNestedAnnotationValues(nestedValue)); + } + result.add(annotationValue); + } else { + Iterable nestedValues = null; + if (value instanceof Iterable iterable) { + nestedValues = iterable; + } else if (value.getClass().isArray()) { + nestedValues = Arrays.asList(value); + } + + if (nestedValues != null) { + for (Object nextValue: nestedValues) { + if (nextValue instanceof AnnotationValue) { + result.addAll(getNestedAnnotationValues(nextValue)); + } + } + } + } + + return result; + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 52d01147fd0..12c0ac4762e 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -27,6 +27,7 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.annotation.EvaluatedExpressionReference; import io.micronaut.core.annotation.InstantiatedMember; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -56,6 +57,9 @@ import java.util.function.Predicate; import java.util.stream.Stream; +import static io.micronaut.core.expression.EvaluatedExpression.EXPRESSION_PATTERN; +import static io.micronaut.core.annotation.EvaluatedExpressionReference.EXPR_SUFFIX; + /** * An abstract implementation that builds {@link AnnotationMetadata}. * @@ -479,11 +483,12 @@ protected AnnotatedElementValidator getElementValidator() { * * @param originatingElement The originating element * @param member The member + * @param annotationName The annotation name * @param memberName The member name * @param annotationValue The value * @return The object */ - protected abstract Object readAnnotationValue(T originatingElement, T member, String memberName, Object annotationValue); + protected abstract Object readAnnotationValue(T originatingElement, T member, String annotationName, String memberName, Object annotationValue); /** * Read the raw default annotation values from the given annotation. @@ -590,6 +595,46 @@ protected AnnotationValue readNestedAnnotationValue(T annotationElement, A an */ protected abstract Optional getAnnotationMirror(String annotationName); + /** + * Detect evaluated expression in annotation value. + * + * @param value - Annotation value + * @return if value contains evaluated expression + */ + protected boolean isEvaluatedExpression(@Nullable Object value) { + return (value instanceof String str && str.matches(EXPRESSION_PATTERN)) + || (value instanceof String[] strArray && + Arrays.stream(strArray).anyMatch(this::isEvaluatedExpression)); + } + + /** + * Wraps original annotation value to make it processable at later stages. + * + * @param originatingElement originating annotated element + * @param annotationName annotation name + * @param memberName annotation member name + * @param initialAnnotationValue original annotation value + * @return expression reference + */ + @NonNull + protected Object buildEvaluatedExpressionReference(@NonNull T originatingElement, + @NonNull String annotationName, + @NonNull String memberName, + @NonNull Object initialAnnotationValue) { + String originatingClassName = getOriginatingClassName(originatingElement); + + String packageName = NameUtils.getPackageName(originatingClassName); + String simpleClassName = NameUtils.getSimpleName(originatingClassName); + String exprClassName = "%s.$%s%s".formatted(packageName, simpleClassName, EXPR_SUFFIX); + + Integer expressionIndex = EvaluatedExpressionReference.nextIndex(exprClassName); + + return new EvaluatedExpressionReference(initialAnnotationValue, annotationName, memberName, exprClassName + expressionIndex); + } + + @NonNull + protected abstract String getOriginatingClassName(@NonNull T orginatingElement); + /** * Get the annotation member. * @@ -835,7 +880,7 @@ private ProcessedAnnotation createAnnotationValue(T originatingElement, } if (isInstantiatedMember) { final String memberName = getAnnotationMemberName(member); - final Object rawValue = readAnnotationValue(originatingElement, member, memberName, annotationValue); + final Object rawValue = readAnnotationValue(originatingElement, member, annotationName, memberName, annotationValue); if (rawValue instanceof AnnotationClassValue annotationClassValue) { annotationValues.put(memberName, new AnnotationClassValue<>(annotationClassValue.getName(), true)); } diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java index e97af9966e8..72024a4cddf 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java @@ -19,12 +19,14 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataDelegate; import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.EvaluatedExpressionReference; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.UsedByGeneratedCode; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.context.AbstractEvaluatedExpression; import io.micronaut.inject.writer.AbstractAnnotationMetadataWriter; import io.micronaut.inject.writer.AbstractClassFileWriter; import io.micronaut.inject.writer.ClassGenerationException; @@ -64,7 +66,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { private static final Type TYPE_DEFAULT_ANNOTATION_METADATA_HIERARCHY = Type.getType(AnnotationMetadataHierarchy.class); private static final Type TYPE_ANNOTATION_CLASS_VALUE = Type.getType(AnnotationClassValue.class); - private static final org.objectweb.asm.commons.Method METHOD_LIST_OF = org.objectweb.asm.commons.Method.getMethod( + private static final org.objectweb.asm.commons.Method METHOD_LIST_OF = Method.getMethod( ReflectionUtils.getRequiredInternalMethod( AnnotationUtil.class, "internListOf", @@ -72,7 +74,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { ) ); - private static final org.objectweb.asm.commons.Method METHOD_REGISTER_ANNOTATION_DEFAULTS = org.objectweb.asm.commons.Method.getMethod( + private static final org.objectweb.asm.commons.Method METHOD_REGISTER_ANNOTATION_DEFAULTS = Method.getMethod( ReflectionUtils.getRequiredInternalMethod( DefaultAnnotationMetadata.class, "registerAnnotationDefaults", @@ -81,7 +83,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { ) ); - private static final org.objectweb.asm.commons.Method METHOD_REGISTER_ANNOTATION_TYPE = org.objectweb.asm.commons.Method.getMethod( + private static final org.objectweb.asm.commons.Method METHOD_REGISTER_ANNOTATION_TYPE = Method.getMethod( ReflectionUtils.getRequiredInternalMethod( DefaultAnnotationMetadata.class, "registerAnnotationType", @@ -89,7 +91,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { ) ); - private static final org.objectweb.asm.commons.Method METHOD_REGISTER_REPEATABLE_ANNOTATIONS = org.objectweb.asm.commons.Method.getMethod( + private static final org.objectweb.asm.commons.Method METHOD_REGISTER_REPEATABLE_ANNOTATIONS = Method.getMethod( ReflectionUtils.getRequiredInternalMethod( DefaultAnnotationMetadata.class, "registerRepeatableAnnotations", @@ -97,7 +99,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { ) ); - private static final org.objectweb.asm.commons.Method METHOD_GET_DEFAULT_VALUES = org.objectweb.asm.commons.Method.getMethod( + private static final org.objectweb.asm.commons.Method METHOD_GET_DEFAULT_VALUES = Method.getMethod( ReflectionUtils.getRequiredInternalMethod( AnnotationMetadataSupport.class, "getDefaultValues", @@ -105,7 +107,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { ) ); - private static final org.objectweb.asm.commons.Method CONSTRUCTOR_ANNOTATION_METADATA = org.objectweb.asm.commons.Method.getMethod( + private static final org.objectweb.asm.commons.Method CONSTRUCTOR_ANNOTATION_METADATA = Method.getMethod( ReflectionUtils.getRequiredInternalConstructor( DefaultAnnotationMetadata.class, Map.class, @@ -113,6 +115,7 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { Map.class, Map.class, Map.class, + boolean.class, boolean.class ) ); @@ -154,6 +157,12 @@ public class AnnotationMetadataWriter extends AbstractClassFileWriter { ) ); + private static final org.objectweb.asm.commons.Method CONSTRUCTOR_CONTEXT_EVALUATED_EXPRESSION = org.objectweb.asm.commons.Method.getMethod( + ReflectionUtils.getRequiredInternalConstructor( + AbstractEvaluatedExpression.class, + Object.class + )); + private static final Type ANNOTATION_UTIL_TYPE = Type.getType(AnnotationUtil.class); private static final Type LIST_TYPE = Type.getType(List.class); private static final String EMPTY_LIST = "EMPTY_LIST"; @@ -481,6 +490,8 @@ private static void instantiateInternal( pushStringMapOf(generatorAdapter, annotationsByStereotype, false, Collections.emptyList(), list -> pushListOfString(generatorAdapter, list)); // 6th argument: has property expressions generatorAdapter.push(annotationMetadata.hasPropertyExpressions()); + // 7th argument: has evaluated expressions + generatorAdapter.push(annotationMetadata.hasEvaluatedExpressions()); // invoke the constructor generatorAdapter.invokeConstructor(TYPE_DEFAULT_ANNOTATION_METADATA, CONSTRUCTOR_ANNOTATION_METADATA); @@ -669,6 +680,28 @@ private static void pushValue(Type declaringType, ClassVisitor declaringClassWri methodVisitor.loadLocal(defaultIndex); } methodVisitor.invokeConstructor(annotationValueType, CONSTRUCTOR_ANNOTATION_VALUE_AND_MAP); + } else if (value instanceof EvaluatedExpressionReference expressionReference) { + Type type = Type.getType(getTypeDescriptor(expressionReference.expressionClassName())); + + methodVisitor.visitTypeInsn(NEW, type.getInternalName()); + methodVisitor.visitInsn(DUP); + + Object annotationValue = expressionReference.annotationValue(); + if (annotationValue instanceof String str) { + methodVisitor.push(str); + } else if (annotationValue instanceof String[] strings) { + int len = Array.getLength(strings); + pushNewArray(methodVisitor, String.class, len); + for (int i = 0; i < len; i++) { + final Object v = Array.get(strings, i); + pushStoreInArray(methodVisitor, Type.getType(String.class), i, len, + () -> pushValue(declaringType, declaringClassWriter, methodVisitor, v, + defaultsStorage, loadTypeMethods, false)); + } + } + + methodVisitor.invokeConstructor(type, CONSTRUCTOR_CONTEXT_EVALUATED_EXPRESSION); + } else { methodVisitor.visitInsn(ACONST_NULL); } diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java index df3d078dbde..12c1befa472 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/MethodElement.java @@ -227,6 +227,15 @@ default boolean isDefault() { return false; } + /** + * If method has varargs parameter. + * @return True if it does + * @since 4.0.0 + */ + default boolean isVarArgs() { + return false; + } + /** * The generic return type of the method. * diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EvaluatedExpressionContextTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EvaluatedExpressionContextTypeElementVisitor.java new file mode 100644 index 00000000000..1752242c18f --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EvaluatedExpressionContextTypeElementVisitor.java @@ -0,0 +1,82 @@ +/* + * 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.inject.beans.visitor; + +import io.micronaut.context.annotation.EvaluatedExpressionContext; +import io.micronaut.core.annotation.Internal; +import io.micronaut.expressions.context.ExpressionContextLoader; +import io.micronaut.expressions.context.ExpressionWithContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; +import io.micronaut.inject.writer.ClassGenerationException; + +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * A {@link TypeElementVisitor} that visits classes annotated with + * {@link EvaluatedExpressionContext} and produces + * {@link ExpressionWithContext} instances at compilation time. This visitor is also responsible + * for assembling expression evaluation context by adding discovered context classes to + * {@link ExpressionContextLoader} + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class EvaluatedExpressionContextTypeElementVisitor implements TypeElementVisitor { + private final Map writers = new LinkedHashMap<>(10); + + @Override + public void start(VisitorContext visitorContext) { + ExpressionContextLoader.reset(); + } + + @Override + public Set getSupportedAnnotationNames() { + return Collections.singleton(EvaluatedExpressionContext.class.getName()); + } + + @Override + public void visitClass(ClassElement element, VisitorContext context) { + if (!element.isPrivate() && element.hasStereotype(EvaluatedExpressionContext.class)) { + ExpressionContextLoader.addContextClass(element, context); + writers.putIfAbsent(element.getName(), new ExpressionContextReferenceWriter(element)); + } + } + + @Override + public void finish(VisitorContext visitorContext) { + for (ExpressionContextReferenceWriter writer: writers.values()) { + try { + writer.accept(visitorContext); + } catch (IOException e) { + throw new ClassGenerationException("I/O error occurred during class generation: " + e.getMessage(), e); + } + } + + writers.clear(); + } + + @Override + public VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/ExpressionContextReferenceWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/ExpressionContextReferenceWriter.java new file mode 100644 index 00000000000..4d8c9b86967 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/ExpressionContextReferenceWriter.java @@ -0,0 +1,91 @@ +/* + * 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.inject.beans.visitor; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.expressions.context.ExpressionContextReference; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.writer.AbstractClassFileWriter; +import io.micronaut.inject.writer.ClassWriterOutputVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A class file writer that writes a {@link ExpressionContextReference}. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +final class ExpressionContextReferenceWriter extends AbstractClassFileWriter { + private static final String EXPRESSION_CTX_SUFFIX = "$ExprCtxRef"; + + private final String referenceName; + private final Type targetClassType; + private final ClassWriter referenceWriter; + + public ExpressionContextReferenceWriter(ClassElement classElement) { + super(classElement); + final String name = classElement.getName(); + this.referenceWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); + this.referenceName = computeReferenceName(name); + this.targetClassType = getTypeReferenceForName(name); + } + + private String computeReferenceName(String className) { + final String packageName = NameUtils.getPackageName(className); + final String shortName = NameUtils.getSimpleName(className); + return packageName + ".$" + shortName + EXPRESSION_CTX_SUFFIX; + } + + @Override + public void accept(ClassWriterOutputVisitor classWriterOutputVisitor) throws IOException { + Type superType = Type.getType(ExpressionContextReference.class); + classWriterOutputVisitor.visitServiceDescriptor( + ExpressionContextReference.class, + referenceName, + getOriginatingElement()); + + String internalName = getTypeReferenceForName(referenceName).getInternalName(); + try (OutputStream referenceStream = classWriterOutputVisitor.visitClass(referenceName, + getOriginatingElements())) { + startService(referenceWriter, ExpressionContextReference.class, internalName, + superType); + ClassWriter classWriter = generateClassBytes(referenceWriter); + referenceStream.write(classWriter.toByteArray()); + } + } + + private ClassWriter generateClassBytes(ClassWriter classWriter) { + GeneratorAdapter cv = startConstructor(classWriter); + cv.loadThis(); + invokeConstructor(cv, ExpressionContextReference.class); + cv.returnValue(); + cv.visitMaxs(2, 1); + GeneratorAdapter getTypeMethod = startPublicMethodZeroArgs(classWriter, String.class, + "getType"); + getTypeMethod.push(targetClassType.getClassName()); + getTypeMethod.returnValue(); + getTypeMethod.visitMaxs(2, 1); + getTypeMethod.endMethod(); + return classWriter; + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index e1923e42785..d7b5120ba44 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -1666,6 +1666,25 @@ protected GeneratorAdapter startPublicMethod(ClassWriter writer, String methodNa getMethodDescriptor(returnType, argumentTypes)); } + /** + * @param writer The class writer + * @param methodName The method name + * @param returnType The return type + * @param argumentTypes The argument types + * @return The {@link GeneratorAdapter} + */ + protected GeneratorAdapter startProtectedVarargsMethod(ClassWriter writer, String methodName, String returnType, String... argumentTypes) { + return new GeneratorAdapter(writer.visitMethod( + ACC_PROTECTED | ACC_VARARGS, + methodName, + getMethodDescriptor(returnType, argumentTypes), + null, + null + ), ACC_PROTECTED | ACC_VARARGS, + methodName, + getMethodDescriptor(returnType, argumentTypes)); + } + /** * @param writer The class writer * @param asmMethod The asm method diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java index e6534e5b26b..b4ff7b1cac0 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.util.Toggleable; +import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; @@ -35,6 +36,7 @@ import java.io.IOException; import java.util.Map; import java.util.Optional; +import java.util.Set; /** * Interface for {@link BeanDefinitionVisitor} implementations such as {@link BeanDefinitionWriter}. @@ -338,6 +340,13 @@ void visitFieldValue(TypedElement declaringType, */ AnnotationMetadata getAnnotationMetadata(); + /** + * @return The evaluated expressions metadata + * @since 4.0.0 + */ + @NonNull + Set getEvaluatedExpressions(); + /** * Begin defining a configuration builder. * diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 10366b64563..f8525e8fe00 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -41,6 +41,7 @@ import io.micronaut.context.annotation.Value; import io.micronaut.context.env.ConfigurationPath; import io.micronaut.core.annotation.AccessorsStyle; +import io.micronaut.core.annotation.EvaluatedExpressionReference; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.core.annotation.AnnotationUtil; @@ -63,6 +64,10 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.Toggleable; +import io.micronaut.expressions.context.ExpressionEvaluationContext; +import io.micronaut.expressions.context.ExpressionWithContext; +import io.micronaut.expressions.context.ExpressionContextFactory; +import io.micronaut.expressions.util.EvaluatedExpressionsUtils; import io.micronaut.inject.AdvisedBeanType; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.DisposableBeanDefinition; @@ -254,6 +259,12 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea int.class, String.class); + private static final Method GET_EVALUATED_EXPRESSION_VALUE_FOR_METHOD_ARGUMENT = ReflectionUtils.getRequiredInternalMethod( + AbstractInitializableBeanDefinition.class, + "getEvaluatedExpressionValueForMethodArgument", + int.class, + int.class); + private static final Method GET_BEAN_FOR_SETTER = ReflectionUtils.getRequiredInternalMethod( AbstractInitializableBeanDefinition.class, "getBeanForSetter", @@ -309,6 +320,11 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea int.class, String.class); + private static final Method GET_EVALUATED_EXPRESSION_VALUE_FOR_CONSTRUCTOR_ARGUMENT = ReflectionUtils.getRequiredInternalMethod( + AbstractInitializableBeanDefinition.class, + "getEvaluatedExpressionValueForConstructorArgument", + int.class); + private static final Method GET_PROPERTY_VALUE_FOR_FIELD = ReflectionUtils.getRequiredInternalMethod( AbstractInitializableBeanDefinition.class, "getPropertyValueForField", @@ -562,6 +578,9 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private final Map isLifeCycleCache = new HashMap<>(2); private ExecutableMethodsDefinitionWriter executableMethodsDefinitionWriter; + private final Collection evaluatedExpressions = new ArrayList<>(2); + private final ExpressionContextFactory expressionContextFactory; + private Object constructor; // MethodElement or FieldElement private boolean disabled = false; @@ -696,6 +715,8 @@ public BeanDefinitionWriter(Element beanProducingElement, this.isConfigurationProperties = isConfigurationProperties(annotationMetadata); validateExposedTypes(annotationMetadata, visitorContext); this.visitorContext = visitorContext; + this.expressionContextFactory = new ExpressionContextFactory(visitorContext); + processEvaluatedExpressions(this.annotationMetadata); beanTypeInnerClasses = beanTypeElement.getEnclosedElements(ElementQuery.of(ClassElement.class)) .stream() @@ -967,6 +988,11 @@ public void visitBeanDefinitionConstructor(MethodElement constructor, // now implement the inject method visitInjectMethodDefinition(); + + processEvaluatedExpressions(constructor.getAnnotationMetadata()); + for (ParameterElement parameter: constructor.getParameters()) { + processEvaluatedExpressions(parameter.getAnnotationMetadata()); + } } } @@ -1521,6 +1547,7 @@ public void visitMethodInjectionPoint(TypedElement declaringType, boolean requiresReflection, VisitorContext visitorContext) { MethodVisitData methodVisitData = new MethodVisitData(declaringType, methodElement, requiresReflection, methodElement.getAnnotationMetadata()); + processEvaluatedExpressions(methodElement.getAnnotationMetadata()); methodInjectionPoints.add(methodVisitData); allMethodVisits.add(methodVisitData); visitMethodInjectionPointInternal(methodVisitData, injectMethodVisitor, injectInstanceLocalVarIndex); @@ -1569,7 +1596,7 @@ public int visitExecutableMethod(TypedElement declaringType, } if (executableMethodsDefinitionWriter == null) { - executableMethodsDefinitionWriter = new ExecutableMethodsDefinitionWriter(beanDefinitionName, getBeanDefinitionReferenceClassName(), originatingElements); + executableMethodsDefinitionWriter = new ExecutableMethodsDefinitionWriter(visitorContext, beanDefinitionName, getBeanDefinitionReferenceClassName(), originatingElements); } return executableMethodsDefinitionWriter.visitExecutableMethod(declaringType, methodElement, interceptedProxyClassName, interceptedProxyBridgeMethodName); } @@ -1596,6 +1623,16 @@ public AnnotationMetadata getAnnotationMetadata() { return this.annotationMetadata; } + @Override + public Set getEvaluatedExpressions() { + return Stream.concat( + evaluatedExpressions.stream(), + executableMethodsDefinitionWriter != null + ? executableMethodsDefinitionWriter.getEvaluatedExpressions().stream() + : Stream.empty()) + .collect(Collectors.toSet()); + } + @Override public void visitConfigBuilderField( ClassElement type, @@ -2146,6 +2183,7 @@ private void visitFieldInjectionPointInternal( Method methodToInvoke, boolean isArray, boolean requiresGenericType) { + processEvaluatedExpressions(annotationMetadata); autoApplyNamedIfPresent(fieldElement, annotationMetadata); MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, annotationMetadata); @@ -2410,6 +2448,7 @@ private void visitMethodInjectionPointInternal(MethodVisitData methodVisitData, for (ParameterElement value : argumentTypes) { MutableAnnotationMetadata.contributeDefaults(this.annotationMetadata, value.getAnnotationMetadata()); VisitorContextUtils.contributeRepeatable(this.annotationMetadata, value.getGenericType()); + processEvaluatedExpressions(value.getAnnotationMetadata()); if (value.hasDeclaredAnnotation(InjectScope.class)) { hasInjectScope = true; } @@ -2491,9 +2530,13 @@ private void pushMethodParameterValue(GeneratorAdapter injectMethodVisitor, int if (property.isPresent()) { pushInvokeGetPropertyValueForMethod(injectMethodVisitor, i, entry, property.get()); } else { - Optional valueValue = entry.getAnnotationMetadata().stringValue(Value.class); - if (valueValue.isPresent()) { - pushInvokeGetPropertyPlaceholderValueForMethod(injectMethodVisitor, i, entry, valueValue.get()); + if (entry.getAnnotationMetadata().getValue(Value.class, EvaluatedExpressionReference.class).isPresent()) { + pushInvokeGetEvaluatedExpressionValueForMethodArgument(injectMethodVisitor, i, entry); + } else { + Optional valueValue = entry.getAnnotationMetadata().stringValue(Value.class); + if (valueValue.isPresent()) { + pushInvokeGetPropertyPlaceholderValueForMethod(injectMethodVisitor, i, entry, valueValue.get()); + } } } return; @@ -2573,6 +2616,19 @@ private void pushInvokeGetPropertyValueForMethod(GeneratorAdapter injectMethodVi pushCastToType(injectMethodVisitor, entry); } + private void pushInvokeGetEvaluatedExpressionValueForMethodArgument(GeneratorAdapter injectMethodVisitor, int i, ParameterElement entry) { + // load 'this' + injectMethodVisitor.loadThis(); + // 1st argument the method index + injectMethodVisitor.push(currentMethodIndex); + // 2nd argument the argument index + injectMethodVisitor.push(i); + + pushInvokeMethodOnSuperClass(injectMethodVisitor, GET_EVALUATED_EXPRESSION_VALUE_FOR_METHOD_ARGUMENT); + // cast the return value to the correct type + pushCastToType(injectMethodVisitor, entry); + } + private void pushInvokeGetPropertyPlaceholderValueForMethod(GeneratorAdapter injectMethodVisitor, int i, ParameterElement entry, String value) { // load 'this' injectMethodVisitor.loadThis(); @@ -3104,6 +3160,10 @@ private void visitBuildFactoryMethodDefinition( ClassElement factoryClass, Element factoryElement, ParameterElement... parameters) { if (buildMethodVisitor == null) { + processEvaluatedExpressions(factoryElement.getAnnotationMetadata()); + for (ParameterElement parameterElement: parameters) { + processEvaluatedExpressions(parameterElement.getAnnotationMetadata()); + } List parameterList = Arrays.asList(parameters); boolean isParametrized = isParametrized(parameters); @@ -3687,8 +3747,12 @@ private void pushConstructorArgument(GeneratorAdapter buildMethodVisitor, if (property.isPresent()) { pushInvokeGetPropertyValueForConstructor(buildMethodVisitor, index, argumentType, property.get()); } else { - Optional valueValue = argumentType.stringValue(Value.class); - valueValue.ifPresent(s -> pushInvokeGetPropertyPlaceholderValueForConstructor(buildMethodVisitor, index, argumentType, s)); + if (argumentType.getValue(Value.class, EvaluatedExpressionReference.class).isPresent()) { + pushInvokeGetEvaluatedExpressionValueForConstructorArgument(buildMethodVisitor, index, argumentType); + } else { + Optional valueValue = argumentType.stringValue(Value.class); + valueValue.ifPresent(s -> pushInvokeGetPropertyPlaceholderValueForConstructor(buildMethodVisitor, index, argumentType, s)); + } } return; } else { @@ -3787,6 +3851,17 @@ private void pushInvokeGetPropertyPlaceholderValueForConstructor(GeneratorAdapte pushCastToType(injectMethodVisitor, entry); } + private void pushInvokeGetEvaluatedExpressionValueForConstructorArgument(GeneratorAdapter injectMethodVisitor, int i, ParameterElement entry) { + // load 'this' + injectMethodVisitor.loadThis(); + // 2nd argument the argument index + injectMethodVisitor.push(i); + + pushInvokeMethodOnSuperClass(injectMethodVisitor, GET_EVALUATED_EXPRESSION_VALUE_FOR_CONSTRUCTOR_ARGUMENT); + // cast the return value to the correct type + pushCastToType(injectMethodVisitor, entry); + } + private void resolveConstructorArgumentGenericType(GeneratorAdapter visitor, ClassElement type, int argumentIndex) { if (!resolveArgumentGenericType(visitor, type)) { resolveConstructorArgument(visitor, argumentIndex); @@ -4502,6 +4577,22 @@ private void populateBeanTypes(Set processedTypes, Set bea } } + private void processEvaluatedExpressions(AnnotationMetadata annotationMetadata) { + if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + annotationMetadata = annotationMetadata.getDeclaredMetadata(); + } + + Collection expressionReferences = + EvaluatedExpressionsUtils.findEvaluatedExpressionReferences(annotationMetadata); + + expressionReferences.stream() + .map(expressionReference -> { + ExpressionEvaluationContext evaluationContext = expressionContextFactory.buildEvaluationContext(expressionReference); + return new ExpressionWithContext(expressionReference, evaluationContext); + }) + .forEach(evaluatedExpressions::add); + } + @Override public Optional getScope() { return annotationMetadata.getAnnotationNameByStereotype(AnnotationUtil.SCOPE); diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java index 1e03bdfadcf..beed5be9c3a 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java @@ -18,17 +18,24 @@ import io.micronaut.context.AbstractExecutableMethodsDefinition; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; +import io.micronaut.expressions.context.ExpressionContextFactory; +import io.micronaut.expressions.context.ExpressionEvaluationContext; +import io.micronaut.expressions.context.ExpressionWithContext; +import io.micronaut.expressions.util.EvaluatedExpressionsUtils; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.AnnotationMetadataReference; import io.micronaut.inject.annotation.AnnotationMetadataWriter; +import io.micronaut.core.annotation.EvaluatedExpressionReference; import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.processing.JavaModelUtils; +import io.micronaut.inject.visitor.VisitorContext; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.Opcodes; @@ -41,6 +48,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -96,8 +104,11 @@ public class ExecutableMethodsDefinitionWriter extends AbstractClassFileWriter i private final DispatchWriter methodDispatchWriter; private final Set methodNames = new HashSet<>(); + private final ExpressionContextFactory expressionContextFactory; + private final Set evaluatedExpressions = new HashSet<>(); - public ExecutableMethodsDefinitionWriter(String beanDefinitionClassName, + public ExecutableMethodsDefinitionWriter(VisitorContext visitorContext, + String beanDefinitionClassName, String beanDefinitionReferenceClassName, OriginatingElements originatingElements) { super(originatingElements); @@ -106,6 +117,7 @@ public ExecutableMethodsDefinitionWriter(String beanDefinitionClassName, this.thisType = Type.getObjectType(internalName); this.beanDefinitionReferenceClassName = beanDefinitionReferenceClassName; this.methodDispatchWriter = new DispatchWriter(thisType); + this.expressionContextFactory = new ExpressionContextFactory(visitorContext); } /** @@ -122,6 +134,14 @@ public Type getClassType() { return thisType; } + /** + * @return list of evaluated expressions. + */ + @NonNull + public Set getEvaluatedExpressions() { + return evaluatedExpressions; + } + private MethodElement getMethodElement(int index) { return ((DispatchWriter.MethodDispatchTarget) methodDispatchWriter.getDispatchTargets().get(index)).methodElement; } @@ -190,6 +210,7 @@ public int visitExecutableMethod(TypedElement declaringType, MethodElement methodElement, String interceptedProxyClassName, String interceptedProxyBridgeMethodName) { + processEvaluatedExpressions(methodElement); String methodKey = methodElement.getName() + "(" + @@ -483,4 +504,16 @@ private void pushAnnotationMetadata(ClassWriter classWriter, throw new IllegalStateException("Unknown metadata: " + annotationMetadata); } } + + private void processEvaluatedExpressions(MethodElement methodElement) { + Collection expressionReferences = + EvaluatedExpressionsUtils.findEvaluatedExpressionReferences(methodElement.getDeclaredMetadata()); + + expressionReferences.stream() + .map(expression -> { + ExpressionEvaluationContext evaluationContext = expressionContextFactory.buildForMethod(expression, methodElement); + return new ExpressionWithContext(expression, evaluationContext); + }) + .forEach(evaluatedExpressions::add); + } } diff --git a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index 46ee8919429..64070596a7c 100644 --- a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -1,4 +1,5 @@ io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor +io.micronaut.inject.beans.visitor.EvaluatedExpressionContextTypeElementVisitor io.micronaut.context.visitor.BeanImportVisitor io.micronaut.context.visitor.ContextConfigurerVisitor io.micronaut.context.visitor.ExecutableVisitor diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java index ace0c41212c..75817324561 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java @@ -92,6 +92,16 @@ default boolean hasPropertyExpressions() { return true; } + /** + * Does the metadata contain any evaluated expressions like {@code #{ T(java.lang.Math).random() }}. + * + * @return True if evaluated expressions are present + * @since 4.0.0 + */ + default boolean hasEvaluatedExpressions() { + return false; + } + /** * Resolve all of the annotation names that feature the given stereotype. * diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index fc9efb1e69c..bd95e14e316 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -19,6 +19,7 @@ import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.ConvertibleValues; +import io.micronaut.core.expression.EvaluatedExpression; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; @@ -1288,6 +1289,17 @@ public Optional> getAnnotation(@NonNul return Optional.empty(); } + /** + * If this AnnotationValue contains Evaluated Expressions. + * + * @return true if it is + * @since 4.0.0 + */ + public boolean hasEvaluatedExpressions() { + return values.values().stream() + .anyMatch(value -> value instanceof EvaluatedExpression); + } + @Override public String toString() { if (values.isEmpty()) { @@ -1592,7 +1604,7 @@ private Object getRawSingleValue(@NonNull String member, Function members(@Nullable Map mem clazz == String.class || clazz == Enum.class || clazz == AnnotationClassValue.class || - clazz == AnnotationValue.class + clazz == AnnotationValue.class || + clazz == EvaluatedExpressionReference.class || + clazz == EvaluatedExpression.class ); if (!isValid) { throw new IllegalArgumentException("The member named [" + entry.getKey().toString() + "] with type [" + value.getClass().getName() + "] is not a valid member type"); diff --git a/core/src/main/java/io/micronaut/core/annotation/EvaluatedExpressionReference.java b/core/src/main/java/io/micronaut/core/annotation/EvaluatedExpressionReference.java new file mode 100644 index 00000000000..a2a5c6ca3ed --- /dev/null +++ b/core/src/main/java/io/micronaut/core/annotation/EvaluatedExpressionReference.java @@ -0,0 +1,77 @@ +/* + * 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.core.annotation; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Wrapper for annotation value, containing evaluated expressions and + * class name for generated expression class. This class is only used + * at compilation time as part of compile-time annotation metadata. + * + * @param annotationValue initial annotation value which is treated as evaluated expression + * @param annotationName name of the annotation in which evaluated expression is used. + * @param annotationMember annotation member for which evaluated expression is used + * @param expressionClassName name for the class which is generated at compilation time and contains expression evaluation logic + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public record EvaluatedExpressionReference(@NonNull Object annotationValue, + @NonNull String annotationName, + @NonNull String annotationMember, + @NonNull String expressionClassName) { + + public static final String EXPR_SUFFIX = "$Expr"; + + private static final Map CLASS_NAME_INDEXES = new ConcurrentHashMap<>(); + + /** + * Provides next expression index for passed class name. In general indexes are needed only + * to make names of generated expression classes unique and avoid conflicts in cases when + * multiple expressions are defined in the same class. On each invocation with the same + * argument this method will return value incremented by 1. On first invocation it will return 0 + * + * @param className name of class owning evaluated expression + * @return next index + */ + public static Integer nextIndex(String className) { + if (CLASS_NAME_INDEXES.containsKey(className)) { + return CLASS_NAME_INDEXES.merge(className, 1, Integer::sum); + } + + CLASS_NAME_INDEXES.put(className, 0); + return 0; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EvaluatedExpressionReference that = (EvaluatedExpressionReference) o; + return expressionClassName.equals(that.expressionClassName); + } + + @Override + public int hashCode() + { + return Objects.hash(expressionClassName); + } +} diff --git a/core/src/main/java/io/micronaut/core/expression/EvaluatedExpression.java b/core/src/main/java/io/micronaut/core/expression/EvaluatedExpression.java new file mode 100644 index 00000000000..9793833bc2a --- /dev/null +++ b/core/src/main/java/io/micronaut/core/expression/EvaluatedExpression.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017-2020 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.core.expression; + +/** + * Expression included in annotation metadata which can be evaluated at runtime. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +public interface EvaluatedExpression { + + /** + * Evaluated expression prefix. + */ + String EXPRESSION_PREFIX = "#{"; + + /** + * RegEx pattern used to determine whether string value in + * annotation includes evaluated expression. + */ + String EXPRESSION_PATTERN = ".*#\\{.*}.*"; + + /** + * Evaluate expression to obtain evaluation result. + * + * @param args Array of arguments which need to be passed to expression + * for evaluation. Args are used when expression itself is used + * on method and references method arguments + * @return evaluation result + */ + Object evaluate(Object... args); + + /** + * Get original annotation value that was used to generated EvaluatedExpression class. + * + * @return the original expression + */ + Object getInitialAnnotationValue(); +} diff --git a/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java b/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java index 2db9f8254a7..0e402d8d41d 100644 --- a/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java +++ b/http/src/main/java/io/micronaut/http/uri/UriMatchTemplate.java @@ -37,7 +37,7 @@ */ public class UriMatchTemplate extends UriTemplate implements UriMatcher { - protected static final String VARIABLE_MATCH_PATTERN = "([^\\/\\?#&;\\+]"; + protected static final String VARIABLE_MATCH_PATTERN = "([^\\/\\?#(?!\\{)&;\\+]"; protected StringBuilder pattern; protected List variables; private final Pattern matchPattern; diff --git a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy new file mode 100644 index 00000000000..3fc2ecc6c68 --- /dev/null +++ b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy @@ -0,0 +1,123 @@ +/* + * Copyright 2017-2020 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.ast.transform.test + +import io.micronaut.context.AbstractEvaluatedExpression +import io.micronaut.core.expression.EvaluatedExpression +import io.micronaut.core.naming.NameUtils +import io.micronaut.core.annotation.EvaluatedExpressionReference +import org.intellij.lang.annotations.Language + +class AbstractEvaluatedExpressionsSpec extends AbstractBeanDefinitionSpec { + + List evaluateMultiple(String... expressions) { + + String classContent = "" + for (int i = 0; i < expressions.size(); i++) { + classContent += """ + + @Value("${expressions[i]}") + Object field${i} + + """ + } + + def cls = """ + package test + import io.micronaut.context.annotation.Value + import io.micronaut.context.annotation.EvaluatedExpressionContext + + class Expr { + ${classContent} + } + """.stripIndent().stripLeading() + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + def exprClassName = 'test.$Expr$Expr' + def startingIndex = EvaluatedExpressionReference.nextIndex(exprClassName) - expressions.length + + List result = new ArrayList<>() + for (int i = startingIndex; i < startingIndex + expressions.size(); i++) { + String exprFullName = exprClassName + i + try { + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName).newInstance() + exprClass.configure(applicationContext) + result.add(exprClass.evaluate()) + } catch (ClassNotFoundException e) { + return null + } + } + + return result + } + + Object evaluate(String expression) { + return evaluateAgainstContext(expression, "") + } + + Object evaluateAgainstContext(String expression, @Language("groovy") String contextClass) { + String exprClassName = 'test.$Expr$Expr'; + + def cls = """ + package test + import io.micronaut.context.annotation.Value + import io.micronaut.context.annotation.EvaluatedExpressionContext + + ${contextClass} + + class Expr { + @Value("${expression}") + Object field + } + """.stripIndent().stripLeading() + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + try { + def index = EvaluatedExpressionReference.nextIndex(exprClassName) + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprClassName + (index == 0 ? index : index - 1)).newInstance() + exprClass.configure(applicationContext) + return exprClass.evaluate(); + } catch (ClassNotFoundException e) { + return null + } + } + + EvaluatedExpression buildSingleExpressionFromClass(String className, + @Language("groovy") String cls) { + + def classSimpleName = NameUtils.getSimpleName(className) + def packageName = NameUtils.getPackageName(className) + def exprClassName = (classSimpleName.startsWith('$') ? '' : '$') + classSimpleName + '$Expr' + + String exprFullName = "${packageName}.${exprClassName}" + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + try { + def index = EvaluatedExpressionReference.nextIndex(exprFullName) + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName + (index == 0 ? index : index - 1)).newInstance() + exprClass.configure(applicationContext) + return exprClass + } catch (ClassNotFoundException e) { + return null + } + } +} diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy index c81b429b294..0b1bf7adfb4 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy @@ -25,6 +25,9 @@ import io.micronaut.ast.groovy.visitor.GroovyPackageElement import io.micronaut.ast.groovy.visitor.GroovyVisitorContext import io.micronaut.context.annotation.Configuration import io.micronaut.context.annotation.Context +import io.micronaut.expressions.context.ExpressionContextLoader +import io.micronaut.expressions.context.ExpressionWithContext +import io.micronaut.inject.processing.ProcessingException import io.micronaut.inject.processing.BeanDefinitionCreator import io.micronaut.inject.processing.BeanDefinitionCreatorFactory import io.micronaut.inject.processing.ProcessingException @@ -39,6 +42,7 @@ import org.codehaus.groovy.ast.ClassNode import org.codehaus.groovy.ast.InnerClassNode import org.codehaus.groovy.ast.ModuleNode import org.codehaus.groovy.ast.PackageNode +import io.micronaut.expressions.EvaluatedExpressionWriter import org.codehaus.groovy.control.CompilationUnit import org.codehaus.groovy.control.CompilePhase import org.codehaus.groovy.control.SourceUnit @@ -73,6 +77,7 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { } else { outputVisitor = new DirectoryClassWriterOutputVisitor(classesDir) } + List classes = moduleNode.getClasses() if (classes.size() == 1) { ClassNode classNode = classes[0] @@ -125,6 +130,11 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { String beanTypeName = beanDefWriter.beanTypeName AnnotatedNode beanClassNode = entry.key try { + for (ExpressionWithContext expression: beanDefWriter.evaluatedExpressions) { + new EvaluatedExpressionWriter(expression, new GroovyVisitorContext(source, unit), beanDefWriter.originatingElement) + .accept(outputVisitor); + } + BeanDefinitionReferenceWriter beanReferenceWriter = new BeanDefinitionReferenceWriter(beanDefWriter) beanReferenceWriter.setRequiresMethodProcessing(beanDefWriter.requiresMethodProcessing()) beanReferenceWriter.setContextScope(beanDefWriter.getAnnotationMetadata().hasDeclaredAnnotation(Context)) diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index 796c3cb99b0..146a60a4c07 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -24,6 +24,7 @@ import io.micronaut.ast.groovy.visitor.GroovyVisitorContext; import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.EvaluatedExpressionReference; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.convert.ConversionService; @@ -149,6 +150,20 @@ protected AnnotatedNode getAnnotationMember(AnnotatedNode annotationElement, Cha return null; } + @Override + protected String getOriginatingClassName(AnnotatedNode originatingElement) + { + if (originatingElement instanceof ClassNode classNode) { + return classNode.getName(); + } else if (originatingElement instanceof ExtendedParameter extendedParameter) { + return extendedParameter.getMethodNode().getDeclaringClass().getName(); + } else if (originatingElement instanceof MethodNode methodNode) { + return methodNode.getDeclaringClass().getName(); + } + + return originatingElement.getDeclaringClass().getName(); + } + @Override protected RetentionPolicy getRetentionPolicy(@NonNull AnnotatedNode annotation) { List annotations = annotation.getAnnotations(); @@ -353,7 +368,7 @@ protected void readAnnotationRawValues( Object annotationValue, Map annotationValues) { if (!annotationValues.containsKey(memberName)) { - final Object v = readAnnotationValue(originatingElement, member, memberName, annotationValue); + Object v = readAnnotationValue(originatingElement, member, annotationName, memberName, annotationValue); if (v != null) { validateAnnotationValue(originatingElement, annotationName, member, memberName, v); annotationValues.put(memberName, v); @@ -423,9 +438,9 @@ protected boolean hasSimpleAnnotation(AnnotatedNode element, String simpleName) } @Override - protected Object readAnnotationValue(AnnotatedNode originatingElement, AnnotatedNode member, String memberName, Object annotationValue) { + protected Object readAnnotationValue(AnnotatedNode originatingElement, AnnotatedNode member, String annotationName, String memberName, Object annotationValue) { if (annotationValue instanceof ConstantExpression constantExpression) { - return readConstantExpression(originatingElement, member, constantExpression); + return readConstantExpression(originatingElement, annotationName, member, constantExpression); } else if (annotationValue instanceof PropertyExpression pe) { if (pe.getObjectExpression() instanceof ClassExpression classExpression) { ClassNode propertyType = classExpression.getType(); @@ -454,15 +469,21 @@ protected Object readAnnotationValue(AnnotatedNode originatingElement, Annotated Expression valueExpression = propertyExpression.getProperty(); Expression objectExpression = propertyExpression.getObjectExpression(); if (valueExpression instanceof ConstantExpression constantExpression && objectExpression instanceof ClassExpression) { - Object value = readConstantExpression(originatingElement, member, constantExpression); + Object value = readConstantExpression(originatingElement, annotationName, member, constantExpression); if (value != null) { converted.add(value); } } } if (exp instanceof ConstantExpression constantExpression) { - Object value = readConstantExpression(originatingElement, member, constantExpression); + Object value = readConstantExpression(originatingElement, annotationName, member, constantExpression); if (value != null) { + // if value is an expression reference, since we're iterating through a list, + // we extract initial annotation value to wrap it into a single expression reference + // after the iteration is complete + if (value instanceof EvaluatedExpressionReference expressionReference) { + value = expressionReference.annotationValue(); + } converted.add(value); } } else if (exp instanceof ClassExpression classExpression) { @@ -475,11 +496,15 @@ protected Object readAnnotationValue(AnnotatedNode originatingElement, Annotated converted.add(new AnnotationClassValue<>(typeName)); } } - return toArray(member, converted); + Object array = toArray(member, converted); + if (isEvaluatedExpression(array)) { + return buildEvaluatedExpressionReference(originatingElement, annotationName, memberName, array); + } + return array; } else if (annotationValue instanceof VariableExpression variableExpression) { Variable variable = variableExpression.getAccessedVariable(); if (variable != null && variable.hasInitialExpression()) { - return readAnnotationValue(originatingElement, member, memberName, variable.getInitialExpression()); + return readAnnotationValue(originatingElement, member, annotationName, memberName, variable.getInitialExpression()); } } else if (annotationValue != null) { if (ClassUtils.isJavaLangType(annotationValue.getClass())) { @@ -514,6 +539,8 @@ private static Object toArray(AnnotatedNode member, Collection collection) { arrayType = AnnotationValue.class; } else if (Class.class.isAssignableFrom(arrayType)) { arrayType = AnnotationClassValue.class; + } else if (EvaluatedExpressionReference.class.isAssignableFrom(arrayType)) { + arrayType = EvaluatedExpressionReference.class; } } } @@ -524,6 +551,8 @@ private static Object toArray(AnnotatedNode member, Collection collection) { arrayType = AnnotationClassValue.class; } else if (collection.stream().allMatch(val -> val instanceof AnnotationValue)) { arrayType = AnnotationValue.class; + } else if (collection.stream().anyMatch(val -> val instanceof EvaluatedExpressionReference)) { + arrayType = Object.class; } if (arrayType.isPrimitive()) { Class wrapperType = ReflectionUtils.getWrapperType(arrayType); @@ -537,7 +566,7 @@ private static Object toArray(AnnotatedNode member, Collection collection) { .orElse(null); } - private Object readConstantExpression(AnnotatedNode originatingElement, AnnotatedNode member, ConstantExpression constantExpression) { + private Object readConstantExpression(AnnotatedNode originatingElement, String annotationName, AnnotatedNode member, ConstantExpression constantExpression) { if (constantExpression instanceof AnnotationConstantExpression ann) { AnnotationNode value = (AnnotationNode) ann.getValue(); return readNestedAnnotationValue(originatingElement, value); @@ -546,9 +575,17 @@ private Object readConstantExpression(AnnotatedNode originatingElement, Annotate if (value == null) { return null; } + if (isEvaluatedExpression(value)) { + String memberName = getAnnotationMemberName(member); + return buildEvaluatedExpressionReference(originatingElement, annotationName, memberName, value); + } if (value instanceof Collection collection) { collection = collection.stream().map(this::convertConstantValue).toList(); - return toArray(member, collection); + Object array = toArray(member, collection); + if (isEvaluatedExpression(array)) { + return buildEvaluatedExpressionReference(originatingElement, annotationName, getAnnotationMemberName(member), array); + } + return array; } return convertConstantValue(value); } diff --git a/inject-groovy/src/main/resources/META-INF/services/org.codehaus.groovy.transform.ASTTransformation b/inject-groovy/src/main/resources/META-INF/services/org.codehaus.groovy.transform.ASTTransformation index 927e76a4c0a..9c6e6b6f9eb 100644 --- a/inject-groovy/src/main/resources/META-INF/services/org.codehaus.groovy.transform.ASTTransformation +++ b/inject-groovy/src/main/resources/META-INF/services/org.codehaus.groovy.transform.ASTTransformation @@ -1,4 +1,4 @@ io.micronaut.ast.groovy.InjectTransform io.micronaut.ast.groovy.TypeElementVisitorTransform io.micronaut.ast.groovy.TypeElementVisitorStart -io.micronaut.ast.groovy.TypeElementVisitorEnd \ No newline at end of file +io.micronaut.ast.groovy.TypeElementVisitorEnd diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy new file mode 100644 index 00000000000..e36ab8d45dc --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy @@ -0,0 +1,73 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + + +class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test annotation level context"() { + given: + Object expr = buildSingleExpressionFromClass("test.Expr", """ + package test + import io.micronaut.context.annotation.EvaluatedExpressionContext;import io.micronaut.context.annotation.Executable; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton + + @Singleton + @CustomAnnotation("#{ #getAnnotationLevelValue() }") + class Expr { + } + + @Singleton + class CustomContext { + String getAnnotationLevelValue() { + return "annotationLevelValue"; + } + } + + @EvaluatedExpressionContext(CustomContext.class) + @interface CustomAnnotation { + String value(); + } + + """).evaluate() + + expect: + expr instanceof String && expr == "annotationLevelValue" + + } + + void "test annotation member level context"() { + given: + Object expr = buildSingleExpressionFromClass("test.Expr", """ + package test + import io.micronaut.context.annotation.EvaluatedExpressionContext + import io.micronaut.context.annotation.Executable + import io.micronaut.context.annotation.Requires + import jakarta.inject.Singleton + + @Singleton + @CustomAnnotation(customValue = "#{ #getAnnotationLevelValue() }") + class Expr { + } + + @Singleton + class CustomContext { + String getAnnotationLevelValue() { + return "annotationLevelValue"; + } + } + + @interface CustomAnnotation { + @EvaluatedExpressionContext(CustomContext.class) + String customValue(); + } + + """).evaluate() + + expect: + expr instanceof String && expr == "annotationLevelValue" + + } + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy new file mode 100644 index 00000000000..57bf945ee2d --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy @@ -0,0 +1,220 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + + +class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test primitive and wrapper varargs methods"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countValues(1, 2, 3) }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(int... array) { + return array.length + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #countValues(1) }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(int... array) { + return array.length + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #countValues() }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(int... array) { + return array.length + } + } + """) + + Object expr4 = evaluateAgainstContext("#{ #countValues(1, 2, T(Integer).valueOf('3')) }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(Integer... array) { + return array.length + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 3 + expr2 instanceof Integer && expr2 == 1 + expr3 instanceof Integer && expr3 == 0 + expr4 instanceof Integer && expr4 == 3 + } + + void "test string varargs methods"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countValues('a', 'b', 'c') }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(String... values) { + return values.length + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #countValues('a') }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(String... values) { + return values.length + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #countValues() }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(String... values) { + return values.length + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 3 + expr2 instanceof Integer && expr2 == 1 + expr3 instanceof Integer && expr3 == 0 + } + + void "test mixed types varargs methods"() { + given: + Object expr1 = evaluateAgainstContext("#{ #multiplyLength(3, '1', 8, null) }", + """ + @EvaluatedExpressionContext + class Context { + int multiplyLength(int time, Object... values) { + return values.length * time + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 9 + } + + void "test arrays as varargs"() { + given: + Object expr1 = evaluateAgainstContext("#{ #multiplyLength(3, '1', 8, null) }", + """ + @EvaluatedExpressionContext + class Context { + int multiplyLength(Integer time, Object[] values) { + return values.length * time + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 9 + } + + void "test non-varargs arrays"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countLength(#values()) }", + """ + @EvaluatedExpressionContext + class Context { + String[] values() { + return [ "a", "b", "c" ] + } + + int countLength(Object[] array) { + return array.length + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #multiplyLength(#values(), 3) }", + """ + @EvaluatedExpressionContext + class Context { + String[] values() { + return ["a", "b"] + } + + int multiplyLength(String[] array, int times) { + return array.length * times + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #multiplyLength(#values(), 1) }", + """ + @EvaluatedExpressionContext + class Context { + int[] values() { + return new int[]{1, 2} + } + + int multiplyLength(int[] array, int times) { + return array.length * times + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 3 + expr2 instanceof Integer && expr2 == 6 + expr3 instanceof Integer && expr3 == 2 + } + + void "test multi-dimensional arrays"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countLength(#values()) }", + """ + import java.util.Arrays + + @EvaluatedExpressionContext + class Context { + String[][] values() { + return [ ["a", "b", "c"], ["a", "b"] ] + } + + int countLength(Object[][] array) { + return Arrays.stream(array) + .map(a -> a.length) + .reduce(0, Integer::sum) + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #countLength(#values()) }", + """ + import java.util.Arrays + + @EvaluatedExpressionContext + class Context { + int[][] values() { + return [[1, 2, 3], [1, 2, 3, 3]] + } + + int countLength(int[][] array) { + return Arrays.stream(array) + .map(a -> a.length) + .reduce(0, Integer::sum) + } + } + """) + + + expect: + expr1 instanceof Integer && expr1 == 5 + expr2 instanceof Integer && expr2 == 7 + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy new file mode 100644 index 00000000000..b8e4f45e7a3 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy @@ -0,0 +1,58 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + +class CompoundExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test compound expressions"() { + given: + List results = evaluateMultiple( + "a #{1}#{'b'} #{3}", + "#{1 + 2}#{2 + 3}", + "#{ '5' } s", + "a#{ null }b" + ) + + expect: + results[0] instanceof String && results[0] == 'a 1b 3' + results[1] instanceof String && results[1] == '35' + results[2] instanceof String && results[2] == '5 s' + results[3] instanceof String && results[3] == 'anullb' + } + + void "test string expressions in arrays"() { + Object expr1 = buildSingleExpressionFromClass("test.Expr", """ + package test + import io.micronaut.context.annotation.Requires + import jakarta.inject.Singleton + + @Singleton + @Requires(env = ["#{ 'a' }", "b", "#{ 'c' + 'd' }"]) + class Expr { + } + """) + .evaluate() + + expect: + expr1 instanceof Object[] && Arrays.equals((Object[]) expr1, new Object[]{'a', 'b', 'cd'}) + } + + + void "test mixed expressions in arrays"() { + Object expr1 = buildSingleExpressionFromClass("test.Expr", """ + package test; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @Requires(env = ["#{ 1 }", "b", "#{ 15l }", "#{ 'c' }"]) + class Expr { + } + """) + .evaluate() + + expect: + expr1 instanceof Object[] && Arrays.equals((Object[]) expr1, new Object[]{1, 'b', 15l, 'c'}) + } + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy new file mode 100644 index 00000000000..d03bafbb399 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy @@ -0,0 +1,142 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + +class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec{ + + void "test context method calls"() { + given: + Object expr1 = evaluateAgainstContext("#{ #getIntValue() }", + """ + @EvaluatedExpressionContext + class ExpressionContext { + public int getIntValue() { + return 15 + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #getStringValue().toUpperCase() }", + """ + @EvaluatedExpressionContext + class ExpressionContext { + String getStringValue() { + return "test" + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #randomizer().nextInt(10) }", + """ + import java.util.Random + + @EvaluatedExpressionContext + class ExpressionContext { + Random randomizer() { + return new Random() + } + } + """) + + Object expr4 = evaluateAgainstContext("#{ #lowercase('TEST') }", + """ + import java.util.Random + + @EvaluatedExpressionContext + class ExpressionContext { + String lowercase(String value) { + return value.toLowerCase() + } + } + """) + + Object expr5 = evaluateAgainstContext("#{ #transform(#getName(), #getRepeat(), #toLower()) }", + """ + import java.util.Random + + @EvaluatedExpressionContext + class FirstContext { + String transform(String value, int repeat, Boolean toLower) { + return (toLower ? value.toLowerCase() : value).repeat(repeat) + } + } + + @EvaluatedExpressionContext + class SecondContext { + String getName() { + return "TEST" + } + } + + @EvaluatedExpressionContext + class ThirdContext { + Integer getRepeat() { + return 2 + } + } + + @EvaluatedExpressionContext + class FourthContext { + boolean toLower() { + return true + } + } + """) + + Object expr6 = evaluateAgainstContext("#{ #getTestObject().name }", + """ + import java.util.Random + + @EvaluatedExpressionContext + class ExpressionContext { + TestObject getTestObject() { + return new TestObject() + } + } + + class TestObject { + String getName() { + return "name" + } + } + """) + + Object expr7 = evaluateAgainstContext("#{ #values().get(#random(#values())) }", + """ + import java.util.Random + import java.util.List + import java.util.Collection + import java.util.concurrent.ThreadLocalRandom + + @EvaluatedExpressionContext + class ExpressionContext { + TestObject getTestObject() { + return new TestObject(); + } + + List values() { + return List.of(1, 2, 3); + } + + int random(Collection values) { + return ThreadLocalRandom.current().nextInt(values.size()); + } + } + + class TestObject { + String getName() { + return "name"; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 15 + expr2 instanceof String && expr2 == "TEST" + expr3 instanceof Integer && expr3 >= 0 && expr3 < 10 + expr4 instanceof String && expr4 == "test" + expr5 instanceof String && expr5 == "testtest" + expr6 instanceof String && expr6 == "name" + expr7 instanceof Integer && (expr7 == 1 || expr7 == 2 || expr7 == 3) + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy new file mode 100644 index 00000000000..9fea56075b8 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -0,0 +1,59 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec; + +class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsSpec +{ + void "test context property access"() { + given: + Object expr1 = evaluateAgainstContext("#{ #intValue }", + """ + @EvaluatedExpressionContext + class ExpressionContext { + int getIntValue() { + return 15 + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #boolean }", + """ + @EvaluatedExpressionContext + class ExpressionContext { + Boolean isBoolean() { + return false + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #stringValue }", + """ + @EvaluatedExpressionContext + class ExpressionContext { + String getStringValue() { + return "test value" + } + } + """) + + Object expr4 = evaluateAgainstContext("#{ #customClass.customProperty }", + """ + @EvaluatedExpressionContext + class ExpressionContext { + CustomClass getCustomClass() { + return new CustomClass() + } + } + + class CustomClass { + String customProperty = "custom property" + } + """) + + expect: + expr1 instanceof Integer && expr1 == 15 + expr2 instanceof Boolean && expr2 == false + expr3 instanceof String && expr3 == "test value" + expr4 instanceof String && expr4 == "custom property" + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/LiteralExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/LiteralExpressionsSpec.groovy new file mode 100644 index 00000000000..411c6483a22 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/LiteralExpressionsSpec.groovy @@ -0,0 +1,96 @@ +package io.micronaut.expressions + + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + +class LiteralExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test literals"() { + given: + List results = evaluateMultiple( + // null + "#{ null }", // 0 + + // string literals + "#{ 'string literal' }", // 1 + "#{ 'testValue' }", // 2 + + // bool literals + "#{ false }", // 3 + "#{ true }", // 4 + + // int literals + "#{ 15 }", // 5 + "#{ 0XAB013 }", // 6 + "#{ 0xFF }", // 7 + "#{ 291231 }", // 8 + "#{ 0 }", // 9 + "#{ 0x0 }", // 10 + "#{ 00 }", // 11 + + // long literals + "#{ 0xFFL }", // 12 + "#{ 0x0123l }", // 13 + "#{ 102L }", // 14 + "#{ 99l }", // 15 + "#{ 0L }", // 16 + + // float literals + "#{ 123.e+14f }", // 17 + "#{ 123.f }", // 18 + "#{ 123.F }", // 19 + "#{ .123f }", // 20 + "#{ 19F }", // 21 + + // double literals + "#{ 123. }", // 22 + "#{ 123.321 }", // 23 + "#{ 123.d }", // 24 + "#{ 123.D }", // 25 + "#{ .123 }", // 26 + "#{ 123D }", // 27 + "#{ 1E-7 }", // 28 + "#{ 1E+1d }", // 29 + "#{ 2e-1 }", // 30 + ) + + expect: + results[0] == null + + results[1] instanceof String && results[1] == 'string literal' + results[2] instanceof String && results[2] == 'testValue' + + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + + results[5] instanceof Integer && results[5] == 15 + results[6] instanceof Integer && results[6] == 0XAB013 + results[7] instanceof Integer && results[7] == 0xFF + results[8] instanceof Integer && results[8] == 291231 + results[9] instanceof Integer && results[9] == 0 + results[10] instanceof Integer && results[10] == 0x0 + results[11] instanceof Integer && results[11] == 00 + + results[12] instanceof Long && results[12] == 0xFFL + results[13] instanceof Long && results[13] == 0x0123l + results[14] instanceof Long && results[14] == 102L + results[15] instanceof Long && results[15] == 99L + results[16] instanceof Long && results[16] == 0L + + results[17] instanceof Float + results[18] instanceof Float + results[19] instanceof Float + results[20] instanceof Float && results[20] == .123f + results[21] instanceof Float && results[21] == 19F + + results[22] instanceof Double && results[22] == Double.valueOf("123.") + results[23] instanceof Double && results[23] == 123.321 + results[24] instanceof Double && results[24] == Double.valueOf("123.d") + results[25] instanceof Double && results[25] == Double.valueOf("123.D") + results[26] instanceof Double && results[26] == .123 + results[27] instanceof Double && results[27] == 123D + results[28] instanceof Double && results[28] == 1E-7 + results[29] instanceof Double && results[29] == 1E+1d + results[30] instanceof Double && results[30] == 2e-1 + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy new file mode 100644 index 00000000000..d25d5d1b5ab --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy @@ -0,0 +1,31 @@ +package io.micronaut.expressions; + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + +class MethodArgumentEvaluationContextExpressionsSpec extends AbstractEvaluatedExpressionsSpec +{ + void "test method argument access"() { + given: + Object expr1 = buildSingleExpressionFromClass("test.Expr", """ + package test + import io.micronaut.context.annotation.Executable + import io.micronaut.context.annotation.Requires + import jakarta.inject.Singleton + + @Singleton + class Expr { + + @Executable + @Requires(value = "#{ #second + \'abc\' }") + void test(String first, String second) { + } + } + + + """) + .evaluate("arg0", "arg1") + + expect: + expr1 instanceof String && expr1 == 'arg1abc' + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/OperatorExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/OperatorExpressionsSpec.groovy new file mode 100644 index 00000000000..f97f3248d40 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/OperatorExpressionsSpec.groovy @@ -0,0 +1,905 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec +import org.codehaus.groovy.control.CompilationFailedException + +class OperatorExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test '/' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 / 5 }", + "#{ 5 / 10 }", + "#{ 10 div 5 }", + "#{ 5 div 10 }", + "#{ 10 / 5 / 2 }", + + // long + "#{ 10 / 5L }", + "#{ 5l / 10 }", + "#{ 10L div 5l }", + "#{ 5 div 10L }", + "#{ 10L div 5 / 2 }", + + // float + "#{ 10f / 5 }", + "#{ 5 / 10f }", + "#{ 10f div 5F }", + "#{ 5 div 10F }", + + // double + "#{ 10d / 5 }", + "#{ 5 / 10d }", + "#{ 10d div 5D }", + "#{ 5 div 10D }", + + // mixed + "#{ 10d / 5f }", + "#{ 5L / 10d }", + "#{ 10L div 5f }" + ) + + expect: + results[0] instanceof Integer && results[0] == 2 + results[1] instanceof Integer && results[1] == 0 + results[2] instanceof Integer && results[2] == 2 + results[3] instanceof Integer && results[3] == 0 + results[4] instanceof Integer && results[4] == 1 + + results[5] instanceof Long && results[5] == 2 + results[6] instanceof Long && results[6] == 0 + results[7] instanceof Long && results[7] == 2 + results[8] instanceof Long && results[8] == 0 + results[9] instanceof Long && results[9] == 1 + + results[10] instanceof Float && results[10] == 10f / 5 + results[11] instanceof Float && results[11] == 5 / 10f + results[12] instanceof Float && results[12] == 10f / 5F + results[13] instanceof Float && results[13] == 5 / 10F + + results[14] instanceof Double && results[14] == 10d / 5 + results[15] instanceof Double && results[15] == 5 / 10d + results[16] instanceof Double && results[16] == 10d / 5D + results[17] instanceof Double && results[17] == 5 / 10D + + results[18] instanceof Double && results[18] == 10d / 5f + results[19] instanceof Double && results[19] == 5L / 10d + results[20] instanceof Float && results[20] == 10L / 5f + } + + void "test '%' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 % 5 }", // 0 + "#{ 5 % 10 }", // 1 + "#{ 10 mod 5 }", // 2 + "#{ 5 mod 10 }", // 3 + "#{ 10 % 5 % 3}", // 4 + + // long + "#{ 10 % 5L }", // 5 + "#{ 5l % 10 }", // 6 + "#{ 10L mod 5l }", // 7 + "#{ 5 mod 10L }", // 8 + "#{ 13L % 5 mod 2 }", // 9 + + // float + "#{ 10f % 5 }", // 10 + "#{ 5 % 10f }", // 11 + "#{ 10f mod 5F }", // 12 + "#{ 5 mod 10F }", // 13 + + // double + "#{ 10d % 5 }", // 14 + "#{ 5 % 10d }", // 15 + "#{ 10d mod 5D }", // 16 + "#{ 5 mod 10D }", // 17 + + // mixed + "#{ 10d % 5f }", // 18 + "#{ 5L % 10d }", // 19 + "#{ 10L mod 5f }" // 20 + ) + + + expect: + results[0] instanceof Integer && results[0] == 0 + results[1] instanceof Integer && results[1] == 5 + results[2] instanceof Integer && results[2] == 0 + results[3] instanceof Integer && results[3] == 5 + results[4] instanceof Integer && results[4] == 0 + + results[5] instanceof Long && results[5] == 0 + results[6] instanceof Long && results[6] == 5 + results[7] instanceof Long && results[7] == 0 + results[8] instanceof Long && results[8] == 5 + results[9] instanceof Long && results[9] == 1 + + results[10] instanceof Float && results[10] == 10f % 5 + results[11] instanceof Float && results[11] == 5 % 10f + results[12] instanceof Float && results[12] == 10f % 5F + results[13] instanceof Float && results[13] == 5 % 10F + + results[14] instanceof Double && results[14] == 10d % 5 + results[15] instanceof Double && results[15] == 5 % 10d + results[16] instanceof Double && results[16] == 10d % 5D + results[17] instanceof Double && results[17] == 5 % 10D + + results[18] instanceof Double && results[18] == 10d % 5f + results[19] instanceof Double && results[19] == 5L % 10d + results[20] instanceof Float && results[20] == 10L % 5f + } + + void "test -' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 - 5 }", // 0 + "#{ 5 - 10 }", // 1 + "#{ 25 - 5 - 10 }", // 2 + + // long + "#{ 10 - 5L }", // 3 + "#{ 5l - 10 }", // 4 + "#{ 10L - 5l }", // 5 + "#{ 5 - 10L }", // 6 + + + // float + "#{ 10f - 5 }", // 7 + "#{ 5 - 10f }", // 8 + "#{ 10f - 5F }", // 9 + "#{ 5 - 10F }", // 10 + + // double + "#{ 10d - 5 }", // 11 + "#{ 5 - 10d }", // 12 + "#{ 10d - 5D }", // 13 + "#{ 5 - 10D }", // 14 + + // mixed + "#{ 10d - 5f }", // 15 + "#{ 5L - 10d }", // 16 + "#{ 10L - 5f }" // 17 + ) + + expect: + results[0] instanceof Integer && results[0] == 5 + results[1] instanceof Integer && results[1] == -5 + results[2] instanceof Integer && results[2] == 10 + + results[3] instanceof Long && results[3] == 10 - 5L + results[4] instanceof Long && results[4] == 5l - 10 + results[5] instanceof Long && results[5] == 10L - 5l + results[6] instanceof Long && results[6] == 5 - 10L + + results[7] instanceof Float && results[7] == 10f - 5 + results[8] instanceof Float && results[8] == 5 - 10f + results[9] instanceof Float && results[9] == 10f - 5F + results[10] instanceof Float && results[10] == 5 - 10F + + results[11] instanceof Double && results[11] == 10d - 5 + results[12] instanceof Double && results[12] == 5 - 10d + results[13] instanceof Double && results[13] == 10d - 5D + results[14] instanceof Double && results[14] == 5 - 10D + + results[15] instanceof Double && results[15] == 10d - 5f + results[16] instanceof Double && results[16] == 5L - 10d + results[17] instanceof Float && results[17] == 10L - 5f + } + + void "test '*' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 * 5 }", // 0 + "#{ -8 * 10 * 5 }", // 1 + + // long + "#{ 10 * 5L }", // 2 + "#{ 5l * 10 }", // 3 + "#{ 10L * 5l }", // 4 + "#{ 5 * 10L }", // 5 + + // float + "#{ 10f * 5 }", // 6 + "#{ 5 * 10f }", // 7 + "#{ 10f * 5F }", // 8 + "#{ 5 * 10F }", // 9 + + // double + "#{ 10d * 5 }", // 10 + "#{ 5 * 10d }", // 11 + "#{ 10d * 5D }", // 12 + "#{ 5 * 10D }", // 13 + + // mixed + "#{ 10d * 5f }", // 14 + "#{ 5L * 10d }", // 15 + "#{ 10L * 5f }" // 16 + ) + + expect: + results[0] instanceof Integer && results[0] == 50 + results[1] instanceof Integer && results[1] == -8 * 10 * 5 + + results[2] instanceof Long && results[2] == 10 * 5L + results[3] instanceof Long && results[3] == 5l * 10 + results[4] instanceof Long && results[4] == 10L * 5l + results[5] instanceof Long && results[5] == 5 * 10L + + results[6] instanceof Float && results[6] == 10f * 5 + results[7] instanceof Float && results[7] == 5 * 10f + results[8] instanceof Float && results[8] == 10f * 5F + results[9] instanceof Float && results[9] == 5 * 10F + + results[10] instanceof Double && results[10] == 10d * 5 + results[11] instanceof Double && results[11] == 5 * 10d + results[12] instanceof Double && results[12] == 10d * 5D + results[13] instanceof Double && results[13] == 5 * 10D + + results[14] instanceof Double && results[14] == 10d * 5f + results[15] instanceof Double && results[15] == 5L * 10d + results[16] instanceof Float && results[16] == 10L * 5f + } + + void "test '+' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 + 5 }", // 0 + "#{ -8 + 10 + 5 }", // 1 + + // long + "#{ 10 + 5L }", // 2 + "#{ 5l + 10 }", // 3 + "#{ 10L + 5l }", // 4 + "#{ 5 + 10L }", // 5 + + // float + "#{ 10f + 5 }", // 6 + "#{ 5 + 10f }", // 7 + "#{ 10f + 5F }", // 8 + "#{ 5 + 10F }", // 9 + + // double + "#{ 10d + 5 }", // 10 + "#{ 5 + 10d }", // 11 + "#{ 10d + 5D }", // 12 + "#{ 5 + 10D }", // 13 + + // mixed + "#{ 10d + 5f }", // 14 + "#{ 5L + 10d }", // 15 + "#{ 10L + 5f }", // 16 + + // string + "#{ '1' + '2' }", // 17 + "#{ '1' + null }", // 18 + "#{ null + '1' }", // 19 + "#{ null + '1' + null + 2D }", // 20 + "#{ 15 + 'str' + 2L }", // 21 + "#{ 2f + 'str' + 2 }", // 22 + "#{ .014 + 'str' + 2L + 'test' }", // 23 + "#{ 1 + 2 + 'str' + 2L + 'test' }", // 24 + "#{ 1 + 2 + 3 + 'str' }", // 25 + "#{ 1 + 2 - 3 + 'str' }" // 26 + ) + + expect: + results[0] instanceof Integer && results[0] == 10 + 5 + results[1] instanceof Integer && results[1] == -8 + 10 + 5 + + results[2] instanceof Long && results[2] == 10 + 5L + results[3] instanceof Long && results[3] == 5l + 10 + results[4] instanceof Long && results[4] == 10L + 5l + results[5] instanceof Long && results[5] == 5 + 10L + + results[6] instanceof Float && results[6] == 10f + 5 + results[7] instanceof Float && results[7] == 5 + 10f + results[8] instanceof Float && results[8] == 10f + 5F + results[9] instanceof Float && results[9] == 5 + 10F + + results[10] instanceof Double && results[10] == 10d + 5 + results[11] instanceof Double && results[11] == 5 + 10d + results[12] instanceof Double && results[12] == 10d + 5D + results[13] instanceof Double && results[13] == 5 + 10D + + results[14] instanceof Double && results[14] == 10d + 5f + results[15] instanceof Double && results[15] == 5L + 10d + results[16] instanceof Float && results[16] == 10L + 5f + + results[17] instanceof String && results[17] == '12' + results[18] instanceof String && results[18] == '1null' + results[19] instanceof String && results[19] == 'null1' + results[20] instanceof String && results[20] == 'null1null2.0' + results[21] instanceof String && results[21] == '15str2' + results[22] instanceof String && results[22] == '2.0str2' + results[23] instanceof String && results[23] == '0.014str2test' + results[24] instanceof String && results[24] == '3str2test' + results[25] instanceof String && results[25] == '6str' + results[26] instanceof String && results[26] == '0str' + } + + void "test '>' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 > 5 }", // 0 + "#{ -8 > -3 }", // 1 + + // long + "#{ 10L > 5l }", // 2 + "#{ 5 > 10L }", // 3 + "#{ 10l > 5 }", // 4 + + // double + "#{ 10d > 5 }", // 5 + "#{ 5 > 10d }", // 6 + "#{ 10D > 5d }", // 7 + "#{ -.4 > 5d }", // 8 + "#{ .123 > .1 }", // 9 + "#{ .123 > .1229 }", // 10 + + // float + "#{ 10f > 5 }", // 11 + "#{ 5 > 10f }", // 12 + "#{ 10F > 5f }", // 13 + "#{ -.4f > 5f }", // 14 + "#{ .123f > -5f }", // 15 + + // mixed + "#{ 10f > 5d }", // 16 + "#{ 5L > 10f }", // 17 + "#{ 10L > 5D }", // 18 + "#{ -.4 > 5l }", // 19 + "#{ .123f > -5 }", // 20 + "#{ 10L > 11 }" // 21 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == true + results[8] instanceof Boolean && results[8] == false + results[9] instanceof Boolean && results[9] == true + results[10] instanceof Boolean && results[10] == true + + results[11] instanceof Boolean && results[11] == true + results[12] instanceof Boolean && results[12] == false + results[13] instanceof Boolean && results[13] == true + results[14] instanceof Boolean && results[14] == false + results[15] instanceof Boolean && results[15] == true + + results[16] instanceof Boolean && results[16] == true + results[17] instanceof Boolean && results[17] == false + results[18] instanceof Boolean && results[18] == true + results[19] instanceof Boolean && results[19] == false + results[20] instanceof Boolean && results[20] == true + results[21] instanceof Boolean && results[21] == false + } + + void "test '<' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 < 5 }", // 0 + "#{ -8 < -3 }", // 1 + + // long + "#{ 10L < 5l }", // 2 + "#{ 5 < 10L }", // 3 + "#{ 10l < 5 }", // 4 + + // double + "#{ 10d < 5 }", // 5 + "#{ 5 < 10d }", // 6 + "#{ 10D < 5d }", // 7 + "#{ -.4 < 5d }", // 8 + "#{ .123 < .1 }", // 9 + "#{ .1229 < .123 }", // 10 + + // float + "#{ 10f < 5 }", // 11 + "#{ 5 < 10f }", // 12 + "#{ 10F < 5f }", // 13 + "#{ -.4f < 5f }", // 14 + "#{ .123f < -5f }", // 15 + + // mixed + "#{ 10f < 5d }", // 16 + "#{ 5L < 10f }", // 17 + "#{ 10L < 5D }", // 18 + "#{ -.4 < 5l }", // 19 + "#{ .123f < -5 }", // 20 + "#{ 10L < 11 }" // 21 + ) + + expect: + results[0] instanceof Boolean && results[0] == false + results[1] instanceof Boolean && results[1] == true + + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == false + + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == true + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + results[10] instanceof Boolean && results[10] == true + + results[11] instanceof Boolean && results[11] == false + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == false + results[14] instanceof Boolean && results[14] == true + results[15] instanceof Boolean && results[15] == false + + results[16] instanceof Boolean && results[16] == false + results[17] instanceof Boolean && results[17] == true + results[18] instanceof Boolean && results[18] == false + results[19] instanceof Boolean && results[19] == true + results[20] instanceof Boolean && results[20] == false + results[21] instanceof Boolean && results[21] == true + } + + void "test '>=' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 >= 5 }", // 0 + "#{ -8 >= -3 }", // 1 + "#{ 3 >= 3 }", // 2 + + // long + "#{ 10L >= 5l }", // 3 + "#{ 5 >= 10L }", // 4 + "#{ 10l >= 5 }", // 5 + "#{ 5l >= 5 }", // 6 + + // double + "#{ 10d >= 5 }", // 7 + "#{ 5 >= 10d }", // 8 + "#{ 10D >= 5d }", // 9 + "#{ -.4 >= 5d }", // 10 + "#{ .123 >= .1 }", // 11 + + // float + "#{ 10f >= 5 }", // 12 + "#{ 5 >= 10f }", // 13 + "#{ 10F >= 5f }", // 14 + "#{ -.4f >= 5f }", // 15 + "#{ .123f >= -5f }", // 16 + + // mixed + "#{ 10f >= 5d }", // 17 + "#{ 5L >= 10f }", // 18 + "#{ 10L >= 5D }", // 19 + "#{ -.4 >= 5l }", // 20 + "#{ .123f >= -5 }", // 21 + "#{ 12L >= 11 }", // 22 + "#{ 11 >= 11L }" // 23 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == true + + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == false + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == true + + results[7] instanceof Boolean && results[7] == true + results[8] instanceof Boolean && results[8] == false + results[9] instanceof Boolean && results[9] == true + results[10] instanceof Boolean && results[10] == false + results[11] instanceof Boolean && results[11] == true + + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == false + results[14] instanceof Boolean && results[14] == true + results[15] instanceof Boolean && results[15] == false + results[16] instanceof Boolean && results[16] == true + + results[17] instanceof Boolean && results[17] == true + results[18] instanceof Boolean && results[18] == false + results[19] instanceof Boolean && results[19] == true + results[20] instanceof Boolean && results[20] == false + results[21] instanceof Boolean && results[21] == true + results[22] instanceof Boolean && results[22] == true + results[23] instanceof Boolean && results[23] == true + } + + void "test '<=' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 <= 5 }", // 0 + "#{ -8 <= -3 }", // 1 + "#{ 3 <= 3 }", // 2 + + // long + "#{ 10L <= 5l }", // 3 + "#{ 5 <= 10L }", // 4 + "#{ 10l <= 5 }", // 5 + "#{ 5l <= 5 }", // 6 + + // double + "#{ 10d <= 5 }", // 7 + "#{ 5 <= 10d }", // 8 + "#{ 10D <= 5d }", // 9 + "#{ -.4 <= 5d }", // 10 + "#{ .123 <= .1 }", // 11 + + // float + "#{ 10f <= 5 }", // 12 + "#{ 5 <= 10f }", // 13 + "#{ 10F <= 5f }", // 14 + "#{ -.4f <= 5f }", // 15 + "#{ .123f <= -5f }", // 16 + + // mixed + "#{ 10f <= 5d }", // 17 + "#{ 5L <= 10f }", // 18 + "#{ 10L <= 5D }", // 19 + "#{ -.4 <= 5l }", // 20 + "#{ .123f <= -5 }", // 21 + "#{ 12L <= 11 }", // 22 + "#{ 11 <= 11L }" // 23 + ) + + expect: + results[0] instanceof Boolean && results[0] == false + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == true + + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + results[10] instanceof Boolean && results[10] == true + results[11] instanceof Boolean && results[11] == false + + results[12] instanceof Boolean && results[12] == false + results[13] instanceof Boolean && results[13] == true + results[14] instanceof Boolean && results[14] == false + results[15] instanceof Boolean && results[15] == true + results[16] instanceof Boolean && results[16] == false + + results[17] instanceof Boolean && results[17] == false + results[18] instanceof Boolean && results[18] == true + results[19] instanceof Boolean && results[19] == false + results[20] instanceof Boolean && results[20] == true + results[21] instanceof Boolean && results[21] == false + results[22] instanceof Boolean && results[22] == false + results[23] instanceof Boolean && results[23] == true + } + + void "test '==' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 == 10}", // 0 + "#{ 8 == 11 }", // 1 + "#{ -3 == 3}", // 2 + "#{ -15 == -15 }", // 3 + + // long + "#{ 10L == 10L}", // 4 + "#{ 8L == 11L }", // 5 + "#{ -3L == 3L }", // 6 + "#{ -15L == -15L }", // 7 + + // float + "#{ 1f == 1f}", // 8 + "#{ 0f == 1f }", // 9 + + // double + "#{ 1d == 1.0 }", // 10 + "#{ .0 == 1d }", // 11 + + // string + "#{ 'str' == 'str' }", // 12 + "#{ 'str1' == 'str2' }" // 13 + ) + + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == true + + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + + results[10] instanceof Boolean && results[10] == true + results[11] instanceof Boolean && results[11] == false + + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == false + } + + void "test '!=' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 != 10}", // 0 + "#{ 8 != 11 }", // 1 + "#{ -3 != 3}", // 2 + "#{ -15 != -15 }", // 3 + + // long + "#{ 10L != 10L}", // 4 + "#{ 8L != 11L }", // 5 + "#{ -3L != 3L }", // 6 + "#{ -15L != -15L }", // 7 + + // float + "#{ 1f != 1f}", // 8 + "#{ 0f != 1f }", // 9 + + // double + "#{ 1d != 1.0 }", // 10 + "#{ .0 != 1d }", // 11 + + // string + "#{ 'str' != 'str' }", // 12 + "#{ 'str1' != 'str2' }" // 13 + ) + + expect: + results[0] instanceof Boolean && results[0] == false + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == false + + results[4] instanceof Boolean && results[4] == false + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == true + results[7] instanceof Boolean && results[7] == false + + results[8] instanceof Boolean && results[8] == false + results[9] instanceof Boolean && results[9] == true + + results[10] instanceof Boolean && results[10] == false + results[11] instanceof Boolean && results[11] == true + + results[12] instanceof Boolean && results[12] == false + results[13] instanceof Boolean && results[13] == true + } + + // '^' operator in expressions means power operation + void "test '^' operator"() { + given: + List results = evaluateMultiple( + "#{ 2^3 }", // 0 + "#{ 3L ^ 2}", // 1 + "#{ 2.0^0}", // 2 + "#{ 2f ^ 2L}", // 3 + "#{ 2^2 ^2}", // 4 + "#{ (2 ^ 32)^2}" // 5 + ) + + expect: + results[0] instanceof Long && results[0] == 8 + results[1] instanceof Long && results[1] == 9 + results[2] instanceof Double && results[2] == 1.0 + results[3] instanceof Double && results[3] == 4.0 + results[4] instanceof Long && results[4] == 16 + results[5] instanceof Long && results[5] == 9223372036854775807L + } + + void "test '&&' operator"() { + given: + List results = evaluateMultiple( + "#{ true && true }", // 0 + "#{ true && false }", // 1 + "#{ false && true }", // 2 + "#{ false && false }", // 3 + "#{ true and true }", // 4 + "#{ true and false }", // 5 + "#{ false and true }", // 6 + "#{ false and false }", // 7 + "#{ true and true && true }", // 8 + "#{ true && true and false }" // 9 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + } + + void "test '||' operator"() { + given: + List results = evaluateMultiple( + "#{ true || true }", // 0 + "#{ true || false }", // 1 + "#{ false || true }", // 2 + "#{ false || false }", // 3 + "#{ true or true }", // 4 + "#{ true or false }", // 5 + "#{ false or true }", // 6 + "#{ false or false }", // 7 + "#{ true or true || true }", // 8 + "#{ true or true or false }", // 9 + "#{ true || false or false }", // 10 + "#{ false or false || false or true }" // 11 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == true + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == true + results[10] instanceof Boolean && results[10] == true + results[11] instanceof Boolean && results[11] == true + } + + void "test 'instanceof' operator"() { + given: + List results = evaluateMultiple( + "#{ 1 instanceof T(java.lang.Integer) }", // 0 + "#{ 1 instanceof T(Integer) }", // 1 + "#{ 1L instanceof T(java.lang.Long) }", // 2 + "#{ 1f instanceof T(java.lang.Float) }", // 3 + "#{ 1d instanceof T(java.lang.Double) }", // 4 + "#{ 'str' instanceof T(java.lang.String) }", // 5 + "#{ 'str' instanceof T(Double) }", // 6 + "#{ 1L instanceof T(Integer) }", // 7 + "#{ 1f instanceof T(Double) }" // 8 + ) + + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == false + } + + void "test 'matches' operator"() { + given: + List results = evaluateMultiple( + "#{ '123' matches '\\\\d+' }", // 0 + "#{ '5.0' matches '[0-9]*\\\\.[0-9]+(d|D)?' }", // 1 + "#{ '5.0' matches '[0-9]*\\\\.[0-9]+(d|D)' }", // 2 + "#{ 'AbC' matches '[A-Za-z]*' }", // 3 + "#{ ' ' matches '\\\\s*' }", // 4 + "#{ '' matches '\\\\s+' }" // 5 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + } + + void "test '!' operator"() { + given: + List results = evaluateMultiple( + "#{ !true }", // 0 + "#{ !false }", // 1 + "#{ !!true }", // 2 + "#{ !!false }", // 3 + "#{ !!!false }", // 4 + "#{ !!!true }" // 5 + ) + + expect: + results[0] == false + results[1] == true + results[2] == true + results[3] == false + results[4] == true + results[5] == false + } + + void "test unary '-'"() { + given: + List results = evaluateMultiple( + "#{ -5 }", + "#{ -(-3) }", + "#{ -3L }", + "#{ -1.0D }" + ) + + expect: + results[0] instanceof Integer && results[0] == -5 + results[1] instanceof Integer && results[1] == 3 + results[2] instanceof Long && results[2] == -3 + results[3] instanceof Double && results[3] == -1.0 + + when: + evaluate("#{ --5 }") + + then: + thrown(Throwable.class) + } + + void "test unary '+'"() { + given: + List results = evaluateMultiple( + "#{ +5 }", + "#{ +(+3) }", + "#{ +3L }", + "#{ +1.0D }" + ) + + expect: + results[0] instanceof Integer && results[0] == 5 + results[1] instanceof Integer && results[1] == 3 + results[2] instanceof Long && results[2] == 3 + results[3] instanceof Double && results[3] == 1.0 + + when: + evaluate("#{ ++5 }") + + then: + thrown(Throwable.class) + } + + void "test parenthesized expressions"() { + given: + List results = evaluateMultiple( + "#{ (2 + 3) * 5 }", // 0 + "#{ 2 * ( 3 + 1) }", // 1 + "#{ (-1 + 9) * (2 + 3) }", // 2 + "#{ 2 ^ ( 2 + 2) }", // 3 + "#{ 5 * ( 2d * (1 - 1)) }", // 4 + "#{ 5L * ( 2d + 2f ) }" // 5 + ) + + expect: + results[0] instanceof Integer && results[0] == 25 + results[1] instanceof Integer && results[1] == 8 + results[2] instanceof Integer && results[2] == 40 + results[3] instanceof Long && results[3] == 16 + results[4] instanceof Double && results[4] == 0 + results[5] instanceof Double && results[5] == 20.0 + } + +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy new file mode 100644 index 00000000000..b50798d6809 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy @@ -0,0 +1,36 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + +class TernaryOperationExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test ternary operator"() { + given: + List results = evaluateMultiple( + "#{ 15 > 10 ? 'a' : 'b' }", + "#{ 15 == 10 ? 'a' : 'b' }", + "#{ 10 > 9 ? 'a' + 'b' : 'b' + 'a' }" + ) + + expect: + results[0] instanceof String && results[0] == 'a' + results[1] instanceof String && results[1] == 'b' + results[2] instanceof String && results[2] == 'ab' + } + + void "test ternary type resolution"() { + given: + List results = evaluateMultiple( + "#{ (15 > 10 ? 'a' : 'b').length() }", + "#{ 15 > 10 ? 15L : 'test' }", + "#{ (15 > 10 ? 15L : 10) + 8}", + "#{ 10 > 15 ? 15L : true}" + ) + + expect: + results[0] instanceof Integer && results[0] == 1 + results[1] instanceof Long && results[1] == 15 + results[2] instanceof Long && results[2] == 23 + results[3] instanceof Boolean && results[3] == true + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy new file mode 100644 index 00000000000..a8b20a684a1 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy @@ -0,0 +1,167 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.NoSuchBeanException + +class TestExpressionsInjectionSpec extends AbstractBeanDefinitionSpec { + + void "test expression field injection"() { + given: + def ctx = buildContext(""" + package test + + import jakarta.inject.Singleton + import io.micronaut.context.annotation.Value + + @Singleton + class Expr { + @Value("#{ 15 ^ 2 }") + int intValue + + @Value("#{ 100 }") + Integer boxedIntValue + + @Value("#{ T(String).join(',', 'a', 'b', 'c') }") + String strValue + + @Value("#{null}") + Object nullValue + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.intValue == 225 + bean.boxedIntValue == 100 + bean.strValue == 'a,b,c' + bean.nullValue == null + + cleanup: + ctx.close() + } + + void "test expression constructor injection"() { + given: + def ctx = buildContext(""" + package test + + import jakarta.inject.Singleton + import io.micronaut.context.annotation.Value + + @Singleton + class Expr { + private Integer wrapper; + private int primitive; + + Expr(@Value("#{ 25 }") Integer wrapper, + @Value("#{ 23 }") int primitive) { + this.wrapper = wrapper + this.primitive = primitive + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.wrapper == 25 + bean.primitive == 23 + + cleanup: + ctx.close() + } + + void "test expression setter injection"() { + given: + def ctx = buildContext(""" + package test + + import jakarta.inject.Inject; + import jakarta.inject.Singleton + import io.micronaut.context.annotation.Value + + @Singleton + class Expr { + private Integer wrapper; + private int primitive; + + @Inject + public void setWrapper(@Value("#{ 25 }") Integer value) { + this.wrapper = value; + } + + @Inject + public void setPrimitive(@Value("#{ 23 }") int value) { + this.primitive = value; + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.wrapper == 25 + bean.primitive == 23 + + cleanup: + ctx.close() + } + + + void "test expressions in @Factory injection"() { + given: + def ctx = buildContext(""" + package test + + import io.micronaut.context.annotation.Bean + import io.micronaut.context.annotation.EvaluatedExpressionContext + import io.micronaut.context.annotation.Factory + import jakarta.inject.Inject + import jakarta.inject.Singleton + import io.micronaut.context.annotation.Value + + @EvaluatedExpressionContext + class TestContext { + String contextValue = "context value" + } + + class Expr { + private final Integer wrapper + private final int primitive + private final String contextValue + + Expr(Integer wrapper, int primitive, String contextValue) { + this.wrapper = wrapper + this.primitive = primitive + this.contextValue = contextValue + } + } + + @Factory + class TestFactory { + @Bean + Expr factoryBean(@Value('#{ 25 }') Integer wrapper, + @Value('#{ 23 }') int primitive, + @Value('#{ #contextValue }') String contextValue) { + return new Expr(wrapper, primitive, contextValue) + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.wrapper == 25 + bean.primitive == 23 + bean.contextValue == "context value" + + cleanup: + ctx.close() + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy new file mode 100644 index 00000000000..7d3e0d189df --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy @@ -0,0 +1,129 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.NoSuchBeanException + +class TestExpressionsUsageSpec extends AbstractBeanDefinitionSpec { + + void "test expression array in requires"() { + given: + def ctx = buildContext(""" + package test + import io.micronaut.context.annotation.Requires + import jakarta.inject.Singleton + + @Singleton + @Requires(env = ["#{ 'test' }"]) + class Expr { + } + """) + + when: + getBean(ctx, "test.Expr") + + then: + noExceptionThrown() + + cleanup: + ctx.close() + } + + void "test requires expression property value"() { + given: + def ctx = buildContext(""" + package test + import io.micronaut.context.annotation.Requires + import jakarta.inject.Singleton + + @Singleton + @Requires(property = 'test-property', value = "#{ 'test-value'.toUpperCase() }") + class Expr { + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + when: + ctx.environment.addPropertySource(PropertySource.of("test", ['test-property': 'TEST-VALUE'])) + ctx.getBean(type) + + then: + noExceptionThrown() + + cleanup: + ctx.close() + } + + void "test requires expression context value"() { + given: + def ctx = buildContext(""" + package test + + import io.micronaut.context.annotation.ConfigurationProperties + import io.micronaut.context.annotation.EvaluatedExpressionContext + import io.micronaut.context.annotation.Requires + + import jakarta.inject.Singleton + + @Singleton + @Requires(property = 'test.enabled', value = "#{ #enabled }") + class Expr { + } + + @ConfigurationProperties('test') + @EvaluatedExpressionContext + class TestContext { + boolean enabled + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + when: + ctx.environment.addPropertySource(PropertySource.of("test", ['test.enabled': false])) + ctx.getBean(type) + + then: + noExceptionThrown() + + cleanup: + ctx.close() + } + + void "test disabled by expression bean"() { + given: + def ctx = buildContext(""" + package test + + import io.micronaut.context.annotation.ConfigurationProperties + import io.micronaut.context.annotation.EvaluatedExpressionContext + import io.micronaut.context.annotation.Requires + + import jakarta.inject.Singleton + + @Singleton + @Requires(property = 'test.property', value = "#{ 5 * 2 }") + class Expr { + } + + @ConfigurationProperties('test') + @EvaluatedExpressionContext + class TestContext { + boolean enabled + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + when: + ctx.environment.addPropertySource(PropertySource.of("test", ['test.property': 15])) + ctx.getBean(type) + + then: + thrown(NoSuchBeanException) + + cleanup: + ctx.close() + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy new file mode 100644 index 00000000000..246954bd1a1 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy @@ -0,0 +1,52 @@ +package io.micronaut.expressions + +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec + +class TypeIdentifierExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test static methods"() { + given: + List results = evaluateMultiple( + "#{ T(Math).random() }", + "#{ T(java.lang.Math).random() }", + "#{ T(Integer).valueOf('10') }", + "#{ T(String).join(',', '1', '2', '3') }", + "#{ T(String).join(',', 'a', 'b').repeat(2) }" + ) + + expect: + results[0] instanceof Double && results[0] >= 0 && results[0] < 1 + results[1] instanceof Double && results[1] >= 0 && results[1] < 1 + results[2] instanceof Integer && results[2] == 10 + results[3] instanceof String && results[3] == "1,2,3" + results[4] instanceof String && results[4] == "a,ba,b" + } + + void "test type identifier as argument"() { + given: + Object expr1 = evaluateAgainstContext("#{ #getType(T(java.lang.String)) }", + """ + @EvaluatedExpressionContext + class Context { + Class getType(Class type) { + return type + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #getType(T(String), T(Object)) }", + """ + @EvaluatedExpressionContext + class Context { + Class getType(Class... types) { + return types[1] + } + } + """) + + expect: + expr1 instanceof Class && expr1 == String.class + expr2 instanceof Class && expr2 == Object.class + } + +} diff --git a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy new file mode 100644 index 00000000000..206ded5dd78 --- /dev/null +++ b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy @@ -0,0 +1,124 @@ +/* + * 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.annotation.processing.test + +import io.micronaut.context.AbstractEvaluatedExpression +import io.micronaut.core.expression.EvaluatedExpression +import io.micronaut.core.naming.NameUtils +import io.micronaut.core.annotation.EvaluatedExpressionReference +import org.intellij.lang.annotations.Language + +abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec { + + List evaluateMultiple(String... expressions) { + + String classContent = "" + for (int i = 0; i < expressions.size(); i++) { + classContent += """ + + @Value("${expressions[i]}") + public Object field${i}; + + """ + } + + def cls = """ + package test; + import io.micronaut.context.annotation.Value; + import io.micronaut.context.annotation.EvaluatedExpressionContext; + + class Expr { + ${classContent} + } + """.stripIndent().stripLeading() + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + def exprClassName = 'test.$Expr$Expr' + def startingIndex = EvaluatedExpressionReference.nextIndex(exprClassName) - expressions.length + + List result = new ArrayList<>() + for (int i = startingIndex; i < startingIndex + expressions.size(); i++) { + String exprFullName = exprClassName + i + try { + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName).newInstance() + exprClass.configure(applicationContext) + result.add(exprClass.evaluate()) + } catch (ClassNotFoundException e) { + return null + } + } + + return result + } + + Object evaluate(String expression) { + return evaluateAgainstContext(expression, "") + } + + Object evaluateAgainstContext(String expression, @Language("java") String contextClass) { + String exprFullName = 'test.$Expr$Expr'; + + def cls = """ + package test; + import io.micronaut.context.annotation.Value; + import io.micronaut.context.annotation.EvaluatedExpressionContext; + import jakarta.inject.Singleton; + + ${contextClass} + + @Singleton + class Expr { + @Value("${expression}") + public Object field; + } + """.stripIndent().stripLeading() + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + try { + def index = EvaluatedExpressionReference.nextIndex(exprFullName) + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName + (index == 0 ? index : index - 1)).newInstance() + exprClass.configure(applicationContext) + return exprClass.evaluate(); + } catch (ClassNotFoundException e) { + return null + } + } + + EvaluatedExpression buildSingleExpressionFromClass(String className, + @Language("java") String cls) { + def classSimpleName = NameUtils.getSimpleName(className) + def packageName = NameUtils.getPackageName(className) + def exprClassName = (classSimpleName.startsWith('$') ? '' : '$') + classSimpleName + '$Expr' + + String exprFullName = "${packageName}.${exprClassName}" + + def applicationContext = buildContext(cls) + def classLoader = applicationContext.classLoader + + try { + def index = EvaluatedExpressionReference.nextIndex(exprFullName) + def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName + (index == 0 ? index : index - 1)).newInstance() + exprClass.configure(applicationContext) + return exprClass + } catch (ClassNotFoundException e) { + return null + } + } +} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index 60f39b11e52..f9ae51c2127 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -27,6 +27,9 @@ import io.micronaut.core.annotation.Vetoed; import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.expressions.EvaluatedExpressionWriter; +import io.micronaut.expressions.context.ExpressionContextLoader; +import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.processing.BeanDefinitionCreator; @@ -262,6 +265,7 @@ public final boolean process(Set annotations, RoundEnviro } finally { AbstractAnnotationMetadataBuilder.clearMutated(); JavaAnnotationMetadataBuilder.clearCaches(); + ExpressionContextLoader.reset(); } } @@ -269,7 +273,8 @@ public final boolean process(Set annotations, RoundEnviro } /** - * Writes {@link io.micronaut.inject.BeanDefinitionReference} into /META-INF/services/io.micronaut.inject.BeanDefinitionReference. + * Writes {@link io.micronaut.inject.BeanDefinitionReference} into /META-INF/services/io + * .micronaut.inject.BeanDefinitionReference. */ private void writeBeanDefinitionsToMetaInf() { try { @@ -285,6 +290,9 @@ private void processBeanDefinitions(BeanDefinitionVisitor beanDefinitionWriter) beanDefinitionWriter.visitBeanDefinitionEnd(); if (beanDefinitionWriter.isEnabled()) { beanDefinitionWriter.accept(classWriterOutputVisitor); + + processEvaluatedExpressions(beanDefinitionWriter); + BeanDefinitionReferenceWriter beanDefinitionReferenceWriter = new BeanDefinitionReferenceWriter(beanDefinitionWriter); beanDefinitionReferenceWriter.setRequiresMethodProcessing(beanDefinitionWriter.requiresMethodProcessing()); @@ -303,4 +311,15 @@ private void processBeanDefinitions(BeanDefinitionVisitor beanDefinitionWriter) } } + private void processEvaluatedExpressions(BeanDefinitionVisitor beanDefinitionWriter) throws IOException { + for (ExpressionWithContext expressionMetadata: beanDefinitionWriter.getEvaluatedExpressions()) { + EvaluatedExpressionWriter expressionWriter = new EvaluatedExpressionWriter( + expressionMetadata, + javaVisitorContext, + beanDefinitionWriter.getOriginatingElement()); + + expressionWriter.accept(classWriterOutputVisitor); + } + } + } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java index 9138dbec95c..5b20bda7438 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/JavaAnnotationMetadataBuilder.java @@ -344,6 +344,19 @@ protected Element getAnnotationMember(Element annotationElement, CharSequence me return null; } + @Override + protected String getOriginatingClassName(Element orginatingElement) { + return JavaModelUtils.getClassName(getOriginatingTypeElement(orginatingElement)); + } + + private TypeElement getOriginatingTypeElement(Element element) { + if (element instanceof TypeElement typeElement) { + return typeElement; + } + + return getOriginatingTypeElement(element.getEnclosingElement()); + } + @Override protected Optional> getAnnotationValues(Element originatingElement, Element member, Class annotationType) { List annotationMirrors = member.getAnnotationMirrors(); @@ -374,8 +387,11 @@ protected void readAnnotationRawValues( if (memberName != null && annotationValue instanceof javax.lang.model.element.AnnotationValue && !annotationValues.containsKey(memberName)) { final MetadataAnnotationValueVisitor resolver = new MetadataAnnotationValueVisitor(originatingElement, (ExecutableElement) member); ((javax.lang.model.element.AnnotationValue) annotationValue).accept(resolver, this); - final Object resolvedValue = resolver.resolvedValue; + Object resolvedValue = resolver.resolvedValue; if (resolvedValue != null) { + if (isEvaluatedExpression(resolvedValue)) { + resolvedValue = buildEvaluatedExpressionReference(originatingElement, annotationName, memberName, resolvedValue); + } validateAnnotationValue(originatingElement, annotationName, member, memberName, resolvedValue); annotationValues.put(memberName, resolvedValue); } @@ -410,13 +426,16 @@ private boolean isValidationRequired(List annotation } @Override - protected Object readAnnotationValue(Element originatingElement, Element member, String memberName, Object annotationValue) { + protected Object readAnnotationValue(Element originatingElement, Element member, String annotationName, String memberName, Object annotationValue) { if (memberName != null && annotationValue instanceof javax.lang.model.element.AnnotationValue) { final MetadataAnnotationValueVisitor visitor = new MetadataAnnotationValueVisitor(originatingElement, (ExecutableElement) member); ((javax.lang.model.element.AnnotationValue) annotationValue).accept(visitor, this); return visitor.resolvedValue; } else if (memberName != null && annotationValue != null && ClassUtils.isJavaLangType(annotationValue.getClass())) { // only allow basic types + if (isEvaluatedExpression(annotationValue)) { + annotationValue = buildEvaluatedExpressionReference(originatingElement, annotationName, memberName, annotationValue); + } return annotationValue; } return null; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java index 28ad0f9168c..2356d0e8187 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java @@ -30,6 +30,7 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.version.VersionUtils; +import io.micronaut.expressions.context.ExpressionContextLoader; import io.micronaut.inject.processing.ProcessingException; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.EnumConstantElement; @@ -295,6 +296,7 @@ public boolean process(Set annotations, RoundEnvironment final List beanDefinitionBuilders = javaVisitorContext.getBeanElementBuilders(); if (CollectionUtils.isNotEmpty(beanDefinitionBuilders)) { try { +// EvaluatedExpressionContextInitializer.initEvaluatedExpressionContext(roundEnv, modelUtils, javaVisitorContext); AbstractBeanDefinitionBuilder.writeBeanDefinitionBuilders(classWriterOutputVisitor, beanDefinitionBuilders); } catch (IOException e) { // raise a compile error @@ -306,6 +308,7 @@ public boolean process(Set annotations, RoundEnvironment if (roundEnv.processingOver()) { javaVisitorContext.finish(); writeBeanDefinitionsToMetaInf(); + ExpressionContextLoader.reset(); } return false; } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java index 99c9c109a16..ca8a4388b2f 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java @@ -155,6 +155,29 @@ public boolean isDefault() { return executableElement.isDefault(); } + @Override + public boolean isVarArgs() { + return executableElement.isVarArgs(); + } + + @Override + public boolean overrides(MethodElement overridden) { +// if (this.equals(overridden) || isStatic() || overridden.isStatic()) { +// return false; +// } +// if (overridden instanceof JavaMethodElement) { +// boolean overrides = visitorContext.getElements().overrides( +// executableElement, +// ((JavaMethodElement) overridden).executableElement, +// owningType.classElement +// ); +// if (overrides) { +// return true; +// } +// } + return MethodElement.super.overrides(overridden); + } + @Override public boolean hides(MemberElement hidden) { if (isStatic() && getDeclaringType().isInterface()) { diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy new file mode 100644 index 00000000000..bb5c522e4c1 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy @@ -0,0 +1,73 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test annotation level context"() { + given: + Object expr = buildSingleExpressionFromClass("test.Expr", """ + + package test; + import io.micronaut.context.annotation.EvaluatedExpressionContext;import io.micronaut.context.annotation.Executable; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @CustomAnnotation("#{ #getAnnotationLevelValue() }") + class Expr { + } + + @Singleton + class CustomContext { + String getAnnotationLevelValue() { + return "annotationLevelValue"; + } + } + + @EvaluatedExpressionContext(CustomContext.class) + @interface CustomAnnotation { + String value(); + } + + """).evaluate() + + expect: + expr instanceof String && expr == "annotationLevelValue" + + } + + void "test annotation member level context"() { + given: + Object expr = buildSingleExpressionFromClass("test.Expr", """ + + package test; + import io.micronaut.context.annotation.EvaluatedExpressionContext;import io.micronaut.context.annotation.Executable; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @CustomAnnotation(customValue = "#{ #getAnnotationLevelValue() }") + class Expr { + } + + @Singleton + class CustomContext { + String getAnnotationLevelValue() { + return "annotationLevelValue"; + } + } + + @interface CustomAnnotation { + @EvaluatedExpressionContext(CustomContext.class) + String customValue(); + } + + """).evaluate() + + expect: + expr instanceof String && expr == "annotationLevelValue" + + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy new file mode 100644 index 00000000000..ea8698eef0c --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy @@ -0,0 +1,219 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test primitive and wrapper varargs methods"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countValues(1, 2, 3) }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(int... array) { + return array.length; + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #countValues(1) }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(int... array) { + return array.length; + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #countValues() }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(int... array) { + return array.length; + } + } + """) + + Object expr4 = evaluateAgainstContext("#{ #countValues(1, 2, T(Integer).valueOf('3')) }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(Integer... array) { + return array.length; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 3 + expr2 instanceof Integer && expr2 == 1 + expr3 instanceof Integer && expr3 == 0 + expr4 instanceof Integer && expr4 == 3 + } + + void "test string varargs methods"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countValues('a', 'b', 'c') }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(String... values) { + return values.length; + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #countValues('a') }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(String... values) { + return values.length; + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #countValues() }", + """ + @EvaluatedExpressionContext + class Context { + int countValues(String... values) { + return values.length; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 3 + expr2 instanceof Integer && expr2 == 1 + expr3 instanceof Integer && expr3 == 0 + } + + void "test mixed types varargs methods"() { + given: + Object expr1 = evaluateAgainstContext("#{ #multiplyLength(3, '1', 8, null) }", + """ + @EvaluatedExpressionContext + class Context { + int multiplyLength(int time, Object... values) { + return values.length * time; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 9 + } + + void "test arrays as varargs"() { + given: + Object expr1 = evaluateAgainstContext("#{ #multiplyLength(3, '1', 8, null) }", + """ + @EvaluatedExpressionContext + class Context { + int multiplyLength(Integer time, Object[] values) { + return values.length * time; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 9 + } + + void "test non-varargs arrays"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countLength(#values()) }", + """ + @EvaluatedExpressionContext + class Context { + String[] values() { + return new String[]{"a", "b", "c"}; + } + + int countLength(Object[] array) { + return array.length; + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #multiplyLength(#values(), 3) }", + """ + @EvaluatedExpressionContext + class Context { + String[] values() { + return new String[]{"a", "b"}; + } + + int multiplyLength(String[] array, int times) { + return array.length * times; + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #multiplyLength(#values(), 1) }", + """ + @EvaluatedExpressionContext + class Context { + int[] values() { + return new int[]{1, 2}; + } + + int multiplyLength(int[] array, int times) { + return array.length * times; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 3 + expr2 instanceof Integer && expr2 == 6 + expr3 instanceof Integer && expr3 == 2 + } + + void "test multi-dimensional arrays"() { + given: + Object expr1 = evaluateAgainstContext("#{ #countLength(#values()) }", + """ + import java.util.Arrays; + + @EvaluatedExpressionContext + class Context { + String[][] values() { + return new String[][]{new String[]{"a", "b", "c"}, new String[]{"a", "b"}}; + } + + int countLength(Object[][] array) { + return Arrays.stream(array) + .map(a -> a.length) + .reduce(0, Integer::sum); + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #countLength(#values()) }", + """ + import java.util.Arrays; + + @EvaluatedExpressionContext + class Context { + int[][] values() { + return new int[][]{new int[]{1, 2, 3}, new int[]{1, 2, 3, 4}}; + } + + int countLength(int[][] array) { + return Arrays.stream(array) + .map(a -> a.length) + .reduce(0, Integer::sum); + } + } + """) + + + expect: + expr1 instanceof Integer && expr1 == 5 + expr2 instanceof Integer && expr2 == 7 + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy new file mode 100644 index 00000000000..a88ce9b0f00 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy @@ -0,0 +1,56 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class CompoundExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test compound expressions"() { + given: + List results = evaluateMultiple( + "a #{1}#{'b'} #{3}", + "#{1 + 2}#{2 + 3}", + "#{ '5' } s", + "a#{ null }b" + ) + + expect: + results[0] instanceof String && results[0] == 'a 1b 3' + results[1] instanceof String && results[1] == '35' + results[2] instanceof String && results[2] == '5 s' + results[3] instanceof String && results[3] == 'anullb' + } + + void "test string expressions in arrays"() { + Object expr1 = buildSingleExpressionFromClass("test.Expr", """ + package test; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @Requires(env = {"#{ 'a' }", "b", "#{ 'c' + 'd' }"}) + class Expr { + } + """) + .evaluate() + + expect: + expr1 instanceof Object[] && Arrays.equals((Object[]) expr1, new Object[]{'a', 'b', 'cd'}) + } + + void "test mixed expressions in arrays"() { + Object expr1 = buildSingleExpressionFromClass("test.Expr", """ + package test; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @Requires(env = {"#{ 1 }", "b", "#{ 15l }", "#{ 'c' }"}) + class Expr { + } + """) + .evaluate() + + expect: + expr1 instanceof Object[] && Arrays.equals((Object[]) expr1, new Object[]{1, 'b', 15l, 'c'}) + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy new file mode 100644 index 00000000000..85d3dc50f01 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy @@ -0,0 +1,142 @@ +package io.micronaut.expressions; + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec; + +class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec +{ + void "test context method calls"() { + given: + Object expr1 = evaluateAgainstContext("#{ #getIntValue() }", + """ + @EvaluatedExpressionContext + class ExpressionContext { + int getIntValue() { + return 15; + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #getStringValue().toUpperCase() }", + """ + @EvaluatedExpressionContext + class ExpressionContext { + String getStringValue() { + return "test"; + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #randomizer().nextInt(10) }", + """ + import java.util.Random; + + @EvaluatedExpressionContext + class ExpressionContext { + Random randomizer() { + return new Random(); + } + } + """) + + Object expr4 = evaluateAgainstContext("#{ #lowercase('TEST') }", + """ + import java.util.Random; + + @EvaluatedExpressionContext + class ExpressionContext { + String lowercase(String value) { + return value.toLowerCase(); + } + } + """) + + Object expr5 = evaluateAgainstContext("#{ #transform(#getName(), #getRepeat(), #toLower()) }", + """ + import java.util.Random; + + @EvaluatedExpressionContext + class FirstContext { + String transform(String value, int repeat, Boolean toLower) { + return (toLower ? value.toLowerCase() : value).repeat(repeat); + } + } + + @EvaluatedExpressionContext + class SecondContext { + String getName() { + return "TEST"; + } + } + + @EvaluatedExpressionContext + class ThirdContext { + Integer getRepeat() { + return 2; + } + } + + @EvaluatedExpressionContext + class FourthContext { + boolean toLower() { + return true; + } + } + """) + + Object expr6 = evaluateAgainstContext("#{ #getTestObject().name }", + """ + import java.util.Random; + + @EvaluatedExpressionContext + class ExpressionContext { + TestObject getTestObject() { + return new TestObject(); + } + } + + class TestObject { + String getName() { + return "name"; + } + } + """) + + Object expr7 = evaluateAgainstContext("#{ #values().get(#random(#values())) }", + """ + import java.util.Random; + import java.util.List; + import java.util.Collection; + import java.util.concurrent.ThreadLocalRandom; + + @EvaluatedExpressionContext + class ExpressionContext { + TestObject getTestObject() { + return new TestObject(); + } + + List values() { + return List.of(1, 2, 3); + } + + int random(Collection values) { + return ThreadLocalRandom.current().nextInt(values.size()); + } + } + + class TestObject { + String getName() { + return "name"; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 15 + expr2 instanceof String && expr2 == "TEST" + expr3 instanceof Integer && expr3 >= 0 && expr3 < 10 + expr4 instanceof String && expr4 == "test" + expr5 instanceof String && expr5 == "testtest" + expr6 instanceof String && expr6 == "name" + expr7 instanceof Integer && (expr7 == 1 || expr7 == 2 || expr7 == 3) + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy new file mode 100644 index 00000000000..6c24894b1d1 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -0,0 +1,61 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test context property access"() { + given: + Object expr1 = evaluateAgainstContext("#{ #intValue }", + """ + @EvaluatedExpressionContext + class ExpressionContext { + int getIntValue() { + return 15; + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #boolean }", + """ + @EvaluatedExpressionContext + class ExpressionContext { + Boolean isBoolean() { + return false; + } + } + """) + + Object expr3 = evaluateAgainstContext("#{ #stringValue }", + """ + @EvaluatedExpressionContext + class ExpressionContext { + String getStringValue() { + return "test value"; + } + } + """) + + Object expr4 = evaluateAgainstContext("#{ #customClass.customProperty }", + """ + @EvaluatedExpressionContext + class ExpressionContext { + CustomClass getCustomClass() { + return new CustomClass(); + } + } + + class CustomClass { + String getCustomProperty() { + return "custom property"; + } + } + """) + + expect: + expr1 instanceof Integer && expr1 == 15 + expr2 instanceof Boolean && expr2 == false + expr3 instanceof String && expr3 == "test value" + expr4 instanceof String && expr4 == "custom property" + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/LiteralExpressionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/LiteralExpressionSpec.groovy new file mode 100644 index 00000000000..bceecd8a9ca --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/LiteralExpressionSpec.groovy @@ -0,0 +1,95 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class LiteralExpressionSpec extends AbstractEvaluatedExpressionsSpec { + + void "test literals"() { + given: + List results = evaluateMultiple( + // null + "#{ null }", // 0 + + // string literals + "#{ 'string literal' }", // 1 + "#{ 'testValue' }", // 2 + + // bool literals + "#{ false }", // 3 + "#{ true }", // 4 + + // int literals + "#{ 15 }", // 5 + "#{ 0XAB013 }", // 6 + "#{ 0xFF }", // 7 + "#{ 291231 }", // 8 + "#{ 0 }", // 9 + "#{ 0x0 }", // 10 + "#{ 00 }", // 11 + + // long literals + "#{ 0xFFL }", // 12 + "#{ 0x0123l }", // 13 + "#{ 102L }", // 14 + "#{ 99l }", // 15 + "#{ 0L }", // 16 + + // float literals + "#{ 123.e+14f }", // 17 + "#{ 123.f }", // 18 + "#{ 123.F }", // 19 + "#{ .123f }", // 20 + "#{ 19F }", // 21 + + // double literals + "#{ 123. }", // 22 + "#{ 123.321 }", // 23 + "#{ 123.d }", // 24 + "#{ 123.D }", // 25 + "#{ .123 }", // 26 + "#{ 123D }", // 27 + "#{ 1E-7 }", // 28 + "#{ 1E+1d }", // 29 + "#{ 2e-1 }", // 30 + ) + + expect: + results[0] == null + + results[1] instanceof String && results[1] == 'string literal' + results[2] instanceof String && results[2] == 'testValue' + + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + + results[5] instanceof Integer && results[5] == 15 + results[6] instanceof Integer && results[6] == 0XAB013 + results[7] instanceof Integer && results[7] == 0xFF + results[8] instanceof Integer && results[8] == 291231 + results[9] instanceof Integer && results[9] == 0 + results[10] instanceof Integer && results[10] == 0x0 + results[11] instanceof Integer && results[11] == 00 + + results[12] instanceof Long && results[12] == 0xFFL + results[13] instanceof Long && results[13] == 0x0123l + results[14] instanceof Long && results[14] == 102L + results[15] instanceof Long && results[15] == 99L + results[16] instanceof Long && results[16] == 0L + + results[17] instanceof Float + results[18] instanceof Float + results[19] instanceof Float + results[20] instanceof Float && results[20] == .123f + results[21] instanceof Float && results[21] == 19F + + results[22] instanceof Double && results[22] == Double.valueOf("123.") + results[23] instanceof Double && results[23] == 123.321 + results[24] instanceof Double && results[24] == Double.valueOf("123.d") + results[25] instanceof Double && results[25] == Double.valueOf("123.D") + results[26] instanceof Double && results[26] == .123 + results[27] instanceof Double && results[27] == 123D + results[28] instanceof Double && results[28] == 1E-7 + results[29] instanceof Double && results[29] == 1E+1d + results[30] instanceof Double && results[30] == 2e-1 + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy new file mode 100644 index 00000000000..fb8a260c983 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy @@ -0,0 +1,33 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec; + + +class MethodArgumentEvaluationContextExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test method argument access"() { + given: + Object expr1 = buildSingleExpressionFromClass("test.Expr", """ + package test; + import io.micronaut.context.annotation.Executable; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + class Expr { + + @Executable + @Requires(value = "#{ #second + 'abc' }") + void test(String first, String second) { + } + } + + + """) + .evaluate("arg0", "arg1") + + expect: + expr1 instanceof String && expr1 == 'arg1abc' + } + +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy new file mode 100644 index 00000000000..ab0fffd64f0 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy @@ -0,0 +1,904 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec +import org.codehaus.groovy.control.CompilationFailedException + +class OperatorExpressionSpec extends AbstractEvaluatedExpressionsSpec { + + void "test '/' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 / 5 }", + "#{ 5 / 10 }", + "#{ 10 div 5 }", + "#{ 5 div 10 }", + "#{ 10 / 5 / 2 }", + + // long + "#{ 10 / 5L }", + "#{ 5l / 10 }", + "#{ 10L div 5l }", + "#{ 5 div 10L }", + "#{ 10L div 5 / 2 }", + + // float + "#{ 10f / 5 }", + "#{ 5 / 10f }", + "#{ 10f div 5F }", + "#{ 5 div 10F }", + + // double + "#{ 10d / 5 }", + "#{ 5 / 10d }", + "#{ 10d div 5D }", + "#{ 5 div 10D }", + + // mixed + "#{ 10d / 5f }", + "#{ 5L / 10d }", + "#{ 10L div 5f }" + ) + + expect: + results[0] instanceof Integer && results[0] == 2 + results[1] instanceof Integer && results[1] == 0 + results[2] instanceof Integer && results[2] == 2 + results[3] instanceof Integer && results[3] == 0 + results[4] instanceof Integer && results[4] == 1 + + results[5] instanceof Long && results[5] == 2 + results[6] instanceof Long && results[6] == 0 + results[7] instanceof Long && results[7] == 2 + results[8] instanceof Long && results[8] == 0 + results[9] instanceof Long && results[9] == 1 + + results[10] instanceof Float && results[10] == 10f / 5 + results[11] instanceof Float && results[11] == 5 / 10f + results[12] instanceof Float && results[12] == 10f / 5F + results[13] instanceof Float && results[13] == 5 / 10F + + results[14] instanceof Double && results[14] == 10d / 5 + results[15] instanceof Double && results[15] == 5 / 10d + results[16] instanceof Double && results[16] == 10d / 5D + results[17] instanceof Double && results[17] == 5 / 10D + + results[18] instanceof Double && results[18] == 10d / 5f + results[19] instanceof Double && results[19] == 5L / 10d + results[20] instanceof Float && results[20] == 10L / 5f + } + + void "test '%' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 % 5 }", // 0 + "#{ 5 % 10 }", // 1 + "#{ 10 mod 5 }", // 2 + "#{ 5 mod 10 }", // 3 + "#{ 10 % 5 % 3}", // 4 + + // long + "#{ 10 % 5L }", // 5 + "#{ 5l % 10 }", // 6 + "#{ 10L mod 5l }", // 7 + "#{ 5 mod 10L }", // 8 + "#{ 13L % 5 mod 2 }", // 9 + + // float + "#{ 10f % 5 }", // 10 + "#{ 5 % 10f }", // 11 + "#{ 10f mod 5F }", // 12 + "#{ 5 mod 10F }", // 13 + + // double + "#{ 10d % 5 }", // 14 + "#{ 5 % 10d }", // 15 + "#{ 10d mod 5D }", // 16 + "#{ 5 mod 10D }", // 17 + + // mixed + "#{ 10d % 5f }", // 18 + "#{ 5L % 10d }", // 19 + "#{ 10L mod 5f }" // 20 + ) + + + expect: + results[0] instanceof Integer && results[0] == 0 + results[1] instanceof Integer && results[1] == 5 + results[2] instanceof Integer && results[2] == 0 + results[3] instanceof Integer && results[3] == 5 + results[4] instanceof Integer && results[4] == 0 + + results[5] instanceof Long && results[5] == 0 + results[6] instanceof Long && results[6] == 5 + results[7] instanceof Long && results[7] == 0 + results[8] instanceof Long && results[8] == 5 + results[9] instanceof Long && results[9] == 1 + + results[10] instanceof Float && results[10] == 10f % 5 + results[11] instanceof Float && results[11] == 5 % 10f + results[12] instanceof Float && results[12] == 10f % 5F + results[13] instanceof Float && results[13] == 5 % 10F + + results[14] instanceof Double && results[14] == 10d % 5 + results[15] instanceof Double && results[15] == 5 % 10d + results[16] instanceof Double && results[16] == 10d % 5D + results[17] instanceof Double && results[17] == 5 % 10D + + results[18] instanceof Double && results[18] == 10d % 5f + results[19] instanceof Double && results[19] == 5L % 10d + results[20] instanceof Float && results[20] == 10L % 5f + } + + void "test -' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 - 5 }", // 0 + "#{ 5 - 10 }", // 1 + "#{ 25 - 5 - 10 }", // 2 + + // long + "#{ 10 - 5L }", // 3 + "#{ 5l - 10 }", // 4 + "#{ 10L - 5l }", // 5 + "#{ 5 - 10L }", // 6 + + + // float + "#{ 10f - 5 }", // 7 + "#{ 5 - 10f }", // 8 + "#{ 10f - 5F }", // 9 + "#{ 5 - 10F }", // 10 + + // double + "#{ 10d - 5 }", // 11 + "#{ 5 - 10d }", // 12 + "#{ 10d - 5D }", // 13 + "#{ 5 - 10D }", // 14 + + // mixed + "#{ 10d - 5f }", // 15 + "#{ 5L - 10d }", // 16 + "#{ 10L - 5f }" // 17 + ) + + expect: + results[0] instanceof Integer && results[0] == 5 + results[1] instanceof Integer && results[1] == -5 + results[2] instanceof Integer && results[2] == 10 + + results[3] instanceof Long && results[3] == 10 - 5L + results[4] instanceof Long && results[4] == 5l - 10 + results[5] instanceof Long && results[5] == 10L - 5l + results[6] instanceof Long && results[6] == 5 - 10L + + results[7] instanceof Float && results[7] == 10f - 5 + results[8] instanceof Float && results[8] == 5 - 10f + results[9] instanceof Float && results[9] == 10f - 5F + results[10] instanceof Float && results[10] == 5 - 10F + + results[11] instanceof Double && results[11] == 10d - 5 + results[12] instanceof Double && results[12] == 5 - 10d + results[13] instanceof Double && results[13] == 10d - 5D + results[14] instanceof Double && results[14] == 5 - 10D + + results[15] instanceof Double && results[15] == 10d - 5f + results[16] instanceof Double && results[16] == 5L - 10d + results[17] instanceof Float && results[17] == 10L - 5f + } + + void "test '*' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 * 5 }", // 0 + "#{ -8 * 10 * 5 }", // 1 + + // long + "#{ 10 * 5L }", // 2 + "#{ 5l * 10 }", // 3 + "#{ 10L * 5l }", // 4 + "#{ 5 * 10L }", // 5 + + // float + "#{ 10f * 5 }", // 6 + "#{ 5 * 10f }", // 7 + "#{ 10f * 5F }", // 8 + "#{ 5 * 10F }", // 9 + + // double + "#{ 10d * 5 }", // 10 + "#{ 5 * 10d }", // 11 + "#{ 10d * 5D }", // 12 + "#{ 5 * 10D }", // 13 + + // mixed + "#{ 10d * 5f }", // 14 + "#{ 5L * 10d }", // 15 + "#{ 10L * 5f }" // 16 + ) + + expect: + results[0] instanceof Integer && results[0] == 50 + results[1] instanceof Integer && results[1] == -8 * 10 * 5 + + results[2] instanceof Long && results[2] == 10 * 5L + results[3] instanceof Long && results[3] == 5l * 10 + results[4] instanceof Long && results[4] == 10L * 5l + results[5] instanceof Long && results[5] == 5 * 10L + + results[6] instanceof Float && results[6] == 10f * 5 + results[7] instanceof Float && results[7] == 5 * 10f + results[8] instanceof Float && results[8] == 10f * 5F + results[9] instanceof Float && results[9] == 5 * 10F + + results[10] instanceof Double && results[10] == 10d * 5 + results[11] instanceof Double && results[11] == 5 * 10d + results[12] instanceof Double && results[12] == 10d * 5D + results[13] instanceof Double && results[13] == 5 * 10D + + results[14] instanceof Double && results[14] == 10d * 5f + results[15] instanceof Double && results[15] == 5L * 10d + results[16] instanceof Float && results[16] == 10L * 5f + } + + void "test '+' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 + 5 }", // 0 + "#{ -8 + 10 + 5 }", // 1 + + // long + "#{ 10 + 5L }", // 2 + "#{ 5l + 10 }", // 3 + "#{ 10L + 5l }", // 4 + "#{ 5 + 10L }", // 5 + + // float + "#{ 10f + 5 }", // 6 + "#{ 5 + 10f }", // 7 + "#{ 10f + 5F }", // 8 + "#{ 5 + 10F }", // 9 + + // double + "#{ 10d + 5 }", // 10 + "#{ 5 + 10d }", // 11 + "#{ 10d + 5D }", // 12 + "#{ 5 + 10D }", // 13 + + // mixed + "#{ 10d + 5f }", // 14 + "#{ 5L + 10d }", // 15 + "#{ 10L + 5f }", // 16 + + // string + "#{ '1' + '2' }", // 17 + "#{ '1' + null }", // 18 + "#{ null + '1' }", // 19 + "#{ null + '1' + null + 2D }", // 20 + "#{ 15 + 'str' + 2L }", // 21 + "#{ 2f + 'str' + 2 }", // 22 + "#{ .014 + 'str' + 2L + 'test' }", // 23 + "#{ 1 + 2 + 'str' + 2L + 'test' }", // 24 + "#{ 1 + 2 + 3 + 'str' }", // 25 + "#{ 1 + 2 - 3 + 'str' }" // 26 + ) + + expect: + results[0] instanceof Integer && results[0] == 10 + 5 + results[1] instanceof Integer && results[1] == -8 + 10 + 5 + + results[2] instanceof Long && results[2] == 10 + 5L + results[3] instanceof Long && results[3] == 5l + 10 + results[4] instanceof Long && results[4] == 10L + 5l + results[5] instanceof Long && results[5] == 5 + 10L + + results[6] instanceof Float && results[6] == 10f + 5 + results[7] instanceof Float && results[7] == 5 + 10f + results[8] instanceof Float && results[8] == 10f + 5F + results[9] instanceof Float && results[9] == 5 + 10F + + results[10] instanceof Double && results[10] == 10d + 5 + results[11] instanceof Double && results[11] == 5 + 10d + results[12] instanceof Double && results[12] == 10d + 5D + results[13] instanceof Double && results[13] == 5 + 10D + + results[14] instanceof Double && results[14] == 10d + 5f + results[15] instanceof Double && results[15] == 5L + 10d + results[16] instanceof Float && results[16] == 10L + 5f + + results[17] instanceof String && results[17] == '12' + results[18] instanceof String && results[18] == '1null' + results[19] instanceof String && results[19] == 'null1' + results[20] instanceof String && results[20] == 'null1null2.0' + results[21] instanceof String && results[21] == '15str2' + results[22] instanceof String && results[22] == '2.0str2' + results[23] instanceof String && results[23] == '0.014str2test' + results[24] instanceof String && results[24] == '3str2test' + results[25] instanceof String && results[25] == '6str' + results[26] instanceof String && results[26] == '0str' + } + + void "test '>' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 > 5 }", // 0 + "#{ -8 > -3 }", // 1 + + // long + "#{ 10L > 5l }", // 2 + "#{ 5 > 10L }", // 3 + "#{ 10l > 5 }", // 4 + + // double + "#{ 10d > 5 }", // 5 + "#{ 5 > 10d }", // 6 + "#{ 10D > 5d }", // 7 + "#{ -.4 > 5d }", // 8 + "#{ .123 > .1 }", // 9 + "#{ .123 > .1229 }", // 10 + + // float + "#{ 10f > 5 }", // 11 + "#{ 5 > 10f }", // 12 + "#{ 10F > 5f }", // 13 + "#{ -.4f > 5f }", // 14 + "#{ .123f > -5f }", // 15 + + // mixed + "#{ 10f > 5d }", // 16 + "#{ 5L > 10f }", // 17 + "#{ 10L > 5D }", // 18 + "#{ -.4 > 5l }", // 19 + "#{ .123f > -5 }", // 20 + "#{ 10L > 11 }" // 21 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == true + results[8] instanceof Boolean && results[8] == false + results[9] instanceof Boolean && results[9] == true + results[10] instanceof Boolean && results[10] == true + + results[11] instanceof Boolean && results[11] == true + results[12] instanceof Boolean && results[12] == false + results[13] instanceof Boolean && results[13] == true + results[14] instanceof Boolean && results[14] == false + results[15] instanceof Boolean && results[15] == true + + results[16] instanceof Boolean && results[16] == true + results[17] instanceof Boolean && results[17] == false + results[18] instanceof Boolean && results[18] == true + results[19] instanceof Boolean && results[19] == false + results[20] instanceof Boolean && results[20] == true + results[21] instanceof Boolean && results[21] == false + } + + void "test '<' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 < 5 }", // 0 + "#{ -8 < -3 }", // 1 + + // long + "#{ 10L < 5l }", // 2 + "#{ 5 < 10L }", // 3 + "#{ 10l < 5 }", // 4 + + // double + "#{ 10d < 5 }", // 5 + "#{ 5 < 10d }", // 6 + "#{ 10D < 5d }", // 7 + "#{ -.4 < 5d }", // 8 + "#{ .123 < .1 }", // 9 + "#{ .1229 < .123 }", // 10 + + // float + "#{ 10f < 5 }", // 11 + "#{ 5 < 10f }", // 12 + "#{ 10F < 5f }", // 13 + "#{ -.4f < 5f }", // 14 + "#{ .123f < -5f }", // 15 + + // mixed + "#{ 10f < 5d }", // 16 + "#{ 5L < 10f }", // 17 + "#{ 10L < 5D }", // 18 + "#{ -.4 < 5l }", // 19 + "#{ .123f < -5 }", // 20 + "#{ 10L < 11 }" // 21 + ) + + expect: + results[0] instanceof Boolean && results[0] == false + results[1] instanceof Boolean && results[1] == true + + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == false + + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == true + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + results[10] instanceof Boolean && results[10] == true + + results[11] instanceof Boolean && results[11] == false + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == false + results[14] instanceof Boolean && results[14] == true + results[15] instanceof Boolean && results[15] == false + + results[16] instanceof Boolean && results[16] == false + results[17] instanceof Boolean && results[17] == true + results[18] instanceof Boolean && results[18] == false + results[19] instanceof Boolean && results[19] == true + results[20] instanceof Boolean && results[20] == false + results[21] instanceof Boolean && results[21] == true + } + + void "test '>=' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 >= 5 }", // 0 + "#{ -8 >= -3 }", // 1 + "#{ 3 >= 3 }", // 2 + + // long + "#{ 10L >= 5l }", // 3 + "#{ 5 >= 10L }", // 4 + "#{ 10l >= 5 }", // 5 + "#{ 5l >= 5 }", // 6 + + // double + "#{ 10d >= 5 }", // 7 + "#{ 5 >= 10d }", // 8 + "#{ 10D >= 5d }", // 9 + "#{ -.4 >= 5d }", // 10 + "#{ .123 >= .1 }", // 11 + + // float + "#{ 10f >= 5 }", // 12 + "#{ 5 >= 10f }", // 13 + "#{ 10F >= 5f }", // 14 + "#{ -.4f >= 5f }", // 15 + "#{ .123f >= -5f }", // 16 + + // mixed + "#{ 10f >= 5d }", // 17 + "#{ 5L >= 10f }", // 18 + "#{ 10L >= 5D }", // 19 + "#{ -.4 >= 5l }", // 20 + "#{ .123f >= -5 }", // 21 + "#{ 12L >= 11 }", // 22 + "#{ 11 >= 11L }" // 23 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == true + + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == false + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == true + + results[7] instanceof Boolean && results[7] == true + results[8] instanceof Boolean && results[8] == false + results[9] instanceof Boolean && results[9] == true + results[10] instanceof Boolean && results[10] == false + results[11] instanceof Boolean && results[11] == true + + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == false + results[14] instanceof Boolean && results[14] == true + results[15] instanceof Boolean && results[15] == false + results[16] instanceof Boolean && results[16] == true + + results[17] instanceof Boolean && results[17] == true + results[18] instanceof Boolean && results[18] == false + results[19] instanceof Boolean && results[19] == true + results[20] instanceof Boolean && results[20] == false + results[21] instanceof Boolean && results[21] == true + results[22] instanceof Boolean && results[22] == true + results[23] instanceof Boolean && results[23] == true + } + + void "test '<=' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 <= 5 }", // 0 + "#{ -8 <= -3 }", // 1 + "#{ 3 <= 3 }", // 2 + + // long + "#{ 10L <= 5l }", // 3 + "#{ 5 <= 10L }", // 4 + "#{ 10l <= 5 }", // 5 + "#{ 5l <= 5 }", // 6 + + // double + "#{ 10d <= 5 }", // 7 + "#{ 5 <= 10d }", // 8 + "#{ 10D <= 5d }", // 9 + "#{ -.4 <= 5d }", // 10 + "#{ .123 <= .1 }", // 11 + + // float + "#{ 10f <= 5 }", // 12 + "#{ 5 <= 10f }", // 13 + "#{ 10F <= 5f }", // 14 + "#{ -.4f <= 5f }", // 15 + "#{ .123f <= -5f }", // 16 + + // mixed + "#{ 10f <= 5d }", // 17 + "#{ 5L <= 10f }", // 18 + "#{ 10L <= 5D }", // 19 + "#{ -.4 <= 5l }", // 20 + "#{ .123f <= -5 }", // 21 + "#{ 12L <= 11 }", // 22 + "#{ 11 <= 11L }" // 23 + ) + + expect: + results[0] instanceof Boolean && results[0] == false + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == true + + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + results[10] instanceof Boolean && results[10] == true + results[11] instanceof Boolean && results[11] == false + + results[12] instanceof Boolean && results[12] == false + results[13] instanceof Boolean && results[13] == true + results[14] instanceof Boolean && results[14] == false + results[15] instanceof Boolean && results[15] == true + results[16] instanceof Boolean && results[16] == false + + results[17] instanceof Boolean && results[17] == false + results[18] instanceof Boolean && results[18] == true + results[19] instanceof Boolean && results[19] == false + results[20] instanceof Boolean && results[20] == true + results[21] instanceof Boolean && results[21] == false + results[22] instanceof Boolean && results[22] == false + results[23] instanceof Boolean && results[23] == true + } + + void "test '==' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 == 10}", // 0 + "#{ 8 == 11 }", // 1 + "#{ -3 == 3}", // 2 + "#{ -15 == -15 }", // 3 + + // long + "#{ 10L == 10L}", // 4 + "#{ 8L == 11L }", // 5 + "#{ -3L == 3L }", // 6 + "#{ -15L == -15L }", // 7 + + // float + "#{ 1f == 1f}", // 8 + "#{ 0f == 1f }", // 9 + + // double + "#{ 1d == 1.0 }", // 10 + "#{ .0 == 1d }", // 11 + + // string + "#{ 'str' == 'str' }", // 12 + "#{ 'str1' == 'str2' }" // 13 + ) + + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == true + + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + + results[10] instanceof Boolean && results[10] == true + results[11] instanceof Boolean && results[11] == false + + results[12] instanceof Boolean && results[12] == true + results[13] instanceof Boolean && results[13] == false + } + + void "test '!=' operator"() { + given: + List results = evaluateMultiple( + // int + "#{ 10 != 10}", // 0 + "#{ 8 != 11 }", // 1 + "#{ -3 != 3}", // 2 + "#{ -15 != -15 }", // 3 + + // long + "#{ 10L != 10L}", // 4 + "#{ 8L != 11L }", // 5 + "#{ -3L != 3L }", // 6 + "#{ -15L != -15L }", // 7 + + // float + "#{ 1f != 1f}", // 8 + "#{ 0f != 1f }", // 9 + + // double + "#{ 1d != 1.0 }", // 10 + "#{ .0 != 1d }", // 11 + + // string + "#{ 'str' != 'str' }", // 12 + "#{ 'str1' != 'str2' }" // 13 + ) + + expect: + results[0] instanceof Boolean && results[0] == false + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == false + + results[4] instanceof Boolean && results[4] == false + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == true + results[7] instanceof Boolean && results[7] == false + + results[8] instanceof Boolean && results[8] == false + results[9] instanceof Boolean && results[9] == true + + results[10] instanceof Boolean && results[10] == false + results[11] instanceof Boolean && results[11] == true + + results[12] instanceof Boolean && results[12] == false + results[13] instanceof Boolean && results[13] == true + } + + // '^' operator in expressions means power operation + void "test '^' operator"() { + given: + List results = evaluateMultiple( + "#{ 2^3 }", // 0 + "#{ 3L ^ 2}", // 1 + "#{ 2.0^0}", // 2 + "#{ 2f ^ 2L}", // 3 + "#{ 2^2 ^2}", // 4 + "#{ (2 ^ 32)^2}" // 5 + ) + + expect: + results[0] instanceof Long && results[0] == 8 + results[1] instanceof Long && results[1] == 9 + results[2] instanceof Double && results[2] == 1.0 + results[3] instanceof Double && results[3] == 4.0 + results[4] instanceof Long && results[4] == 16 + results[5] instanceof Long && results[5] == 9223372036854775807L + } + + void "test '&&' operator"() { + given: + List results = evaluateMultiple( + "#{ true && true }", // 0 + "#{ true && false }", // 1 + "#{ false && true }", // 2 + "#{ false && false }", // 3 + "#{ true and true }", // 4 + "#{ true and false }", // 5 + "#{ false and true }", // 6 + "#{ false and false }", // 7 + "#{ true and true && true }", // 8 + "#{ true && true and false }" // 9 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == false + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == false + } + + void "test '||' operator"() { + given: + List results = evaluateMultiple( + "#{ true || true }", // 0 + "#{ true || false }", // 1 + "#{ false || true }", // 2 + "#{ false || false }", // 3 + "#{ true or true }", // 4 + "#{ true or false }", // 5 + "#{ false or true }", // 6 + "#{ false or false }", // 7 + "#{ true or true || true }", // 8 + "#{ true or true or false }", // 9 + "#{ true || false or false }", // 10 + "#{ false or false || false or true }" // 11 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == false + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == true + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == true + results[9] instanceof Boolean && results[9] == true + results[10] instanceof Boolean && results[10] == true + results[11] instanceof Boolean && results[11] == true + } + + void "test 'instanceof' operator"() { + given: + List results = evaluateMultiple( + "#{ 1 instanceof T(java.lang.Integer) }", // 0 + "#{ 1 instanceof T(Integer) }", // 1 + "#{ 1L instanceof T(java.lang.Long) }", // 2 + "#{ 1f instanceof T(java.lang.Float) }", // 3 + "#{ 1d instanceof T(java.lang.Double) }", // 4 + "#{ 'str' instanceof T(java.lang.String) }", // 5 + "#{ 'str' instanceof T(Double) }", // 6 + "#{ 1L instanceof T(Integer) }", // 7 + "#{ 1f instanceof T(Double) }" // 8 + ) + + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == true + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == true + results[6] instanceof Boolean && results[6] == false + results[7] instanceof Boolean && results[7] == false + results[8] instanceof Boolean && results[8] == false + } + + void "test 'matches' operator"() { + given: + List results = evaluateMultiple( + "#{ '123' matches '\\\\d+' }", // 0 + "#{ '5.0' matches '[0-9]*\\\\.[0-9]+(d|D)?' }", // 1 + "#{ '5.0' matches '[0-9]*\\\\.[0-9]+(d|D)' }", // 2 + "#{ 'AbC' matches '[A-Za-z]*' }", // 3 + "#{ ' ' matches '\\\\s*' }", // 4 + "#{ '' matches '\\\\s+' }" // 5 + ) + + expect: + results[0] instanceof Boolean && results[0] == true + results[1] instanceof Boolean && results[1] == true + results[2] instanceof Boolean && results[2] == false + results[3] instanceof Boolean && results[3] == true + results[4] instanceof Boolean && results[4] == true + results[5] instanceof Boolean && results[5] == false + } + + void "test '!' operator"() { + given: + List results = evaluateMultiple( + "#{ !true }", // 0 + "#{ !false }", // 1 + "#{ !!true }", // 2 + "#{ !!false }", // 3 + "#{ !!!false }", // 4 + "#{ !!!true }" // 5 + ) + + expect: + results[0] == false + results[1] == true + results[2] == true + results[3] == false + results[4] == true + results[5] == false + } + + void "test unary '-'"() { + given: + List results = evaluateMultiple( + "#{ -5 }", + "#{ -(-3) }", + "#{ -3L }", + "#{ -1.0D }" + ) + + expect: + results[0] instanceof Integer && results[0] == -5 + results[1] instanceof Integer && results[1] == 3 + results[2] instanceof Long && results[2] == -3 + results[3] instanceof Double && results[3] == -1.0 + + when: + evaluate("#{ --5 }") + + then: + thrown(Throwable.class) + } + + void "test unary '+'"() { + given: + List results = evaluateMultiple( + "#{ +5 }", + "#{ +(+3) }", + "#{ +3L }", + "#{ +1.0D }" + ) + + expect: + results[0] instanceof Integer && results[0] == 5 + results[1] instanceof Integer && results[1] == 3 + results[2] instanceof Long && results[2] == 3 + results[3] instanceof Double && results[3] == 1.0 + + when: + evaluate("#{ ++5 }") + + then: + thrown(Throwable.class) + } + + void "test parenthesized expressions"() { + given: + List results = evaluateMultiple( + "#{ (2 + 3) * 5 }", // 0 + "#{ 2 * ( 3 + 1) }", // 1 + "#{ (-1 + 9) * (2 + 3) }", // 2 + "#{ 2 ^ ( 2 + 2) }", // 3 + "#{ 5 * ( 2d * (1 - 1)) }", // 4 + "#{ 5L * ( 2d + 2f ) }" // 5 + ) + + expect: + results[0] instanceof Integer && results[0] == 25 + results[1] instanceof Integer && results[1] == 8 + results[2] instanceof Integer && results[2] == 40 + results[3] instanceof Long && results[3] == 16 + results[4] instanceof Double && results[4] == 0 + results[5] instanceof Double && results[5] == 20.0 + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy new file mode 100644 index 00000000000..91e81a9ce52 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy @@ -0,0 +1,36 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class TernaryOperationExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test ternary operator"() { + given: + List results = evaluateMultiple( + "#{ 15 > 10 ? 'a' : 'b' }", + "#{ 15 == 10 ? 'a' : 'b' }", + "#{ 10 > 9 ? 'a' + 'b' : 'b' + 'a' }" + ) + + expect: + results[0] instanceof String && results[0] == 'a' + results[1] instanceof String && results[1] == 'b' + results[2] instanceof String && results[2] == 'ab' + } + + void "test ternary type resolution"() { + given: + List results = evaluateMultiple( + "#{ (15 > 10 ? 'a' : 'b').length() }", + "#{ 15 > 10 ? 15L : 'test' }", + "#{ (15 > 10 ? 15L : 10) + 8}", + "#{ 10 > 15 ? 15L : true}" + ) + + expect: + results[0] instanceof Integer && results[0] == 1 + results[1] instanceof Long && results[1] == 15 + results[2] instanceof Long && results[2] == 23 + results[3] instanceof Boolean && results[3] == true + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy new file mode 100644 index 00000000000..dbe10d12a4f --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy @@ -0,0 +1,168 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec +import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.NoSuchBeanException + +class TestExpressionsInjectionSpec extends AbstractEvaluatedExpressionsSpec { + + void "test expression field injection"() { + given: + def ctx = buildContext(""" + package test; + + import jakarta.inject.Singleton; + import io.micronaut.context.annotation.Value; + + @Singleton + class Expr { + @Value("#{ 15 ^ 2 }") + private int intValue; + + @Value("#{ 100 }") + protected Integer boxedIntValue; + + @Value("#{ T(String).join(',', 'a', 'b', 'c') }") + public String strValue; + + @Value("#{null}") + private Object nullValue; + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.intValue == 225 + bean.boxedIntValue == 100 + bean.strValue == 'a,b,c' + bean.nullValue == null + + cleanup: + ctx.close() + } + + void "test expression constructor injection"() { + given: + def ctx = buildContext(""" + package test; + + import jakarta.inject.Singleton; + import io.micronaut.context.annotation.Value; + + @Singleton + class Expr { + private final Integer wrapper; + private final Integer primitive; + + public Expr(@Value("#{ 25 }") Integer wrapper, + @Value("#{ 23 }") int primitive) { + this.wrapper = wrapper; + this.primitive = primitive; + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.wrapper == 25 + bean.primitive == 23 + + cleanup: + ctx.close() + } + + void "test expression setter injection"() { + given: + def ctx = buildContext(""" + package test; + + import jakarta.inject.Inject; + import jakarta.inject.Singleton; + import io.micronaut.context.annotation.Value; + + @Singleton + class Expr { + private Integer wrapper; + private int primitive; + + @Inject + public void setWrapper(@Value("#{ 25 }") Integer value) { + this.wrapper = value; + } + + @Inject + public void setPrimitive(@Value("#{ 23 }") int value) { + this.primitive = value; + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.wrapper == 25 + bean.primitive == 23 + + cleanup: + ctx.close() + } + + void "test expressions in @Factory injection"() { + given: + def ctx = buildContext(""" + package test; + + import io.micronaut.context.annotation.Bean; + import io.micronaut.context.annotation.EvaluatedExpressionContext; + import io.micronaut.context.annotation.Factory; + import jakarta.inject.Inject; + import jakarta.inject.Singleton; + import io.micronaut.context.annotation.Value; + + @EvaluatedExpressionContext + class TestContext { + public String getContextValue() { + return "context value"; + } + } + + class Expr { + private final Integer wrapper; + private final int primitive; + private final String contextValue; + + Expr(Integer wrapper, int primitive, String contextValue) { + this.wrapper = wrapper; + this.primitive = primitive; + this.contextValue = contextValue; + } + } + + @Factory + class TestFactory { + @Bean + public Expr factoryBean(@Value("#{ 25 }") Integer wrapper, + @Value("#{ 23 }") int primitive, + @Value("#{ #contextValue }") String contextValue) { + return new Expr(wrapper, primitive, contextValue); + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.wrapper == 25 + bean.primitive == 23 + bean.contextValue == "context value" + + cleanup: + ctx.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy new file mode 100644 index 00000000000..dbd79ae465d --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy @@ -0,0 +1,129 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec +import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.NoSuchBeanException + +class TestExpressionsUsageSpec extends AbstractEvaluatedExpressionsSpec { + void "test expression array in requires"() { + given: + def ctx = buildContext(""" + package test; + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @Requires(env = {"#{ 'test' }"}) + class Expr { + } + """) + + when: + getBean(ctx, "test.Expr") + + then: + noExceptionThrown() + + cleanup: + ctx.close() + } + + void "test requires expression property value"() { + given: + def ctx = buildContext(""" + package test; + + import io.micronaut.context.annotation.Requires; + import jakarta.inject.Singleton; + + @Singleton + @Requires(property = "test-property", value = "#{ 'test-value'.toUpperCase() }") + class Expr { + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + when: + ctx.environment.addPropertySource(PropertySource.of("test", ['test-property': 'TEST-VALUE'])) + ctx.getBean(type) + + then: + noExceptionThrown() + + cleanup: + ctx.close() + } + + void "test requires expression context value"() { + given: + def ctx = buildContext(""" + package test; + + import io.micronaut.context.annotation.ConfigurationProperties; + import io.micronaut.context.annotation.EvaluatedExpressionContext; + import io.micronaut.context.annotation.Requires; + + import jakarta.inject.Singleton; + + @Singleton + @Requires(property = "test.enabled", value = "#{ #enabled }") + class Expr { + } + + @ConfigurationProperties("test") + @EvaluatedExpressionContext + class TestContext { + private boolean enabled; + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + when: + ctx.environment.addPropertySource(PropertySource.of("test", ['test.enabled': false])) + ctx.getBean(type) + + then: + noExceptionThrown() + + cleanup: + ctx.close() + } + + void "test disabled by expression bean"() { + given: + def ctx = buildContext(""" + package test; + + import io.micronaut.context.annotation.Requires; + + import jakarta.inject.Singleton; + + @Singleton + @Requires(property = "test.property", value = "#{ 5 * 2 }") + class Expr { + } + """) + + def type = ctx.classLoader.loadClass('test.Expr') + + when: + ctx.environment.addPropertySource(PropertySource.of("test", ['test.property': 15])) + ctx.getBean(type) + + then: + thrown(NoSuchBeanException) + + cleanup: + ctx.close() + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy new file mode 100644 index 00000000000..0f5399f6817 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy @@ -0,0 +1,52 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class TypeIdentifierExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + + void "test static methods"() { + given: + List results = evaluateMultiple( + "#{ T(Math).random() }", + "#{ T(java.lang.Math).random() }", + "#{ T(Integer).valueOf('10') }", + "#{ T(String).join(',', '1', '2', '3') }", + "#{ T(String).join(',', 'a', 'b').repeat(2) }" + ) + + expect: + results[0] instanceof Double && results[0] >= 0 && results[0] < 1 + results[1] instanceof Double && results[1] >= 0 && results[1] < 1 + results[2] instanceof Integer && results[2] == 10 + results[3] instanceof String && results[3] == "1,2,3" + results[4] instanceof String && results[4] == "a,ba,b" + } + + void "test type identifier as argument"() { + given: + Object expr1 = evaluateAgainstContext("#{ #getType(T(java.lang.String)) }", + """ + @EvaluatedExpressionContext + class Context { + Class getType(Class type) { + return type; + } + } + """) + + Object expr2 = evaluateAgainstContext("#{ #getType(T(String), T(Object)) }", + """ + @EvaluatedExpressionContext + class Context { + Class getType(Class... types) { + return types[1]; + } + } + """) + + expect: + expr1 instanceof Class && expr1 == String.class + expr2 instanceof Class && expr2 == Object.class + } + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt index dcab23e5b9c..def04f0ec98 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt @@ -282,7 +282,7 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro annotationValues: MutableMap ) { if (!annotationValues.containsKey(memberName)) { - val value = readAnnotationValue(originatingElement, member, memberName, annotationValue) + val value = readAnnotationValue(originatingElement, member, annotationName, memberName, annotationValue) if (value != null) { validateAnnotationValue(originatingElement, annotationName, member, memberName, value) annotationValues[memberName] = value @@ -315,6 +315,7 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro override fun readAnnotationValue( originatingElement: KSAnnotated, member: KSAnnotated, + annotationName: String, memberName: String, annotationValue: Any ): Any? { @@ -367,6 +368,10 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro } } + override fun getOriginatingClassName(orginatingElement: KSAnnotated?): String { + TODO("Not yet implemented") + } + private fun readDefaultValuesReflectively(classDeclaration : KSClassDeclaration, annotationType: KSAnnotated, vararg path : String): MutableMap { var o: Any? = findValueReflectively(annotationType, *path) val declaredProperties = classDeclaration.getDeclaredProperties() @@ -581,6 +586,7 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro if (value is KSAnnotation) { return readNestedAnnotationValue(originatingElement, value) } + return value } diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy index c9e5532887d..a7782b80071 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy @@ -18,6 +18,8 @@ import io.micronaut.core.type.Argument import io.micronaut.core.type.GenericPlaceholder import io.micronaut.inject.ExecutableMethod import io.micronaut.inject.beans.visitor.MappedSuperClassIntrospectionMapper +import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor +import io.micronaut.inject.beans.visitor.EvaluatedExpressionContextTypeElementVisitor import io.micronaut.kotlin.processing.elementapi.SomeEnum import io.micronaut.kotlin.processing.elementapi.TestClass @@ -1332,7 +1334,7 @@ class Test then:"The reference is valid" reference != null - reference.getBeanType() == MappedSuperClassIntrospectionMapper + reference.getBeanType() == EvaluatedExpressionContextTypeElementVisitor } void "test write bean introspection data"() { diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java index 569c4ab3e45..d22a436bcd5 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanResolutionContext.java @@ -728,6 +728,11 @@ public InjectionPoint getInjectionPoint() { public BeanDefinition getDeclaringBean() { return getDeclaringType(); } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return getArgument().getAnnotationMetadata(); + } } /** diff --git a/inject/src/main/java/io/micronaut/context/AbstractEvaluatedExpression.java b/inject/src/main/java/io/micronaut/context/AbstractEvaluatedExpression.java new file mode 100644 index 00000000000..a6f8959d750 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/AbstractEvaluatedExpression.java @@ -0,0 +1,115 @@ +/* + * Copyright 2017-2020 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.context; + +import io.micronaut.context.exceptions.ExpressionEvaluationException; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.UsedByGeneratedCode; +import io.micronaut.core.expression.EvaluatedExpression; +import io.micronaut.core.type.Argument; +import io.micronaut.inject.BeanDefinition; + +/** + * Default implementation for evaluated expressions. This class is subclassed + * by evaluated expressions classes at compilation time. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +@UsedByGeneratedCode +public abstract class AbstractEvaluatedExpression implements EvaluatedExpression, + ContextConfigurable, + BeanDefinitionAware { + private final Object initialAnnotationValue; + + private BeanContext beanContext; + private BeanResolutionContext resolutionContext; + private BeanDefinition owningBean; + + public AbstractEvaluatedExpression(Object initialAnnotationValue) { + this.initialAnnotationValue = initialAnnotationValue; + } + + @Override + public Object getInitialAnnotationValue() { + return initialAnnotationValue; + } + + @Override + public void setBeanDefinition(BeanDefinition beanDefinition) { + this.owningBean = beanDefinition; + } + + @Override + public void configure(BeanContext context) { + this.beanContext = context; + } + + @Override + public final Object evaluate(Object... args) { + try { + return doEvaluate(args); + } catch (Throwable ex) { + throw new ExpressionEvaluationException(ex); + } finally { + if (resolutionContext != null) { + resolutionContext = null; + } + } + } + + /** + * This method is overridden by expression classes generated at compilation time and + * contains concrete expression evaluation logic. + * + * @param args Array of arguments which need to be passed to expression + * for evaluation. Args are used when expression itself is used + * on method and references method arguments + * @return evaluation result + */ + protected Object doEvaluate(Object... args) { + return initialAnnotationValue; + } + + protected final T getBean(Class type) { + if (beanContext == null) { + throw new ExpressionEvaluationException( + "Can not evaluate expression [" + getInitialAnnotationValue() + "]. " + + "Can not obtain bean of type [" + type + "] since bean context is not set"); + } + + if (beanContext instanceof DefaultBeanContext defaultBeanContext) { + if (resolutionContext == null && owningBean != null) { + resolutionContext = new DefaultBeanResolutionContext(defaultBeanContext, owningBean); + } + + if (resolutionContext != null) { + try (BeanResolutionContext.Path ignored = + resolutionContext.getPath().pushAnnotationResolve(owningBean, Argument.of(type))) { + return defaultBeanContext.getBean(resolutionContext, type); + } + } + } + + return beanContext.getBean(type); + } + + @Override + public String toString() { + return initialAnnotationValue.toString(); + } +} diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java index 5984ac1e81f..68e5e04ac2d 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java @@ -16,7 +16,11 @@ package io.micronaut.context; import io.micronaut.context.env.Environment; -import io.micronaut.core.annotation.*; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.annotation.UsedByGeneratedCode; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; @@ -25,10 +29,17 @@ import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.ExecutableMethodsDefinition; import io.micronaut.inject.annotation.AbstractEnvironmentAnnotationMetadata; +import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import java.lang.reflect.Method; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -40,11 +51,12 @@ * @since 3.0 */ @Internal -public abstract class AbstractExecutableMethodsDefinition implements ExecutableMethodsDefinition, EnvironmentConfigurable { +public abstract class AbstractExecutableMethodsDefinition implements ExecutableMethodsDefinition, EnvironmentConfigurable, ContextConfigurable { private final MethodReference[] methodsReferences; private final DispatchedExecutableMethod[] executableMethods; private Environment environment; + private BeanContext beanContext; private List> executableMethodsList; protected AbstractExecutableMethodsDefinition(MethodReference[] methodsReferences) { @@ -62,6 +74,16 @@ public void configure(Environment environment) { } } + @Override + public void configure(BeanContext beanContext) { + this.beanContext = beanContext; + for (DispatchedExecutableMethod executableMethod : executableMethods) { + if (executableMethod != null) { + executableMethod.configure(beanContext); + } + } + } + @Override public Collection> getExecutableMethods() { if (executableMethodsList == null) { @@ -102,6 +124,9 @@ public ExecutableMethod getExecutableMethodByIndex(int index) { if (environment != null) { executableMethod.configure(environment); } + if (beanContext != null) { + executableMethod.configure(beanContext); + } executableMethods[index] = executableMethod; } return executableMethod; @@ -302,6 +327,8 @@ public String toString() { * @param The type * @param The result type */ + private static final class DispatchedExecutableMethod implements ExecutableMethod, ReturnType, + EnvironmentConfigurable, ContextConfigurable { private static final class DispatchedExecutableMethod implements ExecutableMethod, EnvironmentConfigurable { private final AbstractExecutableMethodsDefinition dispatcher; @@ -327,11 +354,24 @@ public void configure(Environment environment) { } } + @Override + public void configure(BeanContext beanContext) { + annotationMetadata = EvaluatedAnnotationMetadata.wrapIfNecessary(annotationMetadata); + if (annotationMetadata instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + } + } + @Override public boolean hasPropertyExpressions() { return annotationMetadata.hasPropertyExpressions(); } + @Override + public boolean hasEvaluatedExpressions() { + return annotationMetadata.hasEvaluatedExpressions(); + } + @Override public boolean isAbstract() { return methodReference.isAbstract; @@ -492,5 +532,4 @@ protected Environment getEnvironment() { return environment; } } - } diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index 5afa2ff4a62..4f066f0e179 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -32,6 +32,7 @@ import io.micronaut.context.exceptions.NoSuchBeanException; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NextMajorVersion; import io.micronaut.core.annotation.NonNull; @@ -58,6 +59,7 @@ import io.micronaut.inject.MethodInjectionPoint; import io.micronaut.inject.ValidatedBeanDefinition; import io.micronaut.inject.annotation.AbstractEnvironmentAnnotationMetadata; +import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; import io.micronaut.inject.qualifiers.InterceptorBindingQualifier; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.inject.qualifiers.TypeAnnotationQualifier; @@ -103,7 +105,7 @@ */ @Internal public abstract class AbstractInitializableBeanDefinition extends AbstractBeanContextConditional - implements InstantiatableBeanDefinition, InjectableBeanDefinition, EnvironmentConfigurable { + implements InstantiatableBeanDefinition, InjectableBeanDefinition, EnvironmentConfigurable, ContextConfigurable { private static final Logger LOG = LoggerFactory.getLogger(AbstractInitializableBeanDefinition.class); private final Class type; @@ -158,13 +160,13 @@ protected AbstractInitializableBeanDefinition( if (annotationMetadata == null || annotationMetadata == AnnotationMetadata.EMPTY_METADATA) { this.annotationMetadata = AnnotationMetadata.EMPTY_METADATA; } else { + AnnotationMetadata beanAnnotationMetadata = annotationMetadata; if (annotationMetadata.hasPropertyExpressions()) { // we make a copy of the result of annotation metadata which is normally a reference // to the class metadata - this.annotationMetadata = new BeanAnnotationMetadata(annotationMetadata); - } else { - this.annotationMetadata = annotationMetadata; + beanAnnotationMetadata = new BeanAnnotationMetadata(annotationMetadata); } + this.annotationMetadata = EvaluatedAnnotationMetadata.wrapIfNecessary(beanAnnotationMetadata); } this.constructor = constructor; this.methodInjection = methodInjection; @@ -271,6 +273,11 @@ public final boolean hasPropertyExpressions() { return getAnnotationMetadata().hasPropertyExpressions(); } + @Override + public boolean hasEvaluatedExpressions() { + return getAnnotationMetadata().hasEvaluatedExpressions(); + } + @Override public final @NonNull List> getTypeArguments(String type) { @@ -641,6 +648,97 @@ public final void configure(Environment environment) { } } + @Override + public void configure(BeanContext beanContext) { + if (beanContext == null) { + return; + } + + if (annotationMetadata instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + eam.setBeanDefinition(this); + } + + if (constructor != null) { + if (constructor instanceof MethodReference mr) { + if (mr.annotationMetadata instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + eam.setBeanDefinition(this); + } + + if (mr.arguments != null) { + for (Argument argument: mr.arguments) { + if (argument instanceof ExpressionsAwareArgument exprArg) { + exprArg.configure(beanContext); + exprArg.setBeanDefinition(this); + } + } + } + } + if (constructor instanceof FieldReference fr + && fr.argument instanceof ExpressionsAwareArgument exprArg) { + exprArg.configure(beanContext); + exprArg.setBeanDefinition(this); + } + } + + if (constructorInjectionPoint != null) { + if (constructorInjectionPoint.getAnnotationMetadata() instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + eam.setBeanDefinition(this); + } + } + + if (methodInjection != null) { + for (MethodReference methodReference: methodInjection) { + if (methodReference.annotationMetadata instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + eam.setBeanDefinition(this); + } + + if (methodReference.arguments != null) { + for (Argument argument: methodReference.arguments) { + if (argument instanceof ExpressionsAwareArgument exprArg) { + exprArg.configure(beanContext); + exprArg.setBeanDefinition(this); + } + } + } + } + } + + if (methodInjectionPoints != null) { + for (MethodInjectionPoint methodInjectionPoint : methodInjectionPoints) { + if (methodInjectionPoint.getAnnotationMetadata() instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + eam.setBeanDefinition(this); + } + } + } + + if (fieldInjection != null) { + for (FieldReference fieldReference: fieldInjection) { + if (fieldReference.argument instanceof ExpressionsAwareArgument exprArg) { + exprArg.configure(beanContext); + exprArg.setBeanDefinition(this); + } + } + } + + if (fieldInjectionPoints != null) { + for (FieldInjectionPoint fieldInjectionPoint : fieldInjectionPoints) { + if (fieldInjectionPoint.getAnnotationMetadata() instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(beanContext); + eam.setBeanDefinition(this); + } + } + } + + if (executableMethodsDefinition instanceof ContextConfigurable ctxConfigurable) { + ctxConfigurable.configure(beanContext); + } + } + /** * Allows printing warning messages produced by the compiler. * @@ -1082,6 +1180,15 @@ protected final Object getPropertyPlaceholderValueForMethodArgument(BeanResoluti } } + @Internal + @UsedByGeneratedCode + protected final Object getEvaluatedExpressionValueForMethodArgument(int methodIndex, + int argIndex) { + MethodReference methodRef = methodInjection[methodIndex]; + Argument argument = methodRef.arguments[argIndex]; + return getExpressionValueForArgument(argument); + } + /** * Obtains a property value for the given method argument. * @@ -1205,7 +1312,7 @@ protected final > R getBeansOfTypeForMethodArgument(B Argument argument = resolveArgument(context, argumentIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { - return resolveBeansOfType(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeansOfType(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1247,7 +1354,7 @@ protected final Object getBeanForSetter(BeanResolutionContext resolutionContext, protected final Collection getBeansOfTypeForSetter(BeanResolutionContext resolutionContext, BeanContext context, String setterName, Argument argument, Argument genericType, Qualifier qualifier) { try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushMethodArgumentResolve(this, setterName, argument, new Argument[]{argument})) { - return resolveBeansOfType(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeansOfType(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1272,7 +1379,7 @@ protected final Optional findBeanForMethodArgument(BeanResolutionContext Argument argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { - return resolveOptionalBean(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveOptionalBean(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1296,7 +1403,7 @@ protected final Stream getStreamOfTypeForMethodArgument(BeanResolutionContext Argument argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { - return resolveStreamOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveStreamOfType(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1327,7 +1434,7 @@ protected final Map getMapOfTypeForMethodArgument( Argument> argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { - return resolveMapOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveMapOfType(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1439,6 +1546,14 @@ protected final Object getPropertyValueForConstructorArgument(BeanResolutionCont } } + @Internal + @UsedByGeneratedCode + protected final Object getEvaluatedExpressionValueForConstructorArgument(int argIndex) { + MethodReference constructorRef = (MethodReference) constructor; + Argument argument = constructorRef.arguments[argIndex]; + return getExpressionValueForArgument(argument); + } + /** * Obtains a property value for a bean definition for a constructor at the given index *

@@ -1495,7 +1610,7 @@ protected final Collection getBeansOfTypeForConstructorArgument(BeanReso MethodReference constructorMethodRef = (MethodReference) constructor; Argument argument = resolveArgument(context, argumentIndex, constructorMethodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, argument)) { - return resolveBeansOfType(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeansOfType(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1519,7 +1634,7 @@ protected final >> R getBeanRegistra MethodReference constructorMethodRef = (MethodReference) constructor; Argument argument = resolveArgument(context, argumentIndex, constructorMethodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, argument)) { - return resolveBeanRegistrations(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeanRegistrations(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1542,7 +1657,7 @@ protected final BeanRegistration getBeanRegistrationForConstructorArgumen MethodReference constructorMethodRef = (MethodReference) constructor; Argument argument = resolveArgument(context, argumentIndex, constructorMethodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, argument)) { - return resolveBeanRegistration(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeanRegistration(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1568,7 +1683,7 @@ protected final >> R getBeanRegistra Argument argument = resolveArgument(context, argIndex, methodReference.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath() .pushMethodArgumentResolve(this, methodReference.methodName, argument, methodReference.arguments)) { - return resolveBeanRegistrations(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeanRegistrations(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1593,7 +1708,7 @@ protected final BeanRegistration getBeanRegistrationForMethodArgument(Bea Argument argument = resolveArgument(context, argIndex, methodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath() .pushMethodArgumentResolve(this, methodRef.methodName, argument, methodRef.arguments)) { - return resolveBeanRegistration(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeanRegistration(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1616,7 +1731,7 @@ protected final Stream getStreamOfTypeForConstructorArgument(BeanResoluti MethodReference constructorMethodRef = (MethodReference) constructor; Argument argument = resolveArgument(context, argIndex, constructorMethodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, argument)) { - return resolveStreamOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveStreamOfType(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1647,7 +1762,7 @@ protected final Map getMapOfTypeForConstructorArgument( } Argument> argument = resolveArgument(context, argIndex, constructorMethodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, argument)) { - return resolveMapOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveMapOfType(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1670,7 +1785,7 @@ protected final Optional findBeanForConstructorArgument(BeanResolutionCon MethodReference constructorMethodRef = (MethodReference) constructor; Argument argument = resolveArgument(context, argIndex, constructorMethodRef.arguments); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushConstructorResolve(this, argument)) { - return resolveOptionalBean(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveOptionalBean(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1689,7 +1804,7 @@ protected final Optional findBeanForConstructorArgument(BeanResolutionCon @Internal @UsedByGeneratedCode protected final K getBeanForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Qualifier qualifier) { - final Argument argument = resolveEnvironmentArgument(context, fieldInjection[fieldIndex].argument); + final Argument argument = resolveArgument(context, fieldInjection[fieldIndex].argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { return resolveBean(resolutionContext, argument, qualifier); } @@ -1698,7 +1813,7 @@ protected final K getBeanForField(BeanResolutionContext resolutionContext, B @Internal @UsedByGeneratedCode protected final K getBeanForAnnotation(BeanResolutionContext resolutionContext, BeanContext context, int annotationBeanIndex, Qualifier qualifier) { - final Argument argument = resolveEnvironmentArgument(context, annotationInjection[annotationBeanIndex].argument); + final Argument argument = resolveArgument(context, annotationInjection[annotationBeanIndex].argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath() .pushAnnotationResolve(this, argument)) { return resolveBean(resolutionContext, argument, qualifier); @@ -1856,9 +1971,9 @@ protected final boolean containsProperties(@SuppressWarnings("unused") BeanResol protected final > Object getBeansOfTypeForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { // Keep Object type for backwards compatibility final FieldReference fieldRef = fieldInjection[fieldIndex]; - final Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); + final Argument argument = resolveArgument(context, fieldRef.argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { - return resolveBeansOfType(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeansOfType(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1880,9 +1995,9 @@ protected final > Object getBeansOfTypeForField(BeanR @UsedByGeneratedCode protected final >> R getBeanRegistrationsForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; - Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); + Argument argument = resolveArgument(context, fieldRef.argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { - return resolveBeanRegistrations(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeanRegistrations(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1903,9 +2018,9 @@ protected final >> R getBeanRegistra @UsedByGeneratedCode protected final BeanRegistration getBeanRegistrationForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; - Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); + Argument argument = resolveArgument(context, fieldRef.argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { - return resolveBeanRegistration(resolutionContext, context, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveBeanRegistration(resolutionContext, context, argument, resolveArgument(context, genericType), qualifier); } } @@ -1926,9 +2041,9 @@ protected final BeanRegistration getBeanRegistrationForField(BeanResoluti @UsedByGeneratedCode protected final Optional findBeanForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; - Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); + Argument argument = resolveArgument(context, fieldRef.argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { - return resolveOptionalBean(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveOptionalBean(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1949,9 +2064,9 @@ protected final Optional findBeanForField(BeanResolutionContext resolutio @UsedByGeneratedCode protected final Stream getStreamOfTypeForField(BeanResolutionContext resolutionContext, BeanContext context, int fieldIndex, Argument genericType, Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; - Argument argument = resolveEnvironmentArgument(context, fieldRef.argument); + Argument argument = resolveArgument(context, fieldRef.argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { - return resolveStreamOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveStreamOfType(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -1977,9 +2092,9 @@ protected final Map getMapOfTypeForField( Qualifier qualifier) { FieldReference fieldRef = fieldInjection[fieldIndex]; @SuppressWarnings("unchecked") - Argument> argument = resolveEnvironmentArgument(context, fieldRef.argument); + Argument> argument = resolveArgument(context, fieldRef.argument); try (BeanResolutionContext.Path ignored = resolutionContext.getPath().pushFieldResolve(this, argument)) { - return resolveMapOfType(resolutionContext, argument, resolveEnvironmentArgument(context, genericType), qualifier); + return resolveMapOfType(resolutionContext, argument, resolveArgument(context, genericType), qualifier); } } @@ -2022,6 +2137,17 @@ private boolean resolveContainsValue(BeanResolutionContext resolutionContext, Be } private Object resolveValue(BeanResolutionContext resolutionContext, BeanContext context, AnnotationMetadata parentAnnotationMetadata, Argument argument, Qualifier qualifier) { + AnnotationMetadata argumentAnnotationMetadata = argument.getAnnotationMetadata(); + if (argumentAnnotationMetadata instanceof EvaluatedAnnotationMetadata eam) { + eam.configure(context); + eam.setBeanDefinition(this); + AnnotationValue annotation = eam.getAnnotation(Value.class); + if (annotation.hasEvaluatedExpressions()) { + Optional value = annotation.getValue(argument); + return value.orElse(null); + } + } + if (!(context instanceof PropertyResolver)) { throw new DependencyInjectionException(resolutionContext, "@Value requires a BeanContext that implements PropertyResolver"); } @@ -2261,7 +2387,7 @@ private > R resolveBeansOfType(BeanResolutionContext throw new DependencyInjectionException(resolutionContext, "Type " + returnType.getType() + " has no generic argument"); } qualifier = qualifier == null ? resolveQualifier(resolutionContext, beanType, returnType) : qualifier; - Collection beansOfType = resolutionContext.getBeansOfType(resolveEnvironmentArgument(context, beanType), qualifier); + Collection beansOfType = resolutionContext.getBeansOfType(resolveArgument(context, beanType), qualifier); return coerceCollectionToCorrectType(returnType.getType(), beansOfType, resolutionContext, returnType); } @@ -2338,17 +2464,17 @@ private Argument resolveArgument(BeanContext context, int argIndex, Argum if (arguments == null) { return null; } - return resolveEnvironmentArgument(context, (Argument) arguments[argIndex]); + return resolveArgument(context, (Argument) arguments[argIndex]); } - private Argument resolveEnvironmentArgument(BeanContext context, Argument argument) { + private Argument resolveArgument(BeanContext context, Argument argument) { if (argument instanceof DefaultArgument) { if (argument.getAnnotationMetadata().hasPropertyExpressions()) { argument = new EnvironmentAwareArgument<>((DefaultArgument) argument); instrumentAnnotationMetadata(context, argument); } } - return argument; + return ExpressionsAwareArgument.wrapIfNecessary(argument, context, this); } private BeanRegistration resolveBeanRegistration(BeanResolutionContext resolutionContext, BeanContext context, @@ -2407,10 +2533,31 @@ private > K coerceCollectionToCorrectType(Class co } private void instrumentAnnotationMetadata(BeanContext context, Object object) { - if (object instanceof final EnvironmentConfigurable ec && context instanceof ApplicationContext) { + if (object instanceof final EnvironmentConfigurable ec && context instanceof ApplicationContext ac) { if (ec.hasPropertyExpressions()) { - ec.configure(((ApplicationContext) context).getEnvironment()); + ec.configure(ac.getEnvironment()); + } + } + } + + private Object getExpressionValueForArgument(Argument argument) { + Optional expressionValue = + argument.getAnnotationMetadata() + .getValue(Value.class, argument.getType()); + + if (argument.isOptional()) { + if (expressionValue.isEmpty()) { + return expressionValue; + } else { + Object convertedOptional = expressionValue.get(); + if (convertedOptional instanceof Optional) { + return convertedOptional; + } else { + return expressionValue; + } } + } else { + return expressionValue.orElse(null); } } @@ -2493,10 +2640,19 @@ public MethodReference(Class declaringType, boolean isPreDestroyMethod) { super(declaringType); this.methodName = methodName; - this.arguments = arguments; - this.annotationMetadata = annotationMetadata == null ? AnnotationMetadata.EMPTY_METADATA : annotationMetadata; this.isPostConstructMethod = isPostConstructMethod; this.isPreDestroyMethod = isPreDestroyMethod; + + this.arguments = arguments == null + ? arguments + : Arrays.stream(arguments) + .map(argument -> ExpressionsAwareArgument.wrapIfNecessary(argument)) + .toArray(Argument[]::new); + + this.annotationMetadata = + annotationMetadata == null + ? AnnotationMetadata.EMPTY_METADATA + : EvaluatedAnnotationMetadata.wrapIfNecessary(annotationMetadata); } } @@ -2516,7 +2672,7 @@ public FieldReference(Class declaringType, Argument argument, boolean requiresRe public FieldReference(Class declaringType, Argument argument) { super(declaringType); - this.argument = argument; + this.argument = ExpressionsAwareArgument.wrapIfNecessary(argument); } } @@ -2549,8 +2705,7 @@ public static final class AnnotationReference { public final Argument argument; public AnnotationReference(Argument argument) { - this.argument = argument; + this.argument = ExpressionsAwareArgument.wrapIfNecessary(argument); } - } } diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java index 48afebbb258..f4ec216943e 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinitionReference.java @@ -183,6 +183,9 @@ public BeanDefinition load(BeanContext context) { } else if (context instanceof ApplicationContext applicationContext && definition instanceof EnvironmentConfigurable environmentConfigurable) { environmentConfigurable.configure(applicationContext.getEnvironment()); } + if (definition instanceof ContextConfigurable ctxConfigurable) { + ctxConfigurable.configure(context); + } return definition; } diff --git a/inject/src/main/java/io/micronaut/context/BeanDefinitionAware.java b/inject/src/main/java/io/micronaut/context/BeanDefinitionAware.java new file mode 100644 index 00000000000..e9f23db8c82 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/BeanDefinitionAware.java @@ -0,0 +1,33 @@ +/* + * 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.context; + +import io.micronaut.inject.BeanDefinition; + +/** + * Interface for components aware of bean definition associated with them. + * + * @author Sergey Gavrilov + * @since 4.0 + */ +public interface BeanDefinitionAware { + /** + * Configure the component for the given bean definition. + * + * @param beanDefinition The bean context + */ + void setBeanDefinition(BeanDefinition beanDefinition); +} diff --git a/inject/src/main/java/io/micronaut/context/ContextConfigurable.java b/inject/src/main/java/io/micronaut/context/ContextConfigurable.java new file mode 100644 index 00000000000..dcc23f5dd5e --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/ContextConfigurable.java @@ -0,0 +1,31 @@ +/* + * 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.context; + +/** + * Interface for components configurable by the bean context. + * + * @author Sergey Gavirlov + * @since 4.0 + */ +public interface ContextConfigurable { + /** + * Configure the component for the given bean context. + * + * @param context The bean context + */ + void configure(BeanContext context); +} diff --git a/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java b/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java new file mode 100644 index 00000000000..6af2669c481 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java @@ -0,0 +1,89 @@ +/* + * 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.context; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.type.Argument; +import io.micronaut.core.type.DefaultArgument; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; + +/** + * An argument that is aware of evaluated expressions which can be used to resolve + * expression placeholders in the annotation metadata. + * + * @param The argument type + * + * @author Sergey Gavrilov + * @since 4.0 + */ +@Internal +final class ExpressionsAwareArgument extends DefaultArgument implements ContextConfigurable, + BeanDefinitionAware { + + private final EvaluatedAnnotationMetadata annotationMetadata; + + private ExpressionsAwareArgument(Argument argument, + EvaluatedAnnotationMetadata annotationMetadata) { + super(argument.getType(), argument.getName(), argument.getAnnotationMetadata(), + argument.getTypeVariables(), argument.getTypeParameters()); + this.annotationMetadata = annotationMetadata; + } + + public static Argument wrapIfNecessary(Argument argument) { + return wrapIfNecessary(argument, null, null); + } + + public static Argument wrapIfNecessary(Argument argument, + @Nullable BeanContext beanContext, + @Nullable BeanDefinition owningBean) { + if (argument == null) { + return null; + } + + AnnotationMetadata annotationMetadata = + EvaluatedAnnotationMetadata.wrapIfNecessary(argument.getAnnotationMetadata()); + if (annotationMetadata instanceof EvaluatedAnnotationMetadata evaluatedAnnotationMetadata) { + if (beanContext != null) { + evaluatedAnnotationMetadata.configure(beanContext); + } + + if (owningBean != null) { + evaluatedAnnotationMetadata.setBeanDefinition(owningBean); + } + + return new ExpressionsAwareArgument<>(argument, evaluatedAnnotationMetadata); + } + return argument; + } + + @Override + public void setBeanDefinition(BeanDefinition beanDefinition) { + annotationMetadata.setBeanDefinition(beanDefinition); + } + + @Override + public void configure(BeanContext context) { + annotationMetadata.configure(context); + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } +} diff --git a/inject/src/main/java/io/micronaut/context/RequiresCondition.java b/inject/src/main/java/io/micronaut/context/RequiresCondition.java index e8b62dbc8b1..fdfa4520ef1 100644 --- a/inject/src/main/java/io/micronaut/context/RequiresCondition.java +++ b/inject/src/main/java/io/micronaut/context/RequiresCondition.java @@ -98,9 +98,16 @@ public boolean matches(ConditionContext context) { } AnnotationMetadataProvider component = context.getComponent(); boolean isBeanReference = component instanceof BeanDefinitionReference; + // here we use AnnotationMetadata to avoid loading the classes referenced in the annotations directly if (isBeanReference) { for (AnnotationValue requirement : requirements) { + // if annotation value has evaluated expressions, postpone + // decision until the bean is loaded + if (requirement.hasEvaluatedExpressions()) { + continue; + } + processPreStartRequirements(context, requirement); if (context.isFailing()) { return false; diff --git a/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java b/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java new file mode 100644 index 00000000000..d5667035d5c --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java @@ -0,0 +1,39 @@ +/* + * 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.context.annotation; + +import jakarta.inject.Singleton; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to mark classes which are considered as context + * for expression evaluation. Being an expression context means that + * expressions can reference methods and properties of this object + * directly with # prefix + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@DefaultScope(Singleton.class) +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface EvaluatedExpressionContext { + Class value() default void.class; +} diff --git a/inject/src/main/java/io/micronaut/context/annotation/Value.java b/inject/src/main/java/io/micronaut/context/annotation/Value.java index 6a168b2aaca..acc4765a7c5 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/Value.java +++ b/inject/src/main/java/io/micronaut/context/annotation/Value.java @@ -24,8 +24,8 @@ import java.lang.annotation.Target; /** - *

Allows configuration injection from the environment on a per property, field, method/constructor parameter - * basis.

+ *

Allows configuration injection from the environment or bean context on a per property, field, + * method/constructor parameter basis.

* * @author Graeme Rocher * @see ConfigurationProperties @@ -38,7 +38,8 @@ public @interface Value { /** - * A string containing a value, which my optionally contain property placeholder expressions. + * A string containing a value, which my optionally contain property placeholder expressions or + * evaluated expressions. * * @return The value to inject. */ diff --git a/inject/src/main/java/io/micronaut/context/exceptions/ExpressionEvaluationException.java b/inject/src/main/java/io/micronaut/context/exceptions/ExpressionEvaluationException.java new file mode 100644 index 00000000000..40ba6334c26 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/exceptions/ExpressionEvaluationException.java @@ -0,0 +1,32 @@ +/* + * 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.context.exceptions; + +/** + * Exception thrown on expression evaluation failure. + * + * @since 4.0 + * @author Sergey Gavrilov + */ +public class ExpressionEvaluationException extends RuntimeException { + public ExpressionEvaluationException(Throwable ex) { + super(ex); + } + + public ExpressionEvaluationException(String message) { + super(message); + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AbstractEnvironmentAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/AbstractEnvironmentAnnotationMetadata.java index 112fee14961..f9cc725319f 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AbstractEnvironmentAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AbstractEnvironmentAnnotationMetadata.java @@ -77,6 +77,11 @@ public T synthesizeDeclared(@NonNull Class annotationC return environmentAnnotationMetadata.synthesizeDeclared(annotationClass); } + @Override + public boolean hasEvaluatedExpressions() { + return environmentAnnotationMetadata.hasEvaluatedExpressions(); + } + @Override public Optional getValue(@NonNull String annotation, @NonNull String member, @NonNull Argument requiredType) { Environment environment = getEnvironment(); diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java index c6298f573fa..e9b46f6a28c 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java @@ -114,6 +114,16 @@ public boolean hasPropertyExpressions() { return false; } + @Override + public boolean hasEvaluatedExpressions() { + for (AnnotationMetadata annotationMetadata: hierarchy) { + if (annotationMetadata.hasEvaluatedExpressions()) { + return true; + } + } + return false; + } + @Override public Optional> getAnnotationType(@NonNull String name) { for (AnnotationMetadata metadata : hierarchy) { diff --git a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java index 31feb48c1c3..160086db905 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/DefaultAnnotationMetadata.java @@ -82,6 +82,7 @@ public class DefaultAnnotationMetadata extends AbstractAnnotationMetadata implem private final Map annotationValuesByType = new ConcurrentHashMap<>(2); private final boolean hasPropertyExpressions; + private final boolean hasEvaluatedExpressions; /** * Constructs empty annotation metadata. @@ -89,6 +90,7 @@ public class DefaultAnnotationMetadata extends AbstractAnnotationMetadata implem @Internal protected DefaultAnnotationMetadata() { hasPropertyExpressions = false; + hasEvaluatedExpressions = false; } /** @@ -133,6 +135,30 @@ public DefaultAnnotationMetadata( this(declaredAnnotations, declaredStereotypes, allStereotypes, allAnnotations, annotationsByStereotype, hasPropertyExpressions, false); } + /** + * This constructor is designed to be used by compile time produced subclasses. + * + * @param declaredAnnotations The directly declared annotations + * @param declaredStereotypes The directly declared stereotypes + * @param allStereotypes All of the stereotypes + * @param allAnnotations All of the annotations + * @param annotationsByStereotype The annotations by stereotype + * @param hasPropertyExpressions Whether property expressions exist in the metadata + * @param hasEvaluatedExpressions Whether evaluated expressions exist in the metadata + */ + @Internal + @UsedByGeneratedCode + public DefaultAnnotationMetadata( + @Nullable Map> declaredAnnotations, + @Nullable Map> declaredStereotypes, + @Nullable Map> allStereotypes, + @Nullable Map> allAnnotations, + @Nullable Map> annotationsByStereotype, + boolean hasPropertyExpressions, + boolean hasEvaluatedExpressions) { + this(declaredAnnotations, declaredStereotypes, allStereotypes, allAnnotations, annotationsByStereotype, hasPropertyExpressions, hasEvaluatedExpressions, false); + } + /** * This constructor is designed to be used by compile time produced subclasses. * @@ -156,6 +182,7 @@ public DefaultAnnotationMetadata( @Nullable Map> allAnnotations, @Nullable Map> annotationsByStereotype, boolean hasPropertyExpressions, + boolean hasEvaluatedExpressions, boolean useRepeatableDefaults) { super(declaredAnnotations, allAnnotations); this.declaredAnnotations = declaredAnnotations; @@ -164,6 +191,7 @@ public DefaultAnnotationMetadata( this.allAnnotations = allAnnotations; this.annotationsByStereotype = annotationsByStereotype; this.hasPropertyExpressions = hasPropertyExpressions; + this.hasEvaluatedExpressions = hasEvaluatedExpressions; } @NonNull @@ -184,6 +212,11 @@ public boolean hasPropertyExpressions() { return hasPropertyExpressions; } + @Override + public boolean hasEvaluatedExpressions() { + return hasEvaluatedExpressions; + } + @NonNull @Override public Map getDefaultValues(@NonNull String annotation) { diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java new file mode 100644 index 00000000000..fa4b4a0039a --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java @@ -0,0 +1,101 @@ +/* + * Copyright 2017-2020 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.inject.annotation; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.BeanDefinitionAware; +import io.micronaut.context.ContextConfigurable; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.BeanDefinition; + +import java.lang.annotation.Annotation; + +/** + * Variation of {@link AnnotationMetadata} that is used when evaluated expression + * in annotation values need to be resolved at runtime. + * + * @author Sergey Gavrilov + * @since 4.0 + */ +@Internal +public final class EvaluatedAnnotationMetadata extends MappingAnnotationMetadataDelegate implements ContextConfigurable, BeanDefinitionAware { + + private BeanContext beanContext; + private BeanDefinition owningBean; + + private final AnnotationMetadata delegateAnnotationMetadata; + private final Object[] args; + + private EvaluatedAnnotationMetadata(AnnotationMetadata targetMetadata, Object[] args) { + this.delegateAnnotationMetadata = targetMetadata; + this.args = args; + } + + private EvaluatedAnnotationMetadata(AnnotationMetadata targetMetadata) { + this(targetMetadata, new Object[0]); + } + + public static AnnotationMetadata wrapIfNecessary(AnnotationMetadata targetMetadata, + Object[] args) { + if (targetMetadata == null) { + return null; + } else if (targetMetadata instanceof EvaluatedAnnotationMetadata eam) { + AnnotationMetadata delegateMetadata = eam.getAnnotationMetadata(); + EvaluatedAnnotationMetadata methodInvocationMetadata = + new EvaluatedAnnotationMetadata(delegateMetadata, args); + methodInvocationMetadata.setBeanDefinition(eam.owningBean); + methodInvocationMetadata.configure(eam.beanContext); + return methodInvocationMetadata; + } + + if (targetMetadata.hasEvaluatedExpressions()) { + return new EvaluatedAnnotationMetadata(targetMetadata); + } + return targetMetadata; + } + + public static AnnotationMetadata wrapIfNecessary(AnnotationMetadata targetMetadata) { + return wrapIfNecessary(targetMetadata, new Object[0]); + } + + @Override + public boolean hasEvaluatedExpressions() { + // this type of metadata always has evaluated expressions + return true; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return delegateAnnotationMetadata; + } + + @Override + public AnnotationValue mapAnnotationValue(AnnotationValue av) { + return new EvaluatedAnnotationValue<>(beanContext, owningBean, args, av); + } + + @Override + public void configure(BeanContext context) { + this.beanContext = context; + } + + @Override + public void setBeanDefinition(BeanDefinition beanDefinition) { + this.owningBean = beanDefinition; + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java new file mode 100644 index 00000000000..059bd37cbbb --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java @@ -0,0 +1,86 @@ +/* + * Copyright 2017-2020 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.inject.annotation; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.BeanDefinitionAware; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionContext; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.value.ConvertibleValues; +import io.micronaut.core.convert.value.ConvertibleValuesMap; +import io.micronaut.context.ContextConfigurable; +import io.micronaut.core.expression.EvaluatedExpression; +import io.micronaut.core.type.Argument; +import io.micronaut.inject.BeanDefinition; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * {@link AnnotationValue} which is evaluated against BeanContext. + * + * @author graemerocher + * @since 1.0 + * @param The annotation type + */ +@Internal +public class EvaluatedAnnotationValue extends AnnotationValue { + + EvaluatedAnnotationValue(@Nullable BeanContext beanContext, + @Nullable BeanDefinition owningBean, + Object[] args, + AnnotationValue target) { + super( + target, + AnnotationMetadataSupport.getDefaultValues(target.getAnnotationName()), + new EvaluatedConvertibleValuesMap<>(beanContext, owningBean, args, target.getConvertibleValues()), + value -> { + if (value instanceof EvaluatedExpression expression) { + if (value instanceof ContextConfigurable ctxConfigurable && beanContext != null) { + ctxConfigurable.configure(beanContext); + if (value instanceof BeanDefinitionAware beanDefinitionAware) { + beanDefinitionAware.setBeanDefinition(owningBean); + } + } + return expression.evaluate(args); + } + return value; + }); + } + + @Override + public Optional get(CharSequence member, ArgumentConversionContext conversionContext) { + Optional value = getConvertibleValues().get(member, conversionContext); + if (value.isPresent()) { + return value; + } + + return super.get(member, conversionContext); + } + + @Override + public boolean hasEvaluatedExpressions() { + return true; + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java new file mode 100644 index 00000000000..04adef44c1a --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java @@ -0,0 +1,108 @@ +/* + * 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.inject.annotation; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.BeanDefinitionAware; +import io.micronaut.context.ContextConfigurable; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.convert.value.ConvertibleValues; +import io.micronaut.core.convert.value.ConvertibleValuesMap; +import io.micronaut.core.expression.EvaluatedExpression; +import io.micronaut.inject.BeanDefinition; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Version of {@link ConvertibleValuesMap} that is aware of evaluated expressions. + * + * @param The generic value + */ +@Internal +public class EvaluatedConvertibleValuesMap implements ConvertibleValues +{ + private final BeanContext beanContext; + private final BeanDefinition owningBean; + private final Object[] args; + private final ConvertibleValues delegateValues; + + EvaluatedConvertibleValuesMap(@Nullable BeanContext beanContext, + @Nullable BeanDefinition owningBean, + Object[] args, + ConvertibleValues delegateValues) { + this.beanContext = beanContext; + this.owningBean = owningBean; + this.args = args; + this.delegateValues = delegateValues; + } + + @Override + public Set names() { + return delegateValues.names(); + } + + @Override + public Optional get(CharSequence name, + ArgumentConversionContext conversionContext) { + V value = delegateValues.getValue(name); + if (value instanceof EvaluatedExpression expression) { + if (value instanceof ContextConfigurable ctxConfigurable && beanContext != null) { + ctxConfigurable.configure(beanContext); + if (value instanceof BeanDefinitionAware beanDefinitionAware && owningBean != null) { + beanDefinitionAware.setBeanDefinition(owningBean); + } + } + + if (EvaluatedExpression.class.isAssignableFrom(conversionContext.getArgument().getClass())) { + return Optional.of((T) value); + } + + Object evaluationResult = expression.evaluate(args); + if (evaluationResult == null || conversionContext.getArgument().isAssignableFrom(evaluationResult.getClass())) { + return Optional.ofNullable((T) evaluationResult); + } + return ConversionService.SHARED.convert(evaluationResult, conversionContext); + } else { + return delegateValues.get(name, conversionContext); + } + } + + @SuppressWarnings("unchecked") + @Override + public Collection values() { + return delegateValues.values().stream().map(v -> { + if (v instanceof EvaluatedExpression expression) { + if (v instanceof ContextConfigurable ctxConfigurable) { + ctxConfigurable.configure(beanContext); + } + + if (v instanceof BeanDefinitionAware beanDefinitionAware && owningBean != null) { + beanDefinitionAware.setBeanDefinition(owningBean); + } + + Object evaluationResult = expression.evaluate(args); + return (V) evaluationResult; + } + return v; + }).collect(Collectors.toList()); + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java b/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java new file mode 100644 index 00000000000..cc1500d7983 --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java @@ -0,0 +1,451 @@ +/* + * 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.inject.annotation; + +import io.micronaut.core.annotation.AnnotationMetadataDelegate; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.core.type.Argument; +import io.micronaut.core.util.StringUtils; +import io.micronaut.core.value.OptionalValues; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.function.Supplier; + +/** + * Abstract annotation metadata delegate for cases when annotation + * values need to be mapped before being returned. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +public abstract class MappingAnnotationMetadataDelegate implements AnnotationMetadataDelegate { + public abstract AnnotationValue mapAnnotationValue(AnnotationValue av); + + @Override + public Optional stringValue(String annotation, String member) { + return findAnnotation(annotation) + .flatMap(av -> av.stringValue(member)); + } + + @Override + public Optional stringValue(Class annotation, String member) { + return stringValue(annotation.getName(), member); + } + + @Override + public Optional stringValue(Class annotation) { + return stringValue(annotation, VALUE_MEMBER); + } + + @Override + public Optional stringValue(String annotation) { + return stringValue(annotation, VALUE_MEMBER); + } + + @Override + public String[] stringValues(String annotation, String member) { + return findAnnotation(annotation) + .map(av -> av.stringValues(member)) + .orElse(StringUtils.EMPTY_STRING_ARRAY); + } + + @Override + public String[] stringValues(Class annotation, String member) { + return stringValues(annotation.getName(), member); + } + + @Override + public String[] stringValues(Class annotation) { + return stringValues(annotation, VALUE_MEMBER); + } + + @Override + public String[] stringValues(String annotation) { + return stringValues(annotation, VALUE_MEMBER); + } + + @Override + public > Optional enumValue(String annotation, Class enumType) { + return enumValue(annotation, VALUE_MEMBER, enumType); + } + + @Override + public > Optional enumValue(String annotation, String member, + Class enumType) { + return findAnnotation(annotation) + .flatMap(av -> av.enumValue(member, enumType)); + } + + @Override + public > Optional enumValue(Class annotation, + Class enumType) { + return enumValue(annotation.getName(), VALUE_MEMBER, enumType); + } + + @Override + public > Optional enumValue(Class annotation, + String member, Class enumType) { + return enumValue(annotation.getName(), member, enumType); + } + + @Override + public > E[] enumValues(String annotation, String member, Class enumType) { + return findAnnotation(annotation) + .map(av -> av.enumValues(member, enumType)) + .orElse((E[]) Array.newInstance(enumType, 0)); + } + + @Override + public > E[] enumValues(String annotation, Class enumType) { + return enumValues(annotation, VALUE_MEMBER, enumType); + } + + @Override + public > E[] enumValues(Class annotation, + Class enumType) { + return enumValues(annotation.getName(), VALUE_MEMBER, enumType); + } + + @Override + public > E[] enumValues(Class annotation, + String member, Class enumType) { + return enumValues(annotation.getName(), member, enumType); + } + + @Override + public Class[] classValues(String annotation, String member) { + return (Class[]) findAnnotation(annotation) + .map(av -> av.classValues(member)) + .orElse(ReflectionUtils.EMPTY_CLASS_ARRAY); + } + + @Override + public Class[] classValues(String annotation) { + return classValues(annotation, VALUE_MEMBER); + } + + @Override + public Class[] classValues(Class annotation) { + return classValues(annotation.getName(), VALUE_MEMBER); + } + + @Override + public Class[] classValues(Class annotation, String member) { + return classValues(annotation.getName(), member); + } + + @Override + public Optional booleanValue(String annotation, String member) { + return findAnnotation(annotation) + .flatMap(av -> av.booleanValue(member)); + } + + @Override + public Optional booleanValue(Class annotation, String member) { + return booleanValue(annotation.getName(), member); + } + + @Override + public Optional booleanValue(Class annotation) { + return booleanValue(annotation.getName(), VALUE_MEMBER); + } + + @Override + public Optional booleanValue(String annotation) { + return booleanValue(annotation, VALUE_MEMBER); + } + + @Override + public boolean isTrue(String annotation, String member) { + return getValue(annotation, member, Boolean.class).orElse(false); + } + + @Override + public boolean isTrue(Class annotation, String member) { + return isTrue(annotation.getName(), member); + } + + @Override + public boolean isFalse(String annotation, String member) { + return !isTrue(annotation, member); + } + + @Override + public boolean isFalse(Class annotation, String member) { + return isFalse(annotation.getName(), member); + } + + @Override + public Optional classValue(String annotation, String member) { + return findAnnotation(annotation) + .flatMap(av -> av.classValue(member)); + } + + @Override + public Optional classValue(String annotation) { + return classValue(annotation, VALUE_MEMBER); + } + + @Override + public Optional classValue(Class annotation) { + return classValue(annotation.getName(), VALUE_MEMBER); + } + + @Override + public Optional classValue(Class annotation, String member) { + return classValue(annotation.getName(), member); + } + + @Override + public OptionalInt intValue(String annotation, String member) { + return findAnnotation(annotation) + .map(AnnotationValue::intValue) + .orElse(OptionalInt.empty()); + } + + @Override + public OptionalInt intValue(Class annotation, String member) { + return intValue(annotation.getName(), member); + } + + @Override + public OptionalInt intValue(Class annotation) { + return intValue(annotation.getName(), VALUE_MEMBER); + } + + @Override + public OptionalLong longValue(String annotation, String member) { + return findAnnotation(annotation) + .map(AnnotationValue::longValue) + .orElse(OptionalLong.empty()); + } + + @Override + public OptionalLong longValue(Class annotation, String member) { + return longValue(annotation.getName(), member); + } + + @Override + public OptionalDouble doubleValue(String annotation, String member) { + return findAnnotation(annotation) + .map(av -> av.doubleValue(member)) + .orElse(OptionalDouble.empty()); + } + + @Override + public OptionalDouble doubleValue(Class annotation, String member) { + return findAnnotation(annotation) + .map(av -> av.doubleValue(member)) + .orElse(OptionalDouble.empty()); + } + + @Override + public OptionalDouble doubleValue(Class annotation) { + return doubleValue(annotation, VALUE_MEMBER); + } + + @Override + public Optional getValue(String annotation, String member, Argument requiredType) { + return findAnnotation(annotation) + .flatMap(av -> av.get(member, requiredType)); + } + + @Override + public Optional getValue(Class annotation, String member, + Argument requiredType) { + return findAnnotation(annotation) + .flatMap(av -> av.get(member, requiredType)); + } + + @Override + public Optional getValue(String annotation, Argument requiredType) { + return getValue(annotation, VALUE_MEMBER, requiredType); + } + + @Override + public Optional getValue(Class annotation, + Argument requiredType) { + return getValue(annotation, VALUE_MEMBER, requiredType); + } + + @Override + public Optional getValue(Class annotation, String member, + Class requiredType) { + return getValue(annotation, member, Argument.of(requiredType)); + } + + @Override + public Optional getValue(Class annotation, Class requiredType) { + return getValue(annotation, VALUE_MEMBER, requiredType); + } + + @Override + public Optional getValue(String annotation, String member, Class requiredType) { + return getValue(annotation, member, Argument.of(requiredType)); + } + + @Override + public Optional getValue(String annotation, Class requiredType) { + return getValue(annotation, VALUE_MEMBER, Argument.of(requiredType)); + } + + @Override + public Optional getValue(String annotation, String member) { + return getValue(annotation, member, Object.class); + } + + @Override + public Optional getValue(Class annotation, String member) { + return getValue(annotation, member, Object.class); + } + + @Override + public Optional getValue(String annotation) { + return getValue(annotation, VALUE_MEMBER, Object.class); + } + + @Override + public Optional getValue(Class annotation) { + return getValue(annotation, VALUE_MEMBER, Object.class); + } + + @Override + public OptionalValues getValues(Class annotation, + Class valueType) { + return getValues(annotation.getName(), valueType); + } + + @Override + public OptionalValues getValues(String annotation, Class valueType) { + return OptionalValues.of(valueType, getValues(annotation)); + } + + @Override + public Map getValues(String annotation) { + return findAnnotation(annotation) + .map(AnnotationValue::getValues) + .orElse(Collections.emptyMap()); + } + + @Override + public AnnotationValue getDeclaredAnnotation(Class annotationClass) { + return AnnotationMetadataDelegate.super.getDeclaredAnnotation(annotationClass); + } + + @Override + public AnnotationValue getAnnotation(Class annotationClass) { + return getAnnotation(annotationClass.getName()); + } + + @Override + public AnnotationValue getAnnotation(String annotation) { + return this.findAnnotation(annotation).orElse(null); + } + + @Override + public Optional> findAnnotation(String annotation) { + Optional> av = getAnnotationMetadata().findAnnotation(annotation); + return av.map(this::mapAnnotationValue); + } + + @Override + public Optional> findAnnotation(Class annotationClass) { + return getAnnotationMetadata().findAnnotation(annotationClass) + .map(this::mapAnnotationValue); + } + + @Override + public Optional> findDeclaredAnnotation(Class annotationClass) { + return findDeclaredAnnotation(annotationClass.getName()); + } + + @Override + public Optional> findDeclaredAnnotation(String annotation) { + Optional> av = + getAnnotationMetadata().findDeclaredAnnotation(annotation); + return av.map(this::mapAnnotationValue); + } + + @Override + public T[] synthesizeDeclaredAnnotationsByType(Class annotationClass) { + return getDeclaredAnnotationValuesByType(annotationClass).stream() + .map(annotation -> AnnotationMetadataSupport.buildAnnotation(annotationClass, + annotation)) + .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); + } + + @Override + public T[] synthesizeAnnotationsByType(Class annotationClass) { + return getAnnotationValuesByType(annotationClass).stream() + .map(annotation -> AnnotationMetadataSupport.buildAnnotation(annotationClass, + annotation)) + .toArray(value -> (T[]) Array.newInstance(annotationClass, value)); + } + + @Override + public T synthesizeDeclared(Class annotationClass) { + return findDeclaredAnnotation(annotationClass) + .map(av -> AnnotationMetadataSupport.buildAnnotation(annotationClass, av)) + .orElse(null); + } + + @Override + public T synthesize(Class annotationClass) { + return findAnnotation(annotationClass) + .map(av -> AnnotationMetadataSupport.buildAnnotation(annotationClass, av)) + .orElse(null); + } + + @Override + public List> getAnnotationValuesByType(Class annotationType) { + return getAnnotationValues(() -> getAnnotationMetadata().getAnnotationValuesByType(annotationType)); + } + + @Override + public List> getDeclaredAnnotationValuesByType(Class annotationType) { + return getAnnotationValues(() -> getAnnotationMetadata().getDeclaredAnnotationValuesByType(annotationType)); + } + + @Override + public List> getAnnotationValuesByStereotype(String stereotype) { + return getAnnotationValues(() -> getAnnotationMetadata().getAnnotationValuesByStereotype(stereotype)); + } + + @Override + public List> getDeclaredAnnotationValuesByName(String annotationType) { + return getAnnotationValues(() -> getAnnotationMetadata().getDeclaredAnnotationValuesByName(annotationType)); + } + + @Override + public List> getAnnotationValuesByName(String annotationType) { + return getAnnotationValues(() -> getAnnotationMetadata().getAnnotationValuesByName(annotationType)); + } + + private List> getAnnotationValues(Supplier>> supplier) { + return supplier.get().stream() + .map(this::mapAnnotationValue) + .toList(); + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java index b16298f0172..6920f93555c 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java @@ -19,6 +19,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.EvaluatedExpressionReference; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; @@ -51,6 +52,8 @@ public class MutableAnnotationMetadata extends DefaultAnnotationMetadata { private boolean hasPropertyExpressions = false; + private boolean hasEvaluatedExpressions = false; + @Nullable Map> annotationDefaultValues; @Nullable @@ -66,12 +69,22 @@ public class MutableAnnotationMetadata extends DefaultAnnotationMetadata { public MutableAnnotationMetadata() { } + private MutableAnnotationMetadata(@Nullable Map> declaredAnnotations, + @Nullable Map> declaredStereotypes, + @Nullable Map> allStereotypes, + @Nullable Map> allAnnotations, + @Nullable Map> annotationsByStereotype, + boolean hasPropertyExpressions) { + this(declaredAnnotations, declaredStereotypes, allStereotypes, allAnnotations, annotationsByStereotype, hasPropertyExpressions, false); + } + private MutableAnnotationMetadata(@Nullable Map> declaredAnnotations, @Nullable Map> declaredStereotypes, @Nullable Map> allStereotypes, @Nullable Map> allAnnotations, @Nullable Map> annotationsByStereotype, - boolean hasPropertyExpressions) { + boolean hasPropertyExpressions, + boolean hasEvaluatedExpressions) { super(declaredAnnotations, declaredStereotypes, allStereotypes, @@ -79,6 +92,7 @@ private MutableAnnotationMetadata(@Nullable Map(annotationDefaultValues); @@ -130,6 +150,7 @@ public MutableAnnotationMetadata clone() { cloned.sourceAnnotationDefaultValues = cloneMapOfMapValue(sourceAnnotationDefaultValues); } cloned.hasPropertyExpressions = hasPropertyExpressions; + cloned.hasEvaluatedExpressions = hasEvaluatedExpressions; return cloned; } @@ -152,6 +173,10 @@ private boolean computeHasPropertyExpressions(Map values, return hasPropertyExpressions || values != null && retentionPolicy == RetentionPolicy.RUNTIME && hasPropertyExpressions(values); } + private boolean computeHasEvaluatedExpressions(Map values, RetentionPolicy retentionPolicy) { + return hasEvaluatedExpressions || values != null && retentionPolicy == RetentionPolicy.RUNTIME && hasEvaluatedExpressions(values); + } + private boolean hasPropertyExpressions(Map values) { if (CollectionUtils.isEmpty(values)) { return false; @@ -544,6 +569,8 @@ private void addAnnotation(String annotation, boolean isDeclared, RetentionPolicy retentionPolicy) { hasPropertyExpressions = computeHasPropertyExpressions(values, retentionPolicy); + hasEvaluatedExpressions = computeHasEvaluatedExpressions(values, retentionPolicy); + if (isDeclared && declaredAnnotations != null) { putValues(annotation, values, declaredAnnotations); } @@ -646,6 +673,8 @@ private void addRepeatableInternal(String repeatableAnnotationContainer, Map> allAnnotations, RetentionPolicy retentionPolicy) { hasPropertyExpressions = computeHasPropertyExpressions(annotationValue.getValues(), retentionPolicy); + hasEvaluatedExpressions = computeHasEvaluatedExpressions(annotationValue.getValues(), retentionPolicy); + if (annotationRepeatableContainer == null) { annotationRepeatableContainer = new HashMap<>(2); } @@ -718,6 +747,7 @@ protected AnnotationValue newAnnotationValue(String an @Internal public void addAnnotationMetadata(DefaultAnnotationMetadata annotationMetadata) { hasPropertyExpressions |= annotationMetadata.hasPropertyExpressions(); + hasEvaluatedExpressions |= annotationMetadata.hasEvaluatedExpressions(); if (annotationMetadata.declaredAnnotations != null && !annotationMetadata.declaredAnnotations.isEmpty()) { if (declaredAnnotations == null) { declaredAnnotations = new LinkedHashMap<>(); @@ -779,6 +809,7 @@ public void addAnnotationMetadata(DefaultAnnotationMetadata annotationMetadata) public void addAnnotationMetadata(MutableAnnotationMetadata annotationMetadata) { addAnnotationMetadata((DefaultAnnotationMetadata) annotationMetadata); hasPropertyExpressions |= annotationMetadata.hasPropertyExpressions; + hasEvaluatedExpressions |= annotationMetadata.hasEvaluatedExpressions; if (annotationMetadata.sourceRetentionAnnotations != null) { if (sourceRetentionAnnotations == null) { sourceRetentionAnnotations = new HashSet<>(annotationMetadata.sourceRetentionAnnotations); @@ -1048,4 +1079,24 @@ protected String findRepeatableAnnotationContainerInternal(String annotation) { } return AnnotationMetadataSupport.getRepeatableAnnotation(annotation); } + + private boolean hasEvaluatedExpressions(Map annotationValues) { + if (CollectionUtils.isEmpty(annotationValues)) { + return false; + } + + return annotationValues.values().stream().anyMatch(value -> { + if (value instanceof EvaluatedExpressionReference) { + return true; + } else if (value instanceof AnnotationValue av) { + return hasEvaluatedExpressions(av.getValues()); + } else if (value instanceof AnnotationValue[] avArray) { + return Arrays.stream(avArray) + .map(AnnotationValue::getValues) + .anyMatch(this::hasEvaluatedExpressions); + } else { + return false; + } + }); + } } From 4f949fb053f2c1d7d895b00acedeec9e9114ab1c Mon Sep 17 00:00:00 2001 From: Sergey Gavrilov Date: Tue, 14 Feb 2023 12:04:15 +0300 Subject: [PATCH 02/35] compile time expressions checkstyle fix --- .../annotation/EvaluatedExpressionReference.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/annotation/EvaluatedExpressionReference.java b/core/src/main/java/io/micronaut/core/annotation/EvaluatedExpressionReference.java index a2a5c6ca3ed..d5d214c8a33 100644 --- a/core/src/main/java/io/micronaut/core/annotation/EvaluatedExpressionReference.java +++ b/core/src/main/java/io/micronaut/core/annotation/EvaluatedExpressionReference.java @@ -61,17 +61,19 @@ public static Integer nextIndex(String className) { } @Override - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } EvaluatedExpressionReference that = (EvaluatedExpressionReference) o; return expressionClassName.equals(that.expressionClassName); } @Override - public int hashCode() - { + public int hashCode() { return Objects.hash(expressionClassName); } } From cdb8f4116d2a8ad1fed5c96e105077c3b70fe96d Mon Sep 17 00:00:00 2001 From: Sergey Gavrilov Date: Sun, 26 Feb 2023 21:37:15 +0300 Subject: [PATCH 03/35] added docs --- .../guide/config/evaluatedExpressions.adoc | 376 ++++++++++++++++++ src/main/docs/guide/toc.yml | 1 + 2 files changed, 377 insertions(+) create mode 100644 src/main/docs/guide/config/evaluatedExpressions.adoc diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc new file mode 100644 index 00000000000..c95c16f5762 --- /dev/null +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -0,0 +1,376 @@ +Since 4.0 Micronaut supports embedding evaluated expressions in annotation values using `#{...}` syntax which +allows to achieve even more flexibility while configuring your application. + +.Evaluated Expression example +[source,groovy] +---- +@Value("#{ T(Math).random() }") +double injectedValue; +---- + +Evaluated expressions can be used wherever annotation value accept string (within array of strings as well). + +.Evaluated Expression in array +[source,java] +---- +@Singleton +@Requires(env = {"dev", "#{ 'test' }"}) +public class EvaluatedExpressionInArray {} +---- + +You can also embed one or more expressions in string template in a way similar to embedding properties with `${...}` syntax. + +.Evaluated Expression template +[source,groovy] +---- +@Value("http://#{'hostname'}/#{'path'}") +String url; +---- + +Evaluated Expressions are validated and compiled at build time which guarantees type safety at runtime. Once application +is running expressions are evaluated on demand as part of annotation metadata resolution. The usage of expressions does +not affect performance as evaluation process is completely reflection free. + +In general, Evaluated Expression can be treated as statement written using a programming language with reduced +set of available features. Even though the complexity of expression is only limited by the list of supported syntax +constructs, it is in general not recommended to place complex logic inside it as there are usually better ways to +achieve the same result. + +== Evaluated Expression language reference + +The Evaluated Expressions syntax supports the following functionality: + +* Literal Values +* Math Operators +* Comparison Operators +* Logical Operators +* Ternary Operator +* Type References +* Method Invocation +* Property Access + +=== Literal Values + +The following types of literal values are supported: + +* `null` +* boolean values (`true`, `false`) +* strings, which need to be surrounded with single quotation mark (`'`) +* numeric values (`int`, `long`, `float`, `double`) + +Integer and Long values can also be specified in hexadecimal or octal notation. Float and Double values can also be +specified in exponential notation. All numeric values can be negative as well. + +.Literal values examples +[source] +---- +#{ null } +#{ true } +#{ 'string value' } +#{ 10 } +#{ 0xFFL } +#{ 10L } +#{ .123f } +#{ 1E+1d } +#{ 123D } +---- + +=== Math Operators + +The supported mathematical operators are `+`, `-`, `*`, `/`, `%`, `^`. Math operators can only be applied to numeric +values (except `+` which can be used for string concatenation as well). Mathematical operations are performed in order +enforced by standard operator precedence. You can also change evaluation order by using brackets `()`. + +`/` and `%` operators can be aliased by `div` and `mod` keywords respectively. + +.Math operators examples +[source] +---- +#{ 1 + 2 } // 3 +#{ 'a' + 'b' + 'c' } // 'abc' +#{ 7 - 3 } // 4 +#{ 7 * 3 } // 21 +#{ 7 * ( 3 + 1) } // 28 + +#{ 15 / 3 } // 5 +#{ 15 div 3 } // 5 + +#{ 15 % 3 } // 0 +#{ 15 mod 3 } // 0 + +// Unlike in Java, ^ operator means exponentiation +#{ 3 ^ 2 } // 9 +---- + +=== Comparison Operators + +The following comparison operators are supported: `==`, `!=`, `>`, `<`, `>=`, `\<=`, `matches` +Comparison operations are performed in order enforced by standard operator precedence. +You can also change evaluation order by using brackets `()`. + +Equality check is supported for both primitive types and objects. It is performed using `Object.equals()` method. + +`>`, `<`, `>=`, `\<=` operations can only be applied to numeric types. + +`matches` keyword can be used to determine whether a string matches provided regular expression which has to +be specified as string literal. The regular expression itself will be checked for validity at compilation time. + +.Comparison operators examples +[source] +---- +#{ 1 + 2 == 3 } // true +#{ 'abc' != 'abc' } // false +#{ 7 > 3 } // true +#{ 7 < 3 } // false +#{ 7 >= 7 } // true +#{ 7 <= 8 } // false + +#{ 'AbC' matches '[A-Za-z*' } // Compilation failure +#{ 'AbC' matches '[A-Za-z]*' } // true +#{ 'AbC' matches '[a-z]*' } // false +---- + +=== Logical Operators + +The following logical operators are supported: `&&` (can be aliased with `and`), `||` (can be aliased with `or`), +`!`. Logical operations are performed in order enforced by standard operator precedence. +You can also change evaluation order by using brackets `()`. + +.Logical operators examples +[source] +---- +#{ true && false } // false +#{ true and true } // true + +#{ true || false } // true +#{ false or false } // false + +#{ !false } // true +#{ !!true } // true +---- + +=== Ternary Operator + +A standard ternary operator is supported to allow specifying if-then-else conditional logic in expression + +[source] +---- +condition ? thenBranch : elseBranch +---- + +where `condition` evaluation should provide boolean value, and the complexity of `then` and `else` branches is not +limited. + +.Ternary operator examples +[source] +---- +#{ 15 > 10 ? 'a' : 'b' } // 'a' +#{ 15 >= 16 ? 'a' : 'b' } // 'b' +---- + +=== Type References + +Predefined syntax construct `T(...)` can be used to reference a class. The value inside brackets should be fully +qualified class name (including package). The only exception is `java.lang.*` classes which can be referenced +directly by only specifying simple class name. Primitive types can not be specified inside brackets. + +Type References are evaluated in different ways depending on the context. + +==== Simple type reference + +Simple type reference is resolved as `Class` object. + +.Type reference example +[source] +---- +#{ T(java.lang.String) } // String.class +---- + +Same rule applies if type reference is specified as method argument. + +==== Type check with `instanceof` + +Type Reference can be used as right-hand side part of `instanceof` operator + +.Type check example +[source] +---- +#{ 'abc' instanceof T(String) } // true +---- + +which is equivalent to the following Java code and will be evaluated as boolean value: + +[source] +---- +"abc" instanceof String +---- + +==== Static method invocation + +Type Reference can be used to invoke static method of a class + +.Static method invocation +[source] +---- +#{ T(Math).random() } +---- + +=== Expression Evaluation Context + +By default, the only methods you can invoke inside Evaluated Expressions are static methods using type references. + +To invoke non-static methods, you need to place `@EvaluatedExpressionContext` annotation on a class owning the +method you want to invoke inside expression. In this case the annotated class is registered within evaluation context +which makes its methods and properties available for referencing in evaluated expressions. Any context reference +needs to be prefixed with `#` sign. + +Consider the following example: + +.User-defined evaluated expression context +[source, java] +---- +import io.micronaut.context.annotation.EvaluatedExpressionContext; +import java.util.Random; + +@EvaluatedExpressionContext +public class CustomEvaluationContext { + + public int generateRandom(int min, int max) { + return new Random().nextInt(max - min) + min; + } + +} +---- + +Method `generateRandom(int, int)` can now be used within Evaluated Expression in the following way: + +.Usage of user-defined evaluated expression context +[source, java] +---- +import io.micronaut.context.annotation.Value; +import jakarta.inject.Singleton; + +@Singleton +public class ContextConsumer { + + @Value("#{ #generateRandom(1, 10) }") + public int randomField; + +} +---- + +Note that annotating a class with `@EvaluatedExpressionContext` makes it a bean. By default, it will be treated as +singleton, but you can specify other scope for it. At runtime, the bean will be retrieved from application context +and respective method will be invoked. + +if matching method is not found within evaluation context at compilation time, the compilation will fail. Same applies +in case of multiple suitable methods are found in evaluation context, keep that in mind if you annotate multiple classes +with `@EvaluatedExpressionContext`. + +The methods will be considered ambiguous (leading to compilation failure) when their names are the same and list of +provided arguments matches multiple methods parameters. + +If the class annotated with `@EvaluatedExpressionContext` resides within separate JAR which is added as a dependency +to your project, it has to be added to the `annotationProcessor` classpath to become available at annotation processing +stage. That's why it is generally preferable to place separate contexts in projects that include few external dependencies +to avoid polluting the annotation processor classpath. + +=== Method Invocation + +You can invoke both static methods using type references, methods from evaluation context and methods on objects, +which means method chaining is supported. + +.Chaining methods in expression +[source, java] +---- +import io.micronaut.context.annotation.EvaluatedExpressionContext; +import io.micronaut.context.annotation.Value; +import jakarta.inject.Singleton; + +@EvaluatedExpressionContext +class CustomEvaluationContext { + + public String stringValue() { + return "stringValue"; + } + +} + +@Singleton +class ContextConsumer { + + @Value("#{ #stringValue().length() }") + public int stringLength; + +} +---- + +Varargs methods invocation is supported as well. Note that if last parameter of a method is an array, you can still +invoke it providing list of arguments separated by comma without explicitly wrapping it into array. So in this case +it will be treated in same way as if last method argument was explicitly specified as varargs parameter. + +.Invoking varargs methods in expressions +[source, java] +---- +import io.micronaut.context.annotation.EvaluatedExpressionContext; +import io.micronaut.context.annotation.Value; +import jakarta.inject.Singleton; + +@EvaluatedExpressionContext +class CustomEvaluationContext { + + public int countIntegers(int... values) { + return values.length; + } + + public int countStrings(String[] values) { + return values.length; + } + +} + +@Singleton +class ContextConsumer { + + @Value("#{ #countIntegers(1, 2, 3) }") + public int totalIntegers; + + @Value("#{ #countStrings('a', 'b', 'c') }") + public int totalStrings; + +} +---- + +=== Property Access + +JavaBean properties can be accessed simply be referencing their names from evaluation context prefixed with `#`. Bean +properties can also be chained with dot in the same way as methods. + +.Accessing bean properties in expressions +[source, java] +---- +import io.micronaut.context.annotation.EvaluatedExpressionContext; +import io.micronaut.context.annotation.Value; +import jakarta.inject.Singleton; + +@EvaluatedExpressionContext +class CustomEvaluationContext { + + public String getName() { + return "Bob"; + } + + public int getAge() { + return 25; + } + +} + +@Singleton +class ContextConsumer { + + @Value("#{ 'Name is ' + #name + ', age is ' + #age }") + public String value; + +} +---- diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index b2ac3db2ef0..b00428ea131 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -56,6 +56,7 @@ config: bootstrap: Bootstrap Configuration jmx: title: JMX Support + evaluatedExpressions: Micronaut Evaluated Expressions aop: title: Aspect Oriented Programming aroundAdvice: Around Advice From ec517edb7da98b854151037218add852e45ab092 Mon Sep 17 00:00:00 2001 From: Sergey Gavrilov Date: Tue, 7 Mar 2023 10:02:02 +0300 Subject: [PATCH 04/35] review improvements --- .../aop/chain/MethodInterceptorChain.java | 2 +- .../EvaluatedExpressionConstants.java | 35 +++++ .../EvaluatedExpressionWriter.java | 11 +- .../CompositeExpressionEvaluationContext.java | 54 ------- ... DefaultExpressionCompilationContext.java} | 80 +++++++---- .../context/ExpressionCompilationContext.java | 62 ++++++++ .../ExpressionCompilationContextFactory.java | 121 ++++++++++++++++ .../ExpressionCompilationContextRegistry.java | 65 +++++++++ .../context/ExpressionContextFactory.java | 136 ------------------ .../context/ExpressionContextLoader.java | 94 ------------ .../context/ExpressionEvaluationContext.java | 56 -------- .../context/ExpressionWithContext.java | 4 +- ...xtendableExpressionCompilationContext.java | 52 +++++++ .../MethodExpressionEvaluationContext.java | 50 ------- ...undEvaluatedEvaluatedExpressionParser.java | 7 +- .../parser/ast/ExpressionNode.java | 12 +- .../parser/ast/access/AbstractMethodCall.java | 16 +-- .../ast/access/ContextElementAccess.java | 40 +++--- .../parser/ast/access/ContextMethodCall.java | 18 +-- .../access/ContextMethodParameterAccess.java | 17 ++- .../parser/ast/access/ElementMethodCall.java | 6 +- .../parser/ast/access/PropertyAccess.java | 4 +- .../ast/collection/OneDimensionalArray.java | 8 +- .../ast/conditional/TernaryExpression.java | 8 +- .../parser/ast/literal/BoolLiteral.java | 6 +- .../parser/ast/literal/DoubleLiteral.java | 6 +- .../parser/ast/literal/FloatLiteral.java | 6 +- .../parser/ast/literal/IntLiteral.java | 6 +- .../parser/ast/literal/LongLiteral.java | 6 +- .../parser/ast/literal/NullLiteral.java | 6 +- .../parser/ast/literal/StringLiteral.java | 6 +- .../ast/operator/binary/AddOperator.java | 8 +- .../ast/operator/binary/AndOperator.java | 6 +- .../ast/operator/binary/BinaryOperator.java | 6 +- .../ast/operator/binary/DivOperator.java | 4 +- .../ast/operator/binary/EqOperator.java | 4 +- .../operator/binary/InstanceofOperator.java | 6 +- .../ast/operator/binary/MatchesOperator.java | 6 +- .../ast/operator/binary/MathOperator.java | 6 +- .../ast/operator/binary/ModOperator.java | 4 +- .../ast/operator/binary/MulOperator.java | 4 +- .../ast/operator/binary/NeqOperator.java | 4 +- .../ast/operator/binary/OrOperator.java | 4 +- .../ast/operator/binary/PowOperator.java | 4 +- .../operator/binary/RelationalOperator.java | 4 +- .../ast/operator/binary/SubOperator.java | 4 +- .../ast/operator/unary/NegOperator.java | 6 +- .../ast/operator/unary/NotOperator.java | 6 +- .../ast/operator/unary/PosOperator.java | 6 +- .../ast/operator/unary/UnaryOperator.java | 4 +- .../parser/ast/types/TypeIdentifier.java | 8 +- .../parser/ast/util/TypeDescriptors.java | 5 +- ...ext.java => ExpressionVisitorContext.java} | 14 +- .../ExpressionCompilationException.java | 3 + .../exception/ExpressionParsingException.java | 3 + .../util/EvaluatedExpressionsUtils.java | 4 +- .../AbstractAnnotationMetadataBuilder.java | 6 +- .../annotation/AnnotationMetadataWriter.java | 4 +- ...edExpressionContextTypeElementVisitor.java | 32 +---- .../ExpressionContextReferenceWriter.java | 91 ------------ .../writer/AbstractClassFileWriter.java | 19 --- .../inject/writer/BeanDefinitionWriter.java | 12 +- .../ExecutableMethodsDefinitionWriter.java | 12 +- .../core/annotation/AnnotationValue.java | 12 +- .../annotation/AnnotationValueBuilder.java | 3 +- .../core/expression/EvaluatedExpression.java | 53 ------- .../core/expressions/EvaluatedExpression.java | 20 +-- .../EvaluatedExpressionReference.java | 5 +- .../ExpressionEvaluationContext.java | 46 ++++++ .../AbstractEvaluatedExpressionsSpec.groovy | 25 ++-- .../ast/groovy/InjectTransform.groovy | 2 - .../GroovyAnnotationMetadataBuilder.java | 2 +- ...notationLevelContextExpressionsSpec.groovy | 12 +- .../CompoundExpressionsSpec.groovy | 10 +- ...entEvaluationContextExpressionsSpec.groovy | 7 +- .../TestExpressionsUsageSpec.groovy | 3 + .../AbstractEvaluatedExpressionsSpec.groovy | 25 ++-- .../BeanDefinitionInjectProcessor.java | 4 +- .../TypeElementVisitorProcessor.java | 4 +- ...notationLevelContextExpressionsSpec.groovy | 12 +- .../CompoundExpressionsSpec.groovy | 10 +- ...entEvaluationContextExpressionsSpec.groovy | 7 +- .../TestExpressionsUsageSpec.groovy | 2 + .../context/AbstractEvaluatedExpression.java | 115 --------------- .../EvaluatedExpressionContext.java | 2 +- .../ExpressionEvaluationException.java | 5 +- .../AbstractEvaluatedExpression.java | 66 +++++++++ ...nfigurableExpressionEvaluationContext.java | 60 ++++++++ .../DefaultExpressionEvaluationContext.java | 108 ++++++++++++++ .../EvaluatedAnnotationMetadata.java | 79 +++++----- .../annotation/EvaluatedAnnotationValue.java | 86 ----------- .../EvaluatedConvertibleValuesMap.java | 46 ++---- .../annotation/MutableAnnotationMetadata.java | 2 +- 93 files changed, 1012 insertions(+), 1130 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionConstants.java delete mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/CompositeExpressionEvaluationContext.java rename core-processor/src/main/java/io/micronaut/expressions/context/{BeanContextExpressionEvaluationContext.java => DefaultExpressionCompilationContext.java} (56%) create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContext.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextRegistry.java delete mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextFactory.java delete mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextLoader.java delete mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContext.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExtendableExpressionCompilationContext.java delete mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/MethodExpressionEvaluationContext.java rename core-processor/src/main/java/io/micronaut/expressions/parser/compilation/{ExpressionCompilationContext.java => ExpressionVisitorContext.java} (65%) delete mode 100644 core-processor/src/main/java/io/micronaut/inject/beans/visitor/ExpressionContextReferenceWriter.java delete mode 100644 core/src/main/java/io/micronaut/core/expression/EvaluatedExpression.java rename core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextReference.java => core/src/main/java/io/micronaut/core/expressions/EvaluatedExpression.java (59%) rename core/src/main/java/io/micronaut/core/{annotation => expressions}/EvaluatedExpressionReference.java (95%) create mode 100644 core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java delete mode 100644 inject/src/main/java/io/micronaut/context/AbstractEvaluatedExpression.java create mode 100644 inject/src/main/java/io/micronaut/context/expressions/AbstractEvaluatedExpression.java create mode 100644 inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java create mode 100644 inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java delete mode 100644 inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java diff --git a/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java b/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java index 4836ce51af1..089f54574a2 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java +++ b/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java @@ -101,7 +101,7 @@ public MethodInterceptorChain(Interceptor[] interceptors, T target, Execut @Override public AnnotationMetadata getAnnotationMetadata() { if (executionHandle.getAnnotationMetadata() instanceof EvaluatedAnnotationMetadata eam) { - return EvaluatedAnnotationMetadata.wrapIfNecessary(eam, originalParameters); + return eam.copyWithArgs(originalParameters); } return executionHandle.getAnnotationMetadata(); } diff --git a/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionConstants.java b/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionConstants.java new file mode 100644 index 00000000000..71249085c3c --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionConstants.java @@ -0,0 +1,35 @@ +/* + * 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.expressions; + +/** + * Set of constants used for evaluated expressions processing. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +public class EvaluatedExpressionConstants { + /** + * Evaluated expression prefix. + */ + public static final String EXPRESSION_PREFIX = "#{"; + + /** + * RegEx pattern used to determine whether string value in + * annotation includes evaluated expression. + */ + public static final String EXPRESSION_PATTERN = ".*#\\{.*}.*"; +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java b/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java index f6f2ba36194..03be8ae3cd3 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java +++ b/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java @@ -15,12 +15,13 @@ */ package io.micronaut.expressions; -import io.micronaut.context.AbstractEvaluatedExpression; +import io.micronaut.context.expressions.AbstractEvaluatedExpression; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.expressions.ExpressionEvaluationContext; import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.expressions.parser.CompoundEvaluatedEvaluatedExpressionParser; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import io.micronaut.expressions.parser.exception.ExpressionParsingException; import io.micronaut.inject.ast.Element; @@ -94,10 +95,10 @@ private ClassWriter generateClassBytes(String expressionClassName) { // MAXLOCALS = 1 cv.visitMaxs(2, 1); - GeneratorAdapter evaluateMethodVisitor = startProtectedVarargsMethod(classWriter, "doEvaluate", - Object.class.getName(), Object.class.getName() + "[]"); + GeneratorAdapter evaluateMethodVisitor = startProtectedMethod(classWriter, "doEvaluate", + Object.class.getName(), ExpressionEvaluationContext.class.getName()); - ExpressionCompilationContext ctx = new ExpressionCompilationContext( + ExpressionVisitorContext ctx = new ExpressionVisitorContext( expressionMetadata.evaluationContext(), visitorContext, evaluateMethodVisitor); diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/CompositeExpressionEvaluationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/CompositeExpressionEvaluationContext.java deleted file mode 100644 index 4d0b7406e42..00000000000 --- a/core-processor/src/main/java/io/micronaut/expressions/context/CompositeExpressionEvaluationContext.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.expressions.context; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.TypedElement; - -import java.util.Arrays; -import java.util.List; - -/** - * Class representing multiple expression evaluation contexts. This class is used as a wrapper - * to simplify obtaining context elements in cases when multiple contexts are available at - * compilation stage. - * - * @author Sergey Gavrilov - * @since 4.0.0 - */ -@Internal -public final class CompositeExpressionEvaluationContext implements ExpressionEvaluationContext { - private final ExpressionEvaluationContext[] evaluationContexts; - - public CompositeExpressionEvaluationContext(ExpressionEvaluationContext... evaluationContexts) { - this.evaluationContexts = evaluationContexts; - } - - @Override - public List getMethods(String name) { - return Arrays.stream(evaluationContexts) - .flatMap(context -> context.getMethods(name).stream()) - .toList(); - } - - @Override - public List getTypedElements(String name) { - return Arrays.stream(evaluationContexts) - .flatMap(context -> context.getTypedElements(name).stream()) - .toList(); - } -} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/BeanContextExpressionEvaluationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java similarity index 56% rename from core-processor/src/main/java/io/micronaut/expressions/context/BeanContextExpressionEvaluationContext.java rename to core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java index 876d3f5cb1f..b5355a77450 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/BeanContextExpressionEvaluationContext.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java @@ -17,55 +17,66 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.naming.NameUtils; -import io.micronaut.inject.ast.AnnotationElement; +import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.ast.PropertyElementQuery; -import io.micronaut.inject.ast.TypedElement; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import static io.micronaut.inject.ast.ElementQuery.ALL_METHODS; import static java.util.function.Predicate.not; /** - * Root expression evaluation context is a context which elements are - * registered in and obtained through bean context. + * Default implementation of {@link ExtendableExpressionCompilationContext}. Extending + * this context will always return new instance instead of modifying the existing one. * - * @author Sergey Gavrilov * @since 4.0.0 + * @author Sergey Gavrilov */ @Internal -public final class BeanContextExpressionEvaluationContext implements ExpressionEvaluationContext { +public class DefaultExpressionCompilationContext implements ExtendableExpressionCompilationContext { + + private final Collection classElements; + private final MethodElement methodElement; - private final Set contextTypes = ConcurrentHashMap.newKeySet(); + DefaultExpressionCompilationContext(ClassElement... classElements) { + this(null, classElements); + } + + private DefaultExpressionCompilationContext(MethodElement methodElement, + ClassElement... classElements) { + this.methodElement = methodElement; + this.classElements = Arrays.asList(classElements); + } - public BeanContextExpressionEvaluationContext(ClassElement... contextClasses) { - Arrays.stream(contextClasses) - .filter(classElement -> !(classElement instanceof AnnotationElement)) - .forEach(contextTypes::add); + @Override + public DefaultExpressionCompilationContext extendWith(MethodElement methodElement) { + return new DefaultExpressionCompilationContext( + methodElement, + classElements.toArray(ClassElement[]::new) + ); } - /** - * @return set of class elements registered in this evaluation context. - */ - public Set getContextTypes() { - return contextTypes; + @Override + public DefaultExpressionCompilationContext extendWith(ClassElement classElement) { + return new DefaultExpressionCompilationContext( + this.methodElement, + ArrayUtils.concat(classElements.toArray(ClassElement[]::new), classElement) + ); } @Override - public List getMethods(String name) { - return contextTypes.stream() - .map(element -> findMatchingMethods(element, name)) - .flatMap(Collection::stream) + public List findMethods(String name) { + return classElements.stream() + .flatMap(element -> findMatchingMethods(element, name).stream()) .toList(); } @@ -84,13 +95,6 @@ private List findMatchingMethods(ClassElement classElement, Strin .toList(); } - @Override - public List getTypedElements(String name) { - return contextTypes.stream() - .flatMap(classElement -> getNamedProperties(classElement, name).stream()) - .toList(); - } - private List getNamedProperties(ClassElement classElement, String name) { return classElement.getBeanProperties( PropertyElementQuery.of(classElement.getAnnotationMetadata()) @@ -99,4 +103,22 @@ private List getNamedProperties(ClassElement classElement, Stri .filter(not(PropertyElement::isExcluded)) .toList(); } + + @Override + public List findProperties(String name) { + return classElements.stream() + .flatMap(classElement -> getNamedProperties(classElement, name).stream()) + .toList(); + } + + @Override + public List findParameters(String name) { + if (this.methodElement == null) { + return Collections.emptyList(); + } + + return Arrays.stream(methodElement.getParameters()) + .filter(parameter -> parameter.getName().equals(name)) + .toList(); + } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContext.java new file mode 100644 index 00000000000..47fd9e494b1 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContext.java @@ -0,0 +1,62 @@ +/* + * 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.expressions.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.PropertyElement; + +import java.util.List; + +/** + * Compilation context is a set of entries which can be referenced in evaluated expression + * using the '#' sign followed by entry name. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public interface ExpressionCompilationContext { + + /** + * Search methods in compilation context by name. + * + * @param name searched method name + * @return list of methods with provided name + */ + @NonNull + List findMethods(@NonNull String name); + + /** + * Search bean properties in compilation context by name. + * + * @param name searched property name + * @return list of properties with provided name + */ + @NonNull + List findProperties(@NonNull String name); + + /** + * Search method parameters in compilation context by name. + * + * @param name searched parameter name + * @return list of parameters with provided name + */ + @NonNull + List findParameters(@NonNull String name); +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java new file mode 100644 index 00000000000..aaea1fbd14d --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java @@ -0,0 +1,121 @@ +/* + * 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.expressions.context; + +import io.micronaut.context.annotation.EvaluatedExpressionContext; +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.expressions.EvaluatedExpressionReference; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.visitor.VisitorContext; + +import java.util.Optional; + +/** + * Factory for producing expression evaluation context. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class ExpressionCompilationContextFactory { + + private final ExtendableExpressionCompilationContext sharedContext; + private final VisitorContext visitorContext; + + public ExpressionCompilationContextFactory(VisitorContext visitorContext) { + this.sharedContext = ExpressionCompilationContextRegistry.getSharedContext(); + this.visitorContext = visitorContext; + } + + /** + * Builds expression evaluation context for method. Expression evaluation context + * for method allows referencing method parameter names in evaluated expressions. + * + * @param expression expression reference + * @param methodElement annotated method + * + * @return evaluation context for method + */ + @NonNull + public ExpressionCompilationContext buildContextForMethod(@NonNull EvaluatedExpressionReference expression, + @NonNull MethodElement methodElement) { + return buildForExpression(expression) + .extendWith(methodElement); + } + + /** + * Builds expression evaluation context for expression reference. + * + * @param expression expression reference + * + * @return evaluation context for method + */ + @NonNull + public ExpressionCompilationContext buildContext(EvaluatedExpressionReference expression) { + return buildForExpression(expression); + } + + private ExtendableExpressionCompilationContext buildForExpression(EvaluatedExpressionReference expression) { + String annotationName = expression.annotationName(); + String memberName = expression.annotationMember(); + + ClassElement annotation = visitorContext.getClassElement(annotationName).orElse(null); + + ExtendableExpressionCompilationContext evaluationContext = sharedContext; + if (annotation != null) { + evaluationContext = addAnnotationEvaluationContext(evaluationContext, annotation); + evaluationContext = addAnnotationMemberEvaluationContext(evaluationContext, annotation, memberName); + } + + return evaluationContext; + } + + private ExtendableExpressionCompilationContext addAnnotationEvaluationContext( + ExtendableExpressionCompilationContext currentEvaluationContext, + ClassElement annotation) { + + return Optional.ofNullable(annotation.getAnnotation(EvaluatedExpressionContext.class)) + .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER)) + .map(AnnotationClassValue::getName) + .flatMap(visitorContext::getClassElement) + .map(currentEvaluationContext::extendWith) + .orElse(currentEvaluationContext); + } + + private ExtendableExpressionCompilationContext addAnnotationMemberEvaluationContext( + ExtendableExpressionCompilationContext currentEvaluationContext, + ClassElement annotation, + String annotationMember) { + + ElementQuery memberQuery = + ElementQuery.ALL_METHODS + .onlyDeclared() + .annotated(am -> am.hasAnnotation(EvaluatedExpressionContext.class)) + .named(annotationMember); + + return annotation.getEnclosedElements(memberQuery).stream() + .flatMap(element -> Optional.ofNullable(element.getDeclaredAnnotation(EvaluatedExpressionContext.class)).stream()) + .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER).stream()) + .map(AnnotationClassValue::getName) + .flatMap(className -> visitorContext.getClassElement(className).stream()) + .reduce(currentEvaluationContext, ExtendableExpressionCompilationContext::extendWith, (a, b) -> a); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextRegistry.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextRegistry.java new file mode 100644 index 00000000000..8f9b8d4c301 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextRegistry.java @@ -0,0 +1,65 @@ +/* + * 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.expressions.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.ClassElement; + +import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +/** + * This class is responsible for assembling expression evaluation context + * from classes annotated with {@link io.micronaut.context.annotation.EvaluatedExpressionContext}. + * The assembled context is considered as shared because elements from this context can + * be referenced in any expression compiled within the same module. It can later be extended + * by annotation level, annotation member level context classes or method element. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public final class ExpressionCompilationContextRegistry { + + private static Collection contextTypes = ConcurrentHashMap.newKeySet(); + + /** + * Adds evaluated expression context class element to context loader + * at compilation time. + * + * @param contextClass context class element + */ + public static void registerContextClass(@NonNull ClassElement contextClass) { + contextTypes.add(contextClass); + } + + /** + * Resets expression evaluation context. + */ + public static void reset() { + contextTypes.clear(); + } + + /** + * @return shared expression evaluation context. + */ + @NonNull + static ExtendableExpressionCompilationContext getSharedContext() { + return new DefaultExpressionCompilationContext(contextTypes.toArray(ClassElement[]::new)); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextFactory.java deleted file mode 100644 index c8ec24f7dd6..00000000000 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextFactory.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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.expressions.context; - -import io.micronaut.context.annotation.EvaluatedExpressionContext; -import io.micronaut.core.annotation.AnnotationClassValue; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.EvaluatedExpressionReference; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Internal; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementQuery; -import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.visitor.VisitorContext; - -import java.util.Optional; - -/** - * Factory for producing expression evaluation context. - * - * @author Sergey Gavrilov - * @since 4.0.0 - */ -@Internal -public final class ExpressionContextFactory { - private final ExpressionEvaluationContext loadedExpressionEvaluationContext; - private final VisitorContext visitorContext; - - public ExpressionContextFactory(VisitorContext visitorContext) { - this.loadedExpressionEvaluationContext = ExpressionContextLoader.getExpressionContext(); - this.visitorContext = visitorContext; - } - - /** - * Builds expression evaluation context for method. Expression evaluation context - * for method allows referencing method parameter names in evaluated expressions. - * - * @param expression expression reference - * @param methodElement annotated method - * @return evaluation context for method - */ - @NonNull - public ExpressionEvaluationContext buildForMethod(@NonNull EvaluatedExpressionReference expression, - @NonNull MethodElement methodElement) { - ExpressionEvaluationContext evaluationContext = buildEvaluationContext(expression); - return new CompositeExpressionEvaluationContext( - evaluationContext, - new MethodExpressionEvaluationContext(methodElement)); - } - - /** - * Builds expression evaluation context for expression reference. - * - * @param expression expression reference - * @return evaluation context for method - */ - @NonNull - public ExpressionEvaluationContext buildEvaluationContext(EvaluatedExpressionReference expression) { - String annotationName = expression.annotationName(); - String memberName = expression.annotationMember(); - - ClassElement annotation = visitorContext.getClassElement(annotationName).orElse(null); - - ExpressionEvaluationContext evaluationContext = loadedExpressionEvaluationContext; - if (annotation != null) { - evaluationContext = addAnnotationLevelEvaluationContext(evaluationContext, annotation); - evaluationContext = addAnnotationMemberEvaluationContext(evaluationContext, annotation, memberName); - } - - return evaluationContext; - } - - private ExpressionEvaluationContext addAnnotationLevelEvaluationContext( - ExpressionEvaluationContext currentEvaluationContext, - ClassElement annotation) { - - ExpressionEvaluationContext annotationLevelContext = - Optional.ofNullable(annotation.getAnnotation(EvaluatedExpressionContext.class)) - .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER)) - .map(AnnotationClassValue::getName) - .flatMap(visitorContext::getClassElement) - .map(BeanContextExpressionEvaluationContext::new) - .orElse(null); - - if (annotationLevelContext != null) { - return new CompositeExpressionEvaluationContext( - currentEvaluationContext, - annotationLevelContext); - } - - return currentEvaluationContext; - } - - private ExpressionEvaluationContext addAnnotationMemberEvaluationContext( - ExpressionEvaluationContext currentEvaluationContext, - ClassElement annotation, - String annotationMember) { - ElementQuery memberQuery = - ElementQuery.ALL_METHODS - .onlyDeclared() - .annotated(am -> am.hasAnnotation(EvaluatedExpressionContext.class)) - .named(annotationMember); - - ExpressionEvaluationContext annMemberContext = - annotation.getEnclosedElements(memberQuery).stream() - .map(element -> Optional.ofNullable(element.getDeclaredAnnotation(EvaluatedExpressionContext.class))) - .flatMap(Optional::stream) - .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER).stream()) - .map(AnnotationClassValue::getName) - .flatMap(className -> visitorContext.getClassElement(className).stream()) - .findFirst() - .map(BeanContextExpressionEvaluationContext::new) - .orElse(null); - - if (annMemberContext != null) { - return new CompositeExpressionEvaluationContext( - currentEvaluationContext, - annMemberContext); - } - - return currentEvaluationContext; - } -} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextLoader.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextLoader.java deleted file mode 100644 index a4954e61f41..00000000000 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextLoader.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.expressions.context; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.io.service.SoftServiceLoader; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.visitor.VisitorContext; - -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Stream; - -/** - * This class is responsible for providing expression evaluation context - * from classes annotated with {@link io.micronaut.context.annotation.EvaluatedExpressionContext}. - * - * Classes from external modules are loaded through {@link ExpressionContextReference} - * provided by those modules. Context classes that are compiled in the same module need to be - * explicitly added to context loader. Once all context classes are added, expression context - * needs to be initialized. After the context is initialized, it can be used by code responsible - * for expressions compilation - * - * @since 4.0.0 - * @author Sergey Gavrilov - */ -@Internal -public final class ExpressionContextLoader { - private static final Set LOADED_CONTEXTS = ConcurrentHashMap.newKeySet(); - - private static volatile BeanContextExpressionEvaluationContext expressionContext = new BeanContextExpressionEvaluationContext(); - - static { - SoftServiceLoader.load( - ExpressionContextReference.class, - ExpressionContextLoader.class.getClassLoader()) - .disableFork() - .collectAll() - .stream() - .map(ExpressionContextReference::getType) - .forEach(LOADED_CONTEXTS::add); - } - - /** - * Adds evaluated expression context class element to context loader - * at compilation time. - * - * @param contextClass context class element - * @param visitorContext visitor context - */ - public static void addContextClass(@NonNull ClassElement contextClass, - @NonNull VisitorContext visitorContext) { - ClassElement[] contextElements = - Stream.concat( - Stream.concat(expressionContext.getContextTypes().stream(), Stream.of(contextClass)), - LOADED_CONTEXTS.stream() - .map(visitorContext::getClassElement) - .filter(Optional::isPresent) - .map(Optional::get)) - .toArray(ClassElement[]::new); - - expressionContext = new BeanContextExpressionEvaluationContext(contextElements); - } - - /** - * Resets expression evaluation context. - */ - public static void reset() { - expressionContext = new BeanContextExpressionEvaluationContext(); - } - - /** - * @return expressions evaluation context. - */ - @NonNull - public static ExpressionEvaluationContext getExpressionContext() { - return expressionContext; - } -} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContext.java deleted file mode 100644 index e0d91f776cf..00000000000 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContext.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.expressions.context; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.TypedElement; - -import java.util.List; - -/** - * The context against which expressions are evaluated. - * Context methods, properties and method parameters can be referenced - * in evaluated expressions. - * - * @author Sergey Gavrilov - * @since 4.0.0 - */ -@Internal -public sealed interface ExpressionEvaluationContext permits BeanContextExpressionEvaluationContext, - MethodExpressionEvaluationContext, - CompositeExpressionEvaluationContext { - /** - * Returns list of methods registered in expression evaluation context - * and matching provided name. - * - * @param name method name to look for - * @return list of matching methods - */ - @NonNull - List getMethods(@NonNull String name); - - /** - * Provides list of typed elements registered in expression evaluation context - * and matching provided name. - * - * @param name element name to look for - * @return list of matching elements - */ - @NonNull - List getTypedElements(@NonNull String name); -} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionWithContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionWithContext.java index 5bc7616225e..aba972bd491 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionWithContext.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionWithContext.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.EvaluatedExpressionReference; +import io.micronaut.core.expressions.EvaluatedExpressionReference; /** * Metadata for evaluated expression used at compilation time @@ -31,7 +31,7 @@ */ @Internal public record ExpressionWithContext(@NonNull EvaluatedExpressionReference expressionReference, - @NonNull ExpressionEvaluationContext evaluationContext) { + @NonNull ExpressionCompilationContext evaluationContext) { /** * Provides initial annotation value treated as evaluated expression. diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExtendableExpressionCompilationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExtendableExpressionCompilationContext.java new file mode 100644 index 00000000000..87a1e3f2007 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExtendableExpressionCompilationContext.java @@ -0,0 +1,52 @@ +/* + * 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.expressions.context; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.MethodElement; + +/** + * Expression compilation context that can be extended with extra elements. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public interface ExtendableExpressionCompilationContext extends ExpressionCompilationContext { + /** + * Extends compilation context with method element. Compilation context can only include + * one method at the same time, so this method will return the context which will + * replace previous context method element if it was set. + * + * @param methodElement extending method + * @return extended context + */ + @NonNull + ExtendableExpressionCompilationContext extendWith(@NonNull MethodElement methodElement); + + /** + * Extends compilation context with class element. Compilation context can include + * multiple class elements at the same time. + * + * @param classElement extending class + * @return extended context + */ + @NonNull + ExtendableExpressionCompilationContext extendWith(@NonNull ClassElement classElement); + +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/MethodExpressionEvaluationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/MethodExpressionEvaluationContext.java deleted file mode 100644 index f7b527df5c4..00000000000 --- a/core-processor/src/main/java/io/micronaut/expressions/context/MethodExpressionEvaluationContext.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.expressions.context; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.TypedElement; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -/** - * Represents expression evaluation context for concrete method. Method's expression - * evaluation context allows referencing method parameters by name in evaluated - * expressions. - * - * @param methodElement method for which evaluation context is built. - * @author Sergey Gavrilov - * @since 4.0.0 - */ -@Internal -public record MethodExpressionEvaluationContext(@NonNull MethodElement methodElement) implements ExpressionEvaluationContext { - - @Override - public List getTypedElements(String name) { - return Arrays.stream(methodElement.getParameters()) - .filter(parameter -> parameter.getName().equals(name)) - .toList(); - } - - @Override - public List getMethods(String name) { - return Collections.emptyList(); - } -} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/CompoundEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/CompoundEvaluatedEvaluatedExpressionParser.java index f5aae15d61c..da5d727bd1b 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/CompoundEvaluatedEvaluatedExpressionParser.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/CompoundEvaluatedEvaluatedExpressionParser.java @@ -18,17 +18,18 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.ast.literal.StringLiteral; -import io.micronaut.expressions.parser.ast.types.TypeIdentifier; import io.micronaut.expressions.parser.ast.collection.OneDimensionalArray; +import io.micronaut.expressions.parser.ast.literal.StringLiteral; import io.micronaut.expressions.parser.ast.operator.binary.AddOperator; +import io.micronaut.expressions.parser.ast.types.TypeIdentifier; import io.micronaut.expressions.parser.exception.ExpressionParsingException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import static io.micronaut.core.expression.EvaluatedExpression.EXPRESSION_PREFIX; +import static io.micronaut.expressions.EvaluatedExpressionConstants.EXPRESSION_PREFIX; + /** * This parser is used to split complex expression into multiple diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java index b20b249f984..824d1a4399a 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java @@ -15,8 +15,9 @@ */ package io.micronaut.expressions.parser.ast; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Type; /** @@ -25,6 +26,7 @@ * @author Sergey Gavrilov * @since 4.0.0 */ +@Internal public abstract class ExpressionNode { protected Type nodeType; @@ -35,7 +37,7 @@ public abstract class ExpressionNode { * * @param ctx expression compilation context */ - public final void compile(@NonNull ExpressionCompilationContext ctx) { + public final void compile(@NonNull ExpressionVisitorContext ctx) { resolveType(ctx); generateBytecode(ctx); } @@ -45,7 +47,7 @@ public final void compile(@NonNull ExpressionCompilationContext ctx) { * * @param ctx expression compilation context */ - protected abstract void generateBytecode(@NonNull ExpressionCompilationContext ctx); + protected abstract void generateBytecode(@NonNull ExpressionVisitorContext ctx); /** * On resolution stage type information is collected and node validity is checked. Once type @@ -56,7 +58,7 @@ public final void compile(@NonNull ExpressionCompilationContext ctx) { * @return resolved type */ @NonNull - public final Type resolveType(@NonNull ExpressionCompilationContext ctx) { + public final Type resolveType(@NonNull ExpressionVisitorContext ctx) { if (nodeType == null) { nodeType = doResolveType(ctx); } @@ -71,5 +73,5 @@ public final Type resolveType(@NonNull ExpressionCompilationContext ctx) { * @return resolved type */ @NonNull - protected abstract Type doResolveType(@NonNull ExpressionCompilationContext ctx); + protected abstract Type doResolveType(@NonNull ExpressionVisitorContext ctx); } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java index 20d35b0e4ac..13e9e2936fa 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java @@ -21,7 +21,7 @@ import io.micronaut.expressions.parser.ast.collection.OneDimensionalArray; import io.micronaut.expressions.parser.ast.util.TypeDescriptors; import io.micronaut.expressions.parser.ast.types.TypeIdentifier; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.visitor.VisitorContext; @@ -59,7 +59,7 @@ public AbstractMethodCall(String name, } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { usedMethod = resolveUsedMethod(ctx); return usedMethod.getReturnType(); } @@ -73,7 +73,7 @@ protected Type doResolveType(ExpressionCompilationContext ctx) { * candidate method can be found or if there is more than one candidate method. */ @NonNull - protected abstract CandidateMethod resolveUsedMethod(ExpressionCompilationContext ctx); + protected abstract CandidateMethod resolveUsedMethod(ExpressionVisitorContext ctx); /** * Builds candidate method for method element. @@ -84,7 +84,7 @@ protected Type doResolveType(ExpressionCompilationContext ctx) { * * @return candidate method */ - protected CandidateMethod toCandidateMethod(ExpressionCompilationContext ctx, + protected CandidateMethod toCandidateMethod(ExpressionVisitorContext ctx, MethodElement methodElement, List argumentTypes) { VisitorContext visitorContext = ctx.visitorContext(); @@ -135,7 +135,7 @@ protected List prepareVarargsArguments() { * * @return types of method arguments */ - protected List resolveArgumentTypes(ExpressionCompilationContext ctx) { + protected List resolveArgumentTypes(ExpressionVisitorContext ctx) { return arguments.stream() .map(argument -> argument instanceof TypeIdentifier ? TypeDescriptors.CLASS @@ -148,7 +148,7 @@ protected List resolveArgumentTypes(ExpressionCompilationContext ctx) { * * @param ctx expression evaluation context */ - protected void compileArguments(ExpressionCompilationContext ctx) { + protected void compileArguments(ExpressionVisitorContext ctx) { List arguments = this.arguments; if (usedMethod.isVarArgs()) { arguments = prepareVarargsArguments(); @@ -166,7 +166,7 @@ protected void compileArguments(ExpressionCompilationContext ctx) { * @param argumentIndex argument index * @param argument compiled argument */ - private void compileArgument(ExpressionCompilationContext ctx, + private void compileArgument(ExpressionVisitorContext ctx, int argumentIndex, ExpressionNode argument) { GeneratorAdapter mv = ctx.methodVisitor(); @@ -190,7 +190,7 @@ private void compileArgument(ExpressionCompilationContext ctx, * * @return arguments string */ - protected String stringifyArguments(ExpressionCompilationContext ctx) { + protected String stringifyArguments(ExpressionVisitorContext ctx) { return arguments.stream() .map(argument -> argument.resolveType(ctx)) .map(Type::getClassName) diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java index b05adfc0ec5..1e51380bbce 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java @@ -16,13 +16,12 @@ package io.micronaut.expressions.parser.ast.access; import io.micronaut.core.annotation.Internal; -import io.micronaut.expressions.context.ExpressionEvaluationContext; +import io.micronaut.expressions.context.ExpressionCompilationContext; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PropertyElement; -import io.micronaut.inject.ast.TypedElement; import org.objectweb.asm.Type; import java.util.List; @@ -54,7 +53,7 @@ public ContextElementAccess(String name) { } @Override - protected void generateBytecode(ExpressionCompilationContext ctx) { + protected void generateBytecode(ExpressionVisitorContext ctx) { if (contextMethodParameterAccess != null) { contextMethodParameterAccess.compile(ctx); } else if (contextPropertyMethodCall != null) { @@ -63,22 +62,26 @@ protected void generateBytecode(ExpressionCompilationContext ctx) { } @Override - public Type doResolveType(ExpressionCompilationContext ctx) { - ExpressionEvaluationContext evaluationContext = ctx.evaluationContext(); - List namedElements = evaluationContext.getTypedElements(name); + public Type doResolveType(ExpressionVisitorContext ctx) { + ExpressionCompilationContext evaluationContext = ctx.compilationContext(); - if (namedElements.size() == 0) { + List propertyElements = evaluationContext.findProperties(name); + List parameterElements = evaluationContext.findParameters(name); + + int totalElements = propertyElements.size() + parameterElements.size(); + + if (totalElements == 0) { throw new ExpressionCompilationException( "No element with name [" + name + "] available in evaluation context"); - } else if (namedElements.size() > 1) { + } else if (totalElements > 1) { throw new ExpressionCompilationException( - "Ambiguous expression evaluation context reference. Found " + namedElements.size() + + "Ambiguous expression evaluation context reference. Found " + totalElements + " elements with name [" + name + "]"); } - TypedElement element = namedElements.iterator().next(); - if (element instanceof PropertyElement property) { + if (!propertyElements.isEmpty()) { + PropertyElement property = propertyElements.iterator().next(); String readMethodName = property.getReadMethod() .orElseThrow(() -> new ExpressionCompilationException( @@ -88,15 +91,10 @@ public Type doResolveType(ExpressionCompilationContext ctx) { contextPropertyMethodCall = new ContextMethodCall(readMethodName, emptyList()); return contextPropertyMethodCall.resolveType(ctx); - } else if (element instanceof ParameterElement parameter) { - - contextMethodParameterAccess = new ContextMethodParameterAccess(parameter); - return contextMethodParameterAccess.resolveType(ctx); - - } else { - throw new ExpressionCompilationException( - "Unsupported element referenced in expression: [" + element + "]. Only " + - "properties, methods and method parameters can be referenced in expressions"); } + + ParameterElement parameter = parameterElements.iterator().next(); + contextMethodParameterAccess = new ContextMethodParameterAccess(parameter); + return contextMethodParameterAccess.resolveType(ctx); } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java index f701056c012..3e72c00f703 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java @@ -16,9 +16,9 @@ package io.micronaut.expressions.parser.ast.access; import io.micronaut.core.annotation.Internal; -import io.micronaut.expressions.context.ExpressionEvaluationContext; +import io.micronaut.expressions.context.ExpressionCompilationContext; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import io.micronaut.inject.ast.ClassElement; import org.objectweb.asm.Type; @@ -27,7 +27,7 @@ import java.util.List; -import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.EVALUATED_EXPRESSION_TYPE; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.EVALUATION_CONTEXT_TYPE; import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; import static org.objectweb.asm.Opcodes.CHECKCAST; @@ -50,12 +50,12 @@ public ContextMethodCall(String name, List arguments) { } @Override - protected CandidateMethod resolveUsedMethod(ExpressionCompilationContext ctx) { + protected CandidateMethod resolveUsedMethod(ExpressionVisitorContext ctx) { List argumentTypes = resolveArgumentTypes(ctx); - ExpressionEvaluationContext evaluationContext = ctx.evaluationContext(); + ExpressionCompilationContext evaluationContext = ctx.compilationContext(); List candidateMethods = - evaluationContext.getMethods(name) + evaluationContext.findMethods(name) .stream() .map(method -> toCandidateMethod(ctx, method, argumentTypes)) .filter(method -> method.isMatching(ctx.visitorContext())) @@ -74,7 +74,7 @@ protected CandidateMethod resolveUsedMethod(ExpressionCompilationContext ctx) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); Type calleeType = usedMethod.getOwningType(); @@ -96,11 +96,11 @@ public void generateBytecode(ExpressionCompilationContext ctx) { * @param beanType required bean typ */ private void pushGetBeanFromContext(GeneratorAdapter mv, Type beanType) { - mv.loadThis(); + mv.loadArg(0); mv.push(beanType); // invoke getBean method - mv.invokeVirtual(EVALUATED_EXPRESSION_TYPE, GET_BEAN_METHOD); + mv.invokeInterface(EVALUATION_CONTEXT_TYPE, GET_BEAN_METHOD); // cast the return value to the correct type mv.visitTypeInsn(CHECKCAST, beanType.getInternalName()); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java index 2990748062c..b25900975c4 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java @@ -17,12 +17,15 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.ast.util.TypeDescriptors; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import io.micronaut.inject.ast.ParameterElement; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; +import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.EVALUATION_CONTEXT_TYPE; import static io.micronaut.inject.processing.JavaModelUtils.getTypeReference; import static org.objectweb.asm.Opcodes.AALOAD; @@ -34,6 +37,11 @@ */ @Internal final class ContextMethodParameterAccess extends ExpressionNode { + + private static final Method GET_ARGUMENT_METHOD = + new Method("getArgument", Type.getType(Object.class), + new Type[]{TypeDescriptors.INT}); + private final ParameterElement parameterElement; private Integer parameterIndex; @@ -43,15 +51,16 @@ public ContextMethodParameterAccess(ParameterElement parameterElement) { } @Override - protected void generateBytecode(ExpressionCompilationContext ctx) { + protected void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); mv.loadArg(0); mv.push(parameterIndex); - mv.visitInsn(AALOAD); + // invoke getArgument method + mv.invokeInterface(EVALUATION_CONTEXT_TYPE, GET_ARGUMENT_METHOD); } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { String parameterName = parameterElement.getName(); ParameterElement[] methodParameters = parameterElement.getMethodElement().getParameters(); 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 2699b6c725a..41322417d96 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 @@ -18,7 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.ast.types.TypeIdentifier; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementQuery; @@ -54,7 +54,7 @@ public ElementMethodCall(ExpressionNode callee, } @Override - protected void generateBytecode(ExpressionCompilationContext ctx) { + protected void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); VisitorContext visitorContext = ctx.visitorContext(); Type calleeType = callee.resolveType(ctx); @@ -82,7 +82,7 @@ protected void generateBytecode(ExpressionCompilationContext ctx) { } @Override - protected CandidateMethod resolveUsedMethod(ExpressionCompilationContext ctx) { + protected CandidateMethod resolveUsedMethod(ExpressionVisitorContext ctx) { List argumentTypes = resolveArgumentTypes(ctx); Type calleeType = callee.resolveType(ctx); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java index 246735f04a4..3e3396ccecd 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; @@ -47,7 +47,7 @@ public PropertyAccess(ExpressionNode callee, String name) { } @Override - protected CandidateMethod resolveUsedMethod(ExpressionCompilationContext ctx) { + protected CandidateMethod resolveUsedMethod(ExpressionVisitorContext ctx) { Type calleeType = callee.resolveType(ctx); ClassElement classElement = getRequiredClassElement(calleeType, ctx.visitorContext()); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java index 0da43b73e26..96067db6844 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java @@ -15,9 +15,10 @@ */ package io.micronaut.expressions.parser.ast.collection; +import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.ast.types.TypeIdentifier; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; @@ -38,6 +39,7 @@ * @author Sergey Gavrilov * @since 4.0.0 */ +@Internal public final class OneDimensionalArray extends ExpressionNode { private final TypeIdentifier elementTypeIdentifier; private final List initializer; @@ -49,7 +51,7 @@ public OneDimensionalArray(TypeIdentifier elementTypeIdentifier, } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); int arraySize = initializer.size(); @@ -80,7 +82,7 @@ public void generateBytecode(ExpressionCompilationContext ctx) { } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { return getTypeReference(getRequiredClassElement(elementTypeIdentifier.resolveType(ctx), ctx.visitorContext()) .toArray()); } 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 0f7f8b31667..25ec177de59 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 @@ -15,8 +15,9 @@ */ package io.micronaut.expressions.parser.ast.conditional; +import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import io.micronaut.inject.ast.ClassElement; import org.objectweb.asm.Label; @@ -44,6 +45,7 @@ * @author Sergey Gavrilov * @since 4.0.0 */ +@Internal public final class TernaryExpression extends ExpressionNode { private final ExpressionNode condition; private final ExpressionNode trueExpr; @@ -57,7 +59,7 @@ public TernaryExpression(ExpressionNode condition, ExpressionNode trueExpr, } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); Label falseLabel = new Label(); Label returnLabel = new Label(); @@ -99,7 +101,7 @@ public void generateBytecode(ExpressionCompilationContext ctx) { } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { if (!isOneOf(condition.resolveType(ctx), BOOLEAN, BOOLEAN_WRAPPER)) { throw new ExpressionCompilationException("Invalid ternary operator. Condition should resolve to boolean type"); } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java index 9d19e0fec81..8bc0f74d6d6 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; @@ -37,12 +37,12 @@ public BoolLiteral(boolean value) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(value); } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { return BOOLEAN; } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java index 63d27eb75d7..e27c0c91c0f 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; @@ -37,12 +37,12 @@ public DoubleLiteral(double value) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(value); } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { return DOUBLE; } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java index 1d40c9ef564..fff4c7e8937 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; @@ -37,12 +37,12 @@ public FloatLiteral(float value) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(value); } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { return FLOAT; } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java index 6d5ebbd3fb6..4fa2cc1dc29 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.INT; @@ -38,12 +38,12 @@ public IntLiteral(int value) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(value); } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { return INT; } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java index ad5399dcd80..08941f1f41b 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; @@ -37,12 +37,12 @@ public LongLiteral(long value) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(value); } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { return LONG; } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java index 7468c03791e..6640cd66b63 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.OBJECT; @@ -32,12 +32,12 @@ @Internal public final class NullLiteral extends ExpressionNode { @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().visitInsn(ACONST_NULL); } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { return OBJECT; } } 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 ec89dbd236f..a781eb0e033 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 @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.STRING; @@ -42,12 +42,12 @@ public String getValue() { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(value); } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { return STRING; } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AddOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AddOperator.java index e2f10596e97..83c67753af0 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AddOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AddOperator.java @@ -18,7 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.ast.util.TypeDescriptors; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; @@ -86,7 +86,7 @@ protected Type resolveOperationType(Type leftOperandType, Type rightOperandType) } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { Type leftType = leftOperand.resolveType(ctx); Type rightType = rightOperand.resolveType(ctx); @@ -113,7 +113,7 @@ public void generateBytecode(ExpressionCompilationContext ctx) { } } - private void concatStrings(ExpressionCompilationContext ctx) { + private void concatStrings(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); initStringBuilder(mv); pushOperand(ctx, leftOperand); @@ -127,7 +127,7 @@ private void initStringBuilder(GeneratorAdapter mv) { mv.invokeConstructor(STRING_BUILDER_TYPE, STRING_BUILD_CONSTRUCTOR); } - private void pushOperand(ExpressionCompilationContext ctx, ExpressionNode operand) { + private void pushOperand(ExpressionVisitorContext ctx, ExpressionNode operand) { GeneratorAdapter mv = ctx.methodVisitor(); if (operand instanceof AddOperator addOperator) { Type operatorType = addOperator.resolveType(ctx); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AndOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AndOperator.java index 7fe3109b7fe..1424815b3d6 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AndOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/AndOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Label; import org.objectweb.asm.commons.GeneratorAdapter; @@ -37,7 +37,7 @@ public AndOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); Label falseLabel = new Label(); Label trueLabel = new Label(); @@ -54,7 +54,7 @@ public void generateBytecode(ExpressionCompilationContext ctx) { mv.visitLabel(trueLabel); } - private void pushOperand(ExpressionCompilationContext ctx, ExpressionNode operand, Label falseLabel) { + private void pushOperand(ExpressionVisitorContext ctx, ExpressionNode operand, Label falseLabel) { if (operand instanceof AndOperator andOperator) { pushOperand(ctx, andOperator.leftOperand, falseLabel); pushOperand(ctx, andOperator.rightOperand, falseLabel); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java index 403b9b30fe7..2503a117b9b 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java @@ -15,8 +15,9 @@ */ package io.micronaut.expressions.parser.ast.operator.binary; +import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Type; /** @@ -25,6 +26,7 @@ * @author Sergey Gavrilov * @since 4.0.0 */ +@Internal public abstract sealed class BinaryOperator extends ExpressionNode permits LogicalOperator, RelationalOperator, PowOperator, @@ -40,7 +42,7 @@ public BinaryOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { Type leftType = leftOperand.resolveType(ctx); Type rightType = rightOperand.resolveType(ctx); return resolveOperationType(leftType, rightType); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/DivOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/DivOperator.java index 738fe1ef871..cbcfeebce9a 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/DivOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/DivOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import org.objectweb.asm.Type; @@ -48,7 +48,7 @@ public DivOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { } @Override - protected int getMathOperationOpcode(ExpressionCompilationContext ctx) { + protected int getMathOperationOpcode(ExpressionVisitorContext ctx) { Type type = resolveType(ctx); String typeDescriptor = type.getDescriptor(); return Optional.ofNullable(DIV_OPERATION_OPCODES.get(typeDescriptor)) diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/EqOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/EqOperator.java index 807481218b5..7445cbc75b5 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/EqOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/EqOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; @@ -41,7 +41,7 @@ public EqOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); Type lefType = leftOperand.resolveType(ctx); Type rightType = rightOperand.resolveType(ctx); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java index 42b2262d745..573b19d558a 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java @@ -18,7 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.ast.types.TypeIdentifier; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; @@ -45,7 +45,7 @@ public InstanceofOperator(ExpressionNode operand, TypeIdentifier typeIdentifier) } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { Type targetType = typeIdentifier.resolveType(ctx); if (isPrimitive(targetType)) { throw new ExpressionCompilationException( @@ -62,7 +62,7 @@ public void generateBytecode(ExpressionCompilationContext ctx) { } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { return BOOLEAN; } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java index f4410f71eb1..696560b83f8 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java @@ -18,7 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.ast.literal.StringLiteral; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; @@ -50,7 +50,7 @@ public MatchesOperator(ExpressionNode operand, StringLiteral pattern) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); operand.compile(ctx); pattern.compile(ctx); @@ -58,7 +58,7 @@ public void generateBytecode(ExpressionCompilationContext ctx) { } @Override - protected Type doResolveType(ExpressionCompilationContext ctx) { + protected Type doResolveType(ExpressionVisitorContext ctx) { if (!operand.resolveType(ctx).equals(STRING)) { throw new ExpressionCompilationException( "Operator 'matches' can only be applied to String operand"); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MathOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MathOperator.java index def1804e796..7687f025b92 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MathOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MathOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; @@ -42,7 +42,7 @@ public MathOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); Type targetType = resolveType(ctx); @@ -64,5 +64,5 @@ protected Type resolveOperationType(Type leftOperandType, Type rightOperandType) return computeNumericOperationTargetType(leftOperandType, rightOperandType); } - protected abstract int getMathOperationOpcode(ExpressionCompilationContext ctx); + protected abstract int getMathOperationOpcode(ExpressionVisitorContext ctx); } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ModOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ModOperator.java index 0249983c35f..92e62d306f8 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ModOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/ModOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import org.objectweb.asm.Type; @@ -48,7 +48,7 @@ public ModOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { } @Override - protected int getMathOperationOpcode(ExpressionCompilationContext ctx) { + protected int getMathOperationOpcode(ExpressionVisitorContext ctx) { Type type = resolveType(ctx); String typeDescriptor = type.getDescriptor(); return Optional.ofNullable(MOD_OPERATION_OPCODES.get(typeDescriptor)) diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MulOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MulOperator.java index d63028d2e0a..3eea3ab5592 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MulOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MulOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import org.objectweb.asm.Type; @@ -49,7 +49,7 @@ public MulOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { } @Override - protected int getMathOperationOpcode(ExpressionCompilationContext ctx) { + protected int getMathOperationOpcode(ExpressionVisitorContext ctx) { Type type = resolveType(ctx); String typeDescriptor = type.getDescriptor(); return Optional.ofNullable(MUL_OPERATION_OPCODES.get(typeDescriptor)) diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NeqOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NeqOperator.java index b2002195eee..428c7ecf68e 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NeqOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/NeqOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Label; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; @@ -39,7 +39,7 @@ public NeqOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { super.generateBytecode(ctx); GeneratorAdapter mv = ctx.methodVisitor(); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/OrOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/OrOperator.java index deffe47150c..283e02b9ca3 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/OrOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/OrOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Label; import org.objectweb.asm.commons.GeneratorAdapter; @@ -37,7 +37,7 @@ public OrOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); Label falseLabel = new Label(); Label returnLabel = new Label(); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/PowOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/PowOperator.java index 2436900b437..c62d15703c8 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/PowOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/PowOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; @@ -50,7 +50,7 @@ public PowOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); Type leftType = leftOperand.resolveType(ctx); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java index e36de48acf9..41fcb9c8309 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/RelationalOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import org.objectweb.asm.Label; import org.objectweb.asm.Type; @@ -66,7 +66,7 @@ protected Type resolveOperationType(Type leftOperandType, } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); Type leftType = leftOperand.resolveType(ctx); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/SubOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/SubOperator.java index 4cdffa92844..932bdcf5652 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/SubOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/SubOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import org.objectweb.asm.Type; @@ -49,7 +49,7 @@ public SubOperator(ExpressionNode leftOperand, ExpressionNode rightOperand) { } @Override - protected int getMathOperationOpcode(ExpressionCompilationContext ctx) { + protected int getMathOperationOpcode(ExpressionVisitorContext ctx) { Type type = resolveType(ctx); String typeDescriptor = type.getDescriptor(); return Optional.ofNullable(SUB_OPERATION_OPCODES.get(typeDescriptor)) diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NegOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NegOperator.java index 8afd8c2bfb2..6b5bab4a3b8 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NegOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NegOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; @@ -51,7 +51,7 @@ public NegOperator(ExpressionNode operand) { } @Override - public Type doResolveType(ExpressionCompilationContext ctx) { + public Type doResolveType(ExpressionVisitorContext ctx) { Type nodeType = super.doResolveType(ctx); if (!isNumeric(nodeType)) { throw new ExpressionCompilationException( @@ -61,7 +61,7 @@ public Type doResolveType(ExpressionCompilationContext ctx) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); operand.compile(ctx); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NotOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NotOperator.java index 2cdd4cfe839..d1d4fb9723c 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NotOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/NotOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import org.objectweb.asm.Label; import org.objectweb.asm.Type; @@ -40,7 +40,7 @@ public NotOperator(ExpressionNode operand) { } @Override - public Type doResolveType(ExpressionCompilationContext ctx) { + public Type doResolveType(ExpressionVisitorContext ctx) { if (nodeType != null) { return nodeType; } @@ -56,7 +56,7 @@ public Type doResolveType(ExpressionCompilationContext ctx) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { GeneratorAdapter mv = ctx.methodVisitor(); Label falseLabel = new Label(); Label returnLabel = new Label(); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/PosOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/PosOperator.java index 4ddafec21f3..907976253cd 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/PosOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/PosOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import org.objectweb.asm.Type; @@ -36,7 +36,7 @@ public PosOperator(ExpressionNode operand) { } @Override - public Type doResolveType(ExpressionCompilationContext ctx) { + public Type doResolveType(ExpressionVisitorContext ctx) { Type nodeType = super.doResolveType(ctx); if (!isNumeric(nodeType)) { @@ -47,7 +47,7 @@ public Type doResolveType(ExpressionCompilationContext ctx) { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { operand.compile(ctx); } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java index efa9cc64cdb..76d49933d5b 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java @@ -17,7 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import org.objectweb.asm.Type; /** @@ -37,7 +37,7 @@ public UnaryOperator(ExpressionNode operand) { } @Override - public Type doResolveType(ExpressionCompilationContext ctx) { + public Type doResolveType(ExpressionVisitorContext ctx) { return operand.resolveType(ctx); } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java index 79bff825d72..beef5e78067 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java @@ -18,7 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.ast.util.TypeDescriptors; -import io.micronaut.expressions.parser.compilation.ExpressionCompilationContext; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import io.micronaut.inject.processing.JavaModelUtils; import org.objectweb.asm.Type; @@ -55,12 +55,12 @@ public boolean isPrimitive() { } @Override - public void generateBytecode(ExpressionCompilationContext ctx) { + public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(resolveType(ctx)); } @Override - public Type doResolveType(ExpressionCompilationContext ctx) { + public Type doResolveType(ExpressionVisitorContext ctx) { String name = this.toString(); if (PRIMITIVES.containsKey(name)) { return PRIMITIVES.get(name); @@ -80,7 +80,7 @@ public Type doResolveType(ExpressionCompilationContext ctx) { return resolvedType; } - private Type resolveObjectType(ExpressionCompilationContext ctx, String name) { + private Type resolveObjectType(ExpressionVisitorContext ctx, String name) { return ctx.visitorContext().getClassElement(name) .map(JavaModelUtils::getTypeReference) .orElse(null); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java index c5aa7ed58c8..fe2df05db22 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java @@ -15,9 +15,10 @@ */ package io.micronaut.expressions.parser.ast.util; -import io.micronaut.context.AbstractEvaluatedExpression; +import io.micronaut.context.expressions.AbstractEvaluatedExpression; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.expressions.ExpressionEvaluationContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import org.objectweb.asm.Type; @@ -34,7 +35,7 @@ */ @Internal public final class TypeDescriptors { - public static final Type EVALUATED_EXPRESSION_TYPE = Type.getType(AbstractEvaluatedExpression.class); + public static final Type EVALUATION_CONTEXT_TYPE = Type.getType(ExpressionEvaluationContext.class); public static final Type STRING = Type.getType(String.class); public static final Type OBJECT = Type.getType(Object.class); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionCompilationContext.java b/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionVisitorContext.java similarity index 65% rename from core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionCompilationContext.java rename to core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionVisitorContext.java index 7585fb4ba50..ad00bccd60f 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionCompilationContext.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionVisitorContext.java @@ -16,21 +16,21 @@ package io.micronaut.expressions.parser.compilation; import io.micronaut.core.annotation.Internal; -import io.micronaut.expressions.context.ExpressionEvaluationContext; +import io.micronaut.expressions.context.ExpressionCompilationContext; import io.micronaut.inject.visitor.VisitorContext; import org.objectweb.asm.commons.GeneratorAdapter; /** * Context class used for compiling expressions. * - * @param evaluationContext expression evaluation context - * @param visitorContext visitor context - * @param methodVisitor method visitor for compiled expression class + * @param compilationContext expression evaluation context + * @param visitorContext visitor context + * @param methodVisitor method visitor for compiled expression class * * @author Sergey Gavrilov * @since 4.0.0 */ @Internal -public record ExpressionCompilationContext(ExpressionEvaluationContext evaluationContext, - VisitorContext visitorContext, - GeneratorAdapter methodVisitor) { } +public record ExpressionVisitorContext(ExpressionCompilationContext compilationContext, + VisitorContext visitorContext, + GeneratorAdapter methodVisitor) {} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionCompilationException.java b/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionCompilationException.java index 4335b0ccfa4..af8d08142cc 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionCompilationException.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionCompilationException.java @@ -15,6 +15,8 @@ */ package io.micronaut.expressions.parser.exception; +import io.micronaut.core.annotation.Internal; + /** * Exception throws when problems with expression compilation occur. * These usually include problems with type resolution. @@ -22,6 +24,7 @@ * @author Sergey Gavrilov * @since 4.0.0 */ +@Internal public class ExpressionCompilationException extends RuntimeException { public ExpressionCompilationException(String message) { super(message); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionParsingException.java b/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionParsingException.java index fe0d8b14e97..a5bb653dbd6 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionParsingException.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/exception/ExpressionParsingException.java @@ -15,12 +15,15 @@ */ package io.micronaut.expressions.parser.exception; +import io.micronaut.core.annotation.Internal; + /** * Exception throws when problems with expression parsing occur. * * @author Sergey Gavrilov * @since 4.0.0 */ +@Internal public class ExpressionParsingException extends RuntimeException { public ExpressionParsingException(String message) { super(message); diff --git a/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java b/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java index 9d08dcb9314..e728759316b 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java +++ b/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java @@ -15,9 +15,10 @@ */ package io.micronaut.expressions.util; -import io.micronaut.core.annotation.EvaluatedExpressionReference; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.annotation.Internal; import java.util.ArrayList; import java.util.Arrays; @@ -32,6 +33,7 @@ * @author Sergey Gavrilov * @since 4.0.0 */ +@Internal public final class EvaluatedExpressionsUtils { /** diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 12c0ac4762e..8727ab4d95f 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -27,7 +27,7 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; -import io.micronaut.core.annotation.EvaluatedExpressionReference; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.InstantiatedMember; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; @@ -57,8 +57,8 @@ import java.util.function.Predicate; import java.util.stream.Stream; -import static io.micronaut.core.expression.EvaluatedExpression.EXPRESSION_PATTERN; -import static io.micronaut.core.annotation.EvaluatedExpressionReference.EXPR_SUFFIX; +import static io.micronaut.core.expressions.EvaluatedExpressionReference.EXPR_SUFFIX; +import static io.micronaut.expressions.EvaluatedExpressionConstants.EXPRESSION_PATTERN; /** * An abstract implementation that builds {@link AnnotationMetadata}. diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java index 72024a4cddf..de184a9aaaf 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataWriter.java @@ -19,14 +19,14 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataDelegate; import io.micronaut.core.annotation.AnnotationUtil; -import io.micronaut.core.annotation.EvaluatedExpressionReference; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.UsedByGeneratedCode; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.context.AbstractEvaluatedExpression; +import io.micronaut.context.expressions.AbstractEvaluatedExpression; import io.micronaut.inject.writer.AbstractAnnotationMetadataWriter; import io.micronaut.inject.writer.AbstractClassFileWriter; import io.micronaut.inject.writer.ClassGenerationException; diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EvaluatedExpressionContextTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EvaluatedExpressionContextTypeElementVisitor.java index 1752242c18f..96942b29009 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EvaluatedExpressionContextTypeElementVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EvaluatedExpressionContextTypeElementVisitor.java @@ -17,36 +17,28 @@ import io.micronaut.context.annotation.EvaluatedExpressionContext; import io.micronaut.core.annotation.Internal; -import io.micronaut.expressions.context.ExpressionContextLoader; -import io.micronaut.expressions.context.ExpressionWithContext; +import io.micronaut.expressions.context.ExpressionCompilationContextRegistry; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; -import io.micronaut.inject.writer.ClassGenerationException; -import java.io.IOException; import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; import java.util.Set; /** * A {@link TypeElementVisitor} that visits classes annotated with - * {@link EvaluatedExpressionContext} and produces - * {@link ExpressionWithContext} instances at compilation time. This visitor is also responsible - * for assembling expression evaluation context by adding discovered context classes to - * {@link ExpressionContextLoader} + * {@link EvaluatedExpressionContext} and adds discovered context classes to + * {@link ExpressionCompilationContextRegistry}. * * @author Sergey Gavrilov * @since 4.0.0 */ @Internal public final class EvaluatedExpressionContextTypeElementVisitor implements TypeElementVisitor { - private final Map writers = new LinkedHashMap<>(10); @Override public void start(VisitorContext visitorContext) { - ExpressionContextLoader.reset(); + ExpressionCompilationContextRegistry.reset(); } @Override @@ -57,24 +49,10 @@ public Set getSupportedAnnotationNames() { @Override public void visitClass(ClassElement element, VisitorContext context) { if (!element.isPrivate() && element.hasStereotype(EvaluatedExpressionContext.class)) { - ExpressionContextLoader.addContextClass(element, context); - writers.putIfAbsent(element.getName(), new ExpressionContextReferenceWriter(element)); + ExpressionCompilationContextRegistry.registerContextClass(element); } } - @Override - public void finish(VisitorContext visitorContext) { - for (ExpressionContextReferenceWriter writer: writers.values()) { - try { - writer.accept(visitorContext); - } catch (IOException e) { - throw new ClassGenerationException("I/O error occurred during class generation: " + e.getMessage(), e); - } - } - - writers.clear(); - } - @Override public VisitorKind getVisitorKind() { return VisitorKind.ISOLATING; diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/ExpressionContextReferenceWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/ExpressionContextReferenceWriter.java deleted file mode 100644 index 4d8c9b86967..00000000000 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/ExpressionContextReferenceWriter.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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.inject.beans.visitor; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.naming.NameUtils; -import io.micronaut.expressions.context.ExpressionContextReference; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.writer.AbstractClassFileWriter; -import io.micronaut.inject.writer.ClassWriterOutputVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Type; -import org.objectweb.asm.commons.GeneratorAdapter; - -import java.io.IOException; -import java.io.OutputStream; - -/** - * A class file writer that writes a {@link ExpressionContextReference}. - * - * @author Sergey Gavrilov - * @since 4.0.0 - */ -@Internal -final class ExpressionContextReferenceWriter extends AbstractClassFileWriter { - private static final String EXPRESSION_CTX_SUFFIX = "$ExprCtxRef"; - - private final String referenceName; - private final Type targetClassType; - private final ClassWriter referenceWriter; - - public ExpressionContextReferenceWriter(ClassElement classElement) { - super(classElement); - final String name = classElement.getName(); - this.referenceWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); - this.referenceName = computeReferenceName(name); - this.targetClassType = getTypeReferenceForName(name); - } - - private String computeReferenceName(String className) { - final String packageName = NameUtils.getPackageName(className); - final String shortName = NameUtils.getSimpleName(className); - return packageName + ".$" + shortName + EXPRESSION_CTX_SUFFIX; - } - - @Override - public void accept(ClassWriterOutputVisitor classWriterOutputVisitor) throws IOException { - Type superType = Type.getType(ExpressionContextReference.class); - classWriterOutputVisitor.visitServiceDescriptor( - ExpressionContextReference.class, - referenceName, - getOriginatingElement()); - - String internalName = getTypeReferenceForName(referenceName).getInternalName(); - try (OutputStream referenceStream = classWriterOutputVisitor.visitClass(referenceName, - getOriginatingElements())) { - startService(referenceWriter, ExpressionContextReference.class, internalName, - superType); - ClassWriter classWriter = generateClassBytes(referenceWriter); - referenceStream.write(classWriter.toByteArray()); - } - } - - private ClassWriter generateClassBytes(ClassWriter classWriter) { - GeneratorAdapter cv = startConstructor(classWriter); - cv.loadThis(); - invokeConstructor(cv, ExpressionContextReference.class); - cv.returnValue(); - cv.visitMaxs(2, 1); - GeneratorAdapter getTypeMethod = startPublicMethodZeroArgs(classWriter, String.class, - "getType"); - getTypeMethod.push(targetClassType.getClassName()); - getTypeMethod.returnValue(); - getTypeMethod.visitMaxs(2, 1); - getTypeMethod.endMethod(); - return classWriter; - } -} diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java index d7b5120ba44..e1923e42785 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/AbstractClassFileWriter.java @@ -1666,25 +1666,6 @@ protected GeneratorAdapter startPublicMethod(ClassWriter writer, String methodNa getMethodDescriptor(returnType, argumentTypes)); } - /** - * @param writer The class writer - * @param methodName The method name - * @param returnType The return type - * @param argumentTypes The argument types - * @return The {@link GeneratorAdapter} - */ - protected GeneratorAdapter startProtectedVarargsMethod(ClassWriter writer, String methodName, String returnType, String... argumentTypes) { - return new GeneratorAdapter(writer.visitMethod( - ACC_PROTECTED | ACC_VARARGS, - methodName, - getMethodDescriptor(returnType, argumentTypes), - null, - null - ), ACC_PROTECTED | ACC_VARARGS, - methodName, - getMethodDescriptor(returnType, argumentTypes)); - } - /** * @param writer The class writer * @param asmMethod The asm method diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index f8525e8fe00..995df6419b4 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -41,7 +41,7 @@ import io.micronaut.context.annotation.Value; import io.micronaut.context.env.ConfigurationPath; import io.micronaut.core.annotation.AccessorsStyle; -import io.micronaut.core.annotation.EvaluatedExpressionReference; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationMetadataProvider; import io.micronaut.core.annotation.AnnotationUtil; @@ -64,9 +64,9 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.Toggleable; -import io.micronaut.expressions.context.ExpressionEvaluationContext; +import io.micronaut.expressions.context.ExpressionCompilationContext; import io.micronaut.expressions.context.ExpressionWithContext; -import io.micronaut.expressions.context.ExpressionContextFactory; +import io.micronaut.expressions.context.ExpressionCompilationContextFactory; import io.micronaut.expressions.util.EvaluatedExpressionsUtils; import io.micronaut.inject.AdvisedBeanType; import io.micronaut.inject.BeanDefinition; @@ -579,7 +579,7 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private ExecutableMethodsDefinitionWriter executableMethodsDefinitionWriter; private final Collection evaluatedExpressions = new ArrayList<>(2); - private final ExpressionContextFactory expressionContextFactory; + private final ExpressionCompilationContextFactory expressionCompilationContextFactory; private Object constructor; // MethodElement or FieldElement private boolean disabled = false; @@ -715,7 +715,7 @@ public BeanDefinitionWriter(Element beanProducingElement, this.isConfigurationProperties = isConfigurationProperties(annotationMetadata); validateExposedTypes(annotationMetadata, visitorContext); this.visitorContext = visitorContext; - this.expressionContextFactory = new ExpressionContextFactory(visitorContext); + this.expressionCompilationContextFactory = new ExpressionCompilationContextFactory(visitorContext); processEvaluatedExpressions(this.annotationMetadata); beanTypeInnerClasses = beanTypeElement.getEnclosedElements(ElementQuery.of(ClassElement.class)) @@ -4587,7 +4587,7 @@ private void processEvaluatedExpressions(AnnotationMetadata annotationMetadata) expressionReferences.stream() .map(expressionReference -> { - ExpressionEvaluationContext evaluationContext = expressionContextFactory.buildEvaluationContext(expressionReference); + ExpressionCompilationContext evaluationContext = expressionCompilationContextFactory.buildContext(expressionReference); return new ExpressionWithContext(expressionReference, evaluationContext); }) .forEach(evaluatedExpressions::add); diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java index beed5be9c3a..c15a9d5b2e1 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java @@ -21,14 +21,14 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; -import io.micronaut.expressions.context.ExpressionContextFactory; -import io.micronaut.expressions.context.ExpressionEvaluationContext; +import io.micronaut.expressions.context.ExpressionCompilationContextFactory; +import io.micronaut.expressions.context.ExpressionCompilationContext; import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.expressions.util.EvaluatedExpressionsUtils; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.AnnotationMetadataReference; import io.micronaut.inject.annotation.AnnotationMetadataWriter; -import io.micronaut.core.annotation.EvaluatedExpressionReference; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; @@ -104,7 +104,7 @@ public class ExecutableMethodsDefinitionWriter extends AbstractClassFileWriter i private final DispatchWriter methodDispatchWriter; private final Set methodNames = new HashSet<>(); - private final ExpressionContextFactory expressionContextFactory; + private final ExpressionCompilationContextFactory expressionCompilationContextFactory; private final Set evaluatedExpressions = new HashSet<>(); public ExecutableMethodsDefinitionWriter(VisitorContext visitorContext, @@ -117,7 +117,7 @@ public ExecutableMethodsDefinitionWriter(VisitorContext visitorContext, this.thisType = Type.getObjectType(internalName); this.beanDefinitionReferenceClassName = beanDefinitionReferenceClassName; this.methodDispatchWriter = new DispatchWriter(thisType); - this.expressionContextFactory = new ExpressionContextFactory(visitorContext); + this.expressionCompilationContextFactory = new ExpressionCompilationContextFactory(visitorContext); } /** @@ -511,7 +511,7 @@ private void processEvaluatedExpressions(MethodElement methodElement) { expressionReferences.stream() .map(expression -> { - ExpressionEvaluationContext evaluationContext = expressionContextFactory.buildForMethod(expression, methodElement); + ExpressionCompilationContext evaluationContext = expressionCompilationContextFactory.buildContextForMethod(expression, methodElement); return new ExpressionWithContext(expression, evaluationContext); }) .forEach(evaluatedExpressions::add); diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index bd95e14e316..50993e9175e 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -19,7 +19,7 @@ import io.micronaut.core.convert.ConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.ConvertibleValues; -import io.micronaut.core.expression.EvaluatedExpression; +import io.micronaut.core.expressions.EvaluatedExpression; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; @@ -158,8 +158,6 @@ public AnnotationValue(String annotationName, ConvertibleValues converti } /** - * Internal copy constructor. - * * @param target The target * @param defaultValues The default values * @param convertibleValues The convertible values @@ -167,10 +165,10 @@ public AnnotationValue(String annotationName, ConvertibleValues converti */ @Internal @UsedByGeneratedCode - protected AnnotationValue(AnnotationValue target, - Map defaultValues, - ConvertibleValues convertibleValues, - Function valueMapper) { + public AnnotationValue(AnnotationValue target, + Map defaultValues, + ConvertibleValues convertibleValues, + Function valueMapper) { this.annotationName = target.annotationName; this.defaultValues = defaultValues; this.values = target.values; diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java index 7f937aa1a0f..3f3bfb314e5 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValueBuilder.java @@ -15,7 +15,8 @@ */ package io.micronaut.core.annotation; -import io.micronaut.core.expression.EvaluatedExpression; +import io.micronaut.core.expressions.EvaluatedExpression; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.reflect.ReflectionUtils; import java.lang.annotation.Annotation; diff --git a/core/src/main/java/io/micronaut/core/expression/EvaluatedExpression.java b/core/src/main/java/io/micronaut/core/expression/EvaluatedExpression.java deleted file mode 100644 index 9793833bc2a..00000000000 --- a/core/src/main/java/io/micronaut/core/expression/EvaluatedExpression.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2017-2020 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.core.expression; - -/** - * Expression included in annotation metadata which can be evaluated at runtime. - * - * @author Sergey Gavrilov - * @since 4.0.0 - */ -public interface EvaluatedExpression { - - /** - * Evaluated expression prefix. - */ - String EXPRESSION_PREFIX = "#{"; - - /** - * RegEx pattern used to determine whether string value in - * annotation includes evaluated expression. - */ - String EXPRESSION_PATTERN = ".*#\\{.*}.*"; - - /** - * Evaluate expression to obtain evaluation result. - * - * @param args Array of arguments which need to be passed to expression - * for evaluation. Args are used when expression itself is used - * on method and references method arguments - * @return evaluation result - */ - Object evaluate(Object... args); - - /** - * Get original annotation value that was used to generated EvaluatedExpression class. - * - * @return the original expression - */ - Object getInitialAnnotationValue(); -} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextReference.java b/core/src/main/java/io/micronaut/core/expressions/EvaluatedExpression.java similarity index 59% rename from core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextReference.java rename to core/src/main/java/io/micronaut/core/expressions/EvaluatedExpression.java index c494d2530ec..6b65133ba2b 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionContextReference.java +++ b/core/src/main/java/io/micronaut/core/expressions/EvaluatedExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 original authors + * Copyright 2017-2020 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,20 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.expressions.context; +package io.micronaut.core.expressions; import io.micronaut.core.annotation.Internal; /** - * A reference for expression context class. This reference is generated at compilation - * time and allows compiled modules expose their classes annotated with - * {@link ExpressionEvaluationContext} in such a way that dependant modules can reference - * external contexts at their compilation time. + * Expression included in annotation metadata which can be evaluated at runtime. * * @author Sergey Gavrilov * @since 4.0.0 */ @Internal -public abstract class ExpressionContextReference { - public abstract String getType(); +public interface EvaluatedExpression { + /** + * Evaluate expression to obtain evaluation result. + * + * @param evaluationContext context that expression might need for evaluation. + * + * @return evaluation result + */ + Object evaluate(ExpressionEvaluationContext evaluationContext); } diff --git a/core/src/main/java/io/micronaut/core/annotation/EvaluatedExpressionReference.java b/core/src/main/java/io/micronaut/core/expressions/EvaluatedExpressionReference.java similarity index 95% rename from core/src/main/java/io/micronaut/core/annotation/EvaluatedExpressionReference.java rename to core/src/main/java/io/micronaut/core/expressions/EvaluatedExpressionReference.java index d5d214c8a33..da1030214dc 100644 --- a/core/src/main/java/io/micronaut/core/annotation/EvaluatedExpressionReference.java +++ b/core/src/main/java/io/micronaut/core/expressions/EvaluatedExpressionReference.java @@ -13,7 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micronaut.core.annotation; +package io.micronaut.core.expressions; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import java.util.Map; import java.util.Objects; diff --git a/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java b/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java new file mode 100644 index 00000000000..3c09ac6cc72 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/expressions/ExpressionEvaluationContext.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 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.core.expressions; + +import io.micronaut.core.annotation.Internal; + +/** + * Context that can be used by evaluated expression to obtain objects required + * for evaluation process. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public interface ExpressionEvaluationContext extends AutoCloseable { + + /** + * Provides method argument by index. + * + * @param index argument index + * @return argument value + */ + Object getArgument(int index); + + /** + * Provides bean by type. + * + * @param type of required bean + * @param type required bean class object + * @return bean instance + */ + T getBean(Class type); +} diff --git a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy index 3fc2ecc6c68..532e3ca1608 100644 --- a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy +++ b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy @@ -15,10 +15,10 @@ */ package io.micronaut.ast.transform.test -import io.micronaut.context.AbstractEvaluatedExpression -import io.micronaut.core.expression.EvaluatedExpression +import io.micronaut.context.expressions.AbstractEvaluatedExpression +import io.micronaut.context.expressions.DefaultExpressionEvaluationContext +import io.micronaut.core.expressions.EvaluatedExpressionReference import io.micronaut.core.naming.NameUtils -import io.micronaut.core.annotation.EvaluatedExpressionReference import org.intellij.lang.annotations.Language class AbstractEvaluatedExpressionsSpec extends AbstractBeanDefinitionSpec { @@ -56,8 +56,7 @@ class AbstractEvaluatedExpressionsSpec extends AbstractBeanDefinitionSpec { String exprFullName = exprClassName + i try { def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName).newInstance() - exprClass.configure(applicationContext) - result.add(exprClass.evaluate()) + result.add(exprClass.evaluate(new DefaultExpressionEvaluationContext(null, applicationContext, null))) } catch (ClassNotFoundException e) { return null } @@ -92,15 +91,20 @@ class AbstractEvaluatedExpressionsSpec extends AbstractBeanDefinitionSpec { try { def index = EvaluatedExpressionReference.nextIndex(exprClassName) def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprClassName + (index == 0 ? index : index - 1)).newInstance() - exprClass.configure(applicationContext) - return exprClass.evaluate(); + return exprClass.evaluate(new DefaultExpressionEvaluationContext(null, applicationContext, null)); } catch (ClassNotFoundException e) { return null } } - EvaluatedExpression buildSingleExpressionFromClass(String className, - @Language("groovy") String cls) { + Object evaluateSingle(String className, + @Language("groovy") String cls) { + return evaluateSingle(className, cls, null); + } + + Object evaluateSingle(String className, + @Language("groovy") String cls, + Object[] args) { def classSimpleName = NameUtils.getSimpleName(className) def packageName = NameUtils.getPackageName(className) @@ -114,8 +118,7 @@ class AbstractEvaluatedExpressionsSpec extends AbstractBeanDefinitionSpec { try { def index = EvaluatedExpressionReference.nextIndex(exprFullName) def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName + (index == 0 ? index : index - 1)).newInstance() - exprClass.configure(applicationContext) - return exprClass + return exprClass.evaluate(new DefaultExpressionEvaluationContext(args, applicationContext, null)); } catch (ClassNotFoundException e) { return null } diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy index 0b1bf7adfb4..79bb85c4d40 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectTransform.groovy @@ -25,9 +25,7 @@ import io.micronaut.ast.groovy.visitor.GroovyPackageElement import io.micronaut.ast.groovy.visitor.GroovyVisitorContext import io.micronaut.context.annotation.Configuration import io.micronaut.context.annotation.Context -import io.micronaut.expressions.context.ExpressionContextLoader import io.micronaut.expressions.context.ExpressionWithContext -import io.micronaut.inject.processing.ProcessingException import io.micronaut.inject.processing.BeanDefinitionCreator import io.micronaut.inject.processing.BeanDefinitionCreatorFactory import io.micronaut.inject.processing.ProcessingException diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index 146a60a4c07..a14571ce414 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -24,7 +24,7 @@ import io.micronaut.ast.groovy.visitor.GroovyVisitorContext; import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.EvaluatedExpressionReference; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.convert.ConversionService; diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy index e36ab8d45dc..82156a199ae 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy @@ -7,7 +7,7 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions void "test annotation level context"() { given: - Object expr = buildSingleExpressionFromClass("test.Expr", """ + Object result = evaluateSingle("test.Expr", """ package test import io.micronaut.context.annotation.EvaluatedExpressionContext;import io.micronaut.context.annotation.Executable; import io.micronaut.context.annotation.Requires; @@ -30,16 +30,16 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions String value(); } - """).evaluate() + """) expect: - expr instanceof String && expr == "annotationLevelValue" + result instanceof String && result == "annotationLevelValue" } void "test annotation member level context"() { given: - Object expr = buildSingleExpressionFromClass("test.Expr", """ + Object result = evaluateSingle("test.Expr", """ package test import io.micronaut.context.annotation.EvaluatedExpressionContext import io.micronaut.context.annotation.Executable @@ -63,10 +63,10 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions String customValue(); } - """).evaluate() + """) expect: - expr instanceof String && expr == "annotationLevelValue" + result instanceof String && result == "annotationLevelValue" } diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy index b8e4f45e7a3..899b9fd3038 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy @@ -21,7 +21,7 @@ class CompoundExpressionsSpec extends AbstractEvaluatedExpressionsSpec { } void "test string expressions in arrays"() { - Object expr1 = buildSingleExpressionFromClass("test.Expr", """ + Object result = evaluateSingle("test.Expr", """ package test import io.micronaut.context.annotation.Requires import jakarta.inject.Singleton @@ -31,15 +31,14 @@ class CompoundExpressionsSpec extends AbstractEvaluatedExpressionsSpec { class Expr { } """) - .evaluate() expect: - expr1 instanceof Object[] && Arrays.equals((Object[]) expr1, new Object[]{'a', 'b', 'cd'}) + result instanceof Object[] && Arrays.equals((Object[]) result, new Object[]{'a', 'b', 'cd'}) } void "test mixed expressions in arrays"() { - Object expr1 = buildSingleExpressionFromClass("test.Expr", """ + Object result = evaluateSingle("test.Expr", """ package test; import io.micronaut.context.annotation.Requires; import jakarta.inject.Singleton; @@ -49,10 +48,9 @@ class CompoundExpressionsSpec extends AbstractEvaluatedExpressionsSpec { class Expr { } """) - .evaluate() expect: - expr1 instanceof Object[] && Arrays.equals((Object[]) expr1, new Object[]{1, 'b', 15l, 'c'}) + result instanceof Object[] && Arrays.equals((Object[]) result, new Object[]{1, 'b', 15l, 'c'}) } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy index d25d5d1b5ab..34753fdeb75 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy @@ -6,7 +6,7 @@ class MethodArgumentEvaluationContextExpressionsSpec extends AbstractEvaluatedEx { void "test method argument access"() { given: - Object expr1 = buildSingleExpressionFromClass("test.Expr", """ + Object result = evaluateSingle("test.Expr", """ package test import io.micronaut.context.annotation.Executable import io.micronaut.context.annotation.Requires @@ -22,10 +22,9 @@ class MethodArgumentEvaluationContextExpressionsSpec extends AbstractEvaluatedEx } - """) - .evaluate("arg0", "arg1") + """, ["arg0", "arg1"] as Object[]); expect: - expr1 instanceof String && expr1 == 'arg1abc' + result instanceof String && result == 'arg1abc' } } diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy index 7d3e0d189df..d7c75fa69c6 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy @@ -2,7 +2,10 @@ package io.micronaut.expressions import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.CircularDependencyException +import io.micronaut.context.exceptions.ExpressionEvaluationException import io.micronaut.context.exceptions.NoSuchBeanException +import io.micronaut.core.expressions.ExpressionEvaluationContext class TestExpressionsUsageSpec extends AbstractBeanDefinitionSpec { diff --git a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy index 206ded5dd78..28484d3c181 100644 --- a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy +++ b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy @@ -15,10 +15,10 @@ */ package io.micronaut.annotation.processing.test -import io.micronaut.context.AbstractEvaluatedExpression -import io.micronaut.core.expression.EvaluatedExpression +import io.micronaut.context.expressions.AbstractEvaluatedExpression +import io.micronaut.context.expressions.DefaultExpressionEvaluationContext import io.micronaut.core.naming.NameUtils -import io.micronaut.core.annotation.EvaluatedExpressionReference +import io.micronaut.core.expressions.EvaluatedExpressionReference import org.intellij.lang.annotations.Language abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec { @@ -56,8 +56,7 @@ abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec String exprFullName = exprClassName + i try { def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName).newInstance() - exprClass.configure(applicationContext) - result.add(exprClass.evaluate()) + result.add(exprClass.evaluate(new DefaultExpressionEvaluationContext(null, applicationContext, null))) } catch (ClassNotFoundException e) { return null } @@ -94,15 +93,20 @@ abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec try { def index = EvaluatedExpressionReference.nextIndex(exprFullName) def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName + (index == 0 ? index : index - 1)).newInstance() - exprClass.configure(applicationContext) - return exprClass.evaluate(); + exprClass.evaluate(new DefaultExpressionEvaluationContext(null, applicationContext, null)) } catch (ClassNotFoundException e) { return null } } - EvaluatedExpression buildSingleExpressionFromClass(String className, - @Language("java") String cls) { + Object evaluateSingle(String className, + @Language("java") String cls) { + return evaluateSingle(className, cls, null) + } + + Object evaluateSingle(String className, + @Language("java") String cls, + Object[] args) { def classSimpleName = NameUtils.getSimpleName(className) def packageName = NameUtils.getPackageName(className) def exprClassName = (classSimpleName.startsWith('$') ? '' : '$') + classSimpleName + '$Expr' @@ -115,8 +119,7 @@ abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec try { def index = EvaluatedExpressionReference.nextIndex(exprFullName) def exprClass = (AbstractEvaluatedExpression) classLoader.loadClass(exprFullName + (index == 0 ? index : index - 1)).newInstance() - exprClass.configure(applicationContext) - return exprClass + return exprClass.evaluate(new DefaultExpressionEvaluationContext(args, applicationContext, null)) } catch (ClassNotFoundException e) { return null } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index f9ae51c2127..b8c71610d3d 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -28,7 +28,7 @@ import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.expressions.EvaluatedExpressionWriter; -import io.micronaut.expressions.context.ExpressionContextLoader; +import io.micronaut.expressions.context.ExpressionCompilationContextRegistry; import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; @@ -265,7 +265,7 @@ public final boolean process(Set annotations, RoundEnviro } finally { AbstractAnnotationMetadataBuilder.clearMutated(); JavaAnnotationMetadataBuilder.clearCaches(); - ExpressionContextLoader.reset(); + ExpressionCompilationContextRegistry.reset(); } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java index 2356d0e8187..627b167ba9e 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java @@ -30,7 +30,7 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.version.VersionUtils; -import io.micronaut.expressions.context.ExpressionContextLoader; +import io.micronaut.expressions.context.ExpressionCompilationContextRegistry; import io.micronaut.inject.processing.ProcessingException; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.EnumConstantElement; @@ -308,7 +308,7 @@ public boolean process(Set annotations, RoundEnvironment if (roundEnv.processingOver()) { javaVisitorContext.finish(); writeBeanDefinitionsToMetaInf(); - ExpressionContextLoader.reset(); + ExpressionCompilationContextRegistry.reset(); } return false; } diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy index bb5c522e4c1..7c3a857ed5a 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy @@ -6,7 +6,7 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions void "test annotation level context"() { given: - Object expr = buildSingleExpressionFromClass("test.Expr", """ + Object result = evaluateSingle("test.Expr", """ package test; import io.micronaut.context.annotation.EvaluatedExpressionContext;import io.micronaut.context.annotation.Executable; @@ -30,16 +30,16 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions String value(); } - """).evaluate() + """) expect: - expr instanceof String && expr == "annotationLevelValue" + result instanceof String && result == "annotationLevelValue" } void "test annotation member level context"() { given: - Object expr = buildSingleExpressionFromClass("test.Expr", """ + Object result = evaluateSingle("test.Expr", """ package test; import io.micronaut.context.annotation.EvaluatedExpressionContext;import io.micronaut.context.annotation.Executable; @@ -63,10 +63,10 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions String customValue(); } - """).evaluate() + """) expect: - expr instanceof String && expr == "annotationLevelValue" + result instanceof String && result == "annotationLevelValue" } diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy index a88ce9b0f00..84f6cd70f8b 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/CompoundExpressionsSpec.groovy @@ -21,7 +21,7 @@ class CompoundExpressionsSpec extends AbstractEvaluatedExpressionsSpec { } void "test string expressions in arrays"() { - Object expr1 = buildSingleExpressionFromClass("test.Expr", """ + Object result = evaluateSingle("test.Expr", """ package test; import io.micronaut.context.annotation.Requires; import jakarta.inject.Singleton; @@ -31,14 +31,13 @@ class CompoundExpressionsSpec extends AbstractEvaluatedExpressionsSpec { class Expr { } """) - .evaluate() expect: - expr1 instanceof Object[] && Arrays.equals((Object[]) expr1, new Object[]{'a', 'b', 'cd'}) + result instanceof Object[] && Arrays.equals((Object[]) result, new Object[]{'a', 'b', 'cd'}) } void "test mixed expressions in arrays"() { - Object expr1 = buildSingleExpressionFromClass("test.Expr", """ + Object result = evaluateSingle("test.Expr", """ package test; import io.micronaut.context.annotation.Requires; import jakarta.inject.Singleton; @@ -48,9 +47,8 @@ class CompoundExpressionsSpec extends AbstractEvaluatedExpressionsSpec { class Expr { } """) - .evaluate() expect: - expr1 instanceof Object[] && Arrays.equals((Object[]) expr1, new Object[]{1, 'b', 15l, 'c'}) + result instanceof Object[] && Arrays.equals((Object[]) result, new Object[]{1, 'b', 15l, 'c'}) } } diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy index fb8a260c983..2c1db7f8bb5 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/MethodArgumentEvaluationContextExpressionsSpec.groovy @@ -7,7 +7,7 @@ class MethodArgumentEvaluationContextExpressionsSpec extends AbstractEvaluatedEx void "test method argument access"() { given: - Object expr1 = buildSingleExpressionFromClass("test.Expr", """ + Object result = evaluateSingle("test.Expr", """ package test; import io.micronaut.context.annotation.Executable; import io.micronaut.context.annotation.Requires; @@ -23,11 +23,10 @@ class MethodArgumentEvaluationContextExpressionsSpec extends AbstractEvaluatedEx } - """) - .evaluate("arg0", "arg1") + """, ["arg0", "arg1"] as Object[]) expect: - expr1 instanceof String && expr1 == 'arg1abc' + result instanceof String && result == 'arg1abc' } } diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy index dbd79ae465d..480a6820ea2 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy @@ -2,6 +2,8 @@ package io.micronaut.expressions import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.CircularDependencyException +import io.micronaut.context.exceptions.ExpressionEvaluationException import io.micronaut.context.exceptions.NoSuchBeanException class TestExpressionsUsageSpec extends AbstractEvaluatedExpressionsSpec { diff --git a/inject/src/main/java/io/micronaut/context/AbstractEvaluatedExpression.java b/inject/src/main/java/io/micronaut/context/AbstractEvaluatedExpression.java deleted file mode 100644 index a6f8959d750..00000000000 --- a/inject/src/main/java/io/micronaut/context/AbstractEvaluatedExpression.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2017-2020 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.context; - -import io.micronaut.context.exceptions.ExpressionEvaluationException; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.UsedByGeneratedCode; -import io.micronaut.core.expression.EvaluatedExpression; -import io.micronaut.core.type.Argument; -import io.micronaut.inject.BeanDefinition; - -/** - * Default implementation for evaluated expressions. This class is subclassed - * by evaluated expressions classes at compilation time. - * - * @author Sergey Gavrilov - * @since 4.0.0 - */ -@Internal -@UsedByGeneratedCode -public abstract class AbstractEvaluatedExpression implements EvaluatedExpression, - ContextConfigurable, - BeanDefinitionAware { - private final Object initialAnnotationValue; - - private BeanContext beanContext; - private BeanResolutionContext resolutionContext; - private BeanDefinition owningBean; - - public AbstractEvaluatedExpression(Object initialAnnotationValue) { - this.initialAnnotationValue = initialAnnotationValue; - } - - @Override - public Object getInitialAnnotationValue() { - return initialAnnotationValue; - } - - @Override - public void setBeanDefinition(BeanDefinition beanDefinition) { - this.owningBean = beanDefinition; - } - - @Override - public void configure(BeanContext context) { - this.beanContext = context; - } - - @Override - public final Object evaluate(Object... args) { - try { - return doEvaluate(args); - } catch (Throwable ex) { - throw new ExpressionEvaluationException(ex); - } finally { - if (resolutionContext != null) { - resolutionContext = null; - } - } - } - - /** - * This method is overridden by expression classes generated at compilation time and - * contains concrete expression evaluation logic. - * - * @param args Array of arguments which need to be passed to expression - * for evaluation. Args are used when expression itself is used - * on method and references method arguments - * @return evaluation result - */ - protected Object doEvaluate(Object... args) { - return initialAnnotationValue; - } - - protected final T getBean(Class type) { - if (beanContext == null) { - throw new ExpressionEvaluationException( - "Can not evaluate expression [" + getInitialAnnotationValue() + "]. " + - "Can not obtain bean of type [" + type + "] since bean context is not set"); - } - - if (beanContext instanceof DefaultBeanContext defaultBeanContext) { - if (resolutionContext == null && owningBean != null) { - resolutionContext = new DefaultBeanResolutionContext(defaultBeanContext, owningBean); - } - - if (resolutionContext != null) { - try (BeanResolutionContext.Path ignored = - resolutionContext.getPath().pushAnnotationResolve(owningBean, Argument.of(type))) { - return defaultBeanContext.getBean(resolutionContext, type); - } - } - } - - return beanContext.getBean(type); - } - - @Override - public String toString() { - return initialAnnotationValue.toString(); - } -} diff --git a/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java b/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java index d5667035d5c..17ab04e3d6d 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java +++ b/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java @@ -33,7 +33,7 @@ */ @DefaultScope(Singleton.class) @Target({ElementType.TYPE, ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.CLASS) public @interface EvaluatedExpressionContext { Class value() default void.class; } diff --git a/inject/src/main/java/io/micronaut/context/exceptions/ExpressionEvaluationException.java b/inject/src/main/java/io/micronaut/context/exceptions/ExpressionEvaluationException.java index 40ba6334c26..3673e7d4b06 100644 --- a/inject/src/main/java/io/micronaut/context/exceptions/ExpressionEvaluationException.java +++ b/inject/src/main/java/io/micronaut/context/exceptions/ExpressionEvaluationException.java @@ -22,8 +22,9 @@ * @author Sergey Gavrilov */ public class ExpressionEvaluationException extends RuntimeException { - public ExpressionEvaluationException(Throwable ex) { - super(ex); + + public ExpressionEvaluationException(String message, Throwable ex) { + super(message, ex); } public ExpressionEvaluationException(String message) { diff --git a/inject/src/main/java/io/micronaut/context/expressions/AbstractEvaluatedExpression.java b/inject/src/main/java/io/micronaut/context/expressions/AbstractEvaluatedExpression.java new file mode 100644 index 00000000000..5b381dca21c --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/expressions/AbstractEvaluatedExpression.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017-2020 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.context.expressions; + +import io.micronaut.context.exceptions.ExpressionEvaluationException; +import io.micronaut.core.expressions.EvaluatedExpression; +import io.micronaut.core.expressions.ExpressionEvaluationContext; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.UsedByGeneratedCode; + +/** + * Default implementation for evaluated expressions. This class is subclassed + * by evaluated expressions classes at compilation time. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +@UsedByGeneratedCode +public abstract class AbstractEvaluatedExpression implements EvaluatedExpression { + + private final Object initialAnnotationValue; + + public AbstractEvaluatedExpression(Object initialAnnotationValue) { + this.initialAnnotationValue = initialAnnotationValue; + } + + @Override + public final Object evaluate(ExpressionEvaluationContext evaluationContext) { + try (evaluationContext) { + return doEvaluate(evaluationContext); + } catch (Throwable ex) { + throw new ExpressionEvaluationException( + "Can not evaluate expression [" + initialAnnotationValue + "]. " + ex.getMessage(), ex); + } + } + + /** + * This method is overridden by expression classes generated at compilation time and + * contains concrete expression evaluation logic. + * + * @param evaluationContext context used for expression evaluation + * @return evaluation result + */ + protected Object doEvaluate(ExpressionEvaluationContext evaluationContext) { + return initialAnnotationValue; + } + + @Override + public String toString() { + return initialAnnotationValue.toString(); + } +} diff --git a/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java b/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java new file mode 100644 index 00000000000..8c3823f338f --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/expressions/ConfigurableExpressionEvaluationContext.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2020 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.context.expressions; + +import io.micronaut.context.BeanContext; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.expressions.ExpressionEvaluationContext; +import io.micronaut.inject.BeanDefinition; + +/** + * Expression evaluation context that can be configured before evaluation. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public interface ConfigurableExpressionEvaluationContext extends ExpressionEvaluationContext { + + /** + * Set arguments passed to invoked method. + * + * @param args method arguments + * @return evaluation context which arguments can be used in evaluation. + */ + @NonNull + ConfigurableExpressionEvaluationContext setArguments(@Nullable Object[] args); + + /** + * Set bean owning evaluated expression. + * + * @param beanDefinition owning bean definition + * @return evaluation context aware of owning bean. + */ + @NonNull + ConfigurableExpressionEvaluationContext setOwningBean(@Nullable BeanDefinition beanDefinition); + + /** + * Set context in which expression is evaluated. + * + * @param beanContext bean context + * @return evaluation context aware of bean context. + */ + @NonNull + ConfigurableExpressionEvaluationContext setBeanContext(@Nullable BeanContext beanContext); +} diff --git a/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java b/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java new file mode 100644 index 00000000000..e480e84eb24 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/expressions/DefaultExpressionEvaluationContext.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017-2020 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.context.expressions; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.BeanResolutionContext; +import io.micronaut.context.DefaultBeanContext; +import io.micronaut.context.DefaultBeanResolutionContext; +import io.micronaut.context.exceptions.ExpressionEvaluationException; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.type.Argument; +import io.micronaut.inject.BeanDefinition; + +/** + * Default implementation of {@link ConfigurableExpressionEvaluationContext}. + * For this implementation, the methods mutating evaluation context return new instance of + * expression evaluation context. + * + * @since 4.0.0 + * @author Sergey Gavrilov + */ +@Internal +public class DefaultExpressionEvaluationContext implements ConfigurableExpressionEvaluationContext { + + private final Object[] args; + private final BeanContext beanContext; + private final BeanDefinition owningBean; + + private BeanResolutionContext resolutionContext; + + public DefaultExpressionEvaluationContext() { + this(null, null, null); + } + + public DefaultExpressionEvaluationContext(@Nullable Object[] args, + @Nullable BeanContext beanContext, + @Nullable BeanDefinition owningBean) { + this.args = args; + this.beanContext = beanContext; + this.owningBean = owningBean; + } + + @Override + public ConfigurableExpressionEvaluationContext setArguments(Object[] args) { + return new DefaultExpressionEvaluationContext(args, this.beanContext, this.owningBean); + } + + @Override + public ConfigurableExpressionEvaluationContext setOwningBean(BeanDefinition beanDefinition) { + return new DefaultExpressionEvaluationContext(this.args, this.beanContext, beanDefinition); + } + + @Override + public ConfigurableExpressionEvaluationContext setBeanContext(BeanContext beanContext) { + return new DefaultExpressionEvaluationContext(this.args, beanContext, this.owningBean); + } + + @Override + public Object getArgument(int index) { + if (args == null || args.length == 0) { + throw new ExpressionEvaluationException( + "Can not obtain argument at index [" + index + "] since arguments are not provided"); + } + + return args[index]; + } + + @Override + public T getBean(Class type) { + if (beanContext == null) { + throw new ExpressionEvaluationException("Can not obtain bean of type [" + type + "] since bean context is not set"); + } + + if (beanContext instanceof DefaultBeanContext defaultBeanContext) { + if (resolutionContext == null && owningBean != null) { + resolutionContext = new DefaultBeanResolutionContext(defaultBeanContext, owningBean); + } + + if (resolutionContext != null) { + try (BeanResolutionContext.Path ignored = + resolutionContext.getPath().pushAnnotationResolve(owningBean, Argument.of(type))) { + return defaultBeanContext.getBean(resolutionContext, type); + } + } + } + + return beanContext.getBean(type); + } + + @Override + public void close() throws Exception { + resolutionContext = null; + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java index fa4b4a0039a..e25e921a25f 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java @@ -18,9 +18,12 @@ import io.micronaut.context.BeanContext; import io.micronaut.context.BeanDefinitionAware; import io.micronaut.context.ContextConfigurable; +import io.micronaut.context.expressions.ConfigurableExpressionEvaluationContext; +import io.micronaut.context.expressions.DefaultExpressionEvaluationContext; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.expressions.EvaluatedExpression; import io.micronaut.inject.BeanDefinition; import java.lang.annotation.Annotation; @@ -35,42 +38,47 @@ @Internal public final class EvaluatedAnnotationMetadata extends MappingAnnotationMetadataDelegate implements ContextConfigurable, BeanDefinitionAware { - private BeanContext beanContext; - private BeanDefinition owningBean; - private final AnnotationMetadata delegateAnnotationMetadata; - private final Object[] args; - private EvaluatedAnnotationMetadata(AnnotationMetadata targetMetadata, Object[] args) { + private ConfigurableExpressionEvaluationContext evaluationContext; + + private EvaluatedAnnotationMetadata(AnnotationMetadata targetMetadata, + ConfigurableExpressionEvaluationContext evaluationContext) { this.delegateAnnotationMetadata = targetMetadata; - this.args = args; + this.evaluationContext = evaluationContext; } - private EvaluatedAnnotationMetadata(AnnotationMetadata targetMetadata) { - this(targetMetadata, new Object[0]); + /** + * Provide a copy of this annotation metadata with passed method arguments. + * + * @param args arguments passed to method + * @return copy of annotation metadata + */ + public EvaluatedAnnotationMetadata copyWithArgs(Object[] args) { + return new EvaluatedAnnotationMetadata( + delegateAnnotationMetadata, + evaluationContext.setArguments(args)); } - public static AnnotationMetadata wrapIfNecessary(AnnotationMetadata targetMetadata, - Object[] args) { - if (targetMetadata == null) { - return null; - } else if (targetMetadata instanceof EvaluatedAnnotationMetadata eam) { - AnnotationMetadata delegateMetadata = eam.getAnnotationMetadata(); - EvaluatedAnnotationMetadata methodInvocationMetadata = - new EvaluatedAnnotationMetadata(delegateMetadata, args); - methodInvocationMetadata.setBeanDefinition(eam.owningBean); - methodInvocationMetadata.configure(eam.beanContext); - return methodInvocationMetadata; - } + @Override + public void configure(BeanContext context) { + evaluationContext = evaluationContext.setBeanContext(context); + } - if (targetMetadata.hasEvaluatedExpressions()) { - return new EvaluatedAnnotationMetadata(targetMetadata); - } - return targetMetadata; + @Override + public void setBeanDefinition(BeanDefinition beanDefinition) { + evaluationContext = evaluationContext.setOwningBean(beanDefinition); } public static AnnotationMetadata wrapIfNecessary(AnnotationMetadata targetMetadata) { - return wrapIfNecessary(targetMetadata, new Object[0]); + if (targetMetadata == null) { + return null; + } else if (targetMetadata instanceof EvaluatedAnnotationMetadata) { + return targetMetadata; + } else if (targetMetadata.hasEvaluatedExpressions()) { + return new EvaluatedAnnotationMetadata(targetMetadata, new DefaultExpressionEvaluationContext()); + } + return targetMetadata; } @Override @@ -86,16 +94,15 @@ public AnnotationMetadata getAnnotationMetadata() { @Override public AnnotationValue mapAnnotationValue(AnnotationValue av) { - return new EvaluatedAnnotationValue<>(beanContext, owningBean, args, av); - } - - @Override - public void configure(BeanContext context) { - this.beanContext = context; - } - - @Override - public void setBeanDefinition(BeanDefinition beanDefinition) { - this.owningBean = beanDefinition; + return new AnnotationValue( + av, + AnnotationMetadataSupport.getDefaultValues(av.getAnnotationName()), + new EvaluatedConvertibleValuesMap<>(evaluationContext, av.getConvertibleValues()), + value -> { + if (value instanceof EvaluatedExpression expression) { + return expression.evaluate(evaluationContext); + } + return value; + }); } } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java deleted file mode 100644 index 059bd37cbbb..00000000000 --- a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2017-2020 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.inject.annotation; - -import io.micronaut.context.BeanContext; -import io.micronaut.context.BeanDefinitionAware; -import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionContext; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.value.ConvertibleValues; -import io.micronaut.core.convert.value.ConvertibleValuesMap; -import io.micronaut.context.ContextConfigurable; -import io.micronaut.core.expression.EvaluatedExpression; -import io.micronaut.core.type.Argument; -import io.micronaut.inject.BeanDefinition; - -import java.lang.annotation.Annotation; -import java.util.Collection; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * {@link AnnotationValue} which is evaluated against BeanContext. - * - * @author graemerocher - * @since 1.0 - * @param The annotation type - */ -@Internal -public class EvaluatedAnnotationValue extends AnnotationValue { - - EvaluatedAnnotationValue(@Nullable BeanContext beanContext, - @Nullable BeanDefinition owningBean, - Object[] args, - AnnotationValue target) { - super( - target, - AnnotationMetadataSupport.getDefaultValues(target.getAnnotationName()), - new EvaluatedConvertibleValuesMap<>(beanContext, owningBean, args, target.getConvertibleValues()), - value -> { - if (value instanceof EvaluatedExpression expression) { - if (value instanceof ContextConfigurable ctxConfigurable && beanContext != null) { - ctxConfigurable.configure(beanContext); - if (value instanceof BeanDefinitionAware beanDefinitionAware) { - beanDefinitionAware.setBeanDefinition(owningBean); - } - } - return expression.evaluate(args); - } - return value; - }); - } - - @Override - public Optional get(CharSequence member, ArgumentConversionContext conversionContext) { - Optional value = getConvertibleValues().get(member, conversionContext); - if (value.isPresent()) { - return value; - } - - return super.get(member, conversionContext); - } - - @Override - public boolean hasEvaluatedExpressions() { - return true; - } -} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java index 04adef44c1a..7e0ad7f1147 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedConvertibleValuesMap.java @@ -15,17 +15,13 @@ */ package io.micronaut.inject.annotation; -import io.micronaut.context.BeanContext; -import io.micronaut.context.BeanDefinitionAware; -import io.micronaut.context.ContextConfigurable; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ArgumentConversionContext; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.convert.value.ConvertibleValues; import io.micronaut.core.convert.value.ConvertibleValuesMap; -import io.micronaut.core.expression.EvaluatedExpression; -import io.micronaut.inject.BeanDefinition; +import io.micronaut.core.expressions.EvaluatedExpression; +import io.micronaut.core.expressions.ExpressionEvaluationContext; import java.util.Collection; import java.util.Optional; @@ -36,22 +32,19 @@ * Version of {@link ConvertibleValuesMap} that is aware of evaluated expressions. * * @param The generic value + * + * @since 4.0.0 + * @author Sergey Gavrilov */ @Internal -public class EvaluatedConvertibleValuesMap implements ConvertibleValues -{ - private final BeanContext beanContext; - private final BeanDefinition owningBean; - private final Object[] args; +public class EvaluatedConvertibleValuesMap implements ConvertibleValues { + + private final ExpressionEvaluationContext evaluationContext; private final ConvertibleValues delegateValues; - EvaluatedConvertibleValuesMap(@Nullable BeanContext beanContext, - @Nullable BeanDefinition owningBean, - Object[] args, + EvaluatedConvertibleValuesMap(ExpressionEvaluationContext evaluationContext, ConvertibleValues delegateValues) { - this.beanContext = beanContext; - this.owningBean = owningBean; - this.args = args; + this.evaluationContext = evaluationContext; this.delegateValues = delegateValues; } @@ -65,18 +58,11 @@ public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { V value = delegateValues.getValue(name); if (value instanceof EvaluatedExpression expression) { - if (value instanceof ContextConfigurable ctxConfigurable && beanContext != null) { - ctxConfigurable.configure(beanContext); - if (value instanceof BeanDefinitionAware beanDefinitionAware && owningBean != null) { - beanDefinitionAware.setBeanDefinition(owningBean); - } - } - if (EvaluatedExpression.class.isAssignableFrom(conversionContext.getArgument().getClass())) { return Optional.of((T) value); } - Object evaluationResult = expression.evaluate(args); + Object evaluationResult = expression.evaluate(evaluationContext); if (evaluationResult == null || conversionContext.getArgument().isAssignableFrom(evaluationResult.getClass())) { return Optional.ofNullable((T) evaluationResult); } @@ -91,15 +77,7 @@ public Optional get(CharSequence name, public Collection values() { return delegateValues.values().stream().map(v -> { if (v instanceof EvaluatedExpression expression) { - if (v instanceof ContextConfigurable ctxConfigurable) { - ctxConfigurable.configure(beanContext); - } - - if (v instanceof BeanDefinitionAware beanDefinitionAware && owningBean != null) { - beanDefinitionAware.setBeanDefinition(owningBean); - } - - Object evaluationResult = expression.evaluate(args); + Object evaluationResult = expression.evaluate(evaluationContext); return (V) evaluationResult; } return v; diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java index 6920f93555c..b2e72610e08 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java @@ -19,7 +19,7 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.EvaluatedExpressionReference; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; From 51222792c45b905141895c7b72af2269dd01ca82 Mon Sep 17 00:00:00 2001 From: Sergey Gavrilov Date: Thu, 9 Mar 2023 15:09:18 +0300 Subject: [PATCH 05/35] =?UTF-8?q?review=20improvements=20=D1=872?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DefaultExpressionCompilationContext.java | 18 ++-- .../ExpressionCompilationContextFactory.java | 8 +- .../AbstractAnnotationMetadataBuilder.java | 1 + .../AbstractEvaluatedExpressionsSpec.groovy | 1 + ...notationLevelContextExpressionsSpec.groovy | 11 ++- ...notationLevelContextExpressionsSpec.groovy | 10 ++- .../AbstractExecutableMethodsDefinition.java | 9 +- .../AnnotationExpressionContext.java | 37 ++++++++ .../EvaluatedExpressionContext.java | 3 +- .../guide/config/evaluatedExpressions.adoc | 85 +++++++++++++------ 10 files changed, 131 insertions(+), 52 deletions(-) create mode 100644 inject/src/main/java/io/micronaut/context/annotation/AnnotationExpressionContext.java diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java index b5355a77450..13070dc3e16 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java @@ -95,15 +95,6 @@ private List findMatchingMethods(ClassElement classElement, Strin .toList(); } - private List getNamedProperties(ClassElement classElement, String name) { - return classElement.getBeanProperties( - PropertyElementQuery.of(classElement.getAnnotationMetadata()) - .includes(Collections.singleton(name))) - .stream() - .filter(not(PropertyElement::isExcluded)) - .toList(); - } - @Override public List findProperties(String name) { return classElements.stream() @@ -121,4 +112,13 @@ public List findParameters(String name) { .filter(parameter -> parameter.getName().equals(name)) .toList(); } + + private List getNamedProperties(ClassElement classElement, String name) { + return classElement.getBeanProperties( + PropertyElementQuery.of(classElement.getAnnotationMetadata()) + .includes(Collections.singleton(name))) + .stream() + .filter(not(PropertyElement::isExcluded)) + .toList(); + } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java index aaea1fbd14d..fe9a0816e93 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java @@ -15,7 +15,7 @@ */ package io.micronaut.expressions.context; -import io.micronaut.context.annotation.EvaluatedExpressionContext; +import io.micronaut.context.annotation.AnnotationExpressionContext; import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.expressions.EvaluatedExpressionReference; @@ -92,7 +92,7 @@ private ExtendableExpressionCompilationContext addAnnotationEvaluationContext( ExtendableExpressionCompilationContext currentEvaluationContext, ClassElement annotation) { - return Optional.ofNullable(annotation.getAnnotation(EvaluatedExpressionContext.class)) + return Optional.ofNullable(annotation.getAnnotation(AnnotationExpressionContext.class)) .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER)) .map(AnnotationClassValue::getName) .flatMap(visitorContext::getClassElement) @@ -108,11 +108,11 @@ private ExtendableExpressionCompilationContext addAnnotationMemberEvaluationCont ElementQuery memberQuery = ElementQuery.ALL_METHODS .onlyDeclared() - .annotated(am -> am.hasAnnotation(EvaluatedExpressionContext.class)) + .annotated(am -> am.hasAnnotation(AnnotationExpressionContext.class)) .named(annotationMember); return annotation.getEnclosedElements(memberQuery).stream() - .flatMap(element -> Optional.ofNullable(element.getDeclaredAnnotation(EvaluatedExpressionContext.class)).stream()) + .flatMap(element -> Optional.ofNullable(element.getDeclaredAnnotation(AnnotationExpressionContext.class)).stream()) .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER).stream()) .map(AnnotationClassValue::getName) .flatMap(className -> visitorContext.getClassElement(className).stream()) diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 8727ab4d95f..c335bb49328 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -43,6 +43,7 @@ import java.lang.annotation.Annotation; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; diff --git a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy index 532e3ca1608..4157c9a646f 100644 --- a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy +++ b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy @@ -75,6 +75,7 @@ class AbstractEvaluatedExpressionsSpec extends AbstractBeanDefinitionSpec { def cls = """ package test import io.micronaut.context.annotation.Value + import jakarta.inject.Singleton import io.micronaut.context.annotation.EvaluatedExpressionContext ${contextClass} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy index 82156a199ae..a7e53946c33 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy @@ -9,7 +9,9 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions given: Object result = evaluateSingle("test.Expr", """ package test - import io.micronaut.context.annotation.EvaluatedExpressionContext;import io.micronaut.context.annotation.Executable; + + import io.micronaut.context.annotation.AnnotationExpressionContext + import io.micronaut.context.annotation.Executable; import io.micronaut.context.annotation.Requires; import jakarta.inject.Singleton @@ -25,7 +27,7 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions } } - @EvaluatedExpressionContext(CustomContext.class) + @AnnotationExpressionContext(CustomContext.class) @interface CustomAnnotation { String value(); } @@ -41,7 +43,8 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions given: Object result = evaluateSingle("test.Expr", """ package test - import io.micronaut.context.annotation.EvaluatedExpressionContext + + import io.micronaut.context.annotation.AnnotationExpressionContext import io.micronaut.context.annotation.Executable import io.micronaut.context.annotation.Requires import jakarta.inject.Singleton @@ -59,7 +62,7 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions } @interface CustomAnnotation { - @EvaluatedExpressionContext(CustomContext.class) + @AnnotationExpressionContext(CustomContext.class) String customValue(); } diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy index 7c3a857ed5a..f8f85658a06 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/AnnotationLevelContextExpressionsSpec.groovy @@ -9,7 +9,8 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions Object result = evaluateSingle("test.Expr", """ package test; - import io.micronaut.context.annotation.EvaluatedExpressionContext;import io.micronaut.context.annotation.Executable; + import io.micronaut.context.annotation.AnnotationExpressionContext; + import io.micronaut.context.annotation.Executable; import io.micronaut.context.annotation.Requires; import jakarta.inject.Singleton; @@ -25,7 +26,7 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions } } - @EvaluatedExpressionContext(CustomContext.class) + @AnnotationExpressionContext(CustomContext.class) @interface CustomAnnotation { String value(); } @@ -42,7 +43,8 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions Object result = evaluateSingle("test.Expr", """ package test; - import io.micronaut.context.annotation.EvaluatedExpressionContext;import io.micronaut.context.annotation.Executable; + import io.micronaut.context.annotation.AnnotationExpressionContext; + import io.micronaut.context.annotation.Executable; import io.micronaut.context.annotation.Requires; import jakarta.inject.Singleton; @@ -59,7 +61,7 @@ class AnnotationLevelContextExpressionsSpec extends AbstractEvaluatedExpressions } @interface CustomAnnotation { - @EvaluatedExpressionContext(CustomContext.class) + @AnnotationExpressionContext(CustomContext.class) String customValue(); } diff --git a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java index 68e5e04ac2d..ecb96501661 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractExecutableMethodsDefinition.java @@ -29,13 +29,12 @@ import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.ExecutableMethodsDefinition; import io.micronaut.inject.annotation.AbstractEnvironmentAnnotationMetadata; -import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; +import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -327,9 +326,9 @@ public String toString() { * @param The type * @param The result type */ - private static final class DispatchedExecutableMethod implements ExecutableMethod, ReturnType, - EnvironmentConfigurable, ContextConfigurable { - private static final class DispatchedExecutableMethod implements ExecutableMethod, EnvironmentConfigurable { + private static final class DispatchedExecutableMethod implements ExecutableMethod, + EnvironmentConfigurable, + ContextConfigurable { private final AbstractExecutableMethodsDefinition dispatcher; private final int index; diff --git a/inject/src/main/java/io/micronaut/context/annotation/AnnotationExpressionContext.java b/inject/src/main/java/io/micronaut/context/annotation/AnnotationExpressionContext.java new file mode 100644 index 00000000000..60dd9228b49 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/annotation/AnnotationExpressionContext.java @@ -0,0 +1,37 @@ +/* + * 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.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A meta annotation used to extend {@link io.micronaut.core.expressions.EvaluatedExpression} + * context with specified type. Being an expression context means that expressions can reference + * methods and properties of this object directly with # prefix. The difference between this + * annotation and {@link EvaluatedExpressionContext} is that this one allows to specify context + * that will only be scoped to this concrete annotation or annotation member. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface AnnotationExpressionContext { + Class value(); +} diff --git a/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java b/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java index 17ab04e3d6d..20017228970 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java +++ b/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java @@ -32,8 +32,7 @@ * @since 4.0.0 */ @DefaultScope(Singleton.class) -@Target({ElementType.TYPE, ElementType.METHOD}) +@Target({ElementType.TYPE}) @Retention(RetentionPolicy.CLASS) public @interface EvaluatedExpressionContext { - Class value() default void.class; } diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index c95c16f5762..a12d097e962 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -8,7 +8,7 @@ allows to achieve even more flexibility while configuring your application. double injectedValue; ---- -Evaluated expressions can be used wherever annotation value accept string (within array of strings as well). +Expressions can be defined whenever an annotation member accepts a string or an array of strings. .Evaluated Expression in array [source,java] @@ -18,7 +18,7 @@ Evaluated expressions can be used wherever annotation value accept string (withi public class EvaluatedExpressionInArray {} ---- -You can also embed one or more expressions in string template in a way similar to embedding properties with `${...}` syntax. +You can also embed one or more expressions in a string template in a similar manner to embedding properties with the `${...}` syntax. .Evaluated Expression template [source,groovy] @@ -27,16 +27,14 @@ You can also embed one or more expressions in string template in a way similar t String url; ---- -Evaluated Expressions are validated and compiled at build time which guarantees type safety at runtime. Once application -is running expressions are evaluated on demand as part of annotation metadata resolution. The usage of expressions does -not affect performance as evaluation process is completely reflection free. +Evaluated Expressions are validated and compiled at build time which guarantees type safety at runtime. +Once an application is running expressions are evaluated on demand as part of annotation metadata resolution. The +usage of expressions does not impact performance as evaluation process is completely reflection free. +Note that, for security reasons expressions cannot be dynamically compiled at runtime from potentially untrusted +input. All expressions are compiled and checked statically during the compilation process of the application with +errors reported as compilation failures. -In general, Evaluated Expression can be treated as statement written using a programming language with reduced -set of available features. Even though the complexity of expression is only limited by the list of supported syntax -constructs, it is in general not recommended to place complex logic inside it as there are usually better ways to -achieve the same result. - -== Evaluated Expression language reference +== Evaluated Expression Language Reference The Evaluated Expressions syntax supports the following functionality: @@ -170,15 +168,15 @@ limited. === Type References -Predefined syntax construct `T(...)` can be used to reference a class. The value inside brackets should be fully -qualified class name (including package). The only exception is `java.lang.*` classes which can be referenced -directly by only specifying simple class name. Primitive types can not be specified inside brackets. +A predefined syntax construct `T(...)` can be used to reference a class. The value inside brackets should be fully +qualified class name (including the package name). The only exception is `java.lang.*` classes which can be referenced +directly by only specifying the simple class name. Primitive types can not be referenced. Type References are evaluated in different ways depending on the context. ==== Simple type reference -Simple type reference is resolved as `Class` object. +A simple type reference is resolved as a `Class` object. .Type reference example [source] @@ -230,6 +228,7 @@ Consider the following example: [source, java] ---- import io.micronaut.context.annotation.EvaluatedExpressionContext; +import jakarta.inject.Singleton; import java.util.Random; @EvaluatedExpressionContext @@ -260,20 +259,58 @@ public class ContextConsumer { ---- Note that annotating a class with `@EvaluatedExpressionContext` makes it a bean. By default, it will be treated as -singleton, but you can specify other scope for it. At runtime, the bean will be retrieved from application context -and respective method will be invoked. +singleton, but you can specify an alternative scope if required. At runtime, the bean will be retrieved from +application context and respective method will be invoked. -if matching method is not found within evaluation context at compilation time, the compilation will fail. Same applies -in case of multiple suitable methods are found in evaluation context, keep that in mind if you annotate multiple classes -with `@EvaluatedExpressionContext`. +If a matching method is not found within evaluation context at compilation time, the compilation will fail. A +compilation error will also occur if multiple suitable methods are found in the evaluation context, keep that in mind +if you annotate multiple classes with `@EvaluatedExpressionContext`. The methods will be considered ambiguous (leading to compilation failure) when their names are the same and list of provided arguments matches multiple methods parameters. -If the class annotated with `@EvaluatedExpressionContext` resides within separate JAR which is added as a dependency -to your project, it has to be added to the `annotationProcessor` classpath to become available at annotation processing -stage. That's why it is generally preferable to place separate contexts in projects that include few external dependencies -to avoid polluting the annotation processor classpath. +The class annotated with `@EvaluatedExpressionContext` needs to reside within the same module as the classes referencing +its methods or properties. + +Annotating a class with `@EvaluatedExpressionContext` makes its methods and properties available for evaluated +expressions within any annotation. However, you can also specify evaluation context scoped to concrete annotation or +annotation member using `@AnnotationExpressionContext`. + +.Usage of annotation level evaluated expression context +[source, java] +---- +import jakarta.inject.Singleton; +import io.micronaut.context.annotation.AnnotationExpressionContext; + +@Singleton +@CustomAnnotation(value = "#{ #firstValue() + #secondValue() }") +class Expr { +} + +@Singleton +class AnnotationContext { + String firstValue() { + return "fist value"; + } +} + +@Singleton +class AnnotationMemberContext { + String secondValue() { + return "second value"; + } +} + +@AnnotationExpressionContext(AnnotationContext.class) +@interface CustomAnnotation { + + @AnnotationExpressionContext(AnnotationMemberContext.class) + String value(); +} +---- + +In this case the context classes need to be explicitly defined as beans to make them available for retrieval from +application context at runtime. === Method Invocation From b0c91799860cebbf224ad4d41a4332e73aa57a44 Mon Sep 17 00:00:00 2001 From: Sergey Gavrilov Date: Thu, 9 Mar 2023 15:23:06 +0300 Subject: [PATCH 06/35] docs improvements --- .../docs/guide/config/evaluatedExpressions.adoc | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index a12d097e962..6a3dd12bbb1 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -28,12 +28,19 @@ String url; ---- Evaluated Expressions are validated and compiled at build time which guarantees type safety at runtime. + Once an application is running expressions are evaluated on demand as part of annotation metadata resolution. The usage of expressions does not impact performance as evaluation process is completely reflection free. + Note that, for security reasons expressions cannot be dynamically compiled at runtime from potentially untrusted input. All expressions are compiled and checked statically during the compilation process of the application with errors reported as compilation failures. +In general, Evaluated Expression can be treated as statement written using a programming language with reduced +set of available features. Even though the complexity of expression is only limited by the list of supported syntax +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. + == Evaluated Expression Language Reference The Evaluated Expressions syntax supports the following functionality: @@ -184,11 +191,11 @@ A simple type reference is resolved as a `Class` object. #{ T(java.lang.String) } // String.class ---- -Same rule applies if type reference is specified as method argument. +Same rule applies if type reference is specified as a method argument. ==== Type check with `instanceof` -Type Reference can be used as right-hand side part of `instanceof` operator +A Type Reference can be used as the right-hand side part of the `instanceof` operator .Type check example [source] @@ -196,7 +203,7 @@ Type Reference can be used as right-hand side part of `instanceof` operator #{ 'abc' instanceof T(String) } // true ---- -which is equivalent to the following Java code and will be evaluated as boolean value: +which is equivalent to the following Java code and will be evaluated as a boolean value: [source] ---- @@ -205,7 +212,7 @@ which is equivalent to the following Java code and will be evaluated as boolean ==== Static method invocation -Type Reference can be used to invoke static method of a class +Type Reference can be used to invoke a static method of a class .Static method invocation [source] From 6686a2d333bc495155b7b912b806f2857b0ba8ac Mon Sep 17 00:00:00 2001 From: Sergey Gavrilov Date: Thu, 9 Mar 2023 16:57:41 +0300 Subject: [PATCH 07/35] checkstyle fixes --- .../context/ExpressionCompilationContextRegistry.java | 1 - .../ast/access/ContextMethodParameterAccess.java | 1 - .../expressions/parser/ast/util/TypeDescriptors.java | 1 - .../parser/compilation/ExpressionVisitorContext.java | 10 ++++++---- .../expressions/OperatorExpressionSpec.groovy | 1 - 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextRegistry.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextRegistry.java index 8f9b8d4c301..5f6158e8974 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextRegistry.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextRegistry.java @@ -21,7 +21,6 @@ import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; /** * This class is responsible for assembling expression evaluation context diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java index b25900975c4..daf2647eb47 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java @@ -27,7 +27,6 @@ import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.EVALUATION_CONTEXT_TYPE; import static io.micronaut.inject.processing.JavaModelUtils.getTypeReference; -import static org.objectweb.asm.Opcodes.AALOAD; /** * Expression AST node used for context method parameter access. diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java index fe2df05db22..80df94db8a2 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/util/TypeDescriptors.java @@ -15,7 +15,6 @@ */ package io.micronaut.expressions.parser.ast.util; -import io.micronaut.context.expressions.AbstractEvaluatedExpression; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.expressions.ExpressionEvaluationContext; diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionVisitorContext.java b/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionVisitorContext.java index ad00bccd60f..fefcc32d70b 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionVisitorContext.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/compilation/ExpressionVisitorContext.java @@ -16,6 +16,7 @@ package io.micronaut.expressions.parser.compilation; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; import io.micronaut.expressions.context.ExpressionCompilationContext; import io.micronaut.inject.visitor.VisitorContext; import org.objectweb.asm.commons.GeneratorAdapter; @@ -23,7 +24,7 @@ /** * Context class used for compiling expressions. * - * @param compilationContext expression evaluation context + * @param compilationContext expression compilation context * @param visitorContext visitor context * @param methodVisitor method visitor for compiled expression class * @@ -31,6 +32,7 @@ * @since 4.0.0 */ @Internal -public record ExpressionVisitorContext(ExpressionCompilationContext compilationContext, - VisitorContext visitorContext, - GeneratorAdapter methodVisitor) {} +public record ExpressionVisitorContext(@NonNull ExpressionCompilationContext compilationContext, + @NonNull VisitorContext visitorContext, + @NonNull GeneratorAdapter methodVisitor) { +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy index ab0fffd64f0..a76b33eb8ac 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy @@ -1,7 +1,6 @@ package io.micronaut.expressions import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec -import org.codehaus.groovy.control.CompilationFailedException class OperatorExpressionSpec extends AbstractEvaluatedExpressionsSpec { From 7afe5979462dbf62ae644af03edb549649e1ef4b Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Sun, 12 Mar 2023 17:11:03 +0100 Subject: [PATCH 08/35] fix merge --- .../GroovyAnnotationMetadataBuilder.java | 37 +------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java index 35f7054c9b8..184b263579d 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/annotation/GroovyAnnotationMetadataBuilder.java @@ -401,42 +401,7 @@ protected void readAnnotationRawValues( } @Override - protected boolean isInheritedAnnotation(@NonNull AnnotationNode annotationMirror) { - final List annotations = annotationMirror.getClassNode().getAnnotations(); - if (CollectionUtils.isNotEmpty(annotations)) { - return annotations.stream().anyMatch((ann) -> - ann.getClassNode().getName().equals(Inherited.class.getName()) - ); - } - return false; - } - - @Override - protected Map getAnnotationMembers(String annotationType) { - final AnnotatedNode node = getAnnotationMirror(annotationType).orElse(null); - if (node instanceof final ClassNode cn) { - if (cn.isAnnotationDefinition()) { - return cn.getDeclaredMethodsMap(); - } - } - return Collections.emptyMap(); - } - - @Override - protected boolean hasSimpleAnnotation(AnnotatedNode element, String simpleName) { - if (element != null) { - final List annotations = element.getAnnotations(); - for (AnnotationNode ann : annotations) { - if (ann.getClassNode().getNameWithoutPackage().equalsIgnoreCase(simpleName)) { - return true; - } - } - } - return false; - } - - @Override - protected Object readAnnotationValue(AnnotatedNode originatingElement, AnnotatedNode member, String memberName, Object annotationValue) { + protected Object readAnnotationValue(AnnotatedNode originatingElement, AnnotatedNode member, String annotationName, String memberName, Object annotationValue) { if (annotationValue instanceof ConstantExpression constantExpression) { return readConstantExpression(originatingElement, annotationName, member, constantExpression); } else if (annotationValue instanceof PropertyExpression pe) { From 1fdbc7ed7f100cbb44d98b7592613c1f2fde0c9c Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 17 Mar 2023 15:17:06 -0400 Subject: [PATCH 09/35] Cleanup / remove ExpressionEvaluationContext annotation --- .../DefaultExpressionCompilationContext.java | 4 +- ...ltExpressionCompilationContextFactory.java | 132 ++++++++++++++++++ .../ExpressionCompilationContextFactory.java | 109 +++------------ .../ExpressionCompilationContextRegistry.java | 64 --------- .../ExpressionEvaluationContextRegistrar.java | 51 +++++++ ...tensibleExpressionCompilationContext.java} | 6 +- ...edExpressionContextTypeElementVisitor.java | 60 -------- .../inject/visitor/VisitorContext.java | 11 +- .../inject/writer/BeanDefinitionVisitor.java | 3 + .../inject/writer/BeanDefinitionWriter.java | 18 ++- .../ExecutableMethodsDefinitionWriter.java | 6 +- ...icronaut.inject.visitor.TypeElementVisitor | 1 - .../AbstractEvaluatedExpressionsSpec.groovy | 2 - .../ast/groovy/TypeElementVisitorEnd.groovy | 3 +- .../ast/groovy/TypeElementVisitorStart.groovy | 3 +- .../groovy/visitor/GroovyVisitorContext.java | 9 ++ .../ArrayMethodsExpressionsSpec.groovy | 28 ++-- .../ContextMethodCallsExpressionsSpec.groovy | 39 +++--- ...ontextPropertyAccessExpressionsSpec.groovy | 16 +-- .../expressions/ContextRegistrar.groovy | 27 ++++ .../TestExpressionsInjectionSpec.groovy | 5 +- .../TestExpressionsUsageSpec.groovy | 10 +- .../TypeIdentifierExpressionsSpec.groovy | 4 +- ...icronaut.inject.visitor.TypeElementVisitor | 1 + .../AbstractEvaluatedExpressionsSpec.groovy | 33 ++++- .../BeanDefinitionInjectProcessor.java | 6 +- .../TypeElementVisitorProcessor.java | 3 - .../visitor/JavaVisitorContext.java | 9 ++ .../ArrayMethodsExpressionsSpec.groovy | 28 ++-- .../ContextMethodCallsExpressionsSpec.groovy | 39 +++--- ...ontextPropertyAccessExpressionsSpec.groovy | 16 +-- .../TestExpressionsInjectionSpec.groovy | 5 +- .../TestExpressionsUsageSpec.groovy | 5 +- .../TypeIdentifierExpressionsSpec.groovy | 4 +- .../beans/BeanDefinitionProcessor.kt | 3 +- .../visitor/BeanIntrospectionSpec.groovy | 3 - .../AnnotationExpressionContext.java | 16 ++- .../EvaluatedExpressionContext.java | 38 ----- .../guide/config/evaluatedExpressions.adoc | 60 ++++---- 39 files changed, 475 insertions(+), 405 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java delete mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextRegistry.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContextRegistrar.java rename core-processor/src/main/java/io/micronaut/expressions/context/{ExtendableExpressionCompilationContext.java => ExtensibleExpressionCompilationContext.java} (89%) delete mode 100644 core-processor/src/main/java/io/micronaut/inject/beans/visitor/EvaluatedExpressionContextTypeElementVisitor.java create mode 100644 inject-groovy/src/test/groovy/io/micronaut/expressions/ContextRegistrar.groovy delete mode 100644 inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java index 13070dc3e16..36bc8c254e2 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContext.java @@ -35,14 +35,14 @@ import static java.util.function.Predicate.not; /** - * Default implementation of {@link ExtendableExpressionCompilationContext}. Extending + * Default implementation of {@link ExtensibleExpressionCompilationContext}. Extending * this context will always return new instance instead of modifying the existing one. * * @since 4.0.0 * @author Sergey Gavrilov */ @Internal -public class DefaultExpressionCompilationContext implements ExtendableExpressionCompilationContext { +public class DefaultExpressionCompilationContext implements ExtensibleExpressionCompilationContext { private final Collection classElements; private final MethodElement methodElement; diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java new file mode 100644 index 00000000000..ac144b2bda8 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java @@ -0,0 +1,132 @@ +/* + * 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.expressions.context; + +import io.micronaut.context.annotation.AnnotationExpressionContext; +import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.expressions.EvaluatedExpressionReference; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.ElementQuery; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.visitor.VisitorContext; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Factory for producing expression evaluation context. + * + * @author Sergey Gavrilov + * @since 4.0.0 + */ +@Internal +public final class DefaultExpressionCompilationContextFactory implements ExpressionCompilationContextFactory { + + private ExtensibleExpressionCompilationContext sharedContext; + private final VisitorContext visitorContext; + + private static final Collection CONTEXT_TYPES = ConcurrentHashMap.newKeySet(); + + public DefaultExpressionCompilationContextFactory(VisitorContext visitorContext) { + this.sharedContext = recreateContext(); + this.visitorContext = visitorContext; + } + + @NotNull + private DefaultExpressionCompilationContext recreateContext() { + return new DefaultExpressionCompilationContext(CONTEXT_TYPES.toArray(ClassElement[]::new)); + } + + @Override + @NonNull + public ExpressionCompilationContext buildContextForMethod(@NonNull EvaluatedExpressionReference expression, + @NonNull MethodElement methodElement) { + return buildForExpression(expression) + .extendWith(methodElement); + } + + @Override + @NonNull + public ExpressionCompilationContext buildContext(EvaluatedExpressionReference expression) { + return buildForExpression(expression); + } + + @Override + public ExpressionCompilationContextFactory registerContextClass(ClassElement contextClass) { + CONTEXT_TYPES.add(contextClass); + this.sharedContext = recreateContext(); + return this; + } + + private ExtensibleExpressionCompilationContext buildForExpression(EvaluatedExpressionReference expression) { + String annotationName = expression.annotationName(); + String memberName = expression.annotationMember(); + + ClassElement annotation = visitorContext.getClassElement(annotationName).orElse(null); + + ExtensibleExpressionCompilationContext evaluationContext = sharedContext; + if (annotation != null) { + evaluationContext = addAnnotationEvaluationContext(evaluationContext, annotation); + evaluationContext = addAnnotationMemberEvaluationContext(evaluationContext, annotation, memberName); + } + + return evaluationContext; + } + + private ExtensibleExpressionCompilationContext addAnnotationEvaluationContext( + ExtensibleExpressionCompilationContext currentEvaluationContext, + ClassElement annotation) { + + return annotation.findAnnotation(AnnotationExpressionContext.class) + .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER)) + .map(AnnotationClassValue::getName) + .flatMap(visitorContext::getClassElement) + .map(currentEvaluationContext::extendWith) + .orElse(currentEvaluationContext); + } + + private ExtensibleExpressionCompilationContext addAnnotationMemberEvaluationContext( + ExtensibleExpressionCompilationContext currentEvaluationContext, + ClassElement annotation, + String annotationMember) { + + ElementQuery memberQuery = + ElementQuery.ALL_METHODS + .onlyDeclared() + .annotated(am -> am.hasAnnotation(AnnotationExpressionContext.class)) + .named(annotationMember); + + return annotation.getEnclosedElements(memberQuery).stream() + .flatMap(element -> Optional.ofNullable(element.getDeclaredAnnotation(AnnotationExpressionContext.class)).stream()) + .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER).stream()) + .map(AnnotationClassValue::getName) + .flatMap(className -> visitorContext.getClassElement(className).stream()) + .reduce(currentEvaluationContext, ExtensibleExpressionCompilationContext::extendWith, (a, b) -> a); + } + + /** + * cleanup any stored contexts. + */ + @Internal + public static void reset() { + CONTEXT_TYPES.clear(); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java index fe9a0816e93..85cccb4429c 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java @@ -1,121 +1,50 @@ -/* - * 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.expressions.context; -import io.micronaut.context.annotation.AnnotationExpressionContext; -import io.micronaut.core.annotation.AnnotationClassValue; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.expressions.EvaluatedExpressionReference; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Internal; import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.visitor.VisitorContext; -import java.util.Optional; +import java.io.Closeable; +import java.io.IOException; /** - * Factory for producing expression evaluation context. - * - * @author Sergey Gavrilov - * @since 4.0.0 + * Factory interface for producing expression evaluation context. */ -@Internal -public final class ExpressionCompilationContextFactory { - - private final ExtendableExpressionCompilationContext sharedContext; - private final VisitorContext visitorContext; - - public ExpressionCompilationContextFactory(VisitorContext visitorContext) { - this.sharedContext = ExpressionCompilationContextRegistry.getSharedContext(); - this.visitorContext = visitorContext; - } - +@Experimental +public interface ExpressionCompilationContextFactory { /** * Builds expression evaluation context for method. Expression evaluation context * for method allows referencing method parameter names in evaluated expressions. * * @param expression expression reference * @param methodElement annotated method - * * @return evaluation context for method */ @NonNull - public ExpressionCompilationContext buildContextForMethod(@NonNull EvaluatedExpressionReference expression, - @NonNull MethodElement methodElement) { - return buildForExpression(expression) - .extendWith(methodElement); - } + ExpressionCompilationContext buildContextForMethod(@NonNull EvaluatedExpressionReference expression, + @NonNull MethodElement methodElement); /** * Builds expression evaluation context for expression reference. * * @param expression expression reference - * * @return evaluation context for method */ @NonNull - public ExpressionCompilationContext buildContext(EvaluatedExpressionReference expression) { - return buildForExpression(expression); - } - - private ExtendableExpressionCompilationContext buildForExpression(EvaluatedExpressionReference expression) { - String annotationName = expression.annotationName(); - String memberName = expression.annotationMember(); - - ClassElement annotation = visitorContext.getClassElement(annotationName).orElse(null); + ExpressionCompilationContext buildContext(EvaluatedExpressionReference expression); - ExtendableExpressionCompilationContext evaluationContext = sharedContext; - if (annotation != null) { - evaluationContext = addAnnotationEvaluationContext(evaluationContext, annotation); - evaluationContext = addAnnotationMemberEvaluationContext(evaluationContext, annotation, memberName); - } - - return evaluationContext; - } - - private ExtendableExpressionCompilationContext addAnnotationEvaluationContext( - ExtendableExpressionCompilationContext currentEvaluationContext, - ClassElement annotation) { - - return Optional.ofNullable(annotation.getAnnotation(AnnotationExpressionContext.class)) - .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER)) - .map(AnnotationClassValue::getName) - .flatMap(visitorContext::getClassElement) - .map(currentEvaluationContext::extendWith) - .orElse(currentEvaluationContext); - } - - private ExtendableExpressionCompilationContext addAnnotationMemberEvaluationContext( - ExtendableExpressionCompilationContext currentEvaluationContext, - ClassElement annotation, - String annotationMember) { - - ElementQuery memberQuery = - ElementQuery.ALL_METHODS - .onlyDeclared() - .annotated(am -> am.hasAnnotation(AnnotationExpressionContext.class)) - .named(annotationMember); + /** + * Adds evaluated expression context class element to context loader + * at compilation time. + * + *

This method should be invoked from the {@link io.micronaut.inject.visitor.TypeElementVisitor#start(VisitorContext)} of a {@link io.micronaut.inject.visitor.TypeElementVisitor}

+ * + * @param contextClass context class element + */ + ExpressionCompilationContextFactory registerContextClass(@NonNull ClassElement contextClass); - return annotation.getEnclosedElements(memberQuery).stream() - .flatMap(element -> Optional.ofNullable(element.getDeclaredAnnotation(AnnotationExpressionContext.class)).stream()) - .flatMap(av -> av.annotationClassValue(AnnotationMetadata.VALUE_MEMBER).stream()) - .map(AnnotationClassValue::getName) - .flatMap(className -> visitorContext.getClassElement(className).stream()) - .reduce(currentEvaluationContext, ExtendableExpressionCompilationContext::extendWith, (a, b) -> a); - } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextRegistry.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextRegistry.java deleted file mode 100644 index 5f6158e8974..00000000000 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextRegistry.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.expressions.context; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.inject.ast.ClassElement; - -import java.util.Collection; -import java.util.concurrent.ConcurrentHashMap; - -/** - * This class is responsible for assembling expression evaluation context - * from classes annotated with {@link io.micronaut.context.annotation.EvaluatedExpressionContext}. - * The assembled context is considered as shared because elements from this context can - * be referenced in any expression compiled within the same module. It can later be extended - * by annotation level, annotation member level context classes or method element. - * - * @since 4.0.0 - * @author Sergey Gavrilov - */ -@Internal -public final class ExpressionCompilationContextRegistry { - - private static Collection contextTypes = ConcurrentHashMap.newKeySet(); - - /** - * Adds evaluated expression context class element to context loader - * at compilation time. - * - * @param contextClass context class element - */ - public static void registerContextClass(@NonNull ClassElement contextClass) { - contextTypes.add(contextClass); - } - - /** - * Resets expression evaluation context. - */ - public static void reset() { - contextTypes.clear(); - } - - /** - * @return shared expression evaluation context. - */ - @NonNull - static ExtendableExpressionCompilationContext getSharedContext() { - return new DefaultExpressionCompilationContext(contextTypes.toArray(ClassElement[]::new)); - } -} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContextRegistrar.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContextRegistrar.java new file mode 100644 index 00000000000..66ed5febb15 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionEvaluationContextRegistrar.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.expressions.context; + +import io.micronaut.context.annotation.AnnotationExpressionContext; +import io.micronaut.core.annotation.Experimental; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.visitor.TypeElementVisitor; +import io.micronaut.inject.visitor.VisitorContext; + +/** + * Custom type that simplifies registering a new context class. + * + *

A {@code META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor} should be created for any new implementations.

+ * + * @since 4.0.0 + */ +@Experimental +public interface ExpressionEvaluationContextRegistrar extends TypeElementVisitor { + @Override + default void start(VisitorContext visitorContext) { + ClassElement contextClass = visitorContext.getClassElement(getContextClassName()) + .orElse(null); + if (contextClass == null) { + visitorContext.fail("Evaluation context class is not on the compilation classpath: " + getContextClassName(), null); + } else { + visitorContext.getExpressionCompilationContextFactory() + .registerContextClass(contextClass); + } + } + + String getContextClassName(); + + @Override + default VisitorKind getVisitorKind() { + return VisitorKind.ISOLATING; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExtendableExpressionCompilationContext.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExtensibleExpressionCompilationContext.java similarity index 89% rename from core-processor/src/main/java/io/micronaut/expressions/context/ExtendableExpressionCompilationContext.java rename to core-processor/src/main/java/io/micronaut/expressions/context/ExtensibleExpressionCompilationContext.java index 87a1e3f2007..59ce4a0ab90 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExtendableExpressionCompilationContext.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExtensibleExpressionCompilationContext.java @@ -27,7 +27,7 @@ * @author Sergey Gavrilov */ @Internal -public interface ExtendableExpressionCompilationContext extends ExpressionCompilationContext { +public interface ExtensibleExpressionCompilationContext extends ExpressionCompilationContext { /** * Extends compilation context with method element. Compilation context can only include * one method at the same time, so this method will return the context which will @@ -37,7 +37,7 @@ public interface ExtendableExpressionCompilationContext extends ExpressionCompil * @return extended context */ @NonNull - ExtendableExpressionCompilationContext extendWith(@NonNull MethodElement methodElement); + ExtensibleExpressionCompilationContext extendWith(@NonNull MethodElement methodElement); /** * Extends compilation context with class element. Compilation context can include @@ -47,6 +47,6 @@ public interface ExtendableExpressionCompilationContext extends ExpressionCompil * @return extended context */ @NonNull - ExtendableExpressionCompilationContext extendWith(@NonNull ClassElement classElement); + ExtensibleExpressionCompilationContext extendWith(@NonNull ClassElement classElement); } diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EvaluatedExpressionContextTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EvaluatedExpressionContextTypeElementVisitor.java deleted file mode 100644 index 96942b29009..00000000000 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/EvaluatedExpressionContextTypeElementVisitor.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.inject.beans.visitor; - -import io.micronaut.context.annotation.EvaluatedExpressionContext; -import io.micronaut.core.annotation.Internal; -import io.micronaut.expressions.context.ExpressionCompilationContextRegistry; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.visitor.TypeElementVisitor; -import io.micronaut.inject.visitor.VisitorContext; - -import java.util.Collections; -import java.util.Set; - -/** - * A {@link TypeElementVisitor} that visits classes annotated with - * {@link EvaluatedExpressionContext} and adds discovered context classes to - * {@link ExpressionCompilationContextRegistry}. - * - * @author Sergey Gavrilov - * @since 4.0.0 - */ -@Internal -public final class EvaluatedExpressionContextTypeElementVisitor implements TypeElementVisitor { - - @Override - public void start(VisitorContext visitorContext) { - ExpressionCompilationContextRegistry.reset(); - } - - @Override - public Set getSupportedAnnotationNames() { - return Collections.singleton(EvaluatedExpressionContext.class.getName()); - } - - @Override - public void visitClass(ClassElement element, VisitorContext context) { - if (!element.isPrivate() && element.hasStereotype(EvaluatedExpressionContext.class)) { - ExpressionCompilationContextRegistry.registerContextClass(element); - } - } - - @Override - public VisitorKind getVisitorKind() { - return VisitorKind.ISOLATING; - } -} diff --git a/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java b/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java index dca9b75d233..f8a3122c960 100644 --- a/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java +++ b/core-processor/src/main/java/io/micronaut/inject/visitor/VisitorContext.java @@ -20,6 +20,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.value.MutableConvertibleValues; +import io.micronaut.expressions.context.ExpressionCompilationContextFactory; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; @@ -64,11 +65,19 @@ public interface VisitorContext extends MutableConvertibleValues, ClassW * Gets the element annotation metadata factory. * * @return The element annotation metadata factory - * @see 4.0.0 + * @since 4.0.0 */ @NonNull ElementAnnotationMetadataFactory getElementAnnotationMetadataFactory(); + /** + * @return The expression compilation context factory. + * @since 4.0.0 + */ + @Experimental + @NonNull + ExpressionCompilationContextFactory getExpressionCompilationContextFactory(); + /** * Gets the annotation metadata builder. * diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java index 4244eaca666..9c62c069b32 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java @@ -16,9 +16,12 @@ package io.micronaut.inject.writer; import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.util.Toggleable; +import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.FieldElement; diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index b3a253be1f7..64d6b3185ef 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -66,7 +66,7 @@ import io.micronaut.core.util.Toggleable; import io.micronaut.expressions.context.ExpressionCompilationContext; import io.micronaut.expressions.context.ExpressionWithContext; -import io.micronaut.expressions.context.ExpressionCompilationContextFactory; +import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; import io.micronaut.expressions.util.EvaluatedExpressionsUtils; import io.micronaut.inject.AdvisedBeanType; import io.micronaut.inject.BeanDefinition; @@ -78,6 +78,7 @@ import io.micronaut.inject.ParametrizedInstantiatableBeanDefinition; import io.micronaut.inject.ProxyBeanDefinition; import io.micronaut.inject.ValidatedBeanDefinition; +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.AnnotationMetadataWriter; import io.micronaut.inject.annotation.MutableAnnotationMetadata; @@ -581,7 +582,7 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea private ExecutableMethodsDefinitionWriter executableMethodsDefinitionWriter; private final Collection evaluatedExpressions = new ArrayList<>(2); - private final ExpressionCompilationContextFactory expressionCompilationContextFactory; + private final DefaultExpressionCompilationContextFactory expressionCompilationContextFactory; private Object constructor; // MethodElement or FieldElement private boolean disabled = false; @@ -717,7 +718,7 @@ public BeanDefinitionWriter(Element beanProducingElement, this.isConfigurationProperties = isConfigurationProperties(annotationMetadata); validateExposedTypes(annotationMetadata, visitorContext); this.visitorContext = visitorContext; - this.expressionCompilationContextFactory = new ExpressionCompilationContextFactory(visitorContext); + this.expressionCompilationContextFactory = new DefaultExpressionCompilationContextFactory(visitorContext); processEvaluatedExpressions(this.annotationMetadata); beanTypeInnerClasses = beanTypeElement.getEnclosedElements(ElementQuery.of(ClassElement.class)) @@ -4633,6 +4634,17 @@ public boolean isProxiedBean() { return proxiedBean; } + + /** + * Finish any work writing beans. + */ + @Internal + public static void finish() { + AbstractAnnotationMetadataBuilder.clearMutated(); + AbstractAnnotationMetadataBuilder.clearCaches(); + DefaultExpressionCompilationContextFactory.reset(); + } + @Internal private static final class AnnotationVisitData { final TypedElement memberBeanType; diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java index dc942b3b7a5..52852719aba 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/ExecutableMethodsDefinitionWriter.java @@ -21,7 +21,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; -import io.micronaut.expressions.context.ExpressionCompilationContextFactory; +import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; import io.micronaut.expressions.context.ExpressionCompilationContext; import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.expressions.util.EvaluatedExpressionsUtils; @@ -104,7 +104,7 @@ public class ExecutableMethodsDefinitionWriter extends AbstractClassFileWriter i private final DispatchWriter methodDispatchWriter; private final Set methodNames = new HashSet<>(); - private final ExpressionCompilationContextFactory expressionCompilationContextFactory; + private final DefaultExpressionCompilationContextFactory expressionCompilationContextFactory; private final Set evaluatedExpressions = new HashSet<>(); private final AnnotationMetadata annotationMetadataWithDefaults; private ClassWriter classWriter; @@ -121,7 +121,7 @@ public ExecutableMethodsDefinitionWriter(VisitorContext visitorContext, this.thisType = Type.getObjectType(internalName); this.beanDefinitionReferenceClassName = beanDefinitionReferenceClassName; this.methodDispatchWriter = new DispatchWriter(thisType); - this.expressionCompilationContextFactory = new ExpressionCompilationContextFactory(visitorContext); + this.expressionCompilationContextFactory = new DefaultExpressionCompilationContextFactory(visitorContext); } /** diff --git a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index 64070596a7c..46ee8919429 100644 --- a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -1,5 +1,4 @@ io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor -io.micronaut.inject.beans.visitor.EvaluatedExpressionContextTypeElementVisitor io.micronaut.context.visitor.BeanImportVisitor io.micronaut.context.visitor.ContextConfigurerVisitor io.micronaut.context.visitor.ExecutableVisitor diff --git a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy index 4157c9a646f..0e57a80a62d 100644 --- a/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy +++ b/inject-groovy-test/src/main/groovy/io/micronaut/ast/transform/test/AbstractEvaluatedExpressionsSpec.groovy @@ -38,7 +38,6 @@ class AbstractEvaluatedExpressionsSpec extends AbstractBeanDefinitionSpec { def cls = """ package test import io.micronaut.context.annotation.Value - import io.micronaut.context.annotation.EvaluatedExpressionContext class Expr { ${classContent} @@ -76,7 +75,6 @@ class AbstractEvaluatedExpressionsSpec extends AbstractBeanDefinitionSpec { package test import io.micronaut.context.annotation.Value import jakarta.inject.Singleton - import io.micronaut.context.annotation.EvaluatedExpressionContext ${contextClass} diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorEnd.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorEnd.groovy index 25953bdc5a9..08b42bdc9dc 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorEnd.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorEnd.groovy @@ -25,6 +25,7 @@ import io.micronaut.ast.groovy.visitor.LoadedVisitor import io.micronaut.core.order.OrderUtil import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder import io.micronaut.inject.writer.AbstractBeanDefinitionBuilder +import io.micronaut.inject.writer.BeanDefinitionWriter import io.micronaut.inject.writer.ClassWriterOutputVisitor import io.micronaut.inject.writer.DirectoryClassWriterOutputVisitor import org.codehaus.groovy.ast.ASTNode @@ -98,7 +99,7 @@ class TypeElementVisitorEnd implements ASTTransformation, CompilationUnitAware { TypeElementVisitorTransform.loadedVisitors.remove() TypeElementVisitorTransform.beanDefinitionBuilders.remove() - AbstractAnnotationMetadataBuilder.clearMutated() + BeanDefinitionWriter.finish() } @Override diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorStart.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorStart.groovy index 0513823831e..9b6125124ed 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorStart.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/TypeElementVisitorStart.groovy @@ -44,7 +44,8 @@ import org.codehaus.groovy.transform.GroovyASTTransformation * @since 1.0 */ @CompileStatic -@GroovyASTTransformation(phase = CompilePhase.INITIALIZATION) +// IMPORTANT NOTE: This transform runs in phase CONVERSION so it runs before TypeElementVisitorTransform +@GroovyASTTransformation(phase = CompilePhase.CONVERSION) class TypeElementVisitorStart implements ASTTransformation, CompilationUnitAware { public static final String ELEMENT_VISITORS_PROPERTY = "micronaut.element.visitors" diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java index d168cf7521d..ffe1753a94e 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyVisitorContext.java @@ -29,6 +29,8 @@ import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; +import io.micronaut.expressions.context.ExpressionCompilationContextFactory; import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; @@ -75,6 +77,7 @@ public class GroovyVisitorContext implements VisitorContext { private final GroovyElementFactory groovyElementFactory; private final List beanDefinitionBuilders = new ArrayList<>(); private final GroovyElementAnnotationMetadataFactory elementAnnotationMetadataFactory; + private final ExpressionCompilationContextFactory expressionCompilationContextFactory; /** * @param sourceUnit The source unit @@ -96,6 +99,7 @@ public GroovyVisitorContext(SourceUnit sourceUnit, @Nullable CompilationUnit com this.attributes = VISITOR_ATTRIBUTES; this.groovyElementFactory = new GroovyElementFactory(this); this.elementAnnotationMetadataFactory = new GroovyElementAnnotationMetadataFactory(false, new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit)); + this.expressionCompilationContextFactory = new DefaultExpressionCompilationContextFactory(this); } @NonNull @@ -179,6 +183,11 @@ public GroovyElementAnnotationMetadataFactory getElementAnnotationMetadataFactor return elementAnnotationMetadataFactory; } + @Override + public ExpressionCompilationContextFactory getExpressionCompilationContextFactory() { + return this.expressionCompilationContextFactory; + } + @Override public AbstractAnnotationMetadataBuilder getAnnotationMetadataBuilder() { return new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit); diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy index 57bf945ee2d..c835d893784 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy @@ -9,7 +9,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { given: Object expr1 = evaluateAgainstContext("#{ #countValues(1, 2, 3) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(int... array) { return array.length @@ -19,7 +19,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr2 = evaluateAgainstContext("#{ #countValues(1) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(int... array) { return array.length @@ -29,7 +29,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr3 = evaluateAgainstContext("#{ #countValues() }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(int... array) { return array.length @@ -39,7 +39,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr4 = evaluateAgainstContext("#{ #countValues(1, 2, T(Integer).valueOf('3')) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(Integer... array) { return array.length @@ -58,7 +58,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { given: Object expr1 = evaluateAgainstContext("#{ #countValues('a', 'b', 'c') }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(String... values) { return values.length @@ -68,7 +68,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr2 = evaluateAgainstContext("#{ #countValues('a') }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(String... values) { return values.length @@ -78,7 +78,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr3 = evaluateAgainstContext("#{ #countValues() }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(String... values) { return values.length @@ -96,7 +96,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { given: Object expr1 = evaluateAgainstContext("#{ #multiplyLength(3, '1', 8, null) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int multiplyLength(int time, Object... values) { return values.length * time @@ -112,7 +112,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { given: Object expr1 = evaluateAgainstContext("#{ #multiplyLength(3, '1', 8, null) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int multiplyLength(Integer time, Object[] values) { return values.length * time @@ -128,7 +128,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { given: Object expr1 = evaluateAgainstContext("#{ #countLength(#values()) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { String[] values() { return [ "a", "b", "c" ] @@ -142,7 +142,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr2 = evaluateAgainstContext("#{ #multiplyLength(#values(), 3) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { String[] values() { return ["a", "b"] @@ -156,7 +156,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr3 = evaluateAgainstContext("#{ #multiplyLength(#values(), 1) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int[] values() { return new int[]{1, 2} @@ -180,7 +180,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { """ import java.util.Arrays - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { String[][] values() { return [ ["a", "b", "c"], ["a", "b"] ] @@ -198,7 +198,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { """ import java.util.Arrays - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int[][] values() { return [[1, 2, 3], [1, 2, 3, 3]] 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 d03bafbb399..45183ca9862 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy @@ -8,8 +8,8 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec given: Object expr1 = evaluateAgainstContext("#{ #getIntValue() }", """ - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { public int getIntValue() { return 15 } @@ -18,8 +18,8 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec Object expr2 = evaluateAgainstContext("#{ #getStringValue().toUpperCase() }", """ - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { String getStringValue() { return "test" } @@ -30,8 +30,8 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec """ import java.util.Random - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { Random randomizer() { return new Random() } @@ -42,40 +42,46 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec """ import java.util.Random - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { String lowercase(String value) { return value.toLowerCase() } } """) + ContextRegistrar.setClasses( + "test.FirstContext", + "test.SecondContext", + "test.ThirdContext", + "test.FourthContext" + ) Object expr5 = evaluateAgainstContext("#{ #transform(#getName(), #getRepeat(), #toLower()) }", """ import java.util.Random - @EvaluatedExpressionContext + @jakarta.inject.Singleton class FirstContext { String transform(String value, int repeat, Boolean toLower) { return (toLower ? value.toLowerCase() : value).repeat(repeat) } } - @EvaluatedExpressionContext + @jakarta.inject.Singleton class SecondContext { String getName() { return "TEST" } } - @EvaluatedExpressionContext + @jakarta.inject.Singleton class ThirdContext { Integer getRepeat() { return 2 } } - @EvaluatedExpressionContext + @jakarta.inject.Singleton class FourthContext { boolean toLower() { return true @@ -83,12 +89,13 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec } """) + ContextRegistrar.reset() Object expr6 = evaluateAgainstContext("#{ #getTestObject().name }", """ import java.util.Random - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { TestObject getTestObject() { return new TestObject() } @@ -108,8 +115,8 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec import java.util.Collection import java.util.concurrent.ThreadLocalRandom - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { TestObject getTestObject() { return new TestObject(); } diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy index 9fea56075b8..e3381b26f49 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -8,8 +8,8 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS given: Object expr1 = evaluateAgainstContext("#{ #intValue }", """ - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { int getIntValue() { return 15 } @@ -18,8 +18,8 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS Object expr2 = evaluateAgainstContext("#{ #boolean }", """ - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { Boolean isBoolean() { return false } @@ -28,8 +28,8 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS Object expr3 = evaluateAgainstContext("#{ #stringValue }", """ - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { String getStringValue() { return "test value" } @@ -38,8 +38,8 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS Object expr4 = evaluateAgainstContext("#{ #customClass.customProperty }", """ - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { CustomClass getCustomClass() { return new CustomClass() } diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextRegistrar.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextRegistrar.groovy new file mode 100644 index 00000000000..281012083fe --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextRegistrar.groovy @@ -0,0 +1,27 @@ +package io.micronaut.expressions + +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext + +class ContextRegistrar implements TypeElementVisitor { + static final List CLASSES = ["test.Context"] + + static void setClasses(String...classes) { + CLASSES.clear() + CLASSES.addAll(classes) + } + + static void reset() { + CLASSES.clear() + CLASSES.add("test.Context") + } + + @Override + void start(VisitorContext visitorContext) { + for (cls in CLASSES) { + visitorContext.getClassElement(cls).ifPresent { + visitorContext.expressionCompilationContextFactory.registerContextClass(it) + } + } + } +} diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy index a8b20a684a1..f5faa5166de 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy @@ -119,14 +119,13 @@ class TestExpressionsInjectionSpec extends AbstractBeanDefinitionSpec { package test import io.micronaut.context.annotation.Bean - import io.micronaut.context.annotation.EvaluatedExpressionContext import io.micronaut.context.annotation.Factory import jakarta.inject.Inject import jakarta.inject.Singleton import io.micronaut.context.annotation.Value - @EvaluatedExpressionContext - class TestContext { + @jakarta.inject.Singleton + class Context { String contextValue = "context value" } diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy index d7c75fa69c6..8391585955a 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy @@ -64,7 +64,6 @@ class TestExpressionsUsageSpec extends AbstractBeanDefinitionSpec { package test import io.micronaut.context.annotation.ConfigurationProperties - import io.micronaut.context.annotation.EvaluatedExpressionContext import io.micronaut.context.annotation.Requires import jakarta.inject.Singleton @@ -75,8 +74,8 @@ class TestExpressionsUsageSpec extends AbstractBeanDefinitionSpec { } @ConfigurationProperties('test') - @EvaluatedExpressionContext - class TestContext { + @jakarta.inject.Singleton + class Context { boolean enabled } """) @@ -100,7 +99,6 @@ class TestExpressionsUsageSpec extends AbstractBeanDefinitionSpec { package test import io.micronaut.context.annotation.ConfigurationProperties - import io.micronaut.context.annotation.EvaluatedExpressionContext import io.micronaut.context.annotation.Requires import jakarta.inject.Singleton @@ -111,8 +109,8 @@ class TestExpressionsUsageSpec extends AbstractBeanDefinitionSpec { } @ConfigurationProperties('test') - @EvaluatedExpressionContext - class TestContext { + @jakarta.inject.Singleton + class Context { boolean enabled } """) diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy index 246954bd1a1..6ee0f805f1a 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy @@ -26,7 +26,7 @@ class TypeIdentifierExpressionsSpec extends AbstractEvaluatedExpressionsSpec { given: Object expr1 = evaluateAgainstContext("#{ #getType(T(java.lang.String)) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { Class getType(Class type) { return type @@ -36,7 +36,7 @@ class TypeIdentifierExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr2 = evaluateAgainstContext("#{ #getType(T(String), T(Object)) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { Class getType(Class... types) { return types[1] diff --git a/inject-groovy/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/inject-groovy/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index b22d1586918..86de026a342 100644 --- a/inject-groovy/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/inject-groovy/src/test/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -4,3 +4,4 @@ io.micronaut.inject.visitor.AllClassesVisitor io.micronaut.inject.visitor.ControllerGetVisitor io.micronaut.inject.visitor.IntroductionVisitor io.micronaut.inject.visitor.MutatingVisitor +io.micronaut.expressions.ContextRegistrar diff --git a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy index 28484d3c181..a58327f9ba3 100644 --- a/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy +++ b/inject-java-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractEvaluatedExpressionsSpec.groovy @@ -19,10 +19,18 @@ import io.micronaut.context.expressions.AbstractEvaluatedExpression import io.micronaut.context.expressions.DefaultExpressionEvaluationContext import io.micronaut.core.naming.NameUtils import io.micronaut.core.expressions.EvaluatedExpressionReference +import io.micronaut.expressions.context.ExpressionEvaluationContextRegistrar +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext import org.intellij.lang.annotations.Language abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec { + @Override + protected Collection getLocalTypeElementVisitors() { + return [new ContextRegistrar()] + } + List evaluateMultiple(String... expressions) { String classContent = "" @@ -38,7 +46,6 @@ abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec def cls = """ package test; import io.micronaut.context.annotation.Value; - import io.micronaut.context.annotation.EvaluatedExpressionContext; class Expr { ${classContent} @@ -75,7 +82,6 @@ abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec def cls = """ package test; import io.micronaut.context.annotation.Value; - import io.micronaut.context.annotation.EvaluatedExpressionContext; import jakarta.inject.Singleton; ${contextClass} @@ -124,4 +130,27 @@ abstract class AbstractEvaluatedExpressionsSpec extends AbstractTypeElementSpec return null } } + + static class ContextRegistrar implements TypeElementVisitor { + static final List CLASSES = ["test.Context"] + + static void setClasses(String...classes) { + CLASSES.clear() + CLASSES.addAll(classes) + } + + static void reset() { + CLASSES.clear() + CLASSES.add("test.Context") + } + + @Override + void start(VisitorContext visitorContext) { + for (cls in CLASSES) { + visitorContext.getClassElement(cls).ifPresent { + visitorContext.expressionCompilationContextFactory.registerContextClass(it) + } + } + } + } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index 6b0ae09e0b4..a3f5880abac 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -28,9 +28,7 @@ import io.micronaut.core.naming.NameUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.expressions.EvaluatedExpressionWriter; -import io.micronaut.expressions.context.ExpressionCompilationContextRegistry; import io.micronaut.expressions.context.ExpressionWithContext; -import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; import io.micronaut.inject.processing.BeanDefinitionCreator; import io.micronaut.inject.processing.BeanDefinitionCreatorFactory; @@ -261,9 +259,7 @@ public final boolean process(Set annotations, RoundEnviro } } } finally { - AbstractAnnotationMetadataBuilder.clearMutated(); - JavaAnnotationMetadataBuilder.clearCaches(); - ExpressionCompilationContextRegistry.reset(); + BeanDefinitionWriter.finish(); } } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java index 627b167ba9e..28ad0f9168c 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java @@ -30,7 +30,6 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.version.VersionUtils; -import io.micronaut.expressions.context.ExpressionCompilationContextRegistry; import io.micronaut.inject.processing.ProcessingException; import io.micronaut.inject.ast.ConstructorElement; import io.micronaut.inject.ast.EnumConstantElement; @@ -296,7 +295,6 @@ public boolean process(Set annotations, RoundEnvironment final List beanDefinitionBuilders = javaVisitorContext.getBeanElementBuilders(); if (CollectionUtils.isNotEmpty(beanDefinitionBuilders)) { try { -// EvaluatedExpressionContextInitializer.initEvaluatedExpressionContext(roundEnv, modelUtils, javaVisitorContext); AbstractBeanDefinitionBuilder.writeBeanDefinitionBuilders(classWriterOutputVisitor, beanDefinitionBuilders); } catch (IOException e) { // raise a compile error @@ -308,7 +306,6 @@ public boolean process(Set annotations, RoundEnvironment if (roundEnv.processingOver()) { javaVisitorContext.finish(); writeBeanDefinitionsToMetaInf(); - ExpressionCompilationContextRegistry.reset(); } return false; } diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java index 300d4968292..3db66d93b21 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaVisitorContext.java @@ -30,6 +30,8 @@ import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; +import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; +import io.micronaut.expressions.context.ExpressionCompilationContextFactory; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.annotation.AbstractAnnotationElement; import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory; @@ -92,6 +94,7 @@ public final class JavaVisitorContext implements VisitorContext, BeanElementVisi private final List beanDefinitionBuilders = new ArrayList<>(); private final JavaElementFactory elementFactory; private final TypeElementVisitor.VisitorKind visitorKind; + private final DefaultExpressionCompilationContextFactory expressionCompilationContextFactory; private @Nullable JavaFileManager standardFileManager; private final JavaAnnotationMetadataBuilder annotationMetadataBuilder; @@ -135,6 +138,7 @@ public JavaVisitorContext( this.visitorKind = visitorKind; this.annotationMetadataBuilder = new JavaAnnotationMetadataBuilder(elements, messager, annotationUtils, modelUtils); this.elementAnnotationMetadataFactory = new JavaElementAnnotationMetadataFactory(false, this.annotationMetadataBuilder); + this.expressionCompilationContextFactory = new DefaultExpressionCompilationContextFactory(this); } /** @@ -216,6 +220,11 @@ public JavaElementAnnotationMetadataFactory getElementAnnotationMetadataFactory( return elementAnnotationMetadataFactory; } + @Override + public ExpressionCompilationContextFactory getExpressionCompilationContextFactory() { + return expressionCompilationContextFactory; + } + @Override public JavaAnnotationMetadataBuilder getAnnotationMetadataBuilder() { return annotationMetadataBuilder; diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy index ea8698eef0c..cefc851a59d 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ArrayMethodsExpressionsSpec.groovy @@ -8,7 +8,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { given: Object expr1 = evaluateAgainstContext("#{ #countValues(1, 2, 3) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(int... array) { return array.length; @@ -18,7 +18,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr2 = evaluateAgainstContext("#{ #countValues(1) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(int... array) { return array.length; @@ -28,7 +28,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr3 = evaluateAgainstContext("#{ #countValues() }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(int... array) { return array.length; @@ -38,7 +38,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr4 = evaluateAgainstContext("#{ #countValues(1, 2, T(Integer).valueOf('3')) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(Integer... array) { return array.length; @@ -57,7 +57,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { given: Object expr1 = evaluateAgainstContext("#{ #countValues('a', 'b', 'c') }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(String... values) { return values.length; @@ -67,7 +67,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr2 = evaluateAgainstContext("#{ #countValues('a') }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(String... values) { return values.length; @@ -77,7 +77,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr3 = evaluateAgainstContext("#{ #countValues() }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int countValues(String... values) { return values.length; @@ -95,7 +95,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { given: Object expr1 = evaluateAgainstContext("#{ #multiplyLength(3, '1', 8, null) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int multiplyLength(int time, Object... values) { return values.length * time; @@ -111,7 +111,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { given: Object expr1 = evaluateAgainstContext("#{ #multiplyLength(3, '1', 8, null) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int multiplyLength(Integer time, Object[] values) { return values.length * time; @@ -127,7 +127,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { given: Object expr1 = evaluateAgainstContext("#{ #countLength(#values()) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { String[] values() { return new String[]{"a", "b", "c"}; @@ -141,7 +141,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr2 = evaluateAgainstContext("#{ #multiplyLength(#values(), 3) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { String[] values() { return new String[]{"a", "b"}; @@ -155,7 +155,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr3 = evaluateAgainstContext("#{ #multiplyLength(#values(), 1) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int[] values() { return new int[]{1, 2}; @@ -179,7 +179,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { """ import java.util.Arrays; - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { String[][] values() { return new String[][]{new String[]{"a", "b", "c"}, new String[]{"a", "b"}}; @@ -197,7 +197,7 @@ class ArrayMethodsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { """ import java.util.Arrays; - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { int[][] values() { return new int[][]{new int[]{1, 2, 3}, new int[]{1, 2, 3, 4}}; diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy index 85d3dc50f01..8bc3d85c4ff 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy @@ -8,8 +8,8 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec given: Object expr1 = evaluateAgainstContext("#{ #getIntValue() }", """ - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { int getIntValue() { return 15; } @@ -18,8 +18,8 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec Object expr2 = evaluateAgainstContext("#{ #getStringValue().toUpperCase() }", """ - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { String getStringValue() { return "test"; } @@ -30,8 +30,8 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec """ import java.util.Random; - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { Random randomizer() { return new Random(); } @@ -42,53 +42,60 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec """ import java.util.Random; - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { String lowercase(String value) { return value.toLowerCase(); } } """) + ContextRegistrar.setClasses( + "test.FirstContext", + "test.SecondContext", + "test.ThirdContext", + "test.FourthContext" + ) Object expr5 = evaluateAgainstContext("#{ #transform(#getName(), #getRepeat(), #toLower()) }", """ import java.util.Random; - @EvaluatedExpressionContext + @jakarta.inject.Singleton class FirstContext { String transform(String value, int repeat, Boolean toLower) { return (toLower ? value.toLowerCase() : value).repeat(repeat); } } - @EvaluatedExpressionContext + @jakarta.inject.Singleton class SecondContext { String getName() { return "TEST"; } } - @EvaluatedExpressionContext + @jakarta.inject.Singleton class ThirdContext { Integer getRepeat() { return 2; } } - @EvaluatedExpressionContext + @jakarta.inject.Singleton class FourthContext { boolean toLower() { return true; } } """) + ContextRegistrar.reset() Object expr6 = evaluateAgainstContext("#{ #getTestObject().name }", """ import java.util.Random; - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { TestObject getTestObject() { return new TestObject(); } @@ -108,8 +115,8 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec import java.util.Collection; import java.util.concurrent.ThreadLocalRandom; - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { TestObject getTestObject() { return new TestObject(); } 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 6c24894b1d1..2fdbd367b79 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -8,8 +8,8 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS given: Object expr1 = evaluateAgainstContext("#{ #intValue }", """ - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { int getIntValue() { return 15; } @@ -18,8 +18,8 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS Object expr2 = evaluateAgainstContext("#{ #boolean }", """ - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { Boolean isBoolean() { return false; } @@ -28,8 +28,8 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS Object expr3 = evaluateAgainstContext("#{ #stringValue }", """ - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { String getStringValue() { return "test value"; } @@ -38,8 +38,8 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS Object expr4 = evaluateAgainstContext("#{ #customClass.customProperty }", """ - @EvaluatedExpressionContext - class ExpressionContext { + @jakarta.inject.Singleton + class Context { CustomClass getCustomClass() { return new CustomClass(); } diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy index dbe10d12a4f..cd3919fb9d4 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsInjectionSpec.groovy @@ -118,14 +118,13 @@ class TestExpressionsInjectionSpec extends AbstractEvaluatedExpressionsSpec { package test; import io.micronaut.context.annotation.Bean; - import io.micronaut.context.annotation.EvaluatedExpressionContext; import io.micronaut.context.annotation.Factory; import jakarta.inject.Inject; import jakarta.inject.Singleton; import io.micronaut.context.annotation.Value; - @EvaluatedExpressionContext - class TestContext { + @jakarta.inject.Singleton + class Context { public String getContextValue() { return "context value"; } diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy index 480a6820ea2..12ec1b2ed6f 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TestExpressionsUsageSpec.groovy @@ -63,7 +63,6 @@ class TestExpressionsUsageSpec extends AbstractEvaluatedExpressionsSpec { package test; import io.micronaut.context.annotation.ConfigurationProperties; - import io.micronaut.context.annotation.EvaluatedExpressionContext; import io.micronaut.context.annotation.Requires; import jakarta.inject.Singleton; @@ -74,8 +73,8 @@ class TestExpressionsUsageSpec extends AbstractEvaluatedExpressionsSpec { } @ConfigurationProperties("test") - @EvaluatedExpressionContext - class TestContext { + @jakarta.inject.Singleton + class Context { private boolean enabled; public void setEnabled(boolean enabled) { diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy index 0f5399f6817..dbd40aa4d78 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TypeIdentifierExpressionsSpec.groovy @@ -26,7 +26,7 @@ class TypeIdentifierExpressionsSpec extends AbstractEvaluatedExpressionsSpec { given: Object expr1 = evaluateAgainstContext("#{ #getType(T(java.lang.String)) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { Class getType(Class type) { return type; @@ -36,7 +36,7 @@ class TypeIdentifierExpressionsSpec extends AbstractEvaluatedExpressionsSpec { Object expr2 = evaluateAgainstContext("#{ #getType(T(String), T(Object)) }", """ - @EvaluatedExpressionContext + @jakarta.inject.Singleton class Context { Class getType(Class... types) { return types[1]; diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt index 0f83136adf3..62e7ea89c4a 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt @@ -27,6 +27,7 @@ import io.micronaut.inject.processing.BeanDefinitionCreatorFactory import io.micronaut.inject.processing.ProcessingException import io.micronaut.inject.writer.BeanDefinitionReferenceWriter import io.micronaut.inject.writer.BeanDefinitionVisitor +import io.micronaut.inject.writer.BeanDefinitionWriter import io.micronaut.kotlin.processing.KotlinOutputVisitor import io.micronaut.kotlin.processing.visitor.KotlinClassElement import io.micronaut.kotlin.processing.visitor.KotlinNativeElement @@ -103,7 +104,7 @@ internal class BeanDefinitionProcessor(private val environment: SymbolProcessorE } catch (e: ProcessingException) { handleProcessingException(environment, e) } finally { - AbstractAnnotationMetadataBuilder.clearMutated() + BeanDefinitionWriter.finish() beanDefinitionMap.clear() } } diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy index ec2cfd69e0c..0c55361084f 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy @@ -17,9 +17,6 @@ import io.micronaut.core.reflect.exception.InstantiationException import io.micronaut.core.type.Argument import io.micronaut.core.type.GenericPlaceholder import io.micronaut.inject.ExecutableMethod -import io.micronaut.inject.beans.visitor.MappedSuperClassIntrospectionMapper -import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor -import io.micronaut.inject.beans.visitor.EvaluatedExpressionContextTypeElementVisitor import io.micronaut.kotlin.processing.elementapi.SomeEnum import io.micronaut.kotlin.processing.elementapi.TestClass diff --git a/inject/src/main/java/io/micronaut/context/annotation/AnnotationExpressionContext.java b/inject/src/main/java/io/micronaut/context/annotation/AnnotationExpressionContext.java index 60dd9228b49..4c6c866273d 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/AnnotationExpressionContext.java +++ b/inject/src/main/java/io/micronaut/context/annotation/AnnotationExpressionContext.java @@ -15,6 +15,8 @@ */ package io.micronaut.context.annotation; +import io.micronaut.core.annotation.AnnotationMetadata; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -23,15 +25,25 @@ /** * A meta annotation used to extend {@link io.micronaut.core.expressions.EvaluatedExpression} * context with specified type. Being an expression context means that expressions can reference - * methods and properties of this object directly with # prefix. The difference between this - * annotation and {@link EvaluatedExpressionContext} is that this one allows to specify context + * methods and properties of this object directly with # prefix. This + * annotation allows to specify context * that will only be scoped to this concrete annotation or annotation member. * * @author Sergey Gavrilov + * @author gkrocher * @since 4.0.0 */ @Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.CLASS) public @interface AnnotationExpressionContext { + /** + * @return The class that should be included in the context where expressions are evaluated. + */ Class value(); + + /** + * @return The name of the class that should be included in the context where expressions are evaluated. + */ + @AliasFor(member = AnnotationMetadata.VALUE_MEMBER) + String className() default ""; } diff --git a/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java b/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java deleted file mode 100644 index 20017228970..00000000000 --- a/inject/src/main/java/io/micronaut/context/annotation/EvaluatedExpressionContext.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.context.annotation; - -import jakarta.inject.Singleton; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation used to mark classes which are considered as context - * for expression evaluation. Being an expression context means that - * expressions can reference methods and properties of this object - * directly with # prefix - * - * @author Sergey Gavrilov - * @since 4.0.0 - */ -@DefaultScope(Singleton.class) -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.CLASS) -public @interface EvaluatedExpressionContext { -} diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index 6a3dd12bbb1..1b973fe8fd5 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -224,27 +224,41 @@ Type Reference can be used to invoke a static method of a class By default, the only methods you can invoke inside Evaluated Expressions are static methods using type references. -To invoke non-static methods, you need to place `@EvaluatedExpressionContext` annotation on a class owning the -method you want to invoke inside expression. In this case the annotated class is registered within evaluation context -which makes its methods and properties available for referencing in evaluated expressions. Any context reference +The available methods can be extended by extended the evaluation context. There are two ways to extend the evaluation context. The first involves registering new context class via a custom api:TypeElementVisitor[]. + +NOTE: The api:TypeElementVisitor[] has to be on the annotation processor classpath, therefore needs to be defined in a separate module that can be included on this classpath. + +Once a class is registered within evaluation context the methods and properties of the class are available for referencing in evaluated expressions. Any context reference needs to be prefixed with `#` sign. Consider the following example: .User-defined evaluated expression context -[source, java] +[source,java] ---- -import io.micronaut.context.annotation.EvaluatedExpressionContext; import jakarta.inject.Singleton; import java.util.Random; -@EvaluatedExpressionContext +@Singleton public class CustomEvaluationContext { - public int generateRandom(int min, int max) { return new Random().nextInt(max - min) + min; } +} +---- + +NOTE: The class should be resolvable as a bean can use `jakarta.inject` annotations to inject other types if necessary. +Registering this class can be achieved with a custom implementation of api:expressions.context.ExpressionEvaluationContextRegistrar[] that is registered via service loader as a api:inject.visitor.TypeElementVisitor[] (create a new `META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor` file referencing the new class) and placed on the annotation processor classpath: + +[source,java] +---- +public class ContextRegistrar implements ExpressionEvaluationContextRegistrar { + @Override + public String getContextClassName() { + return "example.CustomEvaluationContext" + } + } } ---- @@ -265,23 +279,21 @@ public class ContextConsumer { } ---- -Note that annotating a class with `@EvaluatedExpressionContext` makes it a bean. By default, it will be treated as -singleton, but you can specify an alternative scope if required. At runtime, the bean will be retrieved from +At runtime, the bean will be retrieved from application context and respective method will be invoked. If a matching method is not found within evaluation context at compilation time, the compilation will fail. A compilation error will also occur if multiple suitable methods are found in the evaluation context, keep that in mind -if you annotate multiple classes with `@EvaluatedExpressionContext`. +if you provide multiple api:expressions.context.ExpressionEvaluationContextRegistrar[] that a conflict can occur as these types are effectively global. The methods will be considered ambiguous (leading to compilation failure) when their names are the same and list of provided arguments matches multiple methods parameters. -The class annotated with `@EvaluatedExpressionContext` needs to reside within the same module as the classes referencing -its methods or properties. +Using a api:expressions.context.ExpressionEvaluationContextRegistrar[] makes its methods and properties available for evaluated +expressions within any annotation in a global manner. -Annotating a class with `@EvaluatedExpressionContext` makes its methods and properties available for evaluated -expressions within any annotation. However, you can also specify evaluation context scoped to concrete annotation or -annotation member using `@AnnotationExpressionContext`. +However, you can also specify evaluation context scoped to concrete annotation or +annotation member using ann:context.annotation.AnnotationExpressionContext[]. .Usage of annotation level evaluated expression context [source, java] @@ -316,7 +328,7 @@ class AnnotationMemberContext { } ---- -In this case the context classes need to be explicitly defined as beans to make them available for retrieval from +Again context classes need to be explicitly defined as beans to make them available for retrieval from application context at runtime. === Method Invocation @@ -325,13 +337,12 @@ You can invoke both static methods using type references, methods from evaluatio which means method chaining is supported. .Chaining methods in expression -[source, java] +[source,java] ---- -import io.micronaut.context.annotation.EvaluatedExpressionContext; import io.micronaut.context.annotation.Value; import jakarta.inject.Singleton; -@EvaluatedExpressionContext +@Singleton class CustomEvaluationContext { public String stringValue() { @@ -354,13 +365,12 @@ invoke it providing list of arguments separated by comma without explicitly wrap it will be treated in same way as if last method argument was explicitly specified as varargs parameter. .Invoking varargs methods in expressions -[source, java] +[source,java] ---- -import io.micronaut.context.annotation.EvaluatedExpressionContext; import io.micronaut.context.annotation.Value; import jakarta.inject.Singleton; -@EvaluatedExpressionContext +@Singleton class CustomEvaluationContext { public int countIntegers(int... values) { @@ -391,13 +401,13 @@ JavaBean properties can be accessed simply be referencing their names from evalu properties can also be chained with dot in the same way as methods. .Accessing bean properties in expressions -[source, java] +[source,java] ---- -import io.micronaut.context.annotation.EvaluatedExpressionContext; + import io.micronaut.context.annotation.Value; import jakarta.inject.Singleton; -@EvaluatedExpressionContext +@Singleton class CustomEvaluationContext { public String getName() { From c8941a615e32ecd0c878404b54b2dd8a7fa07776 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 17 Mar 2023 16:22:20 -0400 Subject: [PATCH 10/35] support safe navigation operator --- .../EvaluatedExpressionWriter.java | 2 +- ...gleEvaluatedEvaluatedExpressionParser.java | 15 ++-- .../parser/ast/access/ElementMethodCall.java | 19 ++++- .../parser/ast/access/PropertyAccess.java | 4 +- .../expressions/parser/token/TokenType.java | 1 + .../expressions/parser/token/Tokenizer.java | 2 + ...ontextPropertyAccessExpressionsSpec.groovy | 76 ++++++++++++++++++- ...ontextPropertyAccessExpressionsSpec.groovy | 22 ++++++ 8 files changed, 131 insertions(+), 10 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java b/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java index 03be8ae3cd3..d08c9e1136e 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java +++ b/core-processor/src/main/java/io/micronaut/expressions/EvaluatedExpressionWriter.java @@ -113,7 +113,7 @@ private ClassWriter generateClassBytes(String expressionClassName) { failCompilation(ex, annotationValue); } - evaluateMethodVisitor.visitMaxs(2, 1); + evaluateMethodVisitor.visitMaxs(2, 3); evaluateMethodVisitor.returnValue(); return classWriter; } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java index ad72ab932c1..b2e5f15b0ef 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java @@ -89,6 +89,7 @@ import static io.micronaut.expressions.parser.token.TokenType.POW; import static io.micronaut.expressions.parser.token.TokenType.QMARK; import static io.micronaut.expressions.parser.token.TokenType.R_PAREN; +import static io.micronaut.expressions.parser.token.TokenType.SAFE_NAV; import static io.micronaut.expressions.parser.token.TokenType.STRING; /** @@ -306,13 +307,14 @@ private ExpressionNode unaryExpression() { // PostfixExpression // : PrimaryExpression // | PostfixExpression '.' MethodOrPropertyAccess + // | PostfixExpression '?.' MethodOrPropertyAccess with safe navigation // | PostfixExpression '++' // | PostfixExpression '--' // ; private ExpressionNode postfixExpression() { ExpressionNode leftNode = primaryExpression(); while (lookahead != null && (lookahead.type() - .isOneOf(DOT, L_SQUARE, INCREMENT, DECREMENT))) { + .isOneOf(DOT, SAFE_NAV, L_SQUARE, INCREMENT, DECREMENT))) { TokenType tokenType = lookahead.type(); if (tokenType == INCREMENT) { throw new ExpressionParsingException("Postfix increment operation is not " + @@ -322,7 +324,10 @@ private ExpressionNode postfixExpression() { "supported"); } else if (tokenType == DOT) { eat(DOT); - leftNode = methodOrPropertyAccess(leftNode); + leftNode = methodOrPropertyAccess(leftNode, false); + } else if (tokenType == SAFE_NAV) { + eat(SAFE_NAV); + leftNode = methodOrPropertyAccess(leftNode, true); } else { throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); } @@ -370,13 +375,13 @@ private ExpressionNode contextAccess() { // : SimpleIdentifier // | SimpleIdentifier MethodArguments // ; - private ExpressionNode methodOrPropertyAccess(ExpressionNode callee) { + private ExpressionNode methodOrPropertyAccess(ExpressionNode callee, boolean nullSafe) { String identifier = identifier(); if (lookahead != null && lookahead.type() == L_PAREN) { List methodArguments = methodArguments(); - return new ElementMethodCall(callee, identifier, methodArguments); + return new ElementMethodCall(callee, identifier, methodArguments, nullSafe); } - return new PropertyAccess(callee, identifier); + return new PropertyAccess(callee, identifier, nullSafe); } // MethodArguments: 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 41322417d96..f99dde9ef90 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 @@ -24,6 +24,7 @@ import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.visitor.VisitorContext; +import org.objectweb.asm.Label; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; @@ -31,6 +32,7 @@ import java.util.List; import static io.micronaut.expressions.parser.ast.util.EvaluatedExpressionCompilationUtils.getRequiredClassElement; +import static org.objectweb.asm.Opcodes.ACONST_NULL; import static org.objectweb.asm.Opcodes.INVOKESTATIC; /** @@ -45,12 +47,15 @@ public sealed class ElementMethodCall extends AbstractMethodCall permits PropertyAccess { protected final ExpressionNode callee; + private final boolean nullSafe; public ElementMethodCall(ExpressionNode callee, String name, - List arguments) { + List arguments, + boolean nullSafe) { super(name, arguments); this.callee = callee; + this.nullSafe = nullSafe; } @Override @@ -72,6 +77,18 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { } } else { callee.compile(ctx); + if (nullSafe) { + // null safe operator is used so we need to check the result is null + Type returnType = method.getReturnType(); + mv.storeLocal(2, returnType); + mv.loadLocal(2, returnType); + Label proceed = new Label(); + mv.ifNonNull(proceed); + mv.visitInsn(ACONST_NULL); + mv.returnValue(); + mv.visitLabel(proceed); + mv.loadLocal(2, returnType); + } compileArguments(ctx); if (calleeClass.isInterface()) { mv.invokeInterface(calleeType, method); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java index 3e3396ccecd..a480986c7f1 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/PropertyAccess.java @@ -42,8 +42,8 @@ */ @Internal public final class PropertyAccess extends ElementMethodCall { - public PropertyAccess(ExpressionNode callee, String name) { - super(callee, name, emptyList()); + public PropertyAccess(ExpressionNode callee, String name, boolean nullSafe) { + super(callee, name, emptyList(), nullSafe); } @Override diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java index 14b2c440b3f..dc4b5274b45 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java @@ -30,6 +30,7 @@ public enum TokenType { EXPRESSION_CONTEXT_REF, DOT, + SAFE_NAV, COMMA, COLON, L_PAREN, diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java index cb619707b7c..636a8d30b85 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java @@ -62,6 +62,7 @@ import static io.micronaut.expressions.parser.token.TokenType.R_CURLY; import static io.micronaut.expressions.parser.token.TokenType.R_PAREN; import static io.micronaut.expressions.parser.token.TokenType.R_SQUARE; +import static io.micronaut.expressions.parser.token.TokenType.SAFE_NAV; import static io.micronaut.expressions.parser.token.TokenType.STRING; import static io.micronaut.expressions.parser.token.TokenType.WHITESPACE; @@ -112,6 +113,7 @@ public final class Tokenizer { // SYMBOLS "^#", EXPRESSION_CONTEXT_REF, + "^\\?\\.", SAFE_NAV, "^\\?", QMARK, "^\\.", DOT, "^,", COMMA, diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy index e3381b26f49..c874c62b782 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.expressions -import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec; +import io.micronaut.ast.transform.test.AbstractEvaluatedExpressionsSpec +import io.micronaut.context.exceptions.ExpressionEvaluationException; class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsSpec { @@ -56,4 +57,77 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS expr3 instanceof String && expr3 == "test value" expr4 instanceof String && expr4 == "custom property" } + + void "test multi-level context property access"() { + given: + Object expr = evaluateAgainstContext("#{ #foo.bar.name }", + """ + @jakarta.inject.Singleton + class Context { + Foo getFoo() { + return new Foo(); + } + } + + class Foo { + Bar bar = new Bar() + } + + class Bar { + String name = "test" + } + """) + + expect: + expr instanceof String && expr == "test" + } + + void "test multi-level context property access safe navigation"() { + given: + Object expr = evaluateAgainstContext("#{ #foo?.bar?.name }", + """ + @jakarta.inject.Singleton + class Context { + Foo getFoo() { + return new Foo(); + } + } + + class Foo { + Bar bar + } + + class Bar { + String name = "test" + } + """) + + expect: + expr == null + } + + void "test multi-level context property access non-safe navigation"() { + when: + Object expr = evaluateAgainstContext("#{ #foo.bar.name }", + """ + @jakarta.inject.Singleton + class Context { + Foo getFoo() { + return new Foo(); + } + } + + class Foo { + Bar bar + } + + class Bar { + String name = "test" + } + """) + + then: + def e = thrown(ExpressionEvaluationException) + e.message.startsWith('Can not evaluate expression [null]') + } } 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 2fdbd367b79..e953ebbe01e 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -58,4 +58,26 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS expr3 instanceof String && expr3 == "test value" expr4 instanceof String && expr4 == "custom property" } + + void "test multi-level context property access - records"() { + given: + Object expr = evaluateAgainstContext("#{ #foo.bar.name }", + """ + @jakarta.inject.Singleton + class Context { + Foo getFoo() { + return new Foo(new Bar("test")); + } + } + + record Foo(Bar bar) { + } + + record Bar(String name) { + } + """) + + expect: + expr instanceof String && expr == "test" + } } From dc6b3f54f3b3e5ca010add5e6d3b55512db2e868 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 17 Mar 2023 16:26:17 -0400 Subject: [PATCH 11/35] fix Kotlin --- .../kotlin/processing/visitor/KotlinVisitorContext.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt index b42cb7217f4..b17a1d95137 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt @@ -25,6 +25,8 @@ import io.micronaut.core.convert.ArgumentConversionContext import io.micronaut.core.convert.value.MutableConvertibleValues import io.micronaut.core.convert.value.MutableConvertibleValuesMap import io.micronaut.core.util.StringUtils +import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory +import io.micronaut.expressions.context.ExpressionCompilationContextFactory import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder import io.micronaut.inject.ast.ClassElement import io.micronaut.inject.ast.Element @@ -51,6 +53,7 @@ internal open class KotlinVisitorContext( private val outputVisitor = KotlinOutputVisitor(environment) val annotationMetadataBuilder: KotlinAnnotationMetadataBuilder private val elementAnnotationMetadataFactory: KotlinElementAnnotationMetadataFactory + private val expressionCompilationContextFactory : ExpressionCompilationContextFactory init { visitorAttributes = MutableConvertibleValuesMap() @@ -58,6 +61,7 @@ internal open class KotlinVisitorContext( elementFactory = KotlinElementFactory(this) elementAnnotationMetadataFactory = KotlinElementAnnotationMetadataFactory(false, annotationMetadataBuilder) + expressionCompilationContextFactory = DefaultExpressionCompilationContextFactory(this) } override fun get( @@ -175,6 +179,10 @@ internal open class KotlinVisitorContext( return elementAnnotationMetadataFactory } + override fun getExpressionCompilationContextFactory(): ExpressionCompilationContextFactory { + return expressionCompilationContextFactory + } + override fun getAnnotationMetadataBuilder(): AbstractAnnotationMetadataBuilder<*, *> { return annotationMetadataBuilder } From 1fd8b3d54f04f899a2433f24cfe6f70317096991 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 17 Mar 2023 16:27:34 -0400 Subject: [PATCH 12/35] license headers --- .../ExpressionCompilationContextFactory.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java index 85cccb4429c..401e3a0ebc9 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java @@ -1,3 +1,18 @@ +/* + * 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.expressions.context; import io.micronaut.core.annotation.Experimental; From 885ccfd3764e3a4ae9d6efa7c406e2382a9bc463 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 17 Mar 2023 16:55:30 -0400 Subject: [PATCH 13/35] Add basic implementation and smoke test for KSP --- .../KotlinAnnotationMetadataBuilder.kt | 35 ++++++++++++++++--- .../beans/BeanDefinitionProcessor.kt | 26 +++++++++++--- .../micronaut/kotlin/processing/extensions.kt | 22 ++++++++++++ .../TestExpressionsInjectionSpec.groovy | 29 +++++++++++++++ 4 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/expressions/TestExpressionsInjectionSpec.groovy diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt index 2bb07bd9ebf..4d591771d56 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt @@ -325,7 +325,18 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro is Array<*> -> { toArray(annotationValue.toList(), originatingElement) } - else -> readAnnotationValue(originatingElement, annotationValue) + else -> { + if (isEvaluatedExpression(annotationValue)) { + return buildEvaluatedExpressionReference( + originatingElement, + annotationName, + memberName, + annotationValue + ) + } else { + return readAnnotationValue(originatingElement, annotationValue) + } + } } } @@ -367,8 +378,13 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro } } - override fun getOriginatingClassName(orginatingElement: KSAnnotated?): String { - TODO("Not yet implemented") + override fun getOriginatingClassName(orginatingElement: KSAnnotated): String { + return if (orginatingElement is KSClassDeclaration) { + orginatingElement.getBinaryName(resolver, visitorContext) + } else { + val classDeclaration = orginatingElement.getClassDeclaration(visitorContext) + classDeclaration.getBinaryName(resolver, visitorContext) + } } private fun readDefaultValuesReflectively(classDeclaration : KSClassDeclaration, annotationType: KSAnnotated, vararg path : String): MutableMap { @@ -446,12 +462,21 @@ internal class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnviro val values: Map = readAnnotationRawValues(annotationMirror) val converted: MutableMap = mutableMapOf() for ((key, value1) in values) { - val value = value1!! + var value = value1!! + val memberName = key.simpleName.asString() + if (isEvaluatedExpression(value)) { + value = buildEvaluatedExpressionReference( + originatingElement, + annotationName, + memberName, + value + ) + } readAnnotationRawValues( originatingElement, annotationName, key, - key.simpleName.asString(), + memberName, value, converted ) diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt index 62e7ea89c4a..b371110638d 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt @@ -21,7 +21,7 @@ import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.symbol.* import io.micronaut.context.annotation.Context import io.micronaut.core.annotation.Generated -import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder +import io.micronaut.expressions.EvaluatedExpressionWriter import io.micronaut.inject.processing.BeanDefinitionCreator import io.micronaut.inject.processing.BeanDefinitionCreatorFactory import io.micronaut.inject.processing.ProcessingException @@ -37,9 +37,10 @@ import java.io.IOException internal class BeanDefinitionProcessor(private val environment: SymbolProcessorEnvironment): SymbolProcessor { private val beanDefinitionMap = mutableMapOf() + private var visitorContext : KotlinVisitorContext? = null override fun process(resolver: Resolver): List { - val visitorContext = KotlinVisitorContext(environment, resolver) + visitorContext = KotlinVisitorContext(environment, resolver) val elements = resolver.getAllFiles() .flatMap { file: KSFile -> @@ -54,7 +55,7 @@ internal class BeanDefinitionProcessor(private val environment: SymbolProcessorE .toList() try { - processClassDeclarations(elements, visitorContext) + processClassDeclarations(elements, visitorContext!!) } catch (e: ProcessingException) { handleProcessingException(environment, e) } @@ -93,7 +94,7 @@ internal class BeanDefinitionProcessor(private val environment: SymbolProcessorE for (beanDefinitionCreator in beanDefinitionMap.values) { for (writer in beanDefinitionCreator.build()) { if (processed.add(writer.beanDefinitionName)) { - processBeanDefinitions(writer, outputVisitor, processed) + processBeanDefinitions(writer, outputVisitor, visitorContext!!, processed) count++ } } @@ -135,12 +136,14 @@ internal class BeanDefinitionProcessor(private val environment: SymbolProcessorE private fun processBeanDefinitions( beanDefinitionWriter: BeanDefinitionVisitor, outputVisitor: KotlinOutputVisitor, + visitorContext: KotlinVisitorContext, processed: HashSet ) { try { beanDefinitionWriter.visitBeanDefinitionEnd() if (beanDefinitionWriter.isEnabled) { beanDefinitionWriter.accept(outputVisitor) + processEvaluatedExpressions(beanDefinitionWriter, visitorContext, outputVisitor) val beanDefinitionReferenceWriter = BeanDefinitionReferenceWriter(beanDefinitionWriter) beanDefinitionReferenceWriter.setRequiresMethodProcessing(beanDefinitionWriter.requiresMethodProcessing()) val className = beanDefinitionReferenceWriter.beanDefinitionQualifiedClassName @@ -157,4 +160,19 @@ internal class BeanDefinitionProcessor(private val environment: SymbolProcessorE } } + private fun processEvaluatedExpressions( + beanDefinitionWriter: BeanDefinitionVisitor, + visitorContext: KotlinVisitorContext, + outputVisitor: KotlinOutputVisitor + ) { + for (expressionMetadata in beanDefinitionWriter.evaluatedExpressions) { + val expressionWriter = EvaluatedExpressionWriter( + expressionMetadata, + visitorContext, + beanDefinitionWriter.originatingElement + ) + expressionWriter.accept(outputVisitor) + } + } + } diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt index e863d7a6d47..cedbb75c802 100644 --- a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt @@ -99,6 +99,28 @@ internal fun KSAnnotated.getClassDeclaration(visitorContext: KotlinVisitorContex val declaration = this.type.resolve().declaration return declaration.getClassDeclaration(visitorContext) } + is KSValueParameter -> { + val p = this.parent + if (p is KSDeclaration) { + return p.getClassDeclaration(visitorContext) + } else { + return visitorContext.resolver.getJavaClassByName(Object::class.java.name)!! + } + } + is KSFunctionDeclaration -> { + val parentDeclaration = this.parentDeclaration + if (parentDeclaration != null) { + return parentDeclaration.getClassDeclaration(visitorContext) + } + return visitorContext.resolver.getJavaClassByName(Object::class.java.name)!! + } + is KSPropertyDeclaration -> { + val parentDeclaration = this.parentDeclaration + if (parentDeclaration != null) { + return parentDeclaration.getClassDeclaration(visitorContext) + } + return visitorContext.resolver.getJavaClassByName(Object::class.java.name)!! + } else -> { return visitorContext.resolver.getJavaClassByName(Object::class.java.name)!! } diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/expressions/TestExpressionsInjectionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/expressions/TestExpressionsInjectionSpec.groovy new file mode 100644 index 00000000000..f2fbe8f08b9 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/expressions/TestExpressionsInjectionSpec.groovy @@ -0,0 +1,29 @@ +package io.micronaut.kotlin.processing.expressions + +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.buildContext + +class TestExpressionsInjectionSpec extends Specification { + void "test expression constructor injection"() { + given: + def ctx = buildContext(""" + package test; + + import jakarta.inject.Singleton; + import io.micronaut.context.annotation.Value; + + @Singleton + class Expr(@Value("#{ 25 }") val num : Int) + """) + + def type = ctx.classLoader.loadClass('test.Expr') + def bean = ctx.getBean(type) + + expect: + bean.num == 25 + + cleanup: + ctx.close() + } +} From 7664dd816127b53437593b20da36f102ba2e434c Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 20 Mar 2023 09:13:58 -0400 Subject: [PATCH 14/35] support conditional job scheduling as an example using expressions --- .../aop/chain/MethodInterceptorChain.java | 2 +- .../DefaultTaskExceptionHandler.java | 4 +- .../scheduling/TaskExceptionHandler.java | 18 ++ .../scheduling/annotation/Scheduled.java | 9 + .../processor/ScheduledMethodProcessor.java | 69 ++++--- .../micronaut/aop/writer/AopProxyWriter.java | 2 +- ...ltExpressionCompilationContextFactory.java | 3 +- .../ExpressionCompilationContextFactory.java | 5 +- .../access/ContextMethodParameterAccess.java | 3 + .../AbstractAnnotationMetadataBuilder.java | 1 - .../inject/writer/BeanDefinitionVisitor.java | 3 - .../core/annotation/AnnotationValue.java | 6 +- .../visitor/BeanIntrospectionSpec.groovy | 2 +- .../DefaultExecutableBeanContextBinder.java | 179 ++++++++++++++++++ .../bind/ExecutableBeanContextBinder.java | 45 +++++ .../EvaluatedAnnotationMetadata.java | 14 +- .../annotation/EvaluatedAnnotationValue.java | 61 ++++++ .../MappingAnnotationMetadataDelegate.java | 1 + .../guide/config/evaluatedExpressions.adoc | 30 ++- src/main/docs/guide/toc.yml | 2 +- .../docs/expressions/ExampleJob.groovy | 38 ++++ test-suite-kotlin/build.gradle | 2 +- .../micronaut/docs/expressions/ExampleJob.kt | 34 ++++ .../docs/expressions/ExampleJobTest.kt | 22 +++ .../docs/client/versioning/HelloClient.java | 2 +- .../docs/expressions/ExampleJob.java | 38 ++++ .../docs/expressions/ExampleJobTest.java | 25 +++ 27 files changed, 564 insertions(+), 56 deletions(-) create mode 100644 inject/src/main/java/io/micronaut/context/bind/DefaultExecutableBeanContextBinder.java create mode 100644 inject/src/main/java/io/micronaut/context/bind/ExecutableBeanContextBinder.java create mode 100644 inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJobTest.java diff --git a/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java b/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java index 089f54574a2..02c50aad48d 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java +++ b/aop/src/main/java/io/micronaut/aop/chain/MethodInterceptorChain.java @@ -101,7 +101,7 @@ public MethodInterceptorChain(Interceptor[] interceptors, T target, Execut @Override public AnnotationMetadata getAnnotationMetadata() { if (executionHandle.getAnnotationMetadata() instanceof EvaluatedAnnotationMetadata eam) { - return eam.copyWithArgs(originalParameters); + return eam.withArguments(originalParameters); } return executionHandle.getAnnotationMetadata(); } diff --git a/context/src/main/java/io/micronaut/scheduling/DefaultTaskExceptionHandler.java b/context/src/main/java/io/micronaut/scheduling/DefaultTaskExceptionHandler.java index 3e219baae90..3465d6dfa21 100644 --- a/context/src/main/java/io/micronaut/scheduling/DefaultTaskExceptionHandler.java +++ b/context/src/main/java/io/micronaut/scheduling/DefaultTaskExceptionHandler.java @@ -34,14 +34,14 @@ @Primary public class DefaultTaskExceptionHandler implements TaskExceptionHandler { - private static final Logger LOG = LoggerFactory.getLogger(DefaultTaskExceptionHandler.class); + static final Logger LOG = LoggerFactory.getLogger(DefaultTaskExceptionHandler.class); @Override public void handle(@Nullable Object bean, @NonNull Throwable throwable) { if (LOG.isErrorEnabled()) { StringBuilder message = new StringBuilder("Error invoking scheduled task "); if (bean != null) { - message.append("for bean [").append(bean.toString()).append("] "); + message.append("for bean [").append(bean).append("] "); } message.append(throwable.getMessage()); LOG.error(message.toString(), throwable); diff --git a/context/src/main/java/io/micronaut/scheduling/TaskExceptionHandler.java b/context/src/main/java/io/micronaut/scheduling/TaskExceptionHandler.java index 52fda32a852..c9e3da26ed1 100644 --- a/context/src/main/java/io/micronaut/scheduling/TaskExceptionHandler.java +++ b/context/src/main/java/io/micronaut/scheduling/TaskExceptionHandler.java @@ -16,6 +16,7 @@ package io.micronaut.scheduling; import io.micronaut.core.exceptions.BeanExceptionHandler; +import io.micronaut.inject.BeanDefinition; /** * An exception handler interface for task related exceptions. @@ -26,4 +27,21 @@ * @param The generic type of the exception */ public interface TaskExceptionHandler extends BeanExceptionHandler { + + /** + * Handle an error that occurs during creation of the scheduled task. + * @param beanType The bean type + * @param throwable The throwable + * @since 4.0.0 + */ + default void handleCreationFailure(BeanDefinition beanType, E throwable) { + if (DefaultTaskExceptionHandler.LOG.isErrorEnabled()) { + StringBuilder message = new StringBuilder("Error creating scheduled task "); + if (beanType != null) { + message.append("for bean [").append(beanType.asArgument()).append("] "); + } + message.append(throwable.getMessage()); + DefaultTaskExceptionHandler.LOG.error(message.toString(), throwable); + } + } } diff --git a/context/src/main/java/io/micronaut/scheduling/annotation/Scheduled.java b/context/src/main/java/io/micronaut/scheduling/annotation/Scheduled.java index 99f2788e215..879cfc8ed79 100644 --- a/context/src/main/java/io/micronaut/scheduling/annotation/Scheduled.java +++ b/context/src/main/java/io/micronaut/scheduling/annotation/Scheduled.java @@ -82,4 +82,13 @@ * {@link java.util.concurrent.ScheduledExecutorService} to use to schedule the task */ String scheduler() default TaskExecutors.SCHEDULED; + + /** + * A custom expression that can be used to indicate whether the job should run. + * Will be evaluated each time the job is scheduled to run and if the condition evaluates to false the job will not run. + * + * @return The condition + * @since 4.0.0 + */ + String condition() default ""; } diff --git a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java index b488ed69cd9..6a2fc9d4d5e 100644 --- a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java +++ b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java @@ -17,14 +17,18 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.context.BeanContext; +import io.micronaut.context.bind.DefaultExecutableBeanContextBinder; +import io.micronaut.context.bind.ExecutableBeanContextBinder; import io.micronaut.context.processor.ExecutableMethodProcessor; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.bind.BoundExecutable; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; import io.micronaut.core.util.StringUtils; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; +import io.micronaut.inject.annotation.EvaluatedAnnotationValue; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.scheduling.ScheduledExecutorTaskScheduler; import io.micronaut.scheduling.TaskExceptionHandler; @@ -64,6 +68,7 @@ public class ScheduledMethodProcessor implements ExecutableMethodProcessor beanDefinition, ExecutableMethod met TaskScheduler taskScheduler = optionalTaskScheduler.orElseThrow(() -> new SchedulerConfigurationException(method, "No scheduler of type TaskScheduler configured for name: " + scheduler)); Runnable task = () -> { - io.micronaut.context.Qualifier qualifer = beanDefinition - .getAnnotationTypeByStereotype(AnnotationUtil.QUALIFIER) - .map(type -> Qualifiers.byAnnotation(beanDefinition, type)) - .orElse(null); - - Class beanType = (Class) beanDefinition.getBeanType(); - Object bean = null; try { - bean = beanContext.getBean(beanType, qualifer); - if (method.getArguments().length == 0) { - ((ExecutableMethod) method).invoke(bean); + ExecutableBeanContextBinder binder = new DefaultExecutableBeanContextBinder(); + BoundExecutable boundExecutable = binder.bind(method, beanContext); + Object bean = beanContext.getBean(beanDefinition); + AnnotationValue finalAnnotationValue = scheduledAnnotation; + if (finalAnnotationValue instanceof EvaluatedAnnotationValue evaluated) { + finalAnnotationValue = evaluated.withArguments(boundExecutable.getBoundArguments()); } - } catch (Throwable e) { - io.micronaut.context.Qualifier qualifier = Qualifiers.byTypeArguments(beanType, e.getClass()); - Collection> definitions = beanContext.getBeanDefinitions(TaskExceptionHandler.class, qualifier); - Optional> mostSpecific = definitions.stream().filter(def -> { - List> typeArguments = def.getTypeArguments(TaskExceptionHandler.class); - if (typeArguments.size() == 2) { - return typeArguments.get(0).getType() == beanType && typeArguments.get(1).getType() == e.getClass(); + boolean shouldRun = finalAnnotationValue.booleanValue(MEMBER_CONDITION).orElse(true); + if (shouldRun) { + io.micronaut.context.Qualifier qualifier = beanDefinition + .getAnnotationTypeByStereotype(AnnotationUtil.QUALIFIER) + .map(type -> Qualifiers.byAnnotation(beanDefinition, type)) + .orElse(null); + + try { + ((BoundExecutable) boundExecutable).invoke(bean); + } catch (Throwable e) { + Class beanType = (Class) beanDefinition.getBeanType(); + handleException(beanType, bean, e); } - return false; - }).findFirst(); - - TaskExceptionHandler finalHandler = mostSpecific.map(bd -> beanContext.getBean(bd.getBeanType(), qualifier)).orElse(this.taskExceptionHandler); - finalHandler.handle(bean, e); + } + } catch (Exception e) { + TaskExceptionHandler finalHandler = findHandler(beanDefinition.getBeanType(), e); + finalHandler.handleCreationFailure(beanDefinition, e); } }; @@ -188,6 +193,26 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met } } + private void handleException(Class beanType, Object bean, Throwable e) { + TaskExceptionHandler finalHandler = findHandler(beanType, e); + finalHandler.handle(bean, e); + } + + private TaskExceptionHandler findHandler(Class beanType, Throwable e) { + io.micronaut.context.Qualifier qualifier = Qualifiers.byTypeArguments(beanType, e.getClass()); + Collection> definitions = beanContext.getBeanDefinitions(TaskExceptionHandler.class, qualifier); + Optional> mostSpecific = definitions.stream().filter(def -> { + List> typeArguments = def.getTypeArguments(TaskExceptionHandler.class); + if (typeArguments.size() == 2) { + return typeArguments.get(0).getType() == beanType && typeArguments.get(1).getType() == e.getClass(); + } + return false; + }).findFirst(); + + TaskExceptionHandler finalHandler = mostSpecific.map(bd -> beanContext.getBean(bd.getBeanType(), qualifier)).orElse(this.taskExceptionHandler); + return finalHandler; + } + @Override @PreDestroy public void close() { diff --git a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java index d2e72632290..9821128df9d 100644 --- a/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java +++ b/core-processor/src/main/java/io/micronaut/aop/writer/AopProxyWriter.java @@ -1619,12 +1619,12 @@ private void processAlreadyVisitedMethods(BeanDefinitionWriter parent) { * Method Reference class with names and a list of argument types. Used as the targets. */ private static final class MethodRef { + int methodIndex; private final String name; private final List argumentTypes; private final List genericArgumentTypes; private final Type returnType; private final List rawTypes; - int methodIndex; public MethodRef(String name, List parameterElements, Type returnType) { this.name = name; diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java index ac144b2bda8..0277f2c56a0 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/DefaultExpressionCompilationContextFactory.java @@ -40,11 +40,10 @@ @Internal public final class DefaultExpressionCompilationContextFactory implements ExpressionCompilationContextFactory { + private static final Collection CONTEXT_TYPES = ConcurrentHashMap.newKeySet(); private ExtensibleExpressionCompilationContext sharedContext; private final VisitorContext visitorContext; - private static final Collection CONTEXT_TYPES = ConcurrentHashMap.newKeySet(); - public DefaultExpressionCompilationContextFactory(VisitorContext visitorContext) { this.sharedContext = recreateContext(); this.visitorContext = visitorContext; diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java index 401e3a0ebc9..0fa2fe0a82d 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java @@ -16,16 +16,12 @@ package io.micronaut.expressions.context; import io.micronaut.core.annotation.Experimental; -import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.visitor.VisitorContext; -import java.io.Closeable; -import java.io.IOException; - /** * Factory interface for producing expression evaluation context. */ @@ -59,6 +55,7 @@ ExpressionCompilationContext buildContextForMethod(@NonNull EvaluatedExpressionR *

This method should be invoked from the {@link io.micronaut.inject.visitor.TypeElementVisitor#start(VisitorContext)} of a {@link io.micronaut.inject.visitor.TypeElementVisitor}

* * @param contextClass context class element + * @return This context factory */ ExpressionCompilationContextFactory registerContextClass(@NonNull ClassElement contextClass); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java index daf2647eb47..79c4b637cf2 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java @@ -56,6 +56,9 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { mv.push(parameterIndex); // invoke getArgument method mv.invokeInterface(EVALUATION_CONTEXT_TYPE, GET_ARGUMENT_METHOD); + if (nodeType != null) { + mv.checkCast(nodeType); + } } @Override diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 430b6dbec82..bf91cd3479d 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -25,7 +25,6 @@ import io.micronaut.core.annotation.AnnotationMetadataDelegate; import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.InstantiatedMember; import io.micronaut.core.annotation.Internal; diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java index 9c62c069b32..4244eaca666 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java @@ -16,12 +16,9 @@ package io.micronaut.inject.writer; import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Internal; import io.micronaut.core.util.Toggleable; -import io.micronaut.expressions.context.DefaultExpressionCompilationContextFactory; import io.micronaut.expressions.context.ExpressionWithContext; import io.micronaut.inject.BeanDefinition; -import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.FieldElement; diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java index 9c3d739b03c..b88b601c811 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationValue.java @@ -751,7 +751,7 @@ public OptionalInt intValue() { @Override public OptionalLong longValue(@NonNull String member) { - return longValue(member, null); + return longValue(member, valueMapper); } /** @@ -788,7 +788,7 @@ public OptionalLong longValue(@NonNull String member, @Nullable Function shortValue(@NonNull String member) { - return shortValue(member, null); + return shortValue(member, valueMapper); } /** @@ -971,7 +971,7 @@ public Optional stringValue() { @Override public Optional booleanValue(@NonNull String member) { - return booleanValue(member, null); + return booleanValue(member, valueMapper); } /** diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy index 0c55361084f..c8138a20684 100644 --- a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy @@ -1331,7 +1331,7 @@ class Test then:"The reference is valid" reference != null - reference.getBeanType() == EvaluatedExpressionContextTypeElementVisitor + reference.getBeanType() == io.micronaut.inject.beans.visitor.MappedSuperClassIntrospectionMapper } void "test write bean introspection data"() { diff --git a/inject/src/main/java/io/micronaut/context/bind/DefaultExecutableBeanContextBinder.java b/inject/src/main/java/io/micronaut/context/bind/DefaultExecutableBeanContextBinder.java new file mode 100644 index 00000000000..de546598a88 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/bind/DefaultExecutableBeanContextBinder.java @@ -0,0 +1,179 @@ +/* + * 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.context.bind; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.BeanContext; +import io.micronaut.context.Qualifier; +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.AnnotationUtil; +import io.micronaut.core.bind.ArgumentBinderRegistry; +import io.micronaut.core.bind.BoundExecutable; +import io.micronaut.core.bind.exceptions.UnsatisfiedArgumentException; +import io.micronaut.core.type.Argument; +import io.micronaut.core.type.Executable; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.inject.qualifiers.Qualifiers; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Implementation of {@link ExecutableBeanContextBinder}. + * + * @since 4.0.0 + */ +public final class DefaultExecutableBeanContextBinder implements ExecutableBeanContextBinder { + + @Override + public BoundExecutable bind(Executable target, ArgumentBinderRegistry registry, BeanContext source) throws UnsatisfiedArgumentException { + return bind(target, registry, source); + } + + @Override + public BoundExecutable tryBind(Executable target, ArgumentBinderRegistry registry, BeanContext source) { + Argument[] arguments = target.getArguments(); + if (arguments.length == 0) { + return new ContextBoundExecutable<>(target, ArrayUtils.EMPTY_OBJECT_ARRAY); + } + Object[] bound = new Object[arguments.length]; + for (int i = 0; i < arguments.length; i++) { + Argument argument = arguments[i]; + Optional v = argument.getAnnotationMetadata().stringValue(Value.class); + if (v.isPresent() && source instanceof ApplicationContext applicationContext) { + bound[i] = applicationContext.getEnvironment().getProperty(v.get(), argument) + .orElse(null); + } else { + v = argument.getAnnotationMetadata().stringValue(Property.class, "name"); + if (v.isPresent() && source instanceof ApplicationContext applicationContext) { + bound[i] = applicationContext.getEnvironment() + .getProperty(v.get(), argument).orElse(null); + } else { + bound[i] = source.findBean(argument, resolveQualifier(argument)) + .orElse(null); + } + } + } + return new ContextBoundExecutable<>(target, bound); + } + + @Override + public BoundExecutable bind(Executable target, BeanContext source) throws UnsatisfiedArgumentException { + Argument[] arguments = target.getArguments(); + if (arguments.length == 0) { + return new ContextBoundExecutable<>(target, ArrayUtils.EMPTY_OBJECT_ARRAY); + } + Object[] bound = new Object[arguments.length]; + for (int i = 0; i < arguments.length; i++) { + Argument argument = arguments[i]; + Optional v = argument.getAnnotationMetadata().stringValue(Value.class); + if (v.isPresent() && source instanceof ApplicationContext applicationContext) { + Optional finalV = v; + bound[i] = applicationContext.getEnvironment().getProperty(v.get(), argument) + .orElseThrow(() -> + new UnsatisfiedArgumentException(argument, "Unresolvable property specified to @Value: " + finalV.get()) + ); + } else { + v = argument.getAnnotationMetadata().stringValue(Property.class, "name"); + if (v.isPresent() && source instanceof ApplicationContext applicationContext) { + Optional finalV1 = v; + bound[i] = applicationContext.getEnvironment() + .getProperty(v.get(), argument).orElseThrow(() -> + new UnsatisfiedArgumentException(argument, "Unresolvable property specified to @Value: " + finalV1.get()) + ); + } else { + bound[i] = source.findBean(argument, resolveQualifier(argument)) + .orElseThrow(() -> + new UnsatisfiedArgumentException(argument, "Unresolvable bean argument: " + argument) + ); + } + } + } + return new ContextBoundExecutable<>(target, bound); + } + + /** + * Build a qualifier for the given argument. + * @param argument The argument + * @param The type + * @return The resolved qualifier + */ + @SuppressWarnings("unchecked") + private static Qualifier resolveQualifier(Argument argument) { + AnnotationMetadata annotationMetadata = Objects.requireNonNull(argument, "Argument cannot be null").getAnnotationMetadata(); + boolean hasMetadata = annotationMetadata != AnnotationMetadata.EMPTY_METADATA; + + List qualifierTypes = hasMetadata ? annotationMetadata.getAnnotationNamesByStereotype(AnnotationUtil.QUALIFIER) : Collections.emptyList(); + if (CollectionUtils.isNotEmpty(qualifierTypes)) { + if (qualifierTypes.size() == 1) { + return Qualifiers.byAnnotation( + annotationMetadata, + qualifierTypes.iterator().next() + ); + } else { + final Qualifier[] qualifiers = qualifierTypes + .stream().map((type) -> Qualifiers.byAnnotation(annotationMetadata, type)) + .toArray(Qualifier[]::new); + return Qualifiers.byQualifiers( + qualifiers + ); + } + } + return null; + } + + private record ContextBoundExecutable (Executable target, Object[] bound) implements BoundExecutable { + @Override + public Executable getTarget() { + return target; + } + + @Override + public R invoke(T instance) { + return target.invoke(instance, bound); + } + + @Override + public Object[] getBoundArguments() { + return bound; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ContextBoundExecutable that = (ContextBoundExecutable) o; + return target.equals(that.target) && Arrays.equals(bound, that.bound); + } + + @Override + public int hashCode() { + int result = Objects.hash(target); + result = 31 * result + Arrays.hashCode(bound); + return result; + } + } +} diff --git a/inject/src/main/java/io/micronaut/context/bind/ExecutableBeanContextBinder.java b/inject/src/main/java/io/micronaut/context/bind/ExecutableBeanContextBinder.java new file mode 100644 index 00000000000..de946970e15 --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/bind/ExecutableBeanContextBinder.java @@ -0,0 +1,45 @@ +/* + * 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.context.bind; + +import io.micronaut.context.BeanContext; +import io.micronaut.core.bind.BoundExecutable; +import io.micronaut.core.bind.ExecutableBinder; +import io.micronaut.core.bind.exceptions.UnsatisfiedArgumentException; +import io.micronaut.core.type.Executable; + +/** + * Sub-interface of {@link ExecutableBinder} that binds arguments from a {@link BeanContext}. + * + * @since 4.0.0 + */ +public interface ExecutableBeanContextBinder extends ExecutableBinder { + + /** + * Binds a given {@link Executable} using the given registry and source object. + * + * @param target The target executable + * @param source The bean context + * @param The executable target type + * @param The executable return type + * @return The bound executable + * @throws UnsatisfiedArgumentException When the executable could not be satisfied + */ + BoundExecutable bind( + Executable target, + BeanContext source + ) throws UnsatisfiedArgumentException; +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java index e25e921a25f..efecbfec77f 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationMetadata.java @@ -23,7 +23,6 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; -import io.micronaut.core.expressions.EvaluatedExpression; import io.micronaut.inject.BeanDefinition; import java.lang.annotation.Annotation; @@ -54,7 +53,7 @@ private EvaluatedAnnotationMetadata(AnnotationMetadata targetMetadata, * @param args arguments passed to method * @return copy of annotation metadata */ - public EvaluatedAnnotationMetadata copyWithArgs(Object[] args) { + public EvaluatedAnnotationMetadata withArguments(Object[] args) { return new EvaluatedAnnotationMetadata( delegateAnnotationMetadata, evaluationContext.setArguments(args)); @@ -94,15 +93,6 @@ public AnnotationMetadata getAnnotationMetadata() { @Override public AnnotationValue mapAnnotationValue(AnnotationValue av) { - return new AnnotationValue( - av, - AnnotationMetadataSupport.getDefaultValues(av.getAnnotationName()), - new EvaluatedConvertibleValuesMap<>(evaluationContext, av.getConvertibleValues()), - value -> { - if (value instanceof EvaluatedExpression expression) { - return expression.evaluate(evaluationContext); - } - return value; - }); + return new EvaluatedAnnotationValue<>(av, evaluationContext); } } diff --git a/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java new file mode 100644 index 00000000000..8e44eaf225b --- /dev/null +++ b/inject/src/main/java/io/micronaut/inject/annotation/EvaluatedAnnotationValue.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2020 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.inject.annotation; + +import io.micronaut.context.expressions.ConfigurableExpressionEvaluationContext; +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.core.expressions.EvaluatedExpression; + +import java.lang.annotation.Annotation; + +/** + * An EvaluatedAnnotationValue is a {@link AnnotationValue} that contains one or more expressions. + * + * @param The annotation + * @since 4.0.0 + */ +public class EvaluatedAnnotationValue extends AnnotationValue { + private final ConfigurableExpressionEvaluationContext evaluationContext; + private final AnnotationValue annotationValue; + + public EvaluatedAnnotationValue(AnnotationValue annotationValue, ConfigurableExpressionEvaluationContext evaluationContext) { + super( + annotationValue, + annotationValue.getDefaultValues(), + new EvaluatedConvertibleValuesMap<>(evaluationContext, annotationValue.getConvertibleValues()), + value -> { + if (value instanceof EvaluatedExpression expression) { + return expression.evaluate(evaluationContext); + } + return value; + } + ); + this.evaluationContext = evaluationContext; + this.annotationValue = annotationValue; + } + + /** + * Provide a copy of this annotation metadata with passed method arguments. + * + * @param args arguments passed to method + * @return copy of annotation metadata + */ + public EvaluatedAnnotationValue withArguments(Object[] args) { + return new EvaluatedAnnotationValue<>( + annotationValue, + evaluationContext.setArguments(args)); + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java b/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java index cc1500d7983..73839203225 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MappingAnnotationMetadataDelegate.java @@ -43,6 +43,7 @@ public abstract class MappingAnnotationMetadataDelegate implements AnnotationMetadataDelegate { public abstract AnnotationValue mapAnnotationValue(AnnotationValue av); + @Override public Optional stringValue(String annotation, String member) { return findAnnotation(annotation) diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index 1b973fe8fd5..9899fa4b44f 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -36,11 +36,20 @@ Note that, for security reasons expressions cannot be dynamically compiled at ru input. All expressions are compiled and checked statically during the compilation process of the application with errors reported as compilation failures. -In general, Evaluated Expression can be treated as statement written using a programming language with reduced +In general, expressions can be treated as statement written using a programming language with reduced set of available features. Even though the complexity of expression is only limited by the list of supported syntax 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 + +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: + +snippet::io.micronaut.docs.expressions.ExampleJob[title="Job Control with Expressions"] + +<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. + == Evaluated Expression Language Reference The Evaluated Expressions syntax supports the following functionality: @@ -173,6 +182,25 @@ limited. #{ 15 >= 16 ? 'a' : 'b' } // 'b' ---- +=== Dot and Safe Navigation Operator + +The dot operator can be use to access methods and properties of a value within an expression. For example: + +.Dot operator usage +[source] +---- +#{ collection.size() > 0 } +#{ foo.bar.name == "Fred" } +---- + +You can also use the safe dereference operator `?.` to navigate paths in a null safe way: + +.Safe dereference operator +[source] +---- +#{ foo?.bar?.name == "Fred" } +---- + === Type References A predefined syntax construct `T(...)` can be used to reference a class. The value inside brackets should be fully diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 079f3f32ab0..6333ddca0c2 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -47,6 +47,7 @@ config: environments: The Environment propertySource: Externalized Configuration with PropertySources valueAnnotation: Configuration Injection + evaluatedExpressions: Expression Language configurationProperties: Configuration Properties customTypeConverter: Custom Type Converters eachProperty: Using @EachProperty to Drive Configuration @@ -56,7 +57,6 @@ config: bootstrap: Bootstrap Configuration jmx: title: JMX Support - evaluatedExpressions: Micronaut Evaluated Expressions aop: title: Aspect Oriented Programming aroundAdvice: Around Advice diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy new file mode 100644 index 00000000000..ccc56309074 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy @@ -0,0 +1,38 @@ +package io.micronaut.docs.expressions + +import io.micronaut.scheduling.annotation.Scheduled +import jakarta.inject.Singleton + +@Singleton +class ExampleJob { + private boolean jobRan = false + + @Scheduled( + fixedRate = "1s", + condition = "#{ #jobControl.paused != true }") // <1> + void run(ExampleJobControl jobControl) { + System.out.println("Job Running") + this.jobRan = true + } + + boolean hasJobRun() { + return jobRan + } +} + +@Singleton +class ExampleJobControl { // <2> + private boolean paused = true + + boolean isPaused() { + return paused + } + + void unpause() { + paused = false + } + + void pause() { + paused = true + } +} diff --git a/test-suite-kotlin/build.gradle b/test-suite-kotlin/build.gradle index d54d5d3ae4b..fa2f4279127 100644 --- a/test-suite-kotlin/build.gradle +++ b/test-suite-kotlin/build.gradle @@ -43,7 +43,7 @@ dependencies { // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too testImplementation libs.junit.jupiter.api - + testImplementation libs.awaitility testImplementation platform(libs.test.boms.micronaut.validation) testImplementation (libs.micronaut.validation) { exclude group: 'io.micronaut' diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt new file mode 100644 index 00000000000..36ed26e4ab1 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt @@ -0,0 +1,34 @@ +package io.micronaut.docs.expressions + +import io.micronaut.scheduling.annotation.Scheduled +import jakarta.inject.Singleton + +@Singleton +class ExampleJob { + private var jobRan = false + @Scheduled( + fixedRate = "1s", + condition = "#{ #jobControl.paused != true }") // <1> + fun run(jobControl: ExampleJobControl) { + println("Job Running") + jobRan = true + } + + fun hasJobRun(): Boolean { + return jobRan + } +} + +@Singleton +class ExampleJobControl { // <2> + var isPaused = true + private set + + fun unpause() { + isPaused = false + } + + fun pause() { + isPaused = true + } +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt new file mode 100644 index 00000000000..a98fd6007ed --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt @@ -0,0 +1,22 @@ +package io.micronaut.docs.expressions + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.util.concurrent.Callable +import java.util.concurrent.TimeUnit + +@MicronautTest +class ExampleJobTest { + @Test + @Throws(InterruptedException::class) + fun testJobCondition(exampleJob: ExampleJob, exampleJobControl: ExampleJobControl) { + Assertions.assertTrue(exampleJobControl.isPaused) + Assertions.assertFalse(exampleJob.hasJobRun()) + Thread.sleep(5000) + Assertions.assertFalse(exampleJob.hasJobRun()) + exampleJobControl.unpause() + org.awaitility.Awaitility.await().atMost(3, TimeUnit.SECONDS).until(Callable { exampleJob.hasJobRun() }) + Assertions.assertTrue(exampleJob.hasJobRun()) + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/client/versioning/HelloClient.java b/test-suite/src/test/java/io/micronaut/docs/client/versioning/HelloClient.java index e20c9966830..09fc05378bc 100644 --- a/test-suite/src/test/java/io/micronaut/docs/client/versioning/HelloClient.java +++ b/test-suite/src/test/java/io/micronaut/docs/client/versioning/HelloClient.java @@ -26,7 +26,7 @@ // tag::clazz[] @Client("/hello") @Version("1") // <1> -public interface HelloClient { +public interface HelloClient { @Get("/greeting/{name}") String sayHello(String name); diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java new file mode 100644 index 00000000000..b34c7433efc --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java @@ -0,0 +1,38 @@ +package io.micronaut.docs.expressions; + +import io.micronaut.scheduling.annotation.Scheduled; +import jakarta.inject.Singleton; + +@Singleton +public class ExampleJob { + private boolean jobRan = false; + + @Scheduled( + fixedRate = "1s", + condition = "#{ #jobControl.paused != true }") // <1> + void run(ExampleJobControl jobControl) { + System.out.println("Job Running"); + this.jobRan = true; + } + + public boolean hasJobRun() { + return jobRan; + } +} + +@Singleton +class ExampleJobControl { // <2> + private boolean paused = true; + + public boolean isPaused() { + return paused; + } + + public void unpause() { + paused = false; + } + + public void pause() { + paused = true; + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJobTest.java b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJobTest.java new file mode 100644 index 00000000000..e061d3db904 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJobTest.java @@ -0,0 +1,25 @@ +package io.micronaut.docs.expressions; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@MicronautTest +public class ExampleJobTest { + + @Test + void testJobCondition(ExampleJob exampleJob, ExampleJobControl jobControl) throws InterruptedException { + assertTrue(jobControl.isPaused()); + assertFalse(exampleJob.hasJobRun()); + Thread.sleep(5000); + assertFalse(exampleJob.hasJobRun()); + jobControl.unpause(); + await().atMost(3, SECONDS).until(exampleJob::hasJobRun); + assertTrue(exampleJob.hasJobRun()); + } +} From 87d953195314c69c3ce4d62414f1a1796e9437f9 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 20 Mar 2023 10:26:01 -0400 Subject: [PATCH 15/35] fix tests --- .../processor/ScheduledMethodProcessor.java | 15 +++++---------- .../io/micronaut/docs/expressions/ExampleJob.kt | 7 +++---- .../micronaut/docs/expressions/ExampleJobTest.kt | 3 +-- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java index 6a2fc9d4d5e..372f6b8447e 100644 --- a/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java +++ b/context/src/main/java/io/micronaut/scheduling/processor/ScheduledMethodProcessor.java @@ -17,10 +17,10 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.context.BeanContext; +import io.micronaut.context.Qualifier; import io.micronaut.context.bind.DefaultExecutableBeanContextBinder; import io.micronaut.context.bind.ExecutableBeanContextBinder; import io.micronaut.context.processor.ExecutableMethodProcessor; -import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.bind.BoundExecutable; import io.micronaut.core.convert.ConversionService; @@ -117,28 +117,23 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met } TaskScheduler taskScheduler = optionalTaskScheduler.orElseThrow(() -> new SchedulerConfigurationException(method, "No scheduler of type TaskScheduler configured for name: " + scheduler)); - + Argument beanType = (Argument) beanDefinition.asArgument(); + Qualifier declaredQualifier = (Qualifier) beanDefinition.getDeclaredQualifier(); Runnable task = () -> { try { ExecutableBeanContextBinder binder = new DefaultExecutableBeanContextBinder(); BoundExecutable boundExecutable = binder.bind(method, beanContext); - Object bean = beanContext.getBean(beanDefinition); + Object bean = beanContext.getBean(beanType, declaredQualifier); AnnotationValue finalAnnotationValue = scheduledAnnotation; if (finalAnnotationValue instanceof EvaluatedAnnotationValue evaluated) { finalAnnotationValue = evaluated.withArguments(boundExecutable.getBoundArguments()); } boolean shouldRun = finalAnnotationValue.booleanValue(MEMBER_CONDITION).orElse(true); if (shouldRun) { - io.micronaut.context.Qualifier qualifier = beanDefinition - .getAnnotationTypeByStereotype(AnnotationUtil.QUALIFIER) - .map(type -> Qualifiers.byAnnotation(beanDefinition, type)) - .orElse(null); - try { ((BoundExecutable) boundExecutable).invoke(bean); } catch (Throwable e) { - Class beanType = (Class) beanDefinition.getBeanType(); - handleException(beanType, bean, e); + handleException(beanType.getType(), bean, e); } } } catch (Exception e) { diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt index 36ed26e4ab1..1bfac6558ba 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt @@ -21,14 +21,13 @@ class ExampleJob { @Singleton class ExampleJobControl { // <2> - var isPaused = true - private set + var paused : Boolean = true fun unpause() { - isPaused = false + paused = false } fun pause() { - isPaused = true + paused = true } } diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt index a98fd6007ed..21620a078fa 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJobTest.kt @@ -9,9 +9,8 @@ import java.util.concurrent.TimeUnit @MicronautTest class ExampleJobTest { @Test - @Throws(InterruptedException::class) fun testJobCondition(exampleJob: ExampleJob, exampleJobControl: ExampleJobControl) { - Assertions.assertTrue(exampleJobControl.isPaused) + Assertions.assertTrue(exampleJobControl.paused) Assertions.assertFalse(exampleJob.hasJobRun()) Thread.sleep(5000) Assertions.assertFalse(exampleJob.hasJobRun()) From ee0a4d89d9a3b25a0ad432a9f457cadfc36cc1af Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 20 Mar 2023 11:26:32 -0400 Subject: [PATCH 16/35] fix tests --- ...ontextPropertyAccessExpressionsSpec.groovy | 71 -------------- ...ontextPropertyAccessExpressionsSpec.groovy | 92 +++++++++++++++++++ 2 files changed, 92 insertions(+), 71 deletions(-) diff --git a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy index c874c62b782..0ff25cb888c 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -58,76 +58,5 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS expr4 instanceof String && expr4 == "custom property" } - void "test multi-level context property access"() { - given: - Object expr = evaluateAgainstContext("#{ #foo.bar.name }", - """ - @jakarta.inject.Singleton - class Context { - Foo getFoo() { - return new Foo(); - } - } - - class Foo { - Bar bar = new Bar() - } - - class Bar { - String name = "test" - } - """) - - expect: - expr instanceof String && expr == "test" - } - - void "test multi-level context property access safe navigation"() { - given: - Object expr = evaluateAgainstContext("#{ #foo?.bar?.name }", - """ - @jakarta.inject.Singleton - class Context { - Foo getFoo() { - return new Foo(); - } - } - class Foo { - Bar bar - } - - class Bar { - String name = "test" - } - """) - - expect: - expr == null - } - - void "test multi-level context property access non-safe navigation"() { - when: - Object expr = evaluateAgainstContext("#{ #foo.bar.name }", - """ - @jakarta.inject.Singleton - class Context { - Foo getFoo() { - return new Foo(); - } - } - - class Foo { - Bar bar - } - - class Bar { - String name = "test" - } - """) - - then: - def e = thrown(ExpressionEvaluationException) - e.message.startsWith('Can not evaluate expression [null]') - } } 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 e953ebbe01e..63a8d62a686 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.expressions import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec +import io.micronaut.context.exceptions.ExpressionEvaluationException class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsSpec { @@ -80,4 +81,95 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS expect: expr instanceof String && expr == "test" } + + void "test multi-level context property access"() { + given: + Object expr = evaluateAgainstContext("#{ #foo.bar.name }", + """ + @jakarta.inject.Singleton + class Context { + public Foo getFoo() { + return new Foo(); + } + } + + class Foo { + private Bar bar = new Bar(); + public Bar getBar() { + return bar; + } + } + + class Bar { + private String name = "test"; + public String getName() { + return name; + } + } + """) + + expect: + expr instanceof String && expr == "test" + } + + void "test multi-level context property access safe navigation"() { + given: + Object expr = evaluateAgainstContext("#{ #foo?.bar?.name }", + """ + @jakarta.inject.Singleton + class Context { + public Foo getFoo() { + return new Foo(); + } + } + + class Foo { + private Bar bar; + public Bar getBar() { + return bar; + } + } + + class Bar { + private String name = "test"; + public String getName() { + return name; + } + } + """) + + expect: + expr == null + } + + void "test multi-level context property access non-safe navigation"() { + when: + Object expr = evaluateAgainstContext("#{ #foo.bar.name }", + """ + @jakarta.inject.Singleton + class Context { + public Foo getFoo() { + return new Foo(); + } + } + + class Foo { + private Bar bar; + public Bar getBar() { + return bar; + } + } + + class Bar { + private String name = "test"; + public String getName() { + return name; + } + } + """) + + then: + def e = thrown(ExpressionEvaluationException) + e.message.startsWith('Can not evaluate expression [null]') + } } From 13ea9e1ff3faa5e3707a62dc1c3b071ba4517f55 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 20 Mar 2023 14:02:10 -0400 Subject: [PATCH 17/35] don't require # before identifier references --- ...gleEvaluatedEvaluatedExpressionParser.java | 31 +++++++------------ .../expressions/parser/token/TokenType.java | 2 +- .../expressions/parser/token/Tokenizer.java | 7 +++-- ...ontextPropertyAccessExpressionsSpec.groovy | 16 +++++----- .../docs/expressions/ExampleJob.groovy | 2 +- .../micronaut/docs/expressions/ExampleJob.kt | 2 +- .../docs/expressions/ExampleJob.java | 2 +- 7 files changed, 28 insertions(+), 34 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java index b2e5f15b0ef..7641878f669 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java @@ -68,7 +68,7 @@ import static io.micronaut.expressions.parser.token.TokenType.FLOAT; import static io.micronaut.expressions.parser.token.TokenType.GT; import static io.micronaut.expressions.parser.token.TokenType.GTE; -import static io.micronaut.expressions.parser.token.TokenType.IDENTIFIER; +import static io.micronaut.expressions.parser.token.TokenType.TYPE_IDENTIFIER; import static io.micronaut.expressions.parser.token.TokenType.INCREMENT; import static io.micronaut.expressions.parser.token.TokenType.INSTANCEOF; import static io.micronaut.expressions.parser.token.TokenType.INT; @@ -344,7 +344,8 @@ private ExpressionNode postfixExpression() { private ExpressionNode primaryExpression() { return switch (lookahead.type()) { case EXPRESSION_CONTEXT_REF -> contextAccess(); - case IDENTIFIER -> typeIdentifier(); + case IDENTIFIER -> identifier(); + case TYPE_IDENTIFIER -> typeIdentifier(); case L_PAREN -> parenthesizedExpression(); case STRING, INT, LONG, DOUBLE, FLOAT, BOOL, NULL -> literal(); default -> throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); @@ -357,13 +358,11 @@ private ExpressionNode primaryExpression() { // ; private ExpressionNode contextAccess() { eat(EXPRESSION_CONTEXT_REF); - String identifier = identifier(); - - // Ambiguous case when 'T(' can be considered as method call - if (identifier.equals("T") && lookahead != null && lookahead.type() == L_PAREN) { - throw new ExpressionParsingException("Expected identifier, got type reference"); - } + return primaryExpression(); + } + private ExpressionNode identifier() { + String identifier = eat(TokenType.IDENTIFIER).value(); if (lookahead != null && lookahead.type() == L_PAREN) { List methodArguments = methodArguments(); return new ContextMethodCall(identifier, methodArguments); @@ -376,7 +375,7 @@ private ExpressionNode contextAccess() { // | SimpleIdentifier MethodArguments // ; private ExpressionNode methodOrPropertyAccess(ExpressionNode callee, boolean nullSafe) { - String identifier = identifier(); + String identifier = eat(TokenType.IDENTIFIER).value(); if (lookahead != null && lookahead.type() == L_PAREN) { List methodArguments = methodArguments(); return new ElementMethodCall(callee, identifier, methodArguments, nullSafe); @@ -418,24 +417,18 @@ private List methodArgumentsList() { // TypeReference // : 'T(' ChainedIdentifier')' // ; - private TypeIdentifier typeIdentifier() { - eat(IDENTIFIER); - eat(L_PAREN); + private TypeIdentifier typeIdentifier() { + eat(TYPE_IDENTIFIER); List parts = new ArrayList<>(); - parts.add(identifier()); + parts.add(eat(TokenType.IDENTIFIER).value()); while (lookahead != null && lookahead.type() == DOT) { eat(DOT); - parts.add(identifier()); + parts.add(eat(TokenType.IDENTIFIER).value()); } eat(R_PAREN); return new TypeIdentifier(String.join(".", parts)); } - private String identifier() { - Token token = eat(IDENTIFIER); - return token.value(); - } - // ParenthesizedExpression // : '(' Expression ')' // ; diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java index dc4b5274b45..1e563ea67da 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java @@ -27,7 +27,7 @@ public enum TokenType { WHITESPACE, IDENTIFIER, - + TYPE_IDENTIFIER, EXPRESSION_CONTEXT_REF, DOT, SAFE_NAV, diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java index 636a8d30b85..9ca7aadd4d5 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java @@ -35,10 +35,11 @@ import static io.micronaut.expressions.parser.token.TokenType.DOUBLE; import static io.micronaut.expressions.parser.token.TokenType.EQ; import static io.micronaut.expressions.parser.token.TokenType.EXPRESSION_CONTEXT_REF; +import static io.micronaut.expressions.parser.token.TokenType.IDENTIFIER; import static io.micronaut.expressions.parser.token.TokenType.FLOAT; import static io.micronaut.expressions.parser.token.TokenType.GT; import static io.micronaut.expressions.parser.token.TokenType.GTE; -import static io.micronaut.expressions.parser.token.TokenType.IDENTIFIER; +import static io.micronaut.expressions.parser.token.TokenType.TYPE_IDENTIFIER; import static io.micronaut.expressions.parser.token.TokenType.INCREMENT; import static io.micronaut.expressions.parser.token.TokenType.INSTANCEOF; import static io.micronaut.expressions.parser.token.TokenType.INT; @@ -147,8 +148,8 @@ public final class Tokenizer { "^\\^", POW, // IDENTIFIERS - "^\\w+", IDENTIFIER - ); + "^T\\(", TYPE_IDENTIFIER, + "\\w+", IDENTIFIER); private static final List PATTERNS = TOKENS.entrySet() 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 63a8d62a686..80198df2750 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ContextPropertyAccessExpressionsSpec.groovy @@ -7,7 +7,7 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS void "test context property access"() { given: - Object expr1 = evaluateAgainstContext("#{ #intValue }", + Object expr1 = evaluateAgainstContext("#{intValue}", """ @jakarta.inject.Singleton class Context { @@ -17,7 +17,7 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS } """) - Object expr2 = evaluateAgainstContext("#{ #boolean }", + Object expr2 = evaluateAgainstContext("#{ boolean }", """ @jakarta.inject.Singleton class Context { @@ -27,7 +27,7 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS } """) - Object expr3 = evaluateAgainstContext("#{ #stringValue }", + Object expr3 = evaluateAgainstContext("#{ stringValue }", """ @jakarta.inject.Singleton class Context { @@ -37,7 +37,7 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS } """) - Object expr4 = evaluateAgainstContext("#{ #customClass.customProperty }", + Object expr4 = evaluateAgainstContext("#{ customClass.customProperty }", """ @jakarta.inject.Singleton class Context { @@ -62,7 +62,7 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS void "test multi-level context property access - records"() { given: - Object expr = evaluateAgainstContext("#{ #foo.bar.name }", + Object expr = evaluateAgainstContext("#{ foo.bar.name }", """ @jakarta.inject.Singleton class Context { @@ -84,7 +84,7 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS void "test multi-level context property access"() { given: - Object expr = evaluateAgainstContext("#{ #foo.bar.name }", + Object expr = evaluateAgainstContext("#{ foo.bar.name }", """ @jakarta.inject.Singleton class Context { @@ -114,7 +114,7 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS void "test multi-level context property access safe navigation"() { given: - Object expr = evaluateAgainstContext("#{ #foo?.bar?.name }", + Object expr = evaluateAgainstContext("#{ foo?.bar?.name }", """ @jakarta.inject.Singleton class Context { @@ -144,7 +144,7 @@ class ContextPropertyAccessExpressionsSpec extends AbstractEvaluatedExpressionsS void "test multi-level context property access non-safe navigation"() { when: - Object expr = evaluateAgainstContext("#{ #foo.bar.name }", + Object expr = evaluateAgainstContext("#{ foo.bar.name }", """ @jakarta.inject.Singleton class Context { diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy index ccc56309074..81592b70482 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy @@ -9,7 +9,7 @@ class ExampleJob { @Scheduled( fixedRate = "1s", - condition = "#{ #jobControl.paused != true }") // <1> + condition = "#{!jobControl.paused}") // <1> void run(ExampleJobControl jobControl) { System.out.println("Job Running") this.jobRan = true diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt index 1bfac6558ba..d1d4335a395 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt @@ -8,7 +8,7 @@ class ExampleJob { private var jobRan = false @Scheduled( fixedRate = "1s", - condition = "#{ #jobControl.paused != true }") // <1> + condition = "#{!jobControl.paused}") // <1> fun run(jobControl: ExampleJobControl) { println("Job Running") jobRan = true diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java index b34c7433efc..fb2e180d9b1 100644 --- a/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/ExampleJob.java @@ -9,7 +9,7 @@ public class ExampleJob { @Scheduled( fixedRate = "1s", - condition = "#{ #jobControl.paused != true }") // <1> + condition = "#{!jobControl.paused}") // <1> void run(ExampleJobControl jobControl) { System.out.println("Job Running"); this.jobRan = true; From d8f2d00a1e56a9d11dad698e6d0dd589659c46a6 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 21 Mar 2023 14:37:22 +0200 Subject: [PATCH 18/35] Add list, map and array dereferencing --- ...gleEvaluatedEvaluatedExpressionParser.java | 31 +++++++ .../parser/ast/ExpressionNode.java | 25 ++++++ .../parser/ast/access/AbstractMethodCall.java | 10 ++- .../parser/ast/access/CandidateMethod.java | 7 ++ .../ast/access/ContextElementAccess.java | 38 +++++++++ .../access/ContextMethodParameterAccess.java | 9 +- .../parser/ast/access/ListOrArrayAccess.java | 85 +++++++++++++++++++ .../parser/ast/access/MapAccess.java | 77 +++++++++++++++++ .../ast/collection/OneDimensionalArray.java | 10 ++- .../ast/conditional/TernaryExpression.java | 5 ++ .../parser/ast/literal/BoolLiteral.java | 7 ++ .../parser/ast/literal/DoubleLiteral.java | 7 ++ .../parser/ast/literal/FloatLiteral.java | 7 ++ .../parser/ast/literal/IntLiteral.java | 14 +++ .../parser/ast/literal/LongLiteral.java | 7 ++ .../parser/ast/literal/NullLiteral.java | 6 ++ .../parser/ast/literal/StringLiteral.java | 7 ++ .../ast/operator/binary/BinaryOperator.java | 12 +++ .../operator/binary/InstanceofOperator.java | 7 ++ .../ast/operator/binary/MatchesOperator.java | 7 ++ .../ast/operator/unary/UnaryOperator.java | 12 +++ .../parser/ast/types/TypeIdentifier.java | 17 ++++ .../CollectionExpressionSpec.groovy | 60 +++++++++++++ .../ContextMethodCallsExpressionsSpec.groovy | 14 +-- 24 files changed, 470 insertions(+), 11 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ListOrArrayAccess.java create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/MapAccess.java create mode 100644 inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java index 7641878f669..b7521cf3543 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java @@ -20,6 +20,8 @@ import io.micronaut.expressions.parser.ast.access.ContextElementAccess; import io.micronaut.expressions.parser.ast.access.ContextMethodCall; import io.micronaut.expressions.parser.ast.access.ElementMethodCall; +import io.micronaut.expressions.parser.ast.access.ListOrArrayAccess; +import io.micronaut.expressions.parser.ast.access.MapAccess; import io.micronaut.expressions.parser.ast.access.PropertyAccess; import io.micronaut.expressions.parser.ast.conditional.TernaryExpression; import io.micronaut.expressions.parser.ast.literal.BoolLiteral; @@ -68,6 +70,7 @@ import static io.micronaut.expressions.parser.token.TokenType.FLOAT; import static io.micronaut.expressions.parser.token.TokenType.GT; import static io.micronaut.expressions.parser.token.TokenType.GTE; +import static io.micronaut.expressions.parser.token.TokenType.R_SQUARE; import static io.micronaut.expressions.parser.token.TokenType.TYPE_IDENTIFIER; import static io.micronaut.expressions.parser.token.TokenType.INCREMENT; import static io.micronaut.expressions.parser.token.TokenType.INSTANCEOF; @@ -328,6 +331,9 @@ private ExpressionNode postfixExpression() { } else if (tokenType == SAFE_NAV) { eat(SAFE_NAV); leftNode = methodOrPropertyAccess(leftNode, true); + } else if (tokenType == L_SQUARE) { + eat(L_SQUARE); + leftNode = mapOrListAccess(leftNode); } else { throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); } @@ -383,6 +389,31 @@ private ExpressionNode methodOrPropertyAccess(ExpressionNode callee, boolean nul return new PropertyAccess(callee, identifier, nullSafe); } + // MapOrListAccess + // : SimpleIdentifier + // | SimpleIdentifier [] + // ; + private ExpressionNode mapOrListAccess(ExpressionNode callee) { + if (lookahead != null) { + if (lookahead.type() == INT) { + ListOrArrayAccess listOrArrayAccess = new ListOrArrayAccess( + callee, + intLiteral().getValue() + ); + eat(R_SQUARE); + return listOrArrayAccess; + } else if (lookahead.type() == STRING) { + MapAccess mapAccess = new MapAccess(callee, stringLiteral().getValue()); + eat(R_SQUARE); + return mapAccess; + } else { + throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); + } + } else { + throw new ExpressionParsingException("Unclosed subscript operator"); + } + } + // MethodArguments: // '(' MethodArgumentsList ')' // ; diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java index 824d1a4399a..2134b16e79d 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/ExpressionNode.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; import org.objectweb.asm.Type; /** @@ -30,6 +31,7 @@ public abstract class ExpressionNode { protected Type nodeType; + protected ClassElement classElement; /** * Compiles this expression AST node against passes compilation context. @@ -65,6 +67,29 @@ public final Type resolveType(@NonNull ExpressionVisitorContext ctx) { return nodeType; } + /** + * On resolution stage type information is collected and node validity is checked. Once type + * is resolved, type resolution result is cached. + * + * @param ctx expression compilation context + * + * @return resolved type + */ + @NonNull + public final ClassElement resolveClassElement(@NonNull ExpressionVisitorContext ctx) { + if (classElement == null) { + classElement = doResolveClassElement(ctx); + } + return classElement; + } + + /** + * Resolves the class element for this node. + * @param ctx The expression compilation context + * @return The resolved type + */ + protected abstract ClassElement doResolveClassElement(ExpressionVisitorContext ctx); + /** * Resolves expression AST node type. * diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java index 13e9e2936fa..bc88b659388 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/AbstractMethodCall.java @@ -60,10 +60,18 @@ public AbstractMethodCall(String name, @Override protected Type doResolveType(ExpressionVisitorContext ctx) { - usedMethod = resolveUsedMethod(ctx); + if (usedMethod == null) { + usedMethod = resolveUsedMethod(ctx); + } return usedMethod.getReturnType(); } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + doResolveType(ctx); + return usedMethod.getMethodElement().getGenericReturnType(); + } + /** * Resolves single {@link CandidateMethod} used by this AST node. * diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/CandidateMethod.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/CandidateMethod.java index d4942f55b43..43102694993 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/CandidateMethod.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/CandidateMethod.java @@ -62,6 +62,13 @@ public CandidateMethod(MethodElement methodElement) { this(methodElement, Collections.emptyList()); } + /** + * @return The method element. + */ + public MethodElement getMethodElement() { + return methodElement; + } + /** * Whether candidate method is vargars method. * diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java index 1e51380bbce..abe34c0dc7b 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextElementAccess.java @@ -20,6 +20,7 @@ import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ParameterElement; import io.micronaut.inject.ast.PropertyElement; import org.objectweb.asm.Type; @@ -61,6 +62,43 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { } } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + ExpressionCompilationContext evaluationContext = ctx.compilationContext(); + + List propertyElements = evaluationContext.findProperties(name); + List parameterElements = evaluationContext.findParameters(name); + + int totalElements = propertyElements.size() + parameterElements.size(); + + if (totalElements == 0) { + throw new ExpressionCompilationException( + "No element with name [" + name + "] available in evaluation context"); + } else if (totalElements > 1) { + throw new ExpressionCompilationException( + "Ambiguous expression evaluation context reference. Found " + totalElements + + " elements with name [" + name + "]"); + } + + if (!propertyElements.isEmpty()) { + + PropertyElement property = propertyElements.iterator().next(); + String readMethodName = + property.getReadMethod() + .orElseThrow(() -> new ExpressionCompilationException( + "Failed to obtain read method for property [" + name + "]")) + .getName(); + + contextPropertyMethodCall = new ContextMethodCall(readMethodName, emptyList()); + return contextPropertyMethodCall.resolveClassElement(ctx); + + } + + ParameterElement parameter = parameterElements.iterator().next(); + contextMethodParameterAccess = new ContextMethodParameterAccess(parameter); + return contextMethodParameterAccess.resolveClassElement(ctx); + } + @Override public Type doResolveType(ExpressionVisitorContext ctx) { ExpressionCompilationContext evaluationContext = ctx.compilationContext(); diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java index 79c4b637cf2..16e72d1afe1 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodParameterAccess.java @@ -20,6 +20,7 @@ import io.micronaut.expressions.parser.ast.util.TypeDescriptors; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ParameterElement; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; @@ -62,7 +63,7 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { } @Override - protected Type doResolveType(ExpressionVisitorContext ctx) { + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { String parameterName = parameterElement.getName(); ParameterElement[] methodParameters = parameterElement.getMethodElement().getParameters(); @@ -81,6 +82,12 @@ protected Type doResolveType(ExpressionVisitorContext ctx) { } this.parameterIndex = paramIndex; + return parameterElement.getGenericType(); + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + doResolveClassElement(ctx); return getTypeReference(parameterElement.getType()); } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ListOrArrayAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ListOrArrayAccess.java new file mode 100644 index 00000000000..81c93ad510f --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ListOrArrayAccess.java @@ -0,0 +1,85 @@ +/* + * 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.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.processing.JavaModelUtils; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.List; + +/** + * Handles list and array de-referencing. + */ +@Internal +public class ListOrArrayAccess extends ExpressionNode { + + private static final Method GET_METHOD = Method.getMethod( + ReflectionUtils.getRequiredMethod(List.class, "get", int.class) + ); + + private final ExpressionNode callee; + private final int index; + private boolean isArray = false; + + public ListOrArrayAccess(ExpressionNode callee, int index) { + this.callee = callee; + this.index = index; + } + + @Override + protected void generateBytecode(ExpressionVisitorContext ctx) { + callee.compile(ctx); + GeneratorAdapter methodVisitor = ctx.methodVisitor(); + methodVisitor.push(index); + if (isArray) { + methodVisitor.arrayLoad(resolveType(ctx)); + } else { + methodVisitor.invokeInterface( + Type.getType(List.class), + GET_METHOD + ); + } + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + ClassElement classElement = callee.resolveClassElement(ctx); + this.isArray = classElement.isArray(); + if (!classElement.isAssignable(List.class) && !isArray) { + throw new ExpressionCompilationException("Invalid subscript operator. Subscript operator can only be applied to maps, lists and arrays"); + } + if (isArray) { + return classElement.fromArray(); + } else { + return classElement.getFirstTypeArgument() + .orElseGet(() -> ClassElement.of(Object.class)); + } + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + ClassElement listElement = resolveClassElement(ctx); + return JavaModelUtils.getTypeReference(listElement); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/MapAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/MapAccess.java new file mode 100644 index 00000000000..1e92738c4ab --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/MapAccess.java @@ -0,0 +1,77 @@ +/* + * 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.expressions.parser.ast.access; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.processing.JavaModelUtils; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.Map; + +/** + * Handles map de-referencing. + */ +@Internal +public class MapAccess extends ExpressionNode { + private static final Method GET_METHOD = Method.getMethod( + ReflectionUtils.getRequiredMethod(Map.class, "get", Object.class) + ); + + private final ExpressionNode callee; + private final String index; + + public MapAccess(ExpressionNode callee, String index) { + this.callee = callee; + this.index = index; + } + + @Override + protected void generateBytecode(ExpressionVisitorContext ctx) { + callee.compile(ctx); + GeneratorAdapter methodVisitor = ctx.methodVisitor(); + methodVisitor.push(index); + methodVisitor.invokeInterface( + Type.getType(Map.class), + GET_METHOD + ); + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + ClassElement classElement = callee.resolveClassElement(ctx); + if (!classElement.isAssignable(Map.class)) { + throw new ExpressionCompilationException("Invalid subscript operator. Subscript operator can only be applied to maps, lists and arrays"); + } + Map typeArguments = classElement.getTypeArguments(); + if (typeArguments.containsKey("V")) { + return typeArguments.get("V"); + } + return classElement; + } + + @Override + protected Type doResolveType(ExpressionVisitorContext ctx) { + ClassElement mapElement = resolveClassElement(ctx); + return JavaModelUtils.getTypeReference(mapElement); + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java index 96067db6844..1f1168227ee 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/collection/OneDimensionalArray.java @@ -19,6 +19,7 @@ import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.ast.types.TypeIdentifier; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; @@ -81,9 +82,14 @@ public void generateBytecode(ExpressionVisitorContext ctx) { } } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return getRequiredClassElement(elementTypeIdentifier.resolveType(ctx), ctx.visitorContext()) + .toArray(); + } + @Override protected Type doResolveType(ExpressionVisitorContext ctx) { - return getTypeReference(getRequiredClassElement(elementTypeIdentifier.resolveType(ctx), ctx.visitorContext()) - .toArray()); + return getTypeReference(doResolveClassElement(ctx)); } } 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 25ec177de59..b4ddd0c1601 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 @@ -100,6 +100,11 @@ public void generateBytecode(ExpressionVisitorContext ctx) { mv.visitLabel(returnLabel); } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return ClassElement.of(doResolveType(ctx).getClassName()); + } + @Override protected Type doResolveType(ExpressionVisitorContext ctx) { if (!isOneOf(condition.resolveType(ctx), BOOLEAN, BOOLEAN_WRAPPER)) { diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java index 8bc0f74d6d6..aabdcc0d0de 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/BoolLiteral.java @@ -18,6 +18,8 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; @@ -41,6 +43,11 @@ public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(value); } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.BOOLEAN; + } + @Override protected Type doResolveType(ExpressionVisitorContext ctx) { return BOOLEAN; diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java index e27c0c91c0f..9ad9b922274 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/DoubleLiteral.java @@ -18,6 +18,8 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.DOUBLE; @@ -41,6 +43,11 @@ public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(value); } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.DOUBLE; + } + @Override protected Type doResolveType(ExpressionVisitorContext ctx) { return DOUBLE; diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java index fff4c7e8937..1e06c17d0a5 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/FloatLiteral.java @@ -18,6 +18,8 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.FLOAT; @@ -41,6 +43,11 @@ public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(value); } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.FLOAT; + } + @Override protected Type doResolveType(ExpressionVisitorContext ctx) { return FLOAT; diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java index 4fa2cc1dc29..a09355e01b9 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/IntLiteral.java @@ -18,6 +18,8 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.INT; @@ -42,8 +44,20 @@ public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(value); } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.INT; + } + @Override protected Type doResolveType(ExpressionVisitorContext ctx) { return INT; } + + /** + * @return The value + */ + public int getValue() { + return value; + } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java index 08941f1f41b..729a0326c21 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/LongLiteral.java @@ -18,6 +18,8 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.LONG; @@ -41,6 +43,11 @@ public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(value); } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.LONG; + } + @Override protected Type doResolveType(ExpressionVisitorContext ctx) { return LONG; diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java index 6640cd66b63..a89f43c1033 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/literal/NullLiteral.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.OBJECT; @@ -36,6 +37,11 @@ public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().visitInsn(ACONST_NULL); } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return ClassElement.of(Object.class); + } + @Override protected Type doResolveType(ExpressionVisitorContext ctx) { return OBJECT; 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 a781eb0e033..7c6cc6ac57b 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 @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; import org.objectweb.asm.Type; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.STRING; @@ -31,6 +32,7 @@ @Internal public final class StringLiteral extends ExpressionNode { + private static final ClassElement STRING_ELEMENT = ClassElement.of(String.class); private final String value; public StringLiteral(String value) { @@ -46,6 +48,11 @@ public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(value); } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return STRING_ELEMENT; + } + @Override protected Type doResolveType(ExpressionVisitorContext ctx) { return STRING; diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java index 2503a117b9b..1d34535ad01 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java @@ -18,6 +18,8 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; import org.objectweb.asm.Type; /** @@ -48,6 +50,16 @@ protected Type doResolveType(ExpressionVisitorContext ctx) { return resolveOperationType(leftType, rightType); } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + Type type = doResolveType(ctx); + try { + return PrimitiveElement.valueOf(type.getClassName()); + } catch (IllegalArgumentException e) { + return ClassElement.of(type.getClassName()); + } + } + protected abstract Type resolveOperationType(Type leftOperandType, Type rightOperandType); } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java index 573b19d558a..2eecb7d9c3c 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/InstanceofOperator.java @@ -20,6 +20,8 @@ import io.micronaut.expressions.parser.ast.types.TypeIdentifier; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; @@ -61,6 +63,11 @@ public void generateBytecode(ExpressionVisitorContext ctx) { mv.visitTypeInsn(INSTANCEOF, targetType.getInternalName()); } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.BOOLEAN; + } + @Override protected Type doResolveType(ExpressionVisitorContext ctx) { return BOOLEAN; diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java index 696560b83f8..e89b5415f77 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/MatchesOperator.java @@ -20,6 +20,8 @@ import io.micronaut.expressions.parser.ast.literal.StringLiteral; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; @@ -57,6 +59,11 @@ public void generateBytecode(ExpressionVisitorContext ctx) { mv.invokeVirtual(STRING, MATCHES); } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.BOOLEAN; + } + @Override protected Type doResolveType(ExpressionVisitorContext ctx) { if (!operand.resolveType(ctx).equals(STRING)) { diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java index 76d49933d5b..c9a897d475d 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java @@ -18,6 +18,8 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; import org.objectweb.asm.Type; /** @@ -40,4 +42,14 @@ public UnaryOperator(ExpressionNode operand) { public Type doResolveType(ExpressionVisitorContext ctx) { return operand.resolveType(ctx); } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + Type type = doResolveType(ctx); + try { + return PrimitiveElement.valueOf(type.getClassName()); + } catch (IllegalArgumentException e) { + return ClassElement.of(type.getClassName()); + } + } } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java index beef5e78067..bf58e550d71 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/types/TypeIdentifier.java @@ -20,10 +20,13 @@ import io.micronaut.expressions.parser.ast.util.TypeDescriptors; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.processing.JavaModelUtils; import org.objectweb.asm.Type; import java.util.Map; +import java.util.Optional; /** * Expression node for type identifier. Bytecode for identifier is not generated @@ -59,6 +62,20 @@ public void generateBytecode(ExpressionVisitorContext ctx) { ctx.methodVisitor().push(resolveType(ctx)); } + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + String name = this.toString(); + if (PRIMITIVES.containsKey(name)) { + return PrimitiveElement.valueOf(name); + } + Optional resolvedType = ctx.visitorContext().getClassElement(name); + if (resolvedType.isEmpty() && !name.contains(".")) { + resolvedType = ctx.visitorContext().getClassElement("java.lang." + name); + } + return resolvedType + .orElseThrow(() -> new ExpressionCompilationException("Unknown type identifier: " + name)); + } + @Override public Type doResolveType(ExpressionVisitorContext ctx) { String name = this.toString(); diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy new file mode 100644 index 00000000000..777e47d7abe --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy @@ -0,0 +1,60 @@ +package io.micronaut.expressions + +import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec + +class CollectionExpressionSpec extends AbstractEvaluatedExpressionsSpec { + + void "test list dereference"() { + given: + Object result = evaluateAgainstContext("#{list[1]}", + """ + import java.util.*; + @jakarta.inject.Singleton + class Context { + List getList() { + return List.of(1, 2, 3); + } + } + """) + + expect: + result == 2 + } + + void "test primitive array dereference"() { + given: + Object result = evaluateAgainstContext("#{array[1]}", + """ + import java.util.*; + @jakarta.inject.Singleton + class Context { + int[] getArray() { + return new int[] {1,2,3}; + } + } + """) + + expect: + result == 2 + } + + void "test map dereference"() { + given: + Object result = evaluateAgainstContext("#{map['foo']}", + """ + import java.util.*; + @jakarta.inject.Singleton + class Context { + Map getMap() { + return Map.of( + "foo", "bar", + "baz", "stuff" + ); + } + } + """) + + expect: + result == "bar" + } +} diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy index 8bc3d85c4ff..9de2c292e03 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/ContextMethodCallsExpressionsSpec.groovy @@ -6,7 +6,7 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec { void "test context method calls"() { given: - Object expr1 = evaluateAgainstContext("#{ #getIntValue() }", + Object expr1 = evaluateAgainstContext("#{ getIntValue() }", """ @jakarta.inject.Singleton class Context { @@ -16,7 +16,7 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec } """) - Object expr2 = evaluateAgainstContext("#{ #getStringValue().toUpperCase() }", + Object expr2 = evaluateAgainstContext("#{ getStringValue().toUpperCase() }", """ @jakarta.inject.Singleton class Context { @@ -26,7 +26,7 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec } """) - Object expr3 = evaluateAgainstContext("#{ #randomizer().nextInt(10) }", + Object expr3 = evaluateAgainstContext("#{ randomizer().nextInt(10) }", """ import java.util.Random; @@ -38,7 +38,7 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec } """) - Object expr4 = evaluateAgainstContext("#{ #lowercase('TEST') }", + Object expr4 = evaluateAgainstContext("#{ lowercase('TEST') }", """ import java.util.Random; @@ -56,7 +56,7 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec "test.ThirdContext", "test.FourthContext" ) - Object expr5 = evaluateAgainstContext("#{ #transform(#getName(), #getRepeat(), #toLower()) }", + Object expr5 = evaluateAgainstContext("#{ transform(getName(), getRepeat(), toLower()) }", """ import java.util.Random; @@ -90,7 +90,7 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec """) ContextRegistrar.reset() - Object expr6 = evaluateAgainstContext("#{ #getTestObject().name }", + Object expr6 = evaluateAgainstContext("#{ getTestObject().name }", """ import java.util.Random; @@ -108,7 +108,7 @@ class ContextMethodCallsExpressionsSpec extends AbstractEvaluatedExpressionsSpec } """) - Object expr7 = evaluateAgainstContext("#{ #values().get(#random(#values())) }", + Object expr7 = evaluateAgainstContext("#{ values().get(random(values())) }", """ import java.util.Random; import java.util.List; From bb994de7fc41b3c49ba10b6c04185541c514a221 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 21 Mar 2023 14:40:49 +0200 Subject: [PATCH 19/35] fix sonar bugs --- .../parser/ast/access/ContextMethodCall.java | 2 +- .../bind/DefaultExecutableBeanContextBinder.java | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java index 3e72c00f703..4ae80e1391a 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ContextMethodCall.java @@ -83,7 +83,7 @@ public void generateBytecode(ExpressionVisitorContext ctx) { pushGetBeanFromContext(mv, calleeType); compileArguments(ctx); if (calleeClass.isInterface()) { - mv.invokeVirtual(calleeType, usedMethod.toAsmMethod()); + mv.invokeInterface(calleeType, usedMethod.toAsmMethod()); } else { mv.invokeVirtual(calleeType, usedMethod.toAsmMethod()); } diff --git a/inject/src/main/java/io/micronaut/context/bind/DefaultExecutableBeanContextBinder.java b/inject/src/main/java/io/micronaut/context/bind/DefaultExecutableBeanContextBinder.java index de546598a88..13b19aac081 100644 --- a/inject/src/main/java/io/micronaut/context/bind/DefaultExecutableBeanContextBinder.java +++ b/inject/src/main/java/io/micronaut/context/bind/DefaultExecutableBeanContextBinder.java @@ -46,7 +46,7 @@ public final class DefaultExecutableBeanContextBinder implements ExecutableBeanC @Override public BoundExecutable bind(Executable target, ArgumentBinderRegistry registry, BeanContext source) throws UnsatisfiedArgumentException { - return bind(target, registry, source); + return bind(target, source); } @Override @@ -175,5 +175,13 @@ public int hashCode() { result = 31 * result + Arrays.hashCode(bound); return result; } + + @Override + public String toString() { + return "ContextBoundExecutable{" + + "target=" + target + + ", bound=" + Arrays.toString(bound) + + '}'; + } } } From adad35d1d3b92ac7b828be4e68438245fd751a90 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 21 Mar 2023 15:18:09 +0200 Subject: [PATCH 20/35] improve documentation --- .../processing/visitor/LoadedVisitor.java | 41 +++++++--- .../guide/config/evaluatedExpressions.adoc | 77 +++---------------- .../AnnotationContextExample.groovy | 30 ++++++++ .../expressions/AnnotationContextExample.kt | 28 +++++++ .../docs/expressions/ContextRegistrar.java | 10 +++ ...icronaut.inject.visitor.TypeElementVisitor | 3 +- .../expressions/AnnotationContextExample.java | 30 ++++++++ .../AnnotationContextExampleTest.java | 19 +++++ .../docs/expressions/ContextConsumer.java | 13 ++++ .../docs/expressions/ContextConsumerTest.java | 13 ++++ .../docs/expressions/ContextRegistrar.java | 10 +++ .../expressions/CustomEvaluationContext.java | 11 +++ 12 files changed, 206 insertions(+), 79 deletions(-) create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExample.groovy create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExample.kt create mode 100644 test-suite/src/main/java/io/micronaut/docs/expressions/ContextRegistrar.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExample.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExampleTest.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumer.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumerTest.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/ContextRegistrar.java create mode 100644 test-suite/src/test/java/io/micronaut/docs/expressions/CustomEvaluationContext.java diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java index 98771517b90..0a482b2ded3 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/LoadedVisitor.java @@ -58,17 +58,38 @@ public LoadedVisitor(TypeElementVisitor visitor, TypeElement typeElement = processingEnvironment.getElementUtils().getTypeElement(aClass.getName()); if (typeElement != null) { List generics = genericUtils.interfaceGenericTypesFor(typeElement, TypeElementVisitor.class.getName()); - String typeName = generics.get(0).toString(); - if (typeName.equals(OBJECT_CLASS)) { - classAnnotation = visitor.getClassType(); - } else { - classAnnotation = typeName; - } - String elementName = generics.get(1).toString(); - if (elementName.equals(OBJECT_CLASS)) { - elementAnnotation = visitor.getElementType(); + if (generics.size() == 2) { + String typeName = generics.get(0).toString(); + if (typeName.equals(OBJECT_CLASS)) { + classAnnotation = visitor.getClassType(); + } else { + classAnnotation = typeName; + } + String elementName = generics.get(1).toString(); + if (elementName.equals(OBJECT_CLASS)) { + elementAnnotation = visitor.getElementType(); + } else { + elementAnnotation = elementName; + } } else { - elementAnnotation = elementName; + Class[] classes = GenericTypeUtils.resolveInterfaceTypeArguments(aClass, TypeElementVisitor.class); + if (classes != null && classes.length == 2) { + Class classGeneric = classes[0]; + if (classGeneric == Object.class) { + classAnnotation = visitor.getClassType(); + } else { + classAnnotation = classGeneric.getName(); + } + Class elementGeneric = classes[1]; + if (elementGeneric == Object.class) { + elementAnnotation = visitor.getElementType(); + } else { + elementAnnotation = elementGeneric.getName(); + } + } else { + classAnnotation = Object.class.getName(); + elementAnnotation = Object.class.getName(); + } } } else { Class[] classes = GenericTypeUtils.resolveInterfaceTypeArguments(aClass, TypeElementVisitor.class); diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index 9899fa4b44f..de8a2ff3b86 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -261,51 +261,17 @@ needs to be prefixed with `#` sign. Consider the following example: -.User-defined evaluated expression context -[source,java] ----- -import jakarta.inject.Singleton; -import java.util.Random; - -@Singleton -public class CustomEvaluationContext { - public int generateRandom(int min, int max) { - return new Random().nextInt(max - min) + min; - } -} ----- +snippet::io.micronaut.docs.expressions.CustomEvaluationContext[title="User-defined evaluated expression context"] NOTE: The class should be resolvable as a bean can use `jakarta.inject` annotations to inject other types if necessary. Registering this class can be achieved with a custom implementation of api:expressions.context.ExpressionEvaluationContextRegistrar[] that is registered via service loader as a api:inject.visitor.TypeElementVisitor[] (create a new `META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor` file referencing the new class) and placed on the annotation processor classpath: -[source,java] ----- -public class ContextRegistrar implements ExpressionEvaluationContextRegistrar { - @Override - public String getContextClassName() { - return "example.CustomEvaluationContext" - } - } -} ----- +snippet::io.micronaut.docs.expressions.ContextRegistrar[title="Defining a ExpressionEvaluationContextRegistrar"] Method `generateRandom(int, int)` can now be used within Evaluated Expression in the following way: -.Usage of user-defined evaluated expression context -[source, java] ----- -import io.micronaut.context.annotation.Value; -import jakarta.inject.Singleton; - -@Singleton -public class ContextConsumer { - - @Value("#{ #generateRandom(1, 10) }") - public int randomField; - -} ----- +snippet::io.micronaut.docs.expressions.ContextConsumer[title="Usage of user-defined evaluated expression context"] At runtime, the bean will be retrieved from application context and respective method will be invoked. @@ -323,38 +289,13 @@ expressions within any annotation in a global manner. However, you can also specify evaluation context scoped to concrete annotation or annotation member using ann:context.annotation.AnnotationExpressionContext[]. -.Usage of annotation level evaluated expression context -[source, java] ----- -import jakarta.inject.Singleton; -import io.micronaut.context.annotation.AnnotationExpressionContext; - -@Singleton -@CustomAnnotation(value = "#{ #firstValue() + #secondValue() }") -class Expr { -} +snippet::io.micronaut.docs.expressions.AnnotationContextExample[title="Usage of annotation level evaluated expression context"] -@Singleton -class AnnotationContext { - String firstValue() { - return "fist value"; - } -} - -@Singleton -class AnnotationMemberContext { - String secondValue() { - return "second value"; - } -} - -@AnnotationExpressionContext(AnnotationContext.class) -@interface CustomAnnotation { - - @AnnotationExpressionContext(AnnotationMemberContext.class) - String value(); -} ----- +<1> Here two new methods are introduced to the context called `firstValue()` and `secondValue()` only for the scope of the `@CustomAnnotation` +<2> The `firstValue()` method is defined in a bean called `AnnotationContext` +<3> The `secondValue()` method is defined in a bean called `AnnotationMemberContext` +<4> On the `@CustomAnnotation` annotation the methods of the `AnnotationContext` type are exposed to all members of the annotation (type level context). +<5> On the `value()` member of the `@CustomAnnotation` annotation the methods of the `AnnotationContextExample` are made available but scoped only to the `value()` member. Again context classes need to be explicitly defined as beans to make them available for retrieval from application context at runtime. diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExample.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExample.groovy new file mode 100644 index 00000000000..929e7ed9299 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExample.groovy @@ -0,0 +1,30 @@ +package io.micronaut.docs.expressions; + +import jakarta.inject.Singleton; +import io.micronaut.context.annotation.AnnotationExpressionContext; + +@Singleton +@CustomAnnotation(value = "#{firstValue() + secondValue()}") // <1> +class Example { +} + +@Singleton +class AnnotationContext { // <2> + String firstValue() { + return "first value" + } +} + +@Singleton +class AnnotationMemberContext { // <3> + String secondValue() { + return "second value" + } +} + +@AnnotationExpressionContext(AnnotationContext.class) // <4> +@interface CustomAnnotation { + + @AnnotationExpressionContext(AnnotationMemberContext.class) // <5> + String value(); +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExample.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExample.kt new file mode 100644 index 00000000000..70302e7bef8 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExample.kt @@ -0,0 +1,28 @@ +package io.micronaut.docs.expressions + +import io.micronaut.context.annotation.AnnotationExpressionContext +import jakarta.inject.Singleton + +@Singleton +@CustomAnnotation(value = "#{firstValue() + secondValue()}") // <1> +class Example + +@Singleton +class AnnotationContext { // <2> + fun firstValue(): String { + return "first value" + } +} + +@Singleton +class AnnotationMemberContext { // <3> + fun secondValue(): String { + return "second value" + } +} + +@AnnotationExpressionContext(AnnotationContext::class) // <4> +annotation class CustomAnnotation( + @get:AnnotationExpressionContext(AnnotationMemberContext::class) // <5> + val value: String +) diff --git a/test-suite/src/main/java/io/micronaut/docs/expressions/ContextRegistrar.java b/test-suite/src/main/java/io/micronaut/docs/expressions/ContextRegistrar.java new file mode 100644 index 00000000000..71ac0a8dab1 --- /dev/null +++ b/test-suite/src/main/java/io/micronaut/docs/expressions/ContextRegistrar.java @@ -0,0 +1,10 @@ +package io.micronaut.docs.expressions; + +import io.micronaut.expressions.context.ExpressionEvaluationContextRegistrar; + +public class ContextRegistrar implements ExpressionEvaluationContextRegistrar { + @Override + public String getContextClassName() { + return "io.micronaut.docs.expressions.CustomEvaluationContext"; + } +} diff --git a/test-suite/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor b/test-suite/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor index 41fbeedc4ca..cef9589929f 100644 --- a/test-suite/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor +++ b/test-suite/src/main/resources/META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor @@ -1 +1,2 @@ -example.micronaut.inject.visitor.AnnotatingVisitor \ No newline at end of file +example.micronaut.inject.visitor.AnnotatingVisitor +io.micronaut.docs.expressions.ContextRegistrar diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExample.java b/test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExample.java new file mode 100644 index 00000000000..7bc0f1621ef --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExample.java @@ -0,0 +1,30 @@ +package io.micronaut.docs.expressions; + +import jakarta.inject.Singleton; +import io.micronaut.context.annotation.AnnotationExpressionContext; + +@Singleton +@CustomAnnotation(value = "#{firstValue() + secondValue()}") // <1> +class Example { +} + +@Singleton +class AnnotationContext { // <2> + String firstValue() { + return "first value"; + } +} + +@Singleton +class AnnotationMemberContext { // <3> + String secondValue() { + return "second value"; + } +} + +@AnnotationExpressionContext(AnnotationContext.class) // <4> +@interface CustomAnnotation { + + @AnnotationExpressionContext(AnnotationMemberContext.class) // <5> + String value(); +} diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExampleTest.java b/test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExampleTest.java new file mode 100644 index 00000000000..5ca9209bd59 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/AnnotationContextExampleTest.java @@ -0,0 +1,19 @@ +package io.micronaut.docs.expressions; + +import io.micronaut.context.BeanContext; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +@MicronautTest +public class AnnotationContextExampleTest { + @Inject BeanContext beanContext; + @Test + void testAnnotationContextEvaluation() { + BeanDefinition beanDefinition = beanContext.getBeanDefinition(Example.class); + String val = beanDefinition.stringValue(CustomAnnotation.class).orElse(null); + Assertions.assertEquals(val, "first valuesecond value"); + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumer.java b/test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumer.java new file mode 100644 index 00000000000..120f36339a5 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumer.java @@ -0,0 +1,13 @@ +package io.micronaut.docs.expressions; + +import io.micronaut.context.annotation.Value; +import jakarta.inject.Singleton; + +@Singleton +public class ContextConsumer { + + @Value("#{ generateRandom(1, 10) }") + public int randomField; + +} + diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumerTest.java b/test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumerTest.java new file mode 100644 index 00000000000..4a9bdec283b --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/ContextConsumerTest.java @@ -0,0 +1,13 @@ +package io.micronaut.docs.expressions; + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +@MicronautTest +public class ContextConsumerTest { + @Test + void testContextConsumer(ContextConsumer consumer) { + Assertions.assertTrue(consumer.randomField > 0 && consumer.randomField < 20); + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/ContextRegistrar.java b/test-suite/src/test/java/io/micronaut/docs/expressions/ContextRegistrar.java new file mode 100644 index 00000000000..71ac0a8dab1 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/ContextRegistrar.java @@ -0,0 +1,10 @@ +package io.micronaut.docs.expressions; + +import io.micronaut.expressions.context.ExpressionEvaluationContextRegistrar; + +public class ContextRegistrar implements ExpressionEvaluationContextRegistrar { + @Override + public String getContextClassName() { + return "io.micronaut.docs.expressions.CustomEvaluationContext"; + } +} diff --git a/test-suite/src/test/java/io/micronaut/docs/expressions/CustomEvaluationContext.java b/test-suite/src/test/java/io/micronaut/docs/expressions/CustomEvaluationContext.java new file mode 100644 index 00000000000..eeb76e339e1 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/expressions/CustomEvaluationContext.java @@ -0,0 +1,11 @@ +package io.micronaut.docs.expressions; + +import jakarta.inject.Singleton; +import java.util.Random; + +@Singleton +public class CustomEvaluationContext { + public int generateRandom(int min, int max) { + return new Random().nextInt(max - min) + min; + } +} From ada0086abb050048a456a3645c799ce625017a6b Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 21 Mar 2023 16:10:27 +0200 Subject: [PATCH 21/35] fix tests --- .../docs/expressions/ContextRegistrar.java | 15 +++++++++++ .../expressions/CustomEvaluationContext.java | 27 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 test-suite/src/main/java/io/micronaut/docs/expressions/CustomEvaluationContext.java diff --git a/test-suite/src/main/java/io/micronaut/docs/expressions/ContextRegistrar.java b/test-suite/src/main/java/io/micronaut/docs/expressions/ContextRegistrar.java index 71ac0a8dab1..87a6774a0a7 100644 --- a/test-suite/src/main/java/io/micronaut/docs/expressions/ContextRegistrar.java +++ b/test-suite/src/main/java/io/micronaut/docs/expressions/ContextRegistrar.java @@ -1,3 +1,18 @@ +/* + * 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.docs.expressions; import io.micronaut.expressions.context.ExpressionEvaluationContextRegistrar; diff --git a/test-suite/src/main/java/io/micronaut/docs/expressions/CustomEvaluationContext.java b/test-suite/src/main/java/io/micronaut/docs/expressions/CustomEvaluationContext.java new file mode 100644 index 00000000000..1086fa8857a --- /dev/null +++ b/test-suite/src/main/java/io/micronaut/docs/expressions/CustomEvaluationContext.java @@ -0,0 +1,27 @@ +/* + * 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.docs.expressions; + +import jakarta.inject.Singleton; + +import java.util.Random; + +@Singleton +public class CustomEvaluationContext { + public int generateRandom(int min, int max) { + return new Random().nextInt(max - min) + min; + } +} From d9477ae338b5c19b38d7f3a0fd5368eeadb44a9f Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Tue, 21 Mar 2023 17:32:36 +0200 Subject: [PATCH 22/35] add empty / not empty operator --- ...gleEvaluatedEvaluatedExpressionParser.java | 5 + .../ast/operator/unary/EmptyOperator.java | 127 ++++++++++++++++++ .../ast/operator/unary/UnaryOperator.java | 4 +- .../expressions/parser/token/TokenType.java | 4 +- .../expressions/parser/token/Tokenizer.java | 3 + .../CollectionExpressionSpec.groovy | 17 ++- .../expressions/OperatorExpressionSpec.groovy | 20 +++ .../guide/config/evaluatedExpressions.adoc | 13 +- .../expressions/CustomEvaluationContext.java | 4 +- 9 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/EmptyOperator.java diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java index b7521cf3543..9f2f61ae0c8 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java @@ -47,6 +47,7 @@ import io.micronaut.expressions.parser.ast.operator.binary.LtOperator; import io.micronaut.expressions.parser.ast.operator.binary.LteOperator; import io.micronaut.expressions.parser.ast.operator.binary.NeqOperator; +import io.micronaut.expressions.parser.ast.operator.unary.EmptyOperator; import io.micronaut.expressions.parser.ast.operator.unary.NegOperator; import io.micronaut.expressions.parser.ast.operator.unary.NotOperator; import io.micronaut.expressions.parser.ast.operator.unary.PosOperator; @@ -65,6 +66,7 @@ import static io.micronaut.expressions.parser.token.TokenType.DIV; import static io.micronaut.expressions.parser.token.TokenType.DOT; import static io.micronaut.expressions.parser.token.TokenType.DOUBLE; +import static io.micronaut.expressions.parser.token.TokenType.EMPTY; import static io.micronaut.expressions.parser.token.TokenType.EQ; import static io.micronaut.expressions.parser.token.TokenType.EXPRESSION_CONTEXT_REF; import static io.micronaut.expressions.parser.token.TokenType.FLOAT; @@ -298,6 +300,9 @@ private ExpressionNode unaryExpression() { } else if (tokenType == NOT) { eat(NOT); return new NotOperator(unaryExpression()); + } else if (tokenType == EMPTY) { + eat(EMPTY); + return new EmptyOperator(unaryExpression()); } else if (tokenType == INCREMENT) { throw new ExpressionParsingException("Prefix increment operation is not supported"); } else if (tokenType == DECREMENT) { diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/EmptyOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/EmptyOperator.java new file mode 100644 index 00000000000..4d205734dd8 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/EmptyOperator.java @@ -0,0 +1,127 @@ +/* + * 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.expressions.parser.ast.operator.unary; + +import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.core.util.ArrayUtils; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.StringUtils; +import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * The empty operator. + */ +public final class EmptyOperator extends UnaryOperator { + public EmptyOperator(ExpressionNode operand) { + super(operand); + } + + @Override + protected void generateBytecode(ExpressionVisitorContext ctx) { + ClassElement type = operand.resolveClassElement(ctx); + + GeneratorAdapter mv = ctx.methodVisitor(); + + operand.compile(ctx); + if (type.isAssignable(CharSequence.class)) { + mv.invokeStatic( + Type.getType(StringUtils.class), + Method.getMethod( + ReflectionUtils.getRequiredMethod( + StringUtils.class, + "isEmpty", + CharSequence.class + ) + ) + ); + } else if (type.isAssignable(Collection.class)) { + mv.invokeStatic( + Type.getType(CollectionUtils.class), + Method.getMethod( + ReflectionUtils.getRequiredMethod( + CollectionUtils.class, + "isEmpty", + Collection.class + ) + ) + ); + } else if (type.isAssignable(Map.class)) { + mv.invokeStatic( + Type.getType(CollectionUtils.class), + Method.getMethod( + ReflectionUtils.getRequiredMethod( + CollectionUtils.class, + "isEmpty", + Map.class + ) + ) + ); + } else if (type.isAssignable(Optional.class)) { + mv.invokeVirtual( + Type.getType(Optional.class), + Method.getMethod( + ReflectionUtils.getRequiredMethod( + Optional.class, + "isEmpty" + ) + ) + ); + } else if (type.isArray() && !type.isPrimitive()) { + mv.invokeStatic( + Type.getType(ArrayUtils.class), + Method.getMethod( + ReflectionUtils.getRequiredMethod( + ArrayUtils.class, + "isEmpty", + Object[].class + ) + ) + ); + } else { + mv.invokeStatic( + Type.getType(Objects.class), + Method.getMethod( + ReflectionUtils.getRequiredMethod( + Objects.class, + "isNull", + Object.class + ) + ) + ); + } + } + + @Override + public Type doResolveType(ExpressionVisitorContext ctx) { + return Type.BOOLEAN_TYPE; + } + + @Override + protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { + return PrimitiveElement.BOOLEAN; + } +} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java index c9a897d475d..9422afaf200 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/UnaryOperator.java @@ -29,9 +29,7 @@ * @since 4.0.0 */ @Internal -public abstract sealed class UnaryOperator extends ExpressionNode permits NegOperator, - NotOperator, - PosOperator { +public abstract sealed class UnaryOperator extends ExpressionNode permits EmptyOperator, NegOperator, NotOperator, PosOperator { protected final ExpressionNode operand; public UnaryOperator(ExpressionNode operand) { diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java index 1e563ea67da..b10ca2d0210 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java @@ -73,7 +73,9 @@ public enum TokenType { LT, LTE, INSTANCEOF, - MATCHES; + MATCHES, + + EMPTY; public boolean isOneOf(TokenType... others) { for (TokenType comparedType: others) { diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java index 9ca7aadd4d5..c4f797dfe33 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java @@ -33,6 +33,7 @@ import static io.micronaut.expressions.parser.token.TokenType.DIV; import static io.micronaut.expressions.parser.token.TokenType.DOT; import static io.micronaut.expressions.parser.token.TokenType.DOUBLE; +import static io.micronaut.expressions.parser.token.TokenType.EMPTY; import static io.micronaut.expressions.parser.token.TokenType.EQ; import static io.micronaut.expressions.parser.token.TokenType.EXPRESSION_CONTEXT_REF; import static io.micronaut.expressions.parser.token.TokenType.IDENTIFIER; @@ -91,6 +92,7 @@ public final class Tokenizer { // KEYWORDS "^instanceof\\b", INSTANCEOF, "^matches\\b", MATCHES, + "^empty\\b", EMPTY, // LITERALS "^null\\b", NULL, // NULL @@ -130,6 +132,7 @@ public final class Tokenizer { // LOGICAL OPERATORS "^!", NOT, + "^not\\b", NOT, "^&&", AND, "^and\\b", AND, "^\\|\\|", OR, diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy index 777e47d7abe..6b8b5704854 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy @@ -1,13 +1,13 @@ package io.micronaut.expressions import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec +import org.intellij.lang.annotations.Language class CollectionExpressionSpec extends AbstractEvaluatedExpressionsSpec { void "test list dereference"() { given: - Object result = evaluateAgainstContext("#{list[1]}", - """ + @Language("java") def context = """ import java.util.*; @jakarta.inject.Singleton class Context { @@ -15,10 +15,13 @@ class CollectionExpressionSpec extends AbstractEvaluatedExpressionsSpec { return List.of(1, 2, 3); } } - """) + """ + Object result = evaluateAgainstContext("#{list[1]}", context) + Object result2 = evaluateAgainstContext("#{not empty list}", context) expect: result == 2 + result2 == true } void "test primitive array dereference"() { @@ -40,8 +43,7 @@ class CollectionExpressionSpec extends AbstractEvaluatedExpressionsSpec { void "test map dereference"() { given: - Object result = evaluateAgainstContext("#{map['foo']}", - """ + @Language("java") def context = """ import java.util.*; @jakarta.inject.Singleton class Context { @@ -52,9 +54,12 @@ class CollectionExpressionSpec extends AbstractEvaluatedExpressionsSpec { ); } } - """) + """ + Object result = evaluateAgainstContext("#{map['foo']}",context) + Object result2 = evaluateAgainstContext("#{not empty map}",context) expect: result == "bar" + result2 == true } } diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy index a76b33eb8ac..037fb7f81eb 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/OperatorExpressionSpec.groovy @@ -817,6 +817,26 @@ class OperatorExpressionSpec extends AbstractEvaluatedExpressionsSpec { results[5] instanceof Boolean && results[5] == false } + void "test 'empty' operator"() { + given: + List results = evaluateMultiple( + "#{ empty '' }", // 0 + "#{ not empty '' }", // 1 + "#{ empty null }", // 2 + "#{ not empty null }", // 3 + "#{ empty 'foo' }", // 4 + "#{ not empty 'foo' }" // 5 + ) + + expect: + results[0] == true + results[1] == false + results[2] == true + results[3] == false + results[4] == false + results[5] == true + } + void "test '!' operator"() { given: List results = evaluateMultiple( diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index de8a2ff3b86..ee3e8c2c317 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -146,8 +146,14 @@ be specified as string literal. The regular expression itself will be checked fo === Logical Operators -The following logical operators are supported: `&&` (can be aliased with `and`), `||` (can be aliased with `or`), -`!`. Logical operations are performed in order enforced by standard operator precedence. +The following logical operators are supported: + +* `&&` (can be aliased with `and`) +* `||` (can be aliased with `or`), +* `!` (can be aliaded with `not`) +* `empty` / `not empty` (works with strings, collections, arrays, and maps) + +Logical operations are performed in order enforced by standard operator precedence. You can also change evaluation order by using brackets `()`. .Logical operators examples @@ -161,6 +167,9 @@ You can also change evaluation order by using brackets `()`. #{ !false } // true #{ !!true } // true + +#{ empty '' } // true +#{ not empty '' } // false ---- === Ternary Operator diff --git a/test-suite/src/main/java/io/micronaut/docs/expressions/CustomEvaluationContext.java b/test-suite/src/main/java/io/micronaut/docs/expressions/CustomEvaluationContext.java index 1086fa8857a..e2c249127d8 100644 --- a/test-suite/src/main/java/io/micronaut/docs/expressions/CustomEvaluationContext.java +++ b/test-suite/src/main/java/io/micronaut/docs/expressions/CustomEvaluationContext.java @@ -21,7 +21,9 @@ @Singleton public class CustomEvaluationContext { + private Random random = random = new Random(); + public int generateRandom(int min, int max) { - return new Random().nextInt(max - min) + min; + return random.nextInt(max - min) + min; } } From fea4f3eede1eeab3eb90ca2a3d8cd7be30a29760 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 22 Mar 2023 13:33:47 +0200 Subject: [PATCH 23/35] improve subscript operator to allow expressions in index --- ...gleEvaluatedEvaluatedExpressionParser.java | 32 +++----- .../parser/ast/access/MapAccess.java | 77 ------------------- ...rrayAccess.java => SubscriptOperator.java} | 63 +++++++++++---- .../CollectionExpressionSpec.groovy | 25 +++++- 4 files changed, 83 insertions(+), 114 deletions(-) delete mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/MapAccess.java rename core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/{ListOrArrayAccess.java => SubscriptOperator.java} (52%) diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java index 9f2f61ae0c8..540d9e5805c 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java @@ -20,8 +20,7 @@ import io.micronaut.expressions.parser.ast.access.ContextElementAccess; import io.micronaut.expressions.parser.ast.access.ContextMethodCall; import io.micronaut.expressions.parser.ast.access.ElementMethodCall; -import io.micronaut.expressions.parser.ast.access.ListOrArrayAccess; -import io.micronaut.expressions.parser.ast.access.MapAccess; +import io.micronaut.expressions.parser.ast.access.SubscriptOperator; import io.micronaut.expressions.parser.ast.access.PropertyAccess; import io.micronaut.expressions.parser.ast.conditional.TernaryExpression; import io.micronaut.expressions.parser.ast.literal.BoolLiteral; @@ -338,7 +337,7 @@ private ExpressionNode postfixExpression() { leftNode = methodOrPropertyAccess(leftNode, true); } else if (tokenType == L_SQUARE) { eat(L_SQUARE); - leftNode = mapOrListAccess(leftNode); + leftNode = subscriptOperator(leftNode); } else { throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); } @@ -394,26 +393,19 @@ private ExpressionNode methodOrPropertyAccess(ExpressionNode callee, boolean nul return new PropertyAccess(callee, identifier, nullSafe); } - // MapOrListAccess + // SubscriptOperator // : SimpleIdentifier - // | SimpleIdentifier [] + // | SimpleIdentifier [index] // ; - private ExpressionNode mapOrListAccess(ExpressionNode callee) { + private ExpressionNode subscriptOperator(ExpressionNode callee) { if (lookahead != null) { - if (lookahead.type() == INT) { - ListOrArrayAccess listOrArrayAccess = new ListOrArrayAccess( - callee, - intLiteral().getValue() - ); - eat(R_SQUARE); - return listOrArrayAccess; - } else if (lookahead.type() == STRING) { - MapAccess mapAccess = new MapAccess(callee, stringLiteral().getValue()); - eat(R_SQUARE); - return mapAccess; - } else { - throw new ExpressionParsingException("Unexpected token: " + lookahead.value()); - } + ExpressionNode indexExpression = expression(); + SubscriptOperator subscriptOperator = new SubscriptOperator( + callee, + indexExpression + ); + eat(R_SQUARE); + return subscriptOperator; } else { throw new ExpressionParsingException("Unclosed subscript operator"); } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/MapAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/MapAccess.java deleted file mode 100644 index 1e92738c4ab..00000000000 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/MapAccess.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.expressions.parser.ast.access; - -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.reflect.ReflectionUtils; -import io.micronaut.expressions.parser.ast.ExpressionNode; -import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; -import io.micronaut.expressions.parser.exception.ExpressionCompilationException; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.processing.JavaModelUtils; -import org.objectweb.asm.Type; -import org.objectweb.asm.commons.GeneratorAdapter; -import org.objectweb.asm.commons.Method; - -import java.util.Map; - -/** - * Handles map de-referencing. - */ -@Internal -public class MapAccess extends ExpressionNode { - private static final Method GET_METHOD = Method.getMethod( - ReflectionUtils.getRequiredMethod(Map.class, "get", Object.class) - ); - - private final ExpressionNode callee; - private final String index; - - public MapAccess(ExpressionNode callee, String index) { - this.callee = callee; - this.index = index; - } - - @Override - protected void generateBytecode(ExpressionVisitorContext ctx) { - callee.compile(ctx); - GeneratorAdapter methodVisitor = ctx.methodVisitor(); - methodVisitor.push(index); - methodVisitor.invokeInterface( - Type.getType(Map.class), - GET_METHOD - ); - } - - @Override - protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { - ClassElement classElement = callee.resolveClassElement(ctx); - if (!classElement.isAssignable(Map.class)) { - throw new ExpressionCompilationException("Invalid subscript operator. Subscript operator can only be applied to maps, lists and arrays"); - } - Map typeArguments = classElement.getTypeArguments(); - if (typeArguments.containsKey("V")) { - return typeArguments.get("V"); - } - return classElement; - } - - @Override - protected Type doResolveType(ExpressionVisitorContext ctx) { - ClassElement mapElement = resolveClassElement(ctx); - return JavaModelUtils.getTypeReference(mapElement); - } -} diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ListOrArrayAccess.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/SubscriptOperator.java similarity index 52% rename from core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ListOrArrayAccess.java rename to core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/SubscriptOperator.java index 81c93ad510f..529301a53be 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/ListOrArrayAccess.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/access/SubscriptOperator.java @@ -21,28 +21,34 @@ import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.PrimitiveElement; import io.micronaut.inject.processing.JavaModelUtils; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; import java.util.List; +import java.util.Map; /** - * Handles list and array de-referencing. + * Handles list, map and array de-referencing. */ @Internal -public class ListOrArrayAccess extends ExpressionNode { +public class SubscriptOperator extends ExpressionNode { - private static final Method GET_METHOD = Method.getMethod( + private static final Method LIST_GET_METHOD = Method.getMethod( ReflectionUtils.getRequiredMethod(List.class, "get", int.class) ); + private static final Method MAP_GET_METHOD = Method.getMethod( + ReflectionUtils.getRequiredMethod(Map.class, "get", Object.class) + ); private final ExpressionNode callee; - private final int index; + private final ExpressionNode index; private boolean isArray = false; + private boolean isMap = false; - public ListOrArrayAccess(ExpressionNode callee, int index) { + public SubscriptOperator(ExpressionNode callee, ExpressionNode index) { this.callee = callee; this.index = index; } @@ -51,14 +57,33 @@ public ListOrArrayAccess(ExpressionNode callee, int index) { protected void generateBytecode(ExpressionVisitorContext ctx) { callee.compile(ctx); GeneratorAdapter methodVisitor = ctx.methodVisitor(); - methodVisitor.push(index); - if (isArray) { - methodVisitor.arrayLoad(resolveType(ctx)); + ClassElement indexType = index.resolveClassElement(ctx); + index.compile(ctx); + + if (isMap) { + if (!indexType.isAssignable(String.class)) { + throw new ExpressionCompilationException("Invalid subscript operator. Map key must be a string."); + } else { + methodVisitor.invokeInterface( + Type.getType(Map.class), + MAP_GET_METHOD + ); + } } else { - methodVisitor.invokeInterface( - Type.getType(List.class), - GET_METHOD - ); + if (!indexType.equals(PrimitiveElement.INT)) { + throw new ExpressionCompilationException("Invalid subscript operator. Index must be an integer."); + } + if (isArray) { + methodVisitor.arrayLoad(resolveType(ctx)); + } else { + methodVisitor.invokeInterface( + Type.getType(List.class), + LIST_GET_METHOD + ); + } + } + if (!isArray) { + methodVisitor.checkCast(resolveType(ctx)); } } @@ -66,11 +91,19 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { ClassElement classElement = callee.resolveClassElement(ctx); this.isArray = classElement.isArray(); - if (!classElement.isAssignable(List.class) && !isArray) { + this.isMap = classElement.isAssignable(Map.class); + if (!isMap && !classElement.isAssignable(List.class) && !isArray) { throw new ExpressionCompilationException("Invalid subscript operator. Subscript operator can only be applied to maps, lists and arrays"); } if (isArray) { return classElement.fromArray(); + } else if (isMap) { + Map typeArguments = classElement.getTypeArguments(); + if (typeArguments.containsKey("V")) { + return typeArguments.get("V"); + } else { + return ClassElement.of(Object.class); + } } else { return classElement.getFirstTypeArgument() .orElseGet(() -> ClassElement.of(Object.class)); @@ -79,7 +112,7 @@ protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { @Override protected Type doResolveType(ExpressionVisitorContext ctx) { - ClassElement listElement = resolveClassElement(ctx); - return JavaModelUtils.getTypeReference(listElement); + ClassElement valueElement = resolveClassElement(ctx); + return JavaModelUtils.getTypeReference(valueElement); } } diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy index 6b8b5704854..806bb156f2b 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/CollectionExpressionSpec.groovy @@ -14,14 +14,35 @@ class CollectionExpressionSpec extends AbstractEvaluatedExpressionsSpec { List getList() { return List.of(1, 2, 3); } + + int index() { + return 1; + } + + List foos() { + List list = new ArrayList<>(); + list.add(new Foo("one")); + list.add(new Foo("two")); + list.add(null); + return list; + } } + + record Foo(String name) {} """ + Object result = evaluateAgainstContext("#{list[1]}", context) - Object result2 = evaluateAgainstContext("#{not empty list}", context) + Object result2 = evaluateAgainstContext("#{list[index()]}", context) + Object result3 = evaluateAgainstContext("#{not empty list}", context) + Object result4 = evaluateAgainstContext("#{foos()[1].name()}", context) + Object result5 = evaluateAgainstContext("#{foos()[2]?.name()}", context) expect: result == 2 - result2 == true + result2 == 2 + result3 == true + result4 == 'two' + result5 == null } void "test primitive array dereference"() { From 844b6530826dbc28890ea47e7ff9caaea7ec5682 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 22 Mar 2023 14:08:22 +0200 Subject: [PATCH 24/35] cleanup --- .../ast/operator/unary/EmptyOperator.java | 18 +++++++++++++----- .../AbstractInitializableBeanDefinition.java | 15 +++++++++------ .../context/ExpressionsAwareArgument.java | 2 +- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/EmptyOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/EmptyOperator.java index 4d205734dd8..b7f87a592fd 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/EmptyOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/unary/EmptyOperator.java @@ -15,6 +15,7 @@ */ package io.micronaut.expressions.parser.ast.operator.unary; +import io.micronaut.core.annotation.Internal; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; @@ -35,7 +36,11 @@ /** * The empty operator. */ +@Internal public final class EmptyOperator extends UnaryOperator { + + private static final String IS_EMPTY = "isEmpty"; + public EmptyOperator(ExpressionNode operand) { super(operand); } @@ -53,7 +58,7 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { Method.getMethod( ReflectionUtils.getRequiredMethod( StringUtils.class, - "isEmpty", + IS_EMPTY, CharSequence.class ) ) @@ -64,7 +69,7 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { Method.getMethod( ReflectionUtils.getRequiredMethod( CollectionUtils.class, - "isEmpty", + IS_EMPTY, Collection.class ) ) @@ -75,7 +80,7 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { Method.getMethod( ReflectionUtils.getRequiredMethod( CollectionUtils.class, - "isEmpty", + IS_EMPTY, Map.class ) ) @@ -86,7 +91,7 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { Method.getMethod( ReflectionUtils.getRequiredMethod( Optional.class, - "isEmpty" + IS_EMPTY ) ) ); @@ -96,11 +101,14 @@ protected void generateBytecode(ExpressionVisitorContext ctx) { Method.getMethod( ReflectionUtils.getRequiredMethod( ArrayUtils.class, - "isEmpty", + IS_EMPTY, Object[].class ) ) ); + } else if (type.isPrimitive()) { + // primitives are never empty + mv.push(false); } else { mv.invokeStatic( Type.getType(Objects.class), diff --git a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java index c251879fdd3..7d152d88365 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractInitializableBeanDefinition.java @@ -2647,12 +2647,15 @@ public MethodReference(Class declaringType, this.methodName = methodName; this.isPostConstructMethod = isPostConstructMethod; this.isPreDestroyMethod = isPreDestroyMethod; - - this.arguments = arguments == null - ? arguments - : Arrays.stream(arguments) - .map(argument -> ExpressionsAwareArgument.wrapIfNecessary(argument)) - .toArray(Argument[]::new); + if (arguments != null) { + for (int i = 0; i < arguments.length; i++) { + Argument argument = arguments[i]; + if (argument.getAnnotationMetadata().hasEvaluatedExpressions()) { + arguments[i] = ExpressionsAwareArgument.wrapIfNecessary(argument); + } + } + } + this.arguments = arguments; this.annotationMetadata = annotationMetadata == null diff --git a/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java b/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java index 6af2669c481..5c31639ab22 100644 --- a/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java +++ b/inject/src/main/java/io/micronaut/context/ExpressionsAwareArgument.java @@ -41,7 +41,7 @@ final class ExpressionsAwareArgument extends DefaultArgument implements Co private ExpressionsAwareArgument(Argument argument, EvaluatedAnnotationMetadata annotationMetadata) { super(argument.getType(), argument.getName(), argument.getAnnotationMetadata(), - argument.getTypeVariables(), argument.getTypeParameters()); + argument.getTypeVariables(), argument.getTypeParameters(), argument.isTypeVariable()); this.annotationMetadata = annotationMetadata; } From 17e288a1cc59fcd0c42889eb691f4b399d836e8d Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Wed, 22 Mar 2023 15:38:30 +0200 Subject: [PATCH 25/35] add elvis operator --- ...gleEvaluatedEvaluatedExpressionParser.java | 20 +++++++--- .../parser/ast/conditional/ElvisOperator.java | 32 +++++++++++++++ .../ast/conditional/TernaryExpression.java | 27 +++++++++++-- .../ast/operator/binary/BinaryOperator.java | 8 +--- .../expressions/parser/token/TokenType.java | 2 + .../expressions/parser/token/Tokenizer.java | 2 + .../io/micronaut/core/util/ObjectUtils.java | 39 +++++++++++++++++++ .../TernaryOperationExpressionsSpec.groovy | 16 ++++++++ 8 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/ElvisOperator.java diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java index 540d9e5805c..24e437bdf6b 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/SingleEvaluatedEvaluatedExpressionParser.java @@ -22,6 +22,7 @@ import io.micronaut.expressions.parser.ast.access.ElementMethodCall; import io.micronaut.expressions.parser.ast.access.SubscriptOperator; import io.micronaut.expressions.parser.ast.access.PropertyAccess; +import io.micronaut.expressions.parser.ast.conditional.ElvisOperator; import io.micronaut.expressions.parser.ast.conditional.TernaryExpression; import io.micronaut.expressions.parser.ast.literal.BoolLiteral; import io.micronaut.expressions.parser.ast.literal.DoubleLiteral; @@ -65,6 +66,7 @@ import static io.micronaut.expressions.parser.token.TokenType.DIV; import static io.micronaut.expressions.parser.token.TokenType.DOT; import static io.micronaut.expressions.parser.token.TokenType.DOUBLE; +import static io.micronaut.expressions.parser.token.TokenType.ELVIS; import static io.micronaut.expressions.parser.token.TokenType.EMPTY; import static io.micronaut.expressions.parser.token.TokenType.EQ; import static io.micronaut.expressions.parser.token.TokenType.EXPRESSION_CONTEXT_REF; @@ -146,12 +148,18 @@ private ExpressionNode expression() { // ; private ExpressionNode ternaryExpression() { ExpressionNode orExpression = orExpression(); - if (lookahead != null && lookahead.type() == QMARK) { - eat(QMARK); - ExpressionNode trueExpr = expression(); - eat(COLON); - ExpressionNode falseExpr = expression(); - return new TernaryExpression(orExpression, trueExpr, falseExpr); + if (lookahead != null) { + if (lookahead.type() == QMARK) { + eat(QMARK); + ExpressionNode trueExpr = expression(); + eat(COLON); + ExpressionNode falseExpr = expression(); + return new TernaryExpression(orExpression, trueExpr, falseExpr); + } else if (lookahead.type() == ELVIS) { + eat(ELVIS); + ExpressionNode falseExpr = expression(); + return new ElvisOperator(orExpression, falseExpr); + } } return orExpression; } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/ElvisOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/ElvisOperator.java new file mode 100644 index 00000000000..9ac33b618e2 --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/conditional/ElvisOperator.java @@ -0,0 +1,32 @@ +/* + * 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.expressions.parser.ast.conditional; + +import io.micronaut.expressions.parser.ast.ExpressionNode; + +/** + * Support for the elvis operator. Example: {@code foo ?: bar}. + */ +public final class ElvisOperator extends TernaryExpression { + public ElvisOperator(ExpressionNode condition, ExpressionNode falseExpr) { + super(condition, condition, falseExpr); + } + + @Override + protected boolean shouldCoerceConditionToBoolean() { + return true; + } +} 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 b4ddd0c1601..f43bbdc4766 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 @@ -16,6 +16,8 @@ package io.micronaut.expressions.parser.ast.conditional; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.core.util.ObjectUtils; import io.micronaut.expressions.parser.ast.ExpressionNode; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.expressions.parser.exception.ExpressionCompilationException; @@ -23,6 +25,7 @@ import org.objectweb.asm.Label; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN; import static io.micronaut.expressions.parser.ast.util.TypeDescriptors.BOOLEAN_WRAPPER; @@ -46,7 +49,10 @@ * @since 4.0.0 */ @Internal -public final class TernaryExpression extends ExpressionNode { +public class TernaryExpression extends ExpressionNode { + private static final Method COERCE_TO_BOOLEAN = Method.getMethod( + ReflectionUtils.getRequiredMethod(ObjectUtils.class, "coerceToBoolean", Object.class) + ); private final ExpressionNode condition; private final ExpressionNode trueExpr; private final ExpressionNode falseExpr; @@ -77,7 +83,15 @@ public void generateBytecode(ExpressionVisitorContext ctx) { Type conditionType = condition.resolveType(ctx); condition.compile(ctx); - pushUnboxPrimitiveIfNecessary(conditionType, mv); + if (shouldCoerceConditionToBoolean()) { + pushBoxPrimitiveIfNecessary(conditionType, mv); + mv.invokeStatic( + Type.getType(ObjectUtils.class), + COERCE_TO_BOOLEAN + ); + } else { + pushUnboxPrimitiveIfNecessary(conditionType, mv); + } mv.ifCmp(BOOLEAN, NE, falseLabel); trueExpr.compile(ctx); @@ -105,9 +119,16 @@ protected ClassElement doResolveClassElement(ExpressionVisitorContext ctx) { return ClassElement.of(doResolveType(ctx).getClassName()); } + /** + * @return Whether the condition should be coerced to a boolean type. + */ + protected boolean shouldCoerceConditionToBoolean() { + return false; + } + @Override protected Type doResolveType(ExpressionVisitorContext ctx) { - if (!isOneOf(condition.resolveType(ctx), BOOLEAN, BOOLEAN_WRAPPER)) { + if (!shouldCoerceConditionToBoolean() && !isOneOf(condition.resolveType(ctx), BOOLEAN, BOOLEAN_WRAPPER)) { throw new ExpressionCompilationException("Invalid ternary operator. Condition should resolve to boolean type"); } diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java index 1d34535ad01..13f7b9075cb 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/ast/operator/binary/BinaryOperator.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.expressions.parser.ast.ExpressionNode; +import io.micronaut.expressions.parser.ast.conditional.ElvisOperator; import io.micronaut.expressions.parser.compilation.ExpressionVisitorContext; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.PrimitiveElement; @@ -29,12 +30,7 @@ * @since 4.0.0 */ @Internal -public abstract sealed class BinaryOperator extends ExpressionNode permits LogicalOperator, - RelationalOperator, - PowOperator, - AddOperator, - EqOperator, - MathOperator { +public abstract sealed class BinaryOperator extends ExpressionNode permits AddOperator, EqOperator, LogicalOperator, MathOperator, PowOperator, RelationalOperator { protected final ExpressionNode leftOperand; protected final ExpressionNode rightOperand; diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java index b10ca2d0210..831aafca89f 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/TokenType.java @@ -31,6 +31,8 @@ public enum TokenType { EXPRESSION_CONTEXT_REF, DOT, SAFE_NAV, + + ELVIS, COMMA, COLON, L_PAREN, diff --git a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java index c4f797dfe33..b7885e2ab18 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java +++ b/core-processor/src/main/java/io/micronaut/expressions/parser/token/Tokenizer.java @@ -33,6 +33,7 @@ import static io.micronaut.expressions.parser.token.TokenType.DIV; import static io.micronaut.expressions.parser.token.TokenType.DOT; import static io.micronaut.expressions.parser.token.TokenType.DOUBLE; +import static io.micronaut.expressions.parser.token.TokenType.ELVIS; import static io.micronaut.expressions.parser.token.TokenType.EMPTY; import static io.micronaut.expressions.parser.token.TokenType.EQ; import static io.micronaut.expressions.parser.token.TokenType.EXPRESSION_CONTEXT_REF; @@ -117,6 +118,7 @@ public final class Tokenizer { // SYMBOLS "^#", EXPRESSION_CONTEXT_REF, "^\\?\\.", SAFE_NAV, + "^\\?\\:", ELVIS, "^\\?", QMARK, "^\\.", DOT, "^,", COMMA, diff --git a/core/src/main/java/io/micronaut/core/util/ObjectUtils.java b/core/src/main/java/io/micronaut/core/util/ObjectUtils.java index 3bac9a947c7..244c8a11355 100644 --- a/core/src/main/java/io/micronaut/core/util/ObjectUtils.java +++ b/core/src/main/java/io/micronaut/core/util/ObjectUtils.java @@ -18,6 +18,10 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + /** *

Utility methods for working with objects

. * @@ -60,4 +64,39 @@ public static int hash(@Nullable Object o1, @Nullable Object o2, @Nullable Obje return result; } + /** + * Coerce the given object to boolean. The following cases are handled: + * + *
    + *
  1. {@code null} results in {@code false}
  2. + *
  3. empty strings result in {@code false}
  4. + *
  5. positive numbers are {@code true}
  6. + *
  7. empty collections, arrays, optionals and maps are {@code false}
  8. + *
+ * @param object The object + * @return The boolean + * @since 4.0.0 + */ + @SuppressWarnings("unused") // used by expressions + public static boolean coerceToBoolean(@Nullable Object object) { + if (object == null) { + return false; + } else if (object instanceof Boolean b) { + return b; + } else if (object instanceof CharSequence charSequence) { + return charSequence.length() > 0; + } else if (object instanceof Number n) { + return n.doubleValue() > 0; + } else if (object instanceof Collection col) { + return col.size() > 0; + } else if (object instanceof Map col) { + return col.size() > 0; + } else if (object instanceof Object[] array) { + return array.length > 0; + } else if (object instanceof Optional opt) { + return opt.isPresent(); + } else { + return true; + } + } } diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy index 91e81a9ce52..18757acc99b 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy @@ -4,6 +4,22 @@ import io.micronaut.annotation.processing.test.AbstractEvaluatedExpressionsSpec class TernaryOperationExpressionsSpec extends AbstractEvaluatedExpressionsSpec { + void "test elvis operator"() { + given: + List results = evaluateMultiple( + "#{ 10 ?: 5 }", + "#{ -10 ?: 5 }", + "#{ '' ?: 'test' }", + "#{ 'foo' ?: 'test' }" + ) + + expect: + results[0] instanceof Integer && results[0] == 10 + results[1] instanceof Integer && results[1] == 5 + results[2] instanceof String && results[2] == 'test' + results[3] instanceof String && results[3] == 'foo' + } + void "test ternary operator"() { given: List results = evaluateMultiple( From ba3c5de0771a3bd22557f87ee9d9d2f50b417cf6 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 23 Mar 2023 17:44:20 +0100 Subject: [PATCH 26/35] test: Test ObjectUtils::coerceToBoolean --- .../io/micronaut/core/util/ObjectUtils.java | 6 ++-- .../core/util/ObjectUtilsSpec.groovy | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/util/ObjectUtils.java b/core/src/main/java/io/micronaut/core/util/ObjectUtils.java index 244c8a11355..d76f37ced46 100644 --- a/core/src/main/java/io/micronaut/core/util/ObjectUtils.java +++ b/core/src/main/java/io/micronaut/core/util/ObjectUtils.java @@ -88,15 +88,15 @@ public static boolean coerceToBoolean(@Nullable Object object) { } else if (object instanceof Number n) { return n.doubleValue() > 0; } else if (object instanceof Collection col) { - return col.size() > 0; + return !col.isEmpty(); } else if (object instanceof Map col) { return col.size() > 0; } else if (object instanceof Object[] array) { return array.length > 0; } else if (object instanceof Optional opt) { return opt.isPresent(); - } else { - return true; } + return true; + } } diff --git a/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy b/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy index e52f1a84901..167b1c2f165 100644 --- a/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.core.util import spock.lang.Specification +import spock.lang.Unroll class ObjectUtilsSpec extends Specification { @@ -21,4 +22,34 @@ class ObjectUtilsSpec extends Specification { o3 << ["abc", null, "xyz", null] } + @Unroll("ObjectUtils.coerceToBoolean with argument #obj returns #expected") + def "ObjectUtils::coerceToBoolean"(boolean expected, Object obj) { + expect: + expected == ObjectUtils.coerceToBoolean(obj) + where: + expected | obj + false | null + false | Boolean.FALSE + true | Boolean.TRUE + true | "string" + false | "" + false | 0L + false | new BigDecimal("0.0") + false | 0 + false | 0.0f + true | 1L + true | new BigDecimal("0.1") + true | 1 + false | -1 + true | 0.1f + false | Collections.emptyList() + true | Collections.singletonList("1") + false | Collections.emptyMap() + true | Collections.singletonMap("foo", "bar") + false | new String[] {} + true | new String[] {"foo"} + false | Optional.empty() + true | Optional.of("foo") + } + } From f503fd35781e9bc8329c7530f269fbbd70ca11c7 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 23 Mar 2023 20:28:05 +0100 Subject: [PATCH 27/35] test: add missing groovy tests (#9000) --- test-suite-groovy/build.gradle | 1 + .../AnnotationContextExampleSpec.groovy | 21 ++++++++++ .../docs/expressions/ExampleJob.groovy | 2 +- .../docs/expressions/ExampleJobSpec.groovy | 40 +++++++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExampleSpec.groovy create mode 100644 test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJobSpec.groovy diff --git a/test-suite-groovy/build.gradle b/test-suite-groovy/build.gradle index 9961729f7f4..697d3f0d357 100644 --- a/test-suite-groovy/build.gradle +++ b/test-suite-groovy/build.gradle @@ -51,6 +51,7 @@ dependencies { testRuntimeOnly libs.bcpkix testImplementation libs.managed.reactor + testImplementation(libs.awaitility) } //compileTestGroovy.groovyOptions.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExampleSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExampleSpec.groovy new file mode 100644 index 00000000000..6bc733755d1 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/AnnotationContextExampleSpec.groovy @@ -0,0 +1,21 @@ +package io.micronaut.docs.expressions + +import io.micronaut.context.BeanContext +import io.micronaut.inject.BeanDefinition +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class AnnotationContextExampleSpec extends Specification { + @Shared + @AutoCleanup + BeanContext beanContext = BeanContext.run() + void "testAnnotationContextEvaluation"() { + given: + BeanDefinition beanDefinition = beanContext.getBeanDefinition(Example) + String val = beanDefinition.stringValue(CustomAnnotation).orElse(null) + + expect: + "first valuesecond value" == val + } +} diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy index 81592b70482..0b239f5e2d0 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJob.groovy @@ -9,7 +9,7 @@ class ExampleJob { @Scheduled( fixedRate = "1s", - condition = "#{!jobControl.paused}") // <1> + condition = '#{!jobControl.paused}') // <1> void run(ExampleJobControl jobControl) { System.out.println("Job Running") this.jobRan = true diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJobSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJobSpec.groovy new file mode 100644 index 00000000000..7646d09fb78 --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/expressions/ExampleJobSpec.groovy @@ -0,0 +1,40 @@ +package io.micronaut.docs.expressions + +import io.micronaut.context.ApplicationContext +import spock.lang.AutoCleanup +import spock.lang.Shared; +import spock.lang.Specification + +import static java.util.concurrent.TimeUnit.SECONDS +import static org.awaitility.Awaitility.await + +class ExampleJobSpec extends Specification { + @Shared + @AutoCleanup + ApplicationContext ctx = ApplicationContext.run() + + void testJobCondition(){ + given: + ExampleJob exampleJob = ctx.getBean(ExampleJob) + ExampleJobControl jobControl = ctx.getBean(ExampleJobControl) + + expect: + jobControl.isPaused() + !exampleJob.hasJobRun() + + when: + Thread.sleep(5000) + + then: + !exampleJob.hasJobRun() + + when: + jobControl.unpause() + + then: + await().atMost(3, SECONDS).until(exampleJob::hasJobRun) + + and: + exampleJob.hasJobRun() + } +} From 94457dbfd9602a862bc3657e224222eb2c65d91a Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 23 Mar 2023 20:28:47 +0100 Subject: [PATCH 28/35] add missing kotlin test and slightly more idiomatic kotlin code for docs (#8999) * add missing test for Kotlin example * don't specify type * more idiomatic kotlin with assigment * don't start application --- .../expressions/AnnotationContextExample.kt | 8 ++----- .../AnnotationContextExampleTest.kt | 21 +++++++++++++++++++ .../micronaut/docs/expressions/ExampleJob.kt | 2 +- 3 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExampleTest.kt diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExample.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExample.kt index 70302e7bef8..f7cc3c90976 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExample.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExample.kt @@ -9,16 +9,12 @@ class Example @Singleton class AnnotationContext { // <2> - fun firstValue(): String { - return "first value" - } + fun firstValue() = "first value" } @Singleton class AnnotationMemberContext { // <3> - fun secondValue(): String { - return "second value" - } + fun secondValue() = "second value" } @AnnotationExpressionContext(AnnotationContext::class) // <4> diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExampleTest.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExampleTest.kt new file mode 100644 index 00000000000..bbc41db6d17 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/AnnotationContextExampleTest.kt @@ -0,0 +1,21 @@ +package io.micronaut.docs.expressions + +import io.micronaut.context.BeanContext +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Inject +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +@MicronautTest(startApplication = false) +class AnnotationContextExampleTest { + + @Inject + lateinit var beanContext: BeanContext + + @Test + fun testAnnotationContextEvaluation() { + val beanDefinition = beanContext.getBeanDefinition(Example::class.java) + val value = beanDefinition.stringValue(CustomAnnotation::class.java).orElse(null) + Assertions.assertEquals(value, "first valuesecond value") + } +} diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt index d1d4335a395..923fe8bcea3 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/expressions/ExampleJob.kt @@ -21,7 +21,7 @@ class ExampleJob { @Singleton class ExampleJobControl { // <2> - var paused : Boolean = true + var paused = true fun unpause() { paused = false From 57c336c3a3813642e453999f9e98a1425cdee277 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Thu, 23 Mar 2023 20:29:12 +0100 Subject: [PATCH 29/35] imp: use class:isInstance and class:cast (#8998) --- .../micronaut/expressions/util/EvaluatedExpressionsUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java b/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java index e728759316b..5a7281896e6 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java +++ b/core-processor/src/main/java/io/micronaut/expressions/util/EvaluatedExpressionsUtils.java @@ -50,8 +50,8 @@ public static Collection findEvaluatedExpressionRe .map(annotationMetadata::getAnnotation) .flatMap(annotation -> getNestedAnnotationValues(annotation).stream()) .flatMap(av -> av.getValues().values().stream()) - .filter(value -> value instanceof EvaluatedExpressionReference) - .map(value -> (EvaluatedExpressionReference) value) + .filter(EvaluatedExpressionReference.class::isInstance) + .map(EvaluatedExpressionReference.class::cast) .distinct() .toList(); } From fc77bb882b23d4fdeb62580632938b7ffbaef98c Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 24 Mar 2023 16:51:56 +0100 Subject: [PATCH 30/35] consistency with Groovy --- core/src/main/java/io/micronaut/core/util/ObjectUtils.java | 4 ++-- .../test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/io/micronaut/core/util/ObjectUtils.java b/core/src/main/java/io/micronaut/core/util/ObjectUtils.java index d76f37ced46..dbcfa510a4a 100644 --- a/core/src/main/java/io/micronaut/core/util/ObjectUtils.java +++ b/core/src/main/java/io/micronaut/core/util/ObjectUtils.java @@ -86,11 +86,11 @@ public static boolean coerceToBoolean(@Nullable Object object) { } else if (object instanceof CharSequence charSequence) { return charSequence.length() > 0; } else if (object instanceof Number n) { - return n.doubleValue() > 0; + return n.doubleValue() != 0; } else if (object instanceof Collection col) { return !col.isEmpty(); } else if (object instanceof Map col) { - return col.size() > 0; + return !col.isEmpty(); } else if (object instanceof Object[] array) { return array.length > 0; } else if (object instanceof Optional opt) { diff --git a/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy b/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy index 167b1c2f165..5b426c39490 100644 --- a/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy +++ b/core/src/test/groovy/io/micronaut/core/util/ObjectUtilsSpec.groovy @@ -40,7 +40,7 @@ class ObjectUtilsSpec extends Specification { true | 1L true | new BigDecimal("0.1") true | 1 - false | -1 + true | -1 true | 0.1f false | Collections.emptyList() true | Collections.singletonList("1") From be13e10197d1ceb32999ec1ee597504fed770d15 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 24 Mar 2023 17:44:33 +0100 Subject: [PATCH 31/35] Fix test --- .../expressions/TernaryOperationExpressionsSpec.groovy | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy index 18757acc99b..2acf2ba7ddb 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy @@ -10,14 +10,16 @@ class TernaryOperationExpressionsSpec extends AbstractEvaluatedExpressionsSpec { "#{ 10 ?: 5 }", "#{ -10 ?: 5 }", "#{ '' ?: 'test' }", - "#{ 'foo' ?: 'test' }" + "#{ 'foo' ?: 'test' }", + "#{ 0 ?: 5 }" ) expect: results[0] instanceof Integer && results[0] == 10 - results[1] instanceof Integer && results[1] == 5 + results[1] instanceof Integer && results[1] == -10 results[2] instanceof String && results[2] == 'test' results[3] instanceof String && results[3] == 'foo' + results[4] instanceof Integer && results[1] == 5 } void "test ternary operator"() { From 97ac14f221f33ef8d2bb33e4030c245fa34489c5 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 24 Mar 2023 18:20:22 +0100 Subject: [PATCH 32/35] Apply suggestions from code review Co-authored-by: Sergio del Amo --- .../context/ExpressionCompilationContextFactory.java | 1 + src/main/docs/guide/config/evaluatedExpressions.adoc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java index 0fa2fe0a82d..6c81cef084c 100644 --- a/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java +++ b/core-processor/src/main/java/io/micronaut/expressions/context/ExpressionCompilationContextFactory.java @@ -57,6 +57,7 @@ ExpressionCompilationContext buildContextForMethod(@NonNull EvaluatedExpressionR * @param contextClass context class element * @return This context factory */ + @NonNull ExpressionCompilationContextFactory registerContextClass(@NonNull ClassElement contextClass); } diff --git a/src/main/docs/guide/config/evaluatedExpressions.adoc b/src/main/docs/guide/config/evaluatedExpressions.adoc index ee3e8c2c317..d36b8f6ad92 100644 --- a/src/main/docs/guide/config/evaluatedExpressions.adoc +++ b/src/main/docs/guide/config/evaluatedExpressions.adoc @@ -1,4 +1,4 @@ -Since 4.0 Micronaut supports embedding evaluated expressions in annotation values using `#{...}` syntax which +Since 4.0, Micronaut framework supports embedding evaluated expressions in annotation values using `#{...}` syntax which allows to achieve even more flexibility while configuring your application. .Evaluated Expression example From b215a757cc23d0283c3be105ae89cd78ee7def0e Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 24 Mar 2023 18:21:46 +0100 Subject: [PATCH 33/35] rollback changes to JavaMethodElement --- .../processing/visitor/JavaMethodElement.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java index ca8a4388b2f..0c1a7d2490f 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java @@ -162,19 +162,19 @@ public boolean isVarArgs() { @Override public boolean overrides(MethodElement overridden) { -// if (this.equals(overridden) || isStatic() || overridden.isStatic()) { -// return false; -// } -// if (overridden instanceof JavaMethodElement) { -// boolean overrides = visitorContext.getElements().overrides( -// executableElement, -// ((JavaMethodElement) overridden).executableElement, -// owningType.classElement -// ); -// if (overrides) { -// return true; -// } -// } + if (this.equals(overridden) || isStatic() || overridden.isStatic()) { + return false; + } + if (overridden instanceof JavaMethodElement javaMethodElement) { + boolean overrides = visitorContext.getElements().overrides( + executableElement, + javaMethodElement.executableElement, + owningType.classElement + ); + if (overrides) { + return true; + } + } return MethodElement.super.overrides(overridden); } From 811d1f52a1c18464ce0d2dac1727b03549983182 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 24 Mar 2023 18:29:41 +0100 Subject: [PATCH 34/35] what's new docs --- .../docs/guide/introduction/whatsNew.adoc | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 64eda6513a7..7672c5ca1eb 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -7,16 +7,42 @@ Micronaut Framework 4.x supports https://groovy-lang.org/releasenotes/groovy-4.0 === Core Changes +==== Java 17 Baseline -* <> -* <> +Micronaut 4 now requires a minimum of Java 17 for building and running applications. -* <> +==== Improved Modularity + +The core of Micronaut has been further refactored to improve modularity and reduce the footprint of a Micronaut application, including: + +* Third-party dependencies on SnakeYAML and Jackson Databind are now optional and can be removed if other implementations are present. +* The runtime and compiler code has been split, allowing the removal of the re-packaging of ASM and Caffeine and reduction of the runtime footprint. +* The built in <>, <>, <>, <> and <> features have been split into separate modules allowing removal of this functionality if not needed. + +==== GraalVM Metadata Repository and Runtime Initialization + +The https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html#metadata-support[GraalVM Metadata Repository] in Micronaut's Gradle and Maven plugins is now enabled by default and Micronaut has been altered to by default primarily initialize at runtime to ensure consistency in behaviour between JIT and Native applications. + +==== Completed `javax` to `jakarta` Migration + +The remaining functionality depending on the `javax` specification has been migrated to `jakarta` including the validation module (for `jakarta.validation`) and support for Hibernate 6 (for `jakarta.persistence`). + +==== Expression Language + +A new fully compilation time, type-safe and reflection-free <> has been added to the framework which unlocks a number of new possibilities (like conditional job scheduling). It is expected that sub-modules will adopt the new EL over time to add features and capabilities. ==== Injection of Maps It is now possible to inject a `java.util.Map` of beans where the key is the bean name. The name of the bean is derived from the <> or (if not present) the simple name of the class. +==== Arbitrary Nesting of Configuration Properties + +With Micronaut 4 it is now possible to arbitrarily nest ann:context.annotation.ConfigurationProperties[] and ann:context.annotation.EachProperty[] annotations allowing for more dynamic configuration possibilities. + +==== Improved Error Messages for Missing Configuration + +When a bean is not present due to missing configuration (such as a bean that uses ann:context.annotation.EachProperty[]) error messages have been improved to display the configuration that is required to activate the bean. + ==== Improved Error Messages for Missing Beans When a bean annotated with ann:context.annotation.EachProperty[] or ann:context.annotation.Bean[] is not found due to missing configuration an error is thrown showing the configuration prefix necessary to resolve the issue. @@ -27,9 +53,27 @@ Beans that are disabled via <> are now trac The disabled beans are also now visible via the <> in the <> aiding in understanding the state of your application configuration. +=== HTTP Changes + +==== Initial Support for Virtual Threads (Loom) + +Preview <> has been added. When using JDK 19 or above with preview features enabled you can off load processing to a virtual thread pool. + +==== Rewritten HTTP layer + +The HTTP layer has been rewritten to improve performance and reduce the presence of reactive stack frames if reactive is not used (such as with Virtual threads). + +==== Annotation-Based HTTP Filters + +See <> + +==== JDK HTTP Client + +<> + === Other Dependency Upgrades -- Kotlin 1.7.10 +- Kotlin 1.8.10 <> From 69bdd51d756560c2fb95a51b0437b39051c652cb Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Fri, 24 Mar 2023 20:28:42 +0100 Subject: [PATCH 35/35] fix test --- .../expressions/TernaryOperationExpressionsSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy b/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy index 2acf2ba7ddb..f3d75af5f84 100644 --- a/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/expressions/TernaryOperationExpressionsSpec.groovy @@ -19,7 +19,7 @@ class TernaryOperationExpressionsSpec extends AbstractEvaluatedExpressionsSpec { results[1] instanceof Integer && results[1] == -10 results[2] instanceof String && results[2] == 'test' results[3] instanceof String && results[3] == 'foo' - results[4] instanceof Integer && results[1] == 5 + results[4] instanceof Integer && results[4] == 5 } void "test ternary operator"() {