From aa489270faae62ca7e53be66cbc2257677bbef08 Mon Sep 17 00:00:00 2001 From: graemerocher Date: Fri, 3 Nov 2017 11:09:23 +0100 Subject: [PATCH] Support BiConsumer/BiFunction in Groovy --- .../core/annotation/Internal.java | 4 +- function-groovy/build.gradle | 7 +- .../function/groovy/FunctionScript.java | 78 +++++++ .../function/groovy/FunctionTransform.groovy | 192 +++++++++++++++--- .../groovy/FunctionTransformSpec.groovy | 154 +++++++++++++- .../function/groovy/MathService.groovy | 15 +- .../function/groovy/MaxFunction.groovy | 26 +++ .../function/groovy/MessageService.groovy | 31 +++ .../function/groovy/NotifyFunction.groovy | 31 +++ .../groovy/NotifyWithArgsFunction.groovy | 24 +++ .../function/groovy/RoundFunction.groovy | 7 +- .../function/groovy/Sum.groovy | 25 +++ .../function/groovy/SumFunction.groovy | 27 +++ .../web/AnnotatedFunctionRouteBuilder.java | 15 +- .../function/executor/AbstractExecutor.java | 5 + .../executor/FunctionInitializer.java | 45 +++- .../converters/ByteBufToArrayConverter.java | 12 +- .../converters/ByteBufToObjectConverter.java | 45 ++++ .../ast/groovy/InjectTransform.groovy | 5 +- .../AnnotationStereoTypeFinder.groovy | 38 +++- .../ast/groovy/utils/AstUtils.groovy | 5 + .../BeanDefinitionInjectProcessor.java | 5 +- .../env/groovy/ConfigurationEvaluator.groovy | 93 +-------- .../env/groovy/SetPropertyTransformer.groovy | 118 +++++++++++ .../context/ApplicationContext.java | 8 + .../web/router/DefaultRouteBuilder.java | 3 +- 26 files changed, 870 insertions(+), 148 deletions(-) create mode 100644 function-groovy/src/main/groovy/org/particleframework/function/groovy/FunctionScript.java create mode 100644 function-groovy/src/test/groovy/org/particleframework/function/groovy/MaxFunction.groovy create mode 100644 function-groovy/src/test/groovy/org/particleframework/function/groovy/MessageService.groovy create mode 100644 function-groovy/src/test/groovy/org/particleframework/function/groovy/NotifyFunction.groovy create mode 100644 function-groovy/src/test/groovy/org/particleframework/function/groovy/NotifyWithArgsFunction.groovy create mode 100644 function-groovy/src/test/groovy/org/particleframework/function/groovy/Sum.groovy create mode 100644 function-groovy/src/test/groovy/org/particleframework/function/groovy/SumFunction.groovy create mode 100644 http-server-netty/src/main/java/org/particleframework/http/server/netty/converters/ByteBufToObjectConverter.java create mode 100644 inject/src/main/groovy/org/particleframework/context/env/groovy/SetPropertyTransformer.groovy diff --git a/core/src/main/java/org/particleframework/core/annotation/Internal.java b/core/src/main/java/org/particleframework/core/annotation/Internal.java index 4e773d2217e..12f7cc7ebdd 100644 --- a/core/src/main/java/org/particleframework/core/annotation/Internal.java +++ b/core/src/main/java/org/particleframework/core/annotation/Internal.java @@ -1,5 +1,6 @@ package org.particleframework.core.annotation; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -9,7 +10,8 @@ * @author Graeme Rocher * @since 1.0 */ -@Retention(RetentionPolicy.SOURCE) +@Retention(RetentionPolicy.RUNTIME) +@Documented public @interface Internal { } \ No newline at end of file diff --git a/function-groovy/build.gradle b/function-groovy/build.gradle index 653a145c14b..8e382189ebc 100644 --- a/function-groovy/build.gradle +++ b/function-groovy/build.gradle @@ -2,4 +2,9 @@ dependencies { compile project(":inject-groovy") compile project(":function") runtime project(":configurations/jackson") -} \ No newline at end of file + + testCompile project(":function-web") + testRuntime project(":http-server-netty") + testCompile 'com.squareup.okhttp3:okhttp:3.8.1' +} +//compileTestGroovy.groovyOptions.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] \ No newline at end of file diff --git a/function-groovy/src/main/groovy/org/particleframework/function/groovy/FunctionScript.java b/function-groovy/src/main/groovy/org/particleframework/function/groovy/FunctionScript.java new file mode 100644 index 00000000000..622651630b8 --- /dev/null +++ b/function-groovy/src/main/groovy/org/particleframework/function/groovy/FunctionScript.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017 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 + * + * http://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 org.particleframework.function.groovy; + +import org.particleframework.context.ApplicationContext; +import org.particleframework.context.env.PropertySource; +import org.particleframework.core.annotation.Internal; +import org.particleframework.function.executor.FunctionInitializer; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Base class for Function scripts + * + * @author Graeme Rocher + * @since 1.0 + */ +public abstract class FunctionScript extends FunctionInitializer implements PropertySource { + + public FunctionScript() { + } + + protected FunctionScript(ApplicationContext applicationContext) { + super(applicationContext, false); + } + + private Map props; + + @Override + @Internal + public Object get(String key) { + return resolveProps().get(key); + } + + @Override + @Internal + public Iterator iterator() { + return resolveProps().keySet().iterator(); + } + + protected void addProperty(String name, Object value) { + resolveProps().put(name, value); + } + + private Map resolveProps() { + if(props == null) { + props = new LinkedHashMap<>(); + } + return props; + } + + @Override + @Internal + protected void startThis(ApplicationContext applicationContext) { + // no-op this, equivalent behaviour will be called from the script constructor + } + + @Override + @Internal + protected void injectThis(ApplicationContext applicationContext) { + // no-op this, equivalent behaviour will be called from the script constructor + } +} diff --git a/function-groovy/src/main/groovy/org/particleframework/function/groovy/FunctionTransform.groovy b/function-groovy/src/main/groovy/org/particleframework/function/groovy/FunctionTransform.groovy index 6bd30ae876a..44782b043b8 100644 --- a/function-groovy/src/main/groovy/org/particleframework/function/groovy/FunctionTransform.groovy +++ b/function-groovy/src/main/groovy/org/particleframework/function/groovy/FunctionTransform.groovy @@ -18,26 +18,34 @@ package org.particleframework.function.groovy import groovy.transform.CompileStatic import groovy.transform.Field import org.codehaus.groovy.ast.* +import org.codehaus.groovy.ast.expr.ArgumentListExpression +import org.codehaus.groovy.ast.expr.BinaryExpression +import org.codehaus.groovy.ast.expr.ConstructorCallExpression import org.codehaus.groovy.ast.expr.DeclarationExpression import org.codehaus.groovy.ast.expr.Expression +import org.codehaus.groovy.ast.expr.MethodCallExpression import org.codehaus.groovy.ast.stmt.BlockStatement import org.codehaus.groovy.ast.stmt.ExpressionStatement +import org.codehaus.groovy.ast.tools.GenericsUtils import org.codehaus.groovy.control.CompilePhase import org.codehaus.groovy.control.SourceUnit import org.codehaus.groovy.transform.ASTTransformation import org.codehaus.groovy.transform.FieldASTTransformation import org.codehaus.groovy.transform.GroovyASTTransformation -import org.codehaus.groovy.transform.sc.transformers.StaticCompilationTransformer -import org.codehaus.groovy.transform.stc.StaticTypeCheckingVisitor import org.particleframework.ast.groovy.InjectTransform -import org.particleframework.ast.groovy.annotation.AnnotationStereoTypeFinder import org.particleframework.ast.groovy.utils.AstMessageUtils import org.particleframework.ast.groovy.utils.AstUtils import org.particleframework.context.ApplicationContext -import org.particleframework.function.executor.FunctionInitializer +import org.particleframework.context.env.groovy.SetPropertyTransformer +import org.particleframework.core.naming.NameUtils +import org.particleframework.function.FunctionBean -import javax.inject.Inject import java.lang.reflect.Modifier +import java.util.function.BiConsumer +import java.util.function.BiFunction +import java.util.function.Consumer +import java.util.function.Function +import java.util.function.Supplier import static org.codehaus.groovy.ast.tools.GeneralUtils.* @@ -51,20 +59,20 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.* @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) class FunctionTransform implements ASTTransformation{ public static final ClassNode FIELD_TYPE = ClassHelper.make(Field) - AnnotationStereoTypeFinder stereoTypeFinder = new AnnotationStereoTypeFinder(); + @Override void visit(ASTNode[] nodes, SourceUnit source) { def uri = source.getSource().getURI() if(uri != null) { def file = uri.toString() - if(!file.contains("src/main/groovy") || file.endsWith("logback.groovy") || file.endsWith("application.groovy") || file ==~ (/\S+\/application-\S+.groovy/)) { + if( !file.endsWith("Function.groovy") && !file.toLowerCase(Locale.ENGLISH).endsWith("-function.groovy")) { return } } for(node in source.getAST().classes) { if(node.isScript()) { - node.setSuperClass(ClassHelper.makeCached(FunctionInitializer)) + node.setSuperClass(ClassHelper.makeCached(FunctionScript)) MethodNode functionMethod = node.methods.find() { method -> !method.isAbstract() && !method.isStatic() && method.isPublic() && method.name != 'run' } if(functionMethod == null) { AstMessageUtils.error(source, node, "Function must have at least one public method") @@ -77,7 +85,28 @@ class FunctionTransform implements ASTTransformation{ MethodNode mainMethod = node.getMethod("main", new Parameter(ClassHelper.make(([] as String[]).class), "args")) Parameter argParam = mainMethod.getParameters()[0] def thisInstance = varX('$this') - def functionCall = callX(thisInstance, functionMethod.getName(), args(callX(varX("it"), "get", args(classX(functionMethod.parameters[0].type.plainNodeReference))))) + def parameters = functionMethod.parameters + int argLength = parameters.length + boolean isVoidReturn = functionMethod.returnType == ClassHelper.VOID_TYPE + + if(argLength > 2) { + AstMessageUtils.error(source, node, "Functions can only have a maximum of 2 arguments") + continue + } + else if(argLength == 0 && isVoidReturn) { + AstMessageUtils.error(source, node, "Zero argument functions must return a value") + continue + } + + MethodCallExpression functionCall + + if(argLength == 1) { + functionCall = callX(thisInstance, functionMethod.getName(), args(callX(varX("it"), "get", args(classX(parameters[0].type.plainNodeReference))))) + } + else { + functionCall = callX(thisInstance, functionMethod.getName()) + } + def closureExpression = closureX(stmt(functionCall)) mainMethod.variableScope.putDeclaredVariable(thisInstance) closureExpression.setVariableScope(mainMethod.variableScope) @@ -87,8 +116,11 @@ class FunctionTransform implements ASTTransformation{ stmt(callX(thisInstance, "run", args(varX(argParam), closureExpression))) ) ) - new StaticCompilationTransformer(source, new StaticTypeCheckingVisitor(source, node)).visitMethod(mainMethod) def code = runMethod.getCode() + def appCtx = varX("applicationContext") + def constructorBody = block( + + ) if(code instanceof BlockStatement) { BlockStatement bs = (BlockStatement)code for(st in bs.statements) { @@ -99,36 +131,150 @@ class FunctionTransform implements ASTTransformation{ DeclarationExpression de = (DeclarationExpression)exp def initial = de.getVariableExpression().getInitialExpression() if ( initial == null) { - de.addAnnotation(new AnnotationNode(ClassHelper.make(Inject))) + de.addAnnotation(new AnnotationNode(AstUtils.INJECT_ANNOTATION)) new FieldASTTransformation().visit([new AnnotationNode(FIELD_TYPE), de] as ASTNode[], source) } } + else if(exp instanceof BinaryExpression || exp instanceof MethodCallExpression) { + def setPropertyTransformer = new SetPropertyTransformer(source) + setPropertyTransformer.setPropertyMethodName = "addProperty" + constructorBody.addStatement( + stmt(setPropertyTransformer.transform(exp)) + ) + } } } } - node.addMethod(new MethodNode( - "injectThis", - Modifier.PROTECTED, - ClassHelper.VOID_TYPE, - params(param(ClassHelper.make(ApplicationContext),"ctx")), - null, - block() - )) - - ConstructorNode constructorNode = new ConstructorNode(Modifier.PUBLIC, block( + constructorBody.addStatement(block( + stmt( + callX(varX("this"), "startEnvironment", appCtx) + ), stmt( - callX(varX("applicationContext"), "inject", varX("this")) + callX(appCtx, "inject", varX("this")) ) )) - constructorNode.addAnnotation(new AnnotationNode(ClassHelper.make(Inject))) + ConstructorNode constructorNode = new ConstructorNode(Modifier.PUBLIC, constructorBody) node.declaredConstructors.clear() node.addConstructor(constructorNode) + def ctxParam = param(ClassHelper.make(ApplicationContext), "ctx") + + def applicationContextConstructor = new ConstructorNode( + Modifier.PUBLIC, + params(ctxParam), + null, + stmt( + ctorX(ClassNode.SUPER, varX(ctxParam)) + ) + + ) + for(field in node.getFields()) { + field.addAnnotation(new AnnotationNode(AstUtils.INJECT_ANNOTATION)) + def setterName = getSetterName(field.getName()) + def setterMethod = node.getMethod(setterName, params(param(field.getType(), "arg"))) + if(setterMethod != null) { + setterMethod.addAnnotation(new AnnotationNode(AstUtils.INTERNAL_ANNOTATION)) + } + } + + applicationContextConstructor.addAnnotation(new AnnotationNode(AstUtils.INJECT_ANNOTATION)) + def functionBean = new AnnotationNode(ClassHelper.make(FunctionBean)) + String functionName = NameUtils.hyphenate(node.nameWithoutPackage) + functionName -= '-function' + + functionBean.setMember("value", constX(functionName)) + node.addAnnotation(functionBean) + node.addConstructor( + applicationContextConstructor + ) + if(isVoidReturn) { + if(argLength == 1) { + implementConsumer(functionMethod, node) + } + else { + implementBiConsumer(functionMethod, node) + } + + } + else { + if(argLength == 0) { + def returnType = ClassHelper.getWrapper(functionMethod.returnType.plainNodeReference) + node.addInterface(GenericsUtils.makeClassSafeWithGenerics( + ClassHelper.make(Supplier).plainNodeReference, + new GenericsType(returnType) + )) + def mn = new MethodNode("get", Modifier.PUBLIC, functionMethod.returnType.plainNodeReference, AstUtils.ZERO_PARAMETERS, null, stmt( + callX(varX("this"), functionMethod.getName()) + )) + mn.addAnnotation(new AnnotationNode(AstUtils.INTERNAL_ANNOTATION)) + node.addMethod(mn) + } + else { + if(argLength == 1) { + implementFunction(functionMethod, node) + } + else { + implementBiFunction(functionMethod, node) + } + + } + } new InjectTransform().visit(nodes, source) } } } } + + protected void implementConsumer(MethodNode functionMethod, ClassNode classNode) { + implementFunction(functionMethod, classNode, Consumer, ClassHelper.VOID_TYPE, "accept") + } + + protected void implementBiConsumer(MethodNode functionMethod, ClassNode classNode) { + implementFunction(functionMethod, classNode, BiConsumer, ClassHelper.VOID_TYPE, "accept") + } + + protected void implementFunction(MethodNode functionMethod, ClassNode classNode) { + ClassNode returnType = ClassHelper.getWrapper(functionMethod.returnType.plainNodeReference) + implementFunction(functionMethod, classNode, Function, returnType, "apply") + } + + protected void implementBiFunction(MethodNode functionMethod, ClassNode classNode) { + ClassNode returnType = ClassHelper.getWrapper(functionMethod.returnType.plainNodeReference) + implementFunction(functionMethod, classNode, BiFunction, returnType, "apply") + } + + protected void implementFunction(MethodNode functionMethod, ClassNode classNode, Class functionType, ClassNode returnType, String methodName) { + List argTypes = [] + for(p in functionMethod.parameters) { + argTypes.add(ClassHelper.getWrapper(p.type.plainNodeReference)) + } + List genericsTypes = [] + for(type in argTypes) { + genericsTypes.add(new GenericsType(type)) + } + if(returnType != ClassHelper.VOID_TYPE) { + genericsTypes.add(new GenericsType(returnType)) + } + classNode.addInterface(GenericsUtils.makeClassSafeWithGenerics( + ClassHelper.make(functionType).plainNodeReference, + genericsTypes as GenericsType[] + )) + List params = [] + int i = 0 + ArgumentListExpression argList = new ArgumentListExpression() + for(type in argTypes) { + def p = param(type, "arg${i++}") + params.add(p) + argList.addExpression(varX(p)) + } + def mn = new MethodNode(methodName, Modifier.PUBLIC, returnType, params as Parameter[], null, stmt( + callX(varX("this"), functionMethod.getName(), argList)) + ) + mn.addAnnotation(new AnnotationNode(AstUtils.INTERNAL_ANNOTATION)) + classNode.addMethod(mn) + } + + } diff --git a/function-groovy/src/test/groovy/org/particleframework/function/groovy/FunctionTransformSpec.groovy b/function-groovy/src/test/groovy/org/particleframework/function/groovy/FunctionTransformSpec.groovy index e07327528eb..26addd01434 100644 --- a/function-groovy/src/test/groovy/org/particleframework/function/groovy/FunctionTransformSpec.groovy +++ b/function-groovy/src/test/groovy/org/particleframework/function/groovy/FunctionTransformSpec.groovy @@ -15,10 +15,16 @@ */ package org.particleframework.function.groovy +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import org.particleframework.context.ApplicationContext +import org.particleframework.context.env.MapPropertySource +import org.particleframework.http.HttpStatus +import org.particleframework.runtime.server.EmbeddedServer import spock.lang.Specification -import java.nio.charset.StandardCharsets - /** * @author Graeme Rocher * @since 1.0 @@ -27,8 +33,150 @@ class FunctionTransformSpec extends Specification{ void "run function"() { expect: - new RoundFunction().round(1.6f) == 2 + new RoundFunction().round(1.6f) == 4 + new SumFunction().sum(new Sum(a: 10,b: 20)) == 30 + new MaxFunction().max() == Integer.MAX_VALUE.toLong() + } + + void "run consumer"() { + given: + NotifyFunction function = new NotifyFunction() + + def message = new Message(title: "Hello", body: "World") + when: + function.send(message) + + then: + function.messageService.messages.contains(message) + } + + void "run bi-consumer"() { + given: + NotifyWithArgsFunction function = new NotifyWithArgsFunction() + + def message = new Message(title: "Hello", body: "World") + when: + function.send("Hello", "World") + then: + function.messageService.messages.contains(message) } + void "test run JSON bi-consumer as REST service"() { + + given: + def applicationContext = ApplicationContext.build() + .environment({ env -> + env.addPropertySource(MapPropertySource.of( + 'math.multiplier': '2' + )) + + }) + EmbeddedServer server = applicationContext.start().getBean(EmbeddedServer).start() + def message = new Message(title: "Hello", body: "World") + String url = "http://localhost:$server.port" + OkHttpClient client = new OkHttpClient() + def data = '{"title":"Hello", "body":"World"}' + + def request = new Request.Builder() + .url("$url/notify-with-args") + .post(RequestBody.create( MediaType.parse(org.particleframework.http.MediaType.APPLICATION_JSON), data)) + + when: + def response = client.newCall(request.build()).execute() + + then: + response.code() == HttpStatus.OK.code + applicationContext.getBean(MessageService).messages.contains(message) + + cleanup: + if(server != null) + server.stop() + } + + void "test run JSON function as REST service"() { + given: + EmbeddedServer server = ApplicationContext.build() + .environment({ env -> + env.addPropertySource(MapPropertySource.of( + 'math.multiplier':'2' + )) + + }).start().getBean(EmbeddedServer).start() + + String url = "http://localhost:$server.port" + OkHttpClient client = new OkHttpClient() + def data = '{"a":10, "b":5}' + def request = new Request.Builder() + .url("$url/sum") + .post(RequestBody.create( MediaType.parse(org.particleframework.http.MediaType.APPLICATION_JSON), data)) + + when: + def response = client.newCall(request.build()).execute() + + then: + response.code() == HttpStatus.OK.code + response.body().string() == '15' + + cleanup: + if(server != null) + server.stop() + } + + void "test run function as REST service"() { + given: + EmbeddedServer server = ApplicationContext.build() + .environment({ env -> + env.addPropertySource(MapPropertySource.of( + 'math.multiplier':'2' + )) + + }).start().getBean(EmbeddedServer).start() + + String url = "http://localhost:$server.port" + OkHttpClient client = new OkHttpClient() + def data = '1.6' + def request = new Request.Builder() + .url("$url/round") + .post(RequestBody.create( MediaType.parse("text/plain"), data)) + + when: + def response = client.newCall(request.build()).execute() + + then: + response.code() == HttpStatus.OK.code + response.body().string() == '4' + + cleanup: + if(server != null) + server.stop() + } + + void "test run supplier as REST service"() { + given: + EmbeddedServer server = ApplicationContext.build() + .environment({ env -> + env.addPropertySource(MapPropertySource.of( + 'math.multiplier':'2' + )) + + }).start().getBean(EmbeddedServer).start() + + String url = "http://localhost:$server.port" + OkHttpClient client = new OkHttpClient() + def request = new Request.Builder() + .url("$url/max") + + + when: + def response = client.newCall(request.build()).execute() + + then: + response.code() == HttpStatus.OK.code + response.body().string() == String.valueOf(Integer.MAX_VALUE) + + cleanup: + if(server != null) + server.stop() + } } diff --git a/function-groovy/src/test/groovy/org/particleframework/function/groovy/MathService.groovy b/function-groovy/src/test/groovy/org/particleframework/function/groovy/MathService.groovy index db2c42d6c99..00cce24eb50 100644 --- a/function-groovy/src/test/groovy/org/particleframework/function/groovy/MathService.groovy +++ b/function-groovy/src/test/groovy/org/particleframework/function/groovy/MathService.groovy @@ -15,6 +15,8 @@ */ package org.particleframework.function.groovy +import org.particleframework.context.annotation.Value + import javax.inject.Singleton /** @@ -23,7 +25,18 @@ import javax.inject.Singleton */ @Singleton class MathService { + @Value('math.multiplier') + Integer multiplier = 1 + int round(float value) { - Math.round(value) + Math.round(value) * multiplier + } + + long sum(Sum sum) { + sum.a + sum.b + } + + Integer max() { + Integer.MAX_VALUE } } diff --git a/function-groovy/src/test/groovy/org/particleframework/function/groovy/MaxFunction.groovy b/function-groovy/src/test/groovy/org/particleframework/function/groovy/MaxFunction.groovy new file mode 100644 index 00000000000..e5911161b59 --- /dev/null +++ b/function-groovy/src/test/groovy/org/particleframework/function/groovy/MaxFunction.groovy @@ -0,0 +1,26 @@ +/* + * Copyright 2017 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 + * + * http://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 org.particleframework.function.groovy + +import groovy.transform.Field + +math.multiplier = 2 + +@Field MathService mathService + +Long max() { + mathService.max() +} \ No newline at end of file diff --git a/function-groovy/src/test/groovy/org/particleframework/function/groovy/MessageService.groovy b/function-groovy/src/test/groovy/org/particleframework/function/groovy/MessageService.groovy new file mode 100644 index 00000000000..2d45d28134a --- /dev/null +++ b/function-groovy/src/test/groovy/org/particleframework/function/groovy/MessageService.groovy @@ -0,0 +1,31 @@ +/* + * Copyright 2017 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 + * + * http://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 org.particleframework.function.groovy + +import javax.inject.Singleton + +/** + * @author Graeme Rocher + * @since 1.0 + */ +@Singleton +class MessageService { + + List messages = [] + void send(Message message) { + messages.add(message) + } +} diff --git a/function-groovy/src/test/groovy/org/particleframework/function/groovy/NotifyFunction.groovy b/function-groovy/src/test/groovy/org/particleframework/function/groovy/NotifyFunction.groovy new file mode 100644 index 00000000000..724e6c2efd6 --- /dev/null +++ b/function-groovy/src/test/groovy/org/particleframework/function/groovy/NotifyFunction.groovy @@ -0,0 +1,31 @@ +/* + * Copyright 2017 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 + * + * http://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 org.particleframework.function.groovy + +import groovy.transform.EqualsAndHashCode +import groovy.transform.Field + +@Field MessageService messageService + +void send(Message message) { + messageService.send(message) +} + +@EqualsAndHashCode +class Message { + String title + String body +} \ No newline at end of file diff --git a/function-groovy/src/test/groovy/org/particleframework/function/groovy/NotifyWithArgsFunction.groovy b/function-groovy/src/test/groovy/org/particleframework/function/groovy/NotifyWithArgsFunction.groovy new file mode 100644 index 00000000000..011c8ae7863 --- /dev/null +++ b/function-groovy/src/test/groovy/org/particleframework/function/groovy/NotifyWithArgsFunction.groovy @@ -0,0 +1,24 @@ +/* + * Copyright 2017 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 + * + * http://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 org.particleframework.function.groovy + +import groovy.transform.Field + +@Field MessageService messageService + +void send(String title, String body) { + messageService.send(new Message(title: title, body: body)) +} diff --git a/function-groovy/src/test/groovy/org/particleframework/function/groovy/RoundFunction.groovy b/function-groovy/src/test/groovy/org/particleframework/function/groovy/RoundFunction.groovy index 1c2c9c6ccc4..868cadb1a8a 100644 --- a/function-groovy/src/test/groovy/org/particleframework/function/groovy/RoundFunction.groovy +++ b/function-groovy/src/test/groovy/org/particleframework/function/groovy/RoundFunction.groovy @@ -1,6 +1,11 @@ package org.particleframework.function.groovy -MathService mathService +import groovy.transform.Field + + +math.multiplier = 2 + +@Field MathService mathService int round(float value) { mathService.round(value) // go diff --git a/function-groovy/src/test/groovy/org/particleframework/function/groovy/Sum.groovy b/function-groovy/src/test/groovy/org/particleframework/function/groovy/Sum.groovy new file mode 100644 index 00000000000..fef2f44f44b --- /dev/null +++ b/function-groovy/src/test/groovy/org/particleframework/function/groovy/Sum.groovy @@ -0,0 +1,25 @@ +/* + * Copyright 2017 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 + * + * http://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 org.particleframework.function.groovy + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class Sum { + int a + Integer b +} \ No newline at end of file diff --git a/function-groovy/src/test/groovy/org/particleframework/function/groovy/SumFunction.groovy b/function-groovy/src/test/groovy/org/particleframework/function/groovy/SumFunction.groovy new file mode 100644 index 00000000000..c6ac44cb4c4 --- /dev/null +++ b/function-groovy/src/test/groovy/org/particleframework/function/groovy/SumFunction.groovy @@ -0,0 +1,27 @@ +/* + * Copyright 2017 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 + * + * http://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 org.particleframework.function.groovy + +import groovy.transform.Field + +math.multiplier = 2 + +@Field MathService mathService + +long sum(Sum sum) { + mathService.sum(sum) +} + diff --git a/function-web/src/main/java/org/particleframework/function/web/AnnotatedFunctionRouteBuilder.java b/function-web/src/main/java/org/particleframework/function/web/AnnotatedFunctionRouteBuilder.java index 51374fc530c..938fb6c1cb1 100644 --- a/function-web/src/main/java/org/particleframework/function/web/AnnotatedFunctionRouteBuilder.java +++ b/function-web/src/main/java/org/particleframework/function/web/AnnotatedFunctionRouteBuilder.java @@ -32,9 +32,11 @@ import javax.inject.Singleton; import java.util.*; +import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Stream; /** * Process methods for {@link FunctionBean} instances @@ -78,24 +80,19 @@ public void process(ExecutableMethod method) { } UriRoute route = null; - if(java.util.function.Function.class.isAssignableFrom(declaringType)) { + if(Stream.of(java.util.function.Function.class, Consumer.class, BiFunction.class, BiConsumer.class).anyMatch(type -> type.isAssignableFrom(declaringType))) { route = POST(functionPath, method); } - else if(Consumer.class.isAssignableFrom(declaringType)) { - route = POST(functionPath, method); - } - else if(BiFunction.class.isAssignableFrom(declaringType)) { - route = POST(functionPath, method); - } else if(Supplier.class.isAssignableFrom(declaringType)) { route = GET(functionPath, method); } if(route != null) { Class[] argumentTypes = method.getArgumentTypes(); - if(argumentTypes.length > 0) { - if(!ClassUtils.isJavaLangType(argumentTypes[0])) { + int argCount = argumentTypes.length; + if(argCount > 0) { + if(argCount == 2 || !ClassUtils.isJavaLangType(argumentTypes[0])) { route.accept(MediaType.APPLICATION_JSON_TYPE); } else { diff --git a/function/src/main/java/org/particleframework/function/executor/AbstractExecutor.java b/function/src/main/java/org/particleframework/function/executor/AbstractExecutor.java index 24710b8d6e5..59ed8209a02 100644 --- a/function/src/main/java/org/particleframework/function/executor/AbstractExecutor.java +++ b/function/src/main/java/org/particleframework/function/executor/AbstractExecutor.java @@ -17,6 +17,7 @@ import org.particleframework.context.ApplicationContext; import org.particleframework.context.env.Environment; +import org.particleframework.context.env.PropertySource; import org.particleframework.core.annotation.Nullable; import org.particleframework.function.FunctionRegistry; import org.particleframework.inject.ExecutableMethod; @@ -66,6 +67,10 @@ protected ApplicationContext buildApplicationContext(@Nullable C context) { } protected Environment startEnvironment(ApplicationContext applicationContext) { + if(this instanceof PropertySource) { + applicationContext.getEnvironment().addPropertySource((PropertySource) this); + } + return applicationContext .start() .getEnvironment(); diff --git a/function/src/main/java/org/particleframework/function/executor/FunctionInitializer.java b/function/src/main/java/org/particleframework/function/executor/FunctionInitializer.java index 12f7178620e..4217c62f2e1 100644 --- a/function/src/main/java/org/particleframework/function/executor/FunctionInitializer.java +++ b/function/src/main/java/org/particleframework/function/executor/FunctionInitializer.java @@ -16,6 +16,7 @@ package org.particleframework.function.executor; import org.particleframework.context.ApplicationContext; +import org.particleframework.core.annotation.Internal; import org.particleframework.core.cli.CommandLine; import org.particleframework.core.reflect.ClassUtils; import org.particleframework.function.FunctionRegistry; @@ -35,22 +36,45 @@ public class FunctionInitializer extends AbstractExecutor implements Closeable, AutoCloseable { protected final ApplicationContext applicationContext; - + protected final boolean closeContext; @SuppressWarnings("unchecked") public FunctionInitializer() { ApplicationContext applicationContext = buildApplicationContext(null); this.applicationContext = applicationContext; - startEnvironment(this.applicationContext); + startThis(applicationContext); injectThis(applicationContext); + this.closeContext = true; + } + + /** + * Start a function for an existing {@link ApplicationContext} + * @param applicationContext The application context + */ + protected FunctionInitializer(ApplicationContext applicationContext) { + this(applicationContext, true); + } + + /** + * Start a function for an existing {@link ApplicationContext} + * @param applicationContext The application context + */ + protected FunctionInitializer(ApplicationContext applicationContext, boolean inject) { + this.applicationContext = applicationContext; + this.closeContext = false; + if(inject) { + injectThis(applicationContext); + } } @Override + @Internal public void close() throws IOException { - if (applicationContext != null) { + if (closeContext && applicationContext != null) { applicationContext.close(); } } + /** * This method is designed to be called when using the {@link FunctionInitializer} from a static Application main method * @@ -73,20 +97,31 @@ protected void run(String[] args, Function supplier) throws IOE } } + /** + * Start this environment + * + * @param applicationContext + */ + protected void startThis(ApplicationContext applicationContext) { + startEnvironment(applicationContext); + } + /** * Injects this instance * @param applicationContext The {@link ApplicationContext} * @return This injected instance */ protected void injectThis(ApplicationContext applicationContext) { - applicationContext.inject(this); + if(applicationContext != null) { + applicationContext.inject(this); + } } /** * The parse context supplied from the {@link #run(String[], Function)} method. Consumers can use the {@link #get(Class)} method to obtain the data is the desired type */ - protected class ParseContext { + public class ParseContext { private final String data; private final boolean debug; diff --git a/http-server-netty/src/main/java/org/particleframework/http/server/netty/converters/ByteBufToArrayConverter.java b/http-server-netty/src/main/java/org/particleframework/http/server/netty/converters/ByteBufToArrayConverter.java index 13fb1110236..fd263263214 100644 --- a/http-server-netty/src/main/java/org/particleframework/http/server/netty/converters/ByteBufToArrayConverter.java +++ b/http-server-netty/src/main/java/org/particleframework/http/server/netty/converters/ByteBufToArrayConverter.java @@ -16,6 +16,7 @@ package org.particleframework.http.server.netty.converters; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import org.particleframework.core.convert.ConversionContext; import org.particleframework.core.convert.TypeConverter; @@ -32,15 +33,6 @@ public class ByteBufToArrayConverter implements TypeConverter { @Override public Optional convert(ByteBuf object, Class targetType, ConversionContext context) { - if( object.hasArray() ) { - return Optional.of(object.array()); - } - int len = object.readableBytes(); - if ( len > 0) { - byte[] bytes = new byte[len]; - object.readBytes(bytes); - return Optional.of(bytes); - } - return Optional.empty(); + return Optional.of(ByteBufUtil.getBytes(object)); } } diff --git a/http-server-netty/src/main/java/org/particleframework/http/server/netty/converters/ByteBufToObjectConverter.java b/http-server-netty/src/main/java/org/particleframework/http/server/netty/converters/ByteBufToObjectConverter.java new file mode 100644 index 00000000000..e159b7c2d74 --- /dev/null +++ b/http-server-netty/src/main/java/org/particleframework/http/server/netty/converters/ByteBufToObjectConverter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017 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 + * + * http://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 org.particleframework.http.server.netty.converters; + +import io.netty.buffer.ByteBuf; +import org.particleframework.core.convert.ConversionContext; +import org.particleframework.core.convert.ConversionService; +import org.particleframework.core.convert.TypeConverter; + +import javax.inject.Singleton; +import java.util.Optional; + +/** + * A byte buf to object converter + * + * @author Graeme Rocher + * @since 1.0 + */ +@Singleton +public class ByteBufToObjectConverter implements TypeConverter { + private final ConversionService conversionService; + + public ByteBufToObjectConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public Optional convert(ByteBuf object, Class targetType, ConversionContext context) { + return conversionService.convert(object, String.class, context) + .flatMap(val -> conversionService.convert(val, targetType, context)); + } +} diff --git a/inject-groovy/src/main/groovy/org/particleframework/ast/groovy/InjectTransform.groovy b/inject-groovy/src/main/groovy/org/particleframework/ast/groovy/InjectTransform.groovy index 49eb0b24b8b..92db064a646 100644 --- a/inject-groovy/src/main/groovy/org/particleframework/ast/groovy/InjectTransform.groovy +++ b/inject-groovy/src/main/groovy/org/particleframework/ast/groovy/InjectTransform.groovy @@ -22,6 +22,7 @@ import org.particleframework.ast.groovy.utils.AstMessageUtils import org.particleframework.ast.groovy.utils.PublicMethodVisitor import org.particleframework.context.annotation.ConfigurationProperties import org.particleframework.context.annotation.* +import org.particleframework.core.annotation.Internal import org.particleframework.core.value.OptionalValues import org.particleframework.core.io.service.ServiceDescriptorGenerator import org.particleframework.core.naming.NameUtils @@ -508,7 +509,9 @@ class InjectTransform implements ASTTransformation, CompilationUnitAware { } else if(!isConstructor) { boolean isPublic = methodNode.isPublic() && !methodNode.isStatic() && !methodNode.isAbstract() - if((isExecutableType && isPublic) || stereoTypeFinder.hasStereoType(methodNode, Executable.name)) { + boolean isExecutable = (isExecutableType && isPublic && !AstAnnotationUtils.hasAnnotation(methodNode, Internal)) || stereoTypeFinder.hasStereoType(methodNode, Executable.name) + isExecutable = isExecutable && !AstAnnotationUtils.hasAnnotation(methodNode, Internal) + if(isExecutable && !AstAnnotationUtils.hasAnnotation(methodNode, Internal)) { if(declaringClass != ClassHelper.OBJECT_TYPE) { defineBeanDefinition(concreteClass) diff --git a/inject-groovy/src/main/groovy/org/particleframework/ast/groovy/annotation/AnnotationStereoTypeFinder.groovy b/inject-groovy/src/main/groovy/org/particleframework/ast/groovy/annotation/AnnotationStereoTypeFinder.groovy index b39dab75d7a..641ffd16645 100644 --- a/inject-groovy/src/main/groovy/org/particleframework/ast/groovy/annotation/AnnotationStereoTypeFinder.groovy +++ b/inject-groovy/src/main/groovy/org/particleframework/ast/groovy/annotation/AnnotationStereoTypeFinder.groovy @@ -1,9 +1,12 @@ package org.particleframework.ast.groovy.annotation +import groovy.transform.Canonical import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode import groovy.transform.Memoized import org.codehaus.groovy.ast.* import org.codehaus.groovy.ast.expr.ConstantExpression +import org.codehaus.groovy.runtime.memoize.LRUCache import org.particleframework.ast.groovy.utils.AstAnnotationUtils import org.particleframework.core.value.OptionalValues @@ -24,6 +27,8 @@ import java.lang.annotation.Target @CompileStatic class AnnotationStereoTypeFinder { private static final List EXCLUDED_ANNOTATIONS = [Retention.name, Documented.name, Target.name, Inject.name, Qualifier.name, Scope.name] + private static final Object NO_RESULT = new Object() + private final LRUCache cache = new LRUCache(100) boolean hasStereoType(AnnotatedNode annotatedNode, Class stereotype) { return hasStereoType(annotatedNode, stereotype.name) @@ -46,8 +51,28 @@ class AnnotationStereoTypeFinder { return findAnnotationWithStereoType(annotatedNode, stereotype.name) } - @Memoized AnnotationNode findAnnotationWithStereoType(AnnotatedNode annotatedNode, String stereotype) { + def key = new Key(annotatedNode, stereotype) + def result = cache.get(key) + if(result == NO_RESULT) { + return null + } + else if(result != null) { + return (AnnotationNode)result + } + else { + AnnotationNode node = findAnnotationWithStereoTypeNoCache(annotatedNode, stereotype) + if(node != null) { + cache.put(key, node) + } + if(node == null) { + cache.put(key, NO_RESULT) + } + return node + } + } + + private AnnotationNode findAnnotationWithStereoTypeNoCache(AnnotatedNode annotatedNode, String stereotype) { List annotations = annotatedNode.getAnnotations() for(AnnotationNode ann in annotations) { ClassNode annotationClassNode = ann.classNode @@ -77,7 +102,6 @@ class AnnotationStereoTypeFinder { } return null } - /** * Find all the annotations for the given stereotype * @@ -179,4 +203,14 @@ class AnnotationStereoTypeFinder { return OptionalValues.empty() } + @EqualsAndHashCode + private static class Key { + AnnotatedNode annotatedNode; String stereotype + + Key(AnnotatedNode annotatedNode, String stereotype) { + this.annotatedNode = annotatedNode + this.stereotype = stereotype + } + } + } diff --git a/inject-groovy/src/main/groovy/org/particleframework/ast/groovy/utils/AstUtils.groovy b/inject-groovy/src/main/groovy/org/particleframework/ast/groovy/utils/AstUtils.groovy index ab311b88da2..4261591831f 100644 --- a/inject-groovy/src/main/groovy/org/particleframework/ast/groovy/utils/AstUtils.groovy +++ b/inject-groovy/src/main/groovy/org/particleframework/ast/groovy/utils/AstUtils.groovy @@ -5,6 +5,9 @@ import org.codehaus.groovy.ast.ClassHelper import org.codehaus.groovy.ast.ClassNode import org.codehaus.groovy.ast.GenericsType import org.codehaus.groovy.ast.Parameter +import org.particleframework.core.annotation.Internal + +import javax.inject.Inject import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpecRecurse @@ -18,6 +21,8 @@ import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpecR class AstUtils { public static final Parameter[] ZERO_PARAMETERS = new Parameter[0] public static final ClassNode[] EMPTY_CLASS_ARRAY = new ClassNode[0] + public static final ClassNode INTERNAL_ANNOTATION = ClassHelper.make(Internal) + public static final ClassNode INJECT_ANNOTATION = ClassHelper.make(Inject) static Parameter[] copyParameters(Parameter[] parameterTypes) { diff --git a/inject-java/src/main/java/org/particleframework/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/org/particleframework/annotation/processing/BeanDefinitionInjectProcessor.java index 47050e09e0f..c1dbf41e451 100644 --- a/inject-java/src/main/java/org/particleframework/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/org/particleframework/annotation/processing/BeanDefinitionInjectProcessor.java @@ -21,6 +21,7 @@ import org.particleframework.aop.writer.AopProxyWriter; import org.particleframework.context.annotation.ConfigurationProperties; import org.particleframework.context.annotation.*; +import org.particleframework.core.annotation.Internal; import org.particleframework.core.value.OptionalValues; import org.particleframework.core.io.service.ServiceDescriptorGenerator; import org.particleframework.core.naming.NameUtils; @@ -388,8 +389,8 @@ public Object visitExecutable(ExecutableElement method, Object o) { } boolean isPublicMethod = method.getModifiers().contains(PUBLIC); - boolean isExecutableMethod = annotationUtils.hasStereotype(method, Executable.class); - if (isExecutableMethod || (isExecutableType && isPublicMethod)) { + boolean isExecutableMethod = annotationUtils.hasStereotype(method, Executable.class) ; + if ((isExecutableMethod || (isExecutableType && isPublicMethod)) && !annotationUtils.hasStereotype(method, Internal.class)) { visitExecutableMethod(method); return null; } diff --git a/inject/src/main/groovy/org/particleframework/context/env/groovy/ConfigurationEvaluator.groovy b/inject/src/main/groovy/org/particleframework/context/env/groovy/ConfigurationEvaluator.groovy index bb48b3bb39c..6d10205b137 100644 --- a/inject/src/main/groovy/org/particleframework/context/env/groovy/ConfigurationEvaluator.groovy +++ b/inject/src/main/groovy/org/particleframework/context/env/groovy/ConfigurationEvaluator.groovy @@ -49,8 +49,8 @@ class ConfigurationEvaluator { Map evaluate(Reader reader) { CompilerConfiguration configuration = new CompilerConfiguration() configuration.addCompilationCustomizers( - new ASTTransformationCustomizer(CompileStatic.class), - new ASTTransformationCustomizer(new ConfigTransform()) + new ASTTransformationCustomizer(CompileStatic.class), + new ASTTransformationCustomizer(new ConfigTransform()) ) GroovyShell shell = new GroovyShell(configuration) @@ -65,95 +65,12 @@ class ConfigurationEvaluator { void visit(ASTNode[] nodes, SourceUnit source) { List classNodes = source.getAST().classes ClassNode scriptClassNode = classNodes.find { it.script } - if(scriptClassNode != null) { + if (scriptClassNode != null) { MethodNode runMethod = scriptClassNode.getMethod('run', [] as Parameter[]) - if(runMethod != null) { + if (runMethod != null) { new SetPropertyTransformer(source).visitMethod(runMethod) } } } } - - private static class SetPropertyTransformer extends ClassCodeExpressionTransformer { - final SourceUnit sourceUnit - - String nestedPath = "" - - SetPropertyTransformer(SourceUnit sourceUnit) { - this.sourceUnit = sourceUnit - } - - @Override - Expression transform(Expression exp) { - if(exp instanceof BinaryExpression) { - BinaryExpression be = (BinaryExpression)exp - if(be.operation.type == ASSIGN.type) { - Expression left = be.leftExpression - Expression right = be.rightExpression - if(left instanceof VariableExpression) { - def varX = (VariableExpression) left - if(varX.accessedVariable.isDynamicTyped()) { - String path = "${nestedPath}${varX.name}" - return callThisX("setProperty", args(constX(path), right)) - } - } - else if(left instanceof PropertyExpression) { - PropertyExpression propX = (PropertyExpression)left - String path = buildPath(propX) - if(path != null) { - return callThisX("setProperty", args(constX(path), right)) - } - - } - } - } - else if(exp instanceof MethodCallExpression) { - MethodCallExpression methodX = (MethodCallExpression)exp - Expression argsX = methodX.arguments - ClosureExpression closureX = findSingleClosure(argsX) - if(closureX != null) { - String currentNestedPath = nestedPath - try { - nestedPath = "${nestedPath ? nestedPath : ''}${methodX.methodAsString}." - visitClosureExpression(closureX) - } finally { - nestedPath = currentNestedPath - } - return callThisX("with", args(closureX)) - } - } - return super.transform(exp) - } - - private ClosureExpression findSingleClosure(Expression argsX) { - if (argsX instanceof ClosureExpression) { - return (ClosureExpression) argsX - } else if (argsX instanceof ArgumentListExpression) { - ArgumentListExpression listX = (ArgumentListExpression) argsX - if (listX.expressions.size() == 1 && listX.getExpression(0) instanceof ClosureExpression) { - return (ClosureExpression)listX.getExpression(0) - } - } - return null - } - - private String buildPath(PropertyExpression propX) { - String path = propX.propertyAsString - Expression objX = propX.objectExpression - while(objX instanceof PropertyExpression) { - propX = ((PropertyExpression)objX) - objX = propX.objectExpression - path = "${propX.propertyAsString}.${path}" - } - if(objX instanceof VariableExpression) { - VariableExpression varX = (VariableExpression)objX - if(varX.accessedVariable.isDynamicTyped()) { - path = "${varX.name}.${path}" - path = "${nestedPath}${path}" - return path - } - } - return null - } - } -} +} \ No newline at end of file diff --git a/inject/src/main/groovy/org/particleframework/context/env/groovy/SetPropertyTransformer.groovy b/inject/src/main/groovy/org/particleframework/context/env/groovy/SetPropertyTransformer.groovy new file mode 100644 index 00000000000..5140e6e0959 --- /dev/null +++ b/inject/src/main/groovy/org/particleframework/context/env/groovy/SetPropertyTransformer.groovy @@ -0,0 +1,118 @@ +/* + * Copyright 2017 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 + * + * http://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 org.particleframework.context.env.groovy + +import org.codehaus.groovy.ast.ClassCodeExpressionTransformer +import org.codehaus.groovy.ast.expr.ArgumentListExpression +import org.codehaus.groovy.ast.expr.BinaryExpression +import org.codehaus.groovy.ast.expr.ClosureExpression +import org.codehaus.groovy.ast.expr.Expression +import org.codehaus.groovy.ast.expr.MethodCallExpression +import org.codehaus.groovy.ast.expr.PropertyExpression +import org.codehaus.groovy.ast.expr.VariableExpression +import org.codehaus.groovy.control.SourceUnit + +import static org.codehaus.groovy.ast.tools.GeneralUtils.* + +/** + * Transforms Groovy properties syntax into calls to setProperty(..) calculating the path + * + * @author Graeme Rocher + * @since 1.0 + */ +class SetPropertyTransformer extends ClassCodeExpressionTransformer { + final SourceUnit sourceUnit + + String nestedPath = "" + String setPropertyMethodName = "setProperty" + + SetPropertyTransformer(SourceUnit sourceUnit) { + this.sourceUnit = sourceUnit + } + + @Override + Expression transform(Expression exp) { + if(exp instanceof BinaryExpression) { + BinaryExpression be = (BinaryExpression)exp + if(be.operation.type == ASSIGN.type) { + Expression left = be.leftExpression + Expression right = be.rightExpression + if(left instanceof VariableExpression) { + def varX = (VariableExpression) left + if(varX.accessedVariable.isDynamicTyped()) { + String path = "${nestedPath}${varX.name}" + return callThisX(setPropertyMethodName, args(constX(path), right)) + } + } + else if(left instanceof PropertyExpression) { + PropertyExpression propX = (PropertyExpression)left + String path = buildPath(propX) + if(path != null) { + return callThisX(setPropertyMethodName, args(constX(path), right)) + } + + } + } + } + else if(exp instanceof MethodCallExpression) { + MethodCallExpression methodX = (MethodCallExpression)exp + Expression argsX = methodX.arguments + ClosureExpression closureX = findSingleClosure(argsX) + if(closureX != null) { + String currentNestedPath = nestedPath + try { + nestedPath = "${nestedPath ? nestedPath : ''}${methodX.methodAsString}." + visitClosureExpression(closureX) + } finally { + nestedPath = currentNestedPath + } + return callThisX("with", args(closureX)) + } + } + return super.transform(exp) + } + + private ClosureExpression findSingleClosure(Expression argsX) { + if (argsX instanceof ClosureExpression) { + return (ClosureExpression) argsX + } else if (argsX instanceof ArgumentListExpression) { + ArgumentListExpression listX = (ArgumentListExpression) argsX + if (listX.expressions.size() == 1 && listX.getExpression(0) instanceof ClosureExpression) { + return (ClosureExpression)listX.getExpression(0) + } + } + return null + } + + private String buildPath(PropertyExpression propX) { + String path = propX.propertyAsString + Expression objX = propX.objectExpression + while(objX instanceof PropertyExpression) { + propX = ((PropertyExpression)objX) + objX = propX.objectExpression + path = "${propX.propertyAsString}.${path}" + } + if(objX instanceof VariableExpression) { + VariableExpression varX = (VariableExpression)objX + if(varX.accessedVariable.isDynamicTyped()) { + path = "${varX.name}.${path}" + path = "${nestedPath}${path}" + return path + } + } + return null + } +} diff --git a/inject/src/main/java/org/particleframework/context/ApplicationContext.java b/inject/src/main/java/org/particleframework/context/ApplicationContext.java index 86b8674a6c6..df9c4828b7b 100644 --- a/inject/src/main/java/org/particleframework/context/ApplicationContext.java +++ b/inject/src/main/java/org/particleframework/context/ApplicationContext.java @@ -99,6 +99,14 @@ static ApplicationContext build(String... environments) { return new DefaultApplicationContext(environments); } + /** + * Build a {@link ApplicationContext} + * + * @return The built, but not yet running {@link ApplicationContext} + */ + static ApplicationContext build() { + return new DefaultApplicationContext(); + } /** * Run the {@link BeanContext}. This method will instantiate a new {@link BeanContext} and call {@link #start()} * diff --git a/router/src/main/java/org/particleframework/web/router/DefaultRouteBuilder.java b/router/src/main/java/org/particleframework/web/router/DefaultRouteBuilder.java index 73d18404b52..91bde07265f 100644 --- a/router/src/main/java/org/particleframework/web/router/DefaultRouteBuilder.java +++ b/router/src/main/java/org/particleframework/web/router/DefaultRouteBuilder.java @@ -32,7 +32,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.net.URI; import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -343,6 +342,7 @@ public Optional> match(Class originatingClass, Throwable excep return Optional.empty(); } + @SuppressWarnings("unchecked") @Override public Optional> match(Throwable exception) { if (error.isInstance(exception)) { @@ -425,6 +425,7 @@ public HttpStatus status() { return status; } + @SuppressWarnings("unchecked") @Override public Optional> match(HttpStatus status) { if (this.status == status) {