From 632e8bee3d70945a9a52805cec5b3231608bdaf3 Mon Sep 17 00:00:00 2001 From: graemerocher Date: Thu, 2 Nov 2017 15:28:44 +0100 Subject: [PATCH] Support functions written as Groovy scripts --- .../convert/DefaultConversionService.java | 18 ++- .../src/main/groovy/example/Book.groovy | 24 ++++ .../main/groovy/example/BookService.groovy | 38 +++++ .../example/UpperCaseTitleFunction.groovy | 27 ++++ function-groovy/build.gradle | 5 + .../function/groovy/FunctionTransform.groovy | 134 ++++++++++++++++++ .../function/groovy/package-info.java | 22 +++ ...odehaus.groovy.transform.ASTTransformation | 1 + .../groovy/FunctionTransformSpec.groovy | 34 +++++ .../function/groovy/MathService.groovy | 29 ++++ .../function/groovy/RoundFunction.groovy | 7 + .../executor/FunctionInitializer.java | 17 ++- .../ast/groovy/utils/AstUtils.groovy | 2 + settings.gradle | 1 + 14 files changed, 353 insertions(+), 6 deletions(-) create mode 100644 examples/simple-groovy-lambda/src/main/groovy/example/Book.groovy create mode 100644 examples/simple-groovy-lambda/src/main/groovy/example/BookService.groovy create mode 100644 examples/simple-groovy-lambda/src/main/groovy/example/UpperCaseTitleFunction.groovy create mode 100644 function-groovy/build.gradle create mode 100644 function-groovy/src/main/groovy/org/particleframework/function/groovy/FunctionTransform.groovy create mode 100644 function-groovy/src/main/groovy/org/particleframework/function/groovy/package-info.java create mode 100644 function-groovy/src/main/resources/META-INF/services/org.codehaus.groovy.transform.ASTTransformation create mode 100644 function-groovy/src/test/groovy/org/particleframework/function/groovy/FunctionTransformSpec.groovy create mode 100644 function-groovy/src/test/groovy/org/particleframework/function/groovy/MathService.groovy create mode 100644 function-groovy/src/test/groovy/org/particleframework/function/groovy/RoundFunction.groovy diff --git a/core/src/main/java/org/particleframework/core/convert/DefaultConversionService.java b/core/src/main/java/org/particleframework/core/convert/DefaultConversionService.java index d3bdbc658c8..38a7d8d7f68 100644 --- a/core/src/main/java/org/particleframework/core/convert/DefaultConversionService.java +++ b/core/src/main/java/org/particleframework/core/convert/DefaultConversionService.java @@ -237,10 +237,21 @@ protected void registerDefaultConverters() { } }); - // String -> Number - addConverter(CharSequence.class, Number.class, (CharSequence object, Class targetType, ConversionContext context) -> { + // String -> Float + addConverter(CharSequence.class, Float.class, (CharSequence object, Class targetType, ConversionContext context) -> { try { - Integer converted = Integer.valueOf(object.toString()); + Float converted = Float.valueOf(object.toString()); + return Optional.of(converted); + } catch (NumberFormatException e) { + context.reject(object, e); + return Optional.empty(); + } + }); + + // String -> Double + addConverter(CharSequence.class, Double.class, (CharSequence object, Class targetType, ConversionContext context) -> { + try { + Double converted = Double.valueOf(object.toString()); return Optional.of(converted); } catch (NumberFormatException e) { context.reject(object, e); @@ -281,6 +292,7 @@ protected void registerDefaultConverters() { } }); + // String -> Boolean addConverter(CharSequence.class, Boolean.class, (CharSequence object, Class targetType, ConversionContext context) -> { String booleanString = object.toString().toLowerCase(Locale.ENGLISH); diff --git a/examples/simple-groovy-lambda/src/main/groovy/example/Book.groovy b/examples/simple-groovy-lambda/src/main/groovy/example/Book.groovy new file mode 100644 index 00000000000..43aca5482e2 --- /dev/null +++ b/examples/simple-groovy-lambda/src/main/groovy/example/Book.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 example + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class Book { + String title +} diff --git a/examples/simple-groovy-lambda/src/main/groovy/example/BookService.groovy b/examples/simple-groovy-lambda/src/main/groovy/example/BookService.groovy new file mode 100644 index 00000000000..2342d376fa3 --- /dev/null +++ b/examples/simple-groovy-lambda/src/main/groovy/example/BookService.groovy @@ -0,0 +1,38 @@ +/* + * 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 example + +import groovy.transform.CompileStatic + +import javax.inject.Singleton + +/** + * @author Graeme Rocher + * @since 1.0 + */ +@Singleton +@CompileStatic +class BookService { + + + Book toUpperCase(Book book) { + String title = book.title + if(title != null) { + book.title = book.title.toUpperCase() + } + return book + } +} diff --git a/examples/simple-groovy-lambda/src/main/groovy/example/UpperCaseTitleFunction.groovy b/examples/simple-groovy-lambda/src/main/groovy/example/UpperCaseTitleFunction.groovy new file mode 100644 index 00000000000..d3210605e67 --- /dev/null +++ b/examples/simple-groovy-lambda/src/main/groovy/example/UpperCaseTitleFunction.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 example + +/** + * @author Graeme Rocher + * @since 1.0 + */ + +BookService bookService + +Book toUpperCase(Book book) { + bookService.toUpperCase(book) +} \ No newline at end of file diff --git a/function-groovy/build.gradle b/function-groovy/build.gradle new file mode 100644 index 00000000000..653a145c14b --- /dev/null +++ b/function-groovy/build.gradle @@ -0,0 +1,5 @@ +dependencies { + compile project(":inject-groovy") + compile project(":function") + runtime project(":configurations/jackson") +} \ No newline at end of file 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 new file mode 100644 index 00000000000..6bd30ae876a --- /dev/null +++ b/function-groovy/src/main/groovy/org/particleframework/function/groovy/FunctionTransform.groovy @@ -0,0 +1,134 @@ +/* + * 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.CompileStatic +import groovy.transform.Field +import org.codehaus.groovy.ast.* +import org.codehaus.groovy.ast.expr.DeclarationExpression +import org.codehaus.groovy.ast.expr.Expression +import org.codehaus.groovy.ast.stmt.BlockStatement +import org.codehaus.groovy.ast.stmt.ExpressionStatement +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 javax.inject.Inject +import java.lang.reflect.Modifier + +import static org.codehaus.groovy.ast.tools.GeneralUtils.* + +/** + * Transforms a Groovy script into a function + * + * @author Graeme Rocher + * @since 1.0 + */ +@CompileStatic +@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/)) { + return + } + } + for(node in source.getAST().classes) { + if(node.isScript()) { + node.setSuperClass(ClassHelper.makeCached(FunctionInitializer)) + 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") + } + else { + + + MethodNode runMethod = node.getMethod("run", AstUtils.ZERO_PARAMETERS) + node.removeMethod(runMethod) + 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 closureExpression = closureX(stmt(functionCall)) + mainMethod.variableScope.putDeclaredVariable(thisInstance) + closureExpression.setVariableScope(mainMethod.variableScope) + mainMethod.setCode( + block( + declS(thisInstance, ctorX(node)), + stmt(callX(thisInstance, "run", args(varX(argParam), closureExpression))) + ) + ) + new StaticCompilationTransformer(source, new StaticTypeCheckingVisitor(source, node)).visitMethod(mainMethod) + def code = runMethod.getCode() + if(code instanceof BlockStatement) { + BlockStatement bs = (BlockStatement)code + for(st in bs.statements) { + if(st instanceof ExpressionStatement) { + ExpressionStatement es = (ExpressionStatement)st + Expression exp = es.expression + if(exp instanceof DeclarationExpression) { + DeclarationExpression de = (DeclarationExpression)exp + def initial = de.getVariableExpression().getInitialExpression() + if ( initial == null) { + de.addAnnotation(new AnnotationNode(ClassHelper.make(Inject))) + new FieldASTTransformation().visit([new AnnotationNode(FIELD_TYPE), de] as ASTNode[], source) + } + } + } + } + } + + 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( + stmt( + callX(varX("applicationContext"), "inject", varX("this")) + ) + )) + constructorNode.addAnnotation(new AnnotationNode(ClassHelper.make(Inject))) + node.declaredConstructors.clear() + node.addConstructor(constructorNode) + + new InjectTransform().visit(nodes, source) + } + + } + } + } +} diff --git a/function-groovy/src/main/groovy/org/particleframework/function/groovy/package-info.java b/function-groovy/src/main/groovy/org/particleframework/function/groovy/package-info.java new file mode 100644 index 00000000000..225fc8556b2 --- /dev/null +++ b/function-groovy/src/main/groovy/org/particleframework/function/groovy/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ +/** + *

Support classes that simplify writing standalone functions as Groovy scripts

+ * + * @author Graeme Rocher + * @since 1.0 + */ +package org.particleframework.function.groovy; \ No newline at end of file diff --git a/function-groovy/src/main/resources/META-INF/services/org.codehaus.groovy.transform.ASTTransformation b/function-groovy/src/main/resources/META-INF/services/org.codehaus.groovy.transform.ASTTransformation new file mode 100644 index 00000000000..d1c4f4b3e84 --- /dev/null +++ b/function-groovy/src/main/resources/META-INF/services/org.codehaus.groovy.transform.ASTTransformation @@ -0,0 +1 @@ +org.particleframework.function.groovy.FunctionTransform \ No newline at end of file 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 new file mode 100644 index 00000000000..e07327528eb --- /dev/null +++ b/function-groovy/src/test/groovy/org/particleframework/function/groovy/FunctionTransformSpec.groovy @@ -0,0 +1,34 @@ +/* + * 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 spock.lang.Specification + +import java.nio.charset.StandardCharsets + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class FunctionTransformSpec extends Specification{ + + void "run function"() { + expect: + new RoundFunction().round(1.6f) == 2 + + } + +} 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 new file mode 100644 index 00000000000..db2c42d6c99 --- /dev/null +++ b/function-groovy/src/test/groovy/org/particleframework/function/groovy/MathService.groovy @@ -0,0 +1,29 @@ +/* + * 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 MathService { + int round(float value) { + Math.round(value) + } +} 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 new file mode 100644 index 00000000000..1c2c9c6ccc4 --- /dev/null +++ b/function-groovy/src/test/groovy/org/particleframework/function/groovy/RoundFunction.groovy @@ -0,0 +1,7 @@ +package org.particleframework.function.groovy + +MathService mathService + +int round(float value) { + mathService.round(value) // go +} \ No newline at end of file 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 42c2c6ac48f..12f7178620e 100644 --- a/function/src/main/java/org/particleframework/function/executor/FunctionInitializer.java +++ b/function/src/main/java/org/particleframework/function/executor/FunctionInitializer.java @@ -38,9 +38,10 @@ public class FunctionInitializer extends AbstractExecutor implements Closeable, @SuppressWarnings("unchecked") public FunctionInitializer() { - this.applicationContext = buildApplicationContext(null); - startEnvironment(applicationContext); - applicationContext.inject(this); + ApplicationContext applicationContext = buildApplicationContext(null); + this.applicationContext = applicationContext; + startEnvironment(this.applicationContext); + injectThis(applicationContext); } @Override @@ -58,6 +59,7 @@ public void close() throws IOException { * @throws IOException If an error occurs */ protected void run(String[] args, Function supplier) throws IOException { + ApplicationContext applicationContext = this.applicationContext; ParseContext context = new ParseContext(args); try { Object result = supplier.apply(context); @@ -71,6 +73,15 @@ protected void run(String[] args, Function supplier) throws IOE } } + /** + * Injects this instance + * @param applicationContext The {@link ApplicationContext} + * @return This injected instance + */ + protected void injectThis(ApplicationContext applicationContext) { + 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 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 11c35590c64..ab311b88da2 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 @@ -16,6 +16,8 @@ import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpecR */ @CompileStatic class AstUtils { + public static final Parameter[] ZERO_PARAMETERS = new Parameter[0] + public static final ClassNode[] EMPTY_CLASS_ARRAY = new ClassNode[0] static Parameter[] copyParameters(Parameter[] parameterTypes) { diff --git a/settings.gradle b/settings.gradle index 6e314115472..c11ec791693 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,6 +4,7 @@ include "aop" include "bootstrap" include "core" include "function" +include "function-groovy" include "function-web" include "function-aws" include "http"