├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── test │ ├── resources │ │ ├── com │ │ │ └── haulmont │ │ │ │ └── scripting │ │ │ │ ├── core │ │ │ │ └── test │ │ │ │ │ ├── mixed │ │ │ │ │ ├── mixed-repo.properties │ │ │ │ │ └── mixed-test-spring.xml │ │ │ │ │ ├── database │ │ │ │ │ ├── datasource.properties │ │ │ │ │ └── db-test-spring.xml │ │ │ │ │ ├── js │ │ │ │ │ └── js-test-spring.xml │ │ │ │ │ ├── mock │ │ │ │ │ └── mock-test-spring.xml │ │ │ │ │ ├── files │ │ │ │ │ └── files-test-spring.xml │ │ │ │ │ └── timeout │ │ │ │ │ └── timeout-test-spring.xml │ │ │ │ └── scripts │ │ │ │ ├── JsScriptRepository.simpleMath.js │ │ │ │ ├── CustomerScriptRepository.sayHello.groovy │ │ │ │ ├── CustomerScriptRepository.renameCustomer.groovy │ │ │ │ ├── CustomerScriptRepository.printData.groovy │ │ │ │ ├── TimeoutTestScriptRepository.doThirdLongJob.groovy │ │ │ │ └── CustomerScriptRepository.createCustomer.groovy │ │ └── logback-test.xml │ ├── test-scripts │ │ ├── MixedConfigScriptRepository.renameCustomer.groovy │ │ └── MixedConfigScriptRepository.createCustomer.groovy │ └── java │ │ └── com │ │ └── haulmont │ │ └── scripting │ │ └── core │ │ └── test │ │ ├── files │ │ ├── Customer.java │ │ ├── FileRepositoryTestConfig.java │ │ ├── CustomerScriptRepository.java │ │ └── FileRepositoryTest.java │ │ ├── mock │ │ ├── MockTestConfig.java │ │ ├── MockTestScriptRepository.java │ │ ├── MockTestService.java │ │ └── MockTest.java │ │ ├── js │ │ ├── JsRepositoryTestConfig.java │ │ ├── JsScriptRepository.java │ │ └── JsRepositoryTest.java │ │ ├── mixed │ │ ├── XmlGroovyScript.java │ │ ├── AnnotatedGroovyScript.java │ │ ├── MixedConfigurationTestConfig.java │ │ ├── MixedConfigScriptRepository.java │ │ └── MixedConfigRepositoryTest.java │ │ ├── timeout │ │ ├── TimeoutTestConfig.java │ │ ├── TestComposedTimeout.java │ │ ├── TimeoutTestScriptRepository.java │ │ └── TimeoutTest.java │ │ └── database │ │ ├── WrongScriptAnnotation.java │ │ ├── DbGroovyScript.java │ │ ├── TestTaxCalculator.java │ │ ├── DatabaseRepositoryTestConfig.java │ │ ├── DatabaseRepositoryTest.java │ │ └── GroovyScriptDbProvider.java └── main │ ├── resources │ ├── META-INF │ │ ├── spring.handlers │ │ ├── spring.schemas │ │ └── spring.factories │ ├── script-repo.properties │ └── com │ │ └── haulmont │ │ └── scripting │ │ └── repository │ │ ├── script-repositories-config.xml │ │ └── script-repositories.xsd │ └── java │ └── com │ └── haulmont │ └── scripting │ └── repository │ ├── evaluator │ ├── EvaluationStatus.java │ ├── GroovyScriptJsrValuator.java │ ├── JavaScriptJsrEvaluator.java │ ├── ScriptEvaluationException.java │ ├── TimeoutAware.java │ ├── ScriptResult.java │ └── Jsr233Evaluator.java │ ├── config │ ├── ScriptRepositoriesAutoConfiguration.java │ ├── ScriptRepositoryNamespaceHandler.java │ ├── EnableScriptRepositories.java │ ├── ScriptRepositoriesRegistrar.java │ ├── AnnotationConfig.java │ └── ScriptRepositoryConfigurationParser.java │ ├── provider │ ├── ScriptNotFoundException.java │ ├── ScriptProvider.java │ ├── GroovyResourceProvider.java │ ├── JavaScriptResourceProvider.java │ └── AbstractResourceProvider.java │ ├── GroovyScript.java │ ├── JavaScript.java │ ├── ScriptParam.java │ ├── ScriptRepository.java │ ├── ScriptMethod.java │ └── factory │ ├── ScriptRepositoryFactoryBean.java │ └── RepositoryMethodsHandler.java ├── .gitignore ├── gradlew.bat ├── gradlew ├── README.md └── LICENSE /settings.gradle: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuba-rnd/spring-script-repositories/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/core/test/mixed/mixed-repo.properties: -------------------------------------------------------------------------------- 1 | groovy.script.source.root.path=file:src/test/test-scripts -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/scripts/JsScriptRepository.simpleMath.js: -------------------------------------------------------------------------------- 1 | function f(a, b) { 2 | return a + b; 3 | } 4 | 5 | f(x, y); -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/core/test/database/datasource.properties: -------------------------------------------------------------------------------- 1 | jdbc.url=jdbc:hsqldb:mem:testDb 2 | jdbc.user=sa 3 | jdbc.password= -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/scripts/CustomerScriptRepository.sayHello.groovy: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.scripts 2 | 3 | return 'Hello!' -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.handlers: -------------------------------------------------------------------------------- 1 | http\://www.cuba-platform.org/schema/script/repositories=com.haulmont.scripting.repository.config.ScriptRepositoryNamespaceHandler -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.schemas: -------------------------------------------------------------------------------- 1 | http\://www.cuba-platform.org/schema/script/repositories/script-repositories.xsd=com/haulmont/scripting/repository/script-repositories.xsd -------------------------------------------------------------------------------- /src/main/resources/script-repo.properties: -------------------------------------------------------------------------------- 1 | groovy.script.source.root.path=classpath:com/haulmont/scripting/scripts 2 | js.script.source.root.path=classpath:com/haulmont/scripting/scripts -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.haulmont.scripting.repository.config.ScriptRepositoriesAutoConfiguration -------------------------------------------------------------------------------- /src/test/test-scripts/MixedConfigScriptRepository.renameCustomer.groovy: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.scripts 2 | 3 | return "2 - Customer with ${customerId} was renamed to ${newName}".toString() -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/scripts/CustomerScriptRepository.renameCustomer.groovy: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.scripts 2 | 3 | return "2 - Customer with ${customerId} was renamed to ${newName}".toString() -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/scripts/CustomerScriptRepository.printData.groovy: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.scripts 2 | 3 | println("Start of void test") 4 | println(stringToPrint) 5 | println("End of void test") 6 | -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/scripts/TimeoutTestScriptRepository.doThirdLongJob.groovy: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.scripts 2 | 3 | try { 4 | println("Sleeping for $timeMillis ms") 5 | Thread.sleep(Long.MAX_VALUE); 6 | } finally { 7 | println("Finally block") 8 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Sep 20 10:05:14 MSK 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 7 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/files/Customer.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.files; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | import java.util.UUID; 6 | 7 | public interface Customer { 8 | 9 | UUID getId(); 10 | String getName(); 11 | Date getBirthDate(); 12 | List getMyData(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/mock/MockTestConfig.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.mock; 2 | 3 | import com.haulmont.scripting.repository.config.EnableScriptRepositories; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @EnableScriptRepositories(basePackages = {"com.haulmont.scripting.core.test.mock"}) 8 | public class MockTestConfig { 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/js/JsRepositoryTestConfig.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.js; 2 | 3 | import com.haulmont.scripting.repository.config.EnableScriptRepositories; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @EnableScriptRepositories(basePackages = {"com.haulmont.scripting.core.test.js"}) 8 | public class JsRepositoryTestConfig { 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/mixed/XmlGroovyScript.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.mixed; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface XmlGroovyScript { 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/timeout/TimeoutTestConfig.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.timeout; 2 | 3 | import com.haulmont.scripting.repository.config.EnableScriptRepositories; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @EnableScriptRepositories(basePackages = {"com.haulmont.scripting.core.test.timeout"}) 8 | public class TimeoutTestConfig { 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/files/FileRepositoryTestConfig.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.files; 2 | 3 | import com.haulmont.scripting.repository.config.EnableScriptRepositories; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @EnableScriptRepositories(basePackages = {"com.haulmont.scripting.core.test.files"}) 8 | public class FileRepositoryTestConfig { 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/database/WrongScriptAnnotation.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.database; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface WrongScriptAnnotation { 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/mock/MockTestScriptRepository.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.mock; 2 | 3 | import com.haulmont.scripting.repository.GroovyScript; 4 | import com.haulmont.scripting.repository.ScriptRepository; 5 | 6 | import java.util.Locale; 7 | 8 | @ScriptRepository 9 | public interface MockTestScriptRepository { 10 | 11 | @GroovyScript 12 | String sayHello(Locale locale); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/timeout/TestComposedTimeout.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.timeout; 2 | 3 | import com.haulmont.scripting.repository.ScriptMethod; 4 | 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @ScriptMethod(timeout = 100L) 10 | public @interface TestComposedTimeout { 11 | long timeout() default -1L; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/evaluator/EvaluationStatus.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.evaluator; 2 | 3 | public enum EvaluationStatus { 4 | SUCCESS(true), 5 | FAILURE(false); 6 | 7 | private final boolean successful; 8 | 9 | public boolean isSuccessful() { 10 | return successful; 11 | } 12 | 13 | EvaluationStatus(boolean successful) { 14 | this.successful = successful; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/database/DbGroovyScript.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.database; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface DbGroovyScript { 11 | 12 | long timeout() default -1; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/js/JsScriptRepository.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.js; 2 | 3 | import com.haulmont.scripting.repository.JavaScript; 4 | import com.haulmont.scripting.repository.ScriptParam; 5 | import com.haulmont.scripting.repository.ScriptRepository; 6 | 7 | @ScriptRepository 8 | public interface JsScriptRepository { 9 | 10 | @JavaScript 11 | double simpleMath(@ScriptParam("x") double x, @ScriptParam("y") double y); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/evaluator/GroovyScriptJsrValuator.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.evaluator; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | /** 6 | * Evaluates Groovy script using JSR-223 javax.script API and bindings. 7 | */ 8 | @Component("groovyJsrEvaluator") 9 | public class GroovyScriptJsrValuator extends Jsr233Evaluator { 10 | 11 | protected String getEngineName() { 12 | return "groovy"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/evaluator/JavaScriptJsrEvaluator.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.evaluator; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | /** 6 | * Evaluates Groovy script using JSR-223 javax.script API and bindings. 7 | */ 8 | @Component("javaScriptJsrEvaluator") 9 | public class JavaScriptJsrEvaluator extends Jsr233Evaluator { 10 | 11 | protected String getEngineName() { 12 | return "javascript"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/evaluator/ScriptEvaluationException.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.evaluator; 2 | 3 | public class ScriptEvaluationException extends RuntimeException { 4 | 5 | public ScriptEvaluationException(String message) { 6 | super(message); 7 | } 8 | 9 | public ScriptEvaluationException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | 13 | public ScriptEvaluationException(Throwable cause) { 14 | super(cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/com/haulmont/scripting/repository/script-repositories-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/config/ScriptRepositoriesAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.config; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @ComponentScan(basePackages = "com.haulmont.scripting.repository") 9 | @PropertySource("classpath:script-repo.properties") 10 | public class ScriptRepositoriesAutoConfiguration { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/provider/ScriptNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.provider; 2 | 3 | public class ScriptNotFoundException extends RuntimeException { 4 | 5 | public ScriptNotFoundException() { 6 | } 7 | 8 | public ScriptNotFoundException(String message) { 9 | super(message); 10 | } 11 | 12 | public ScriptNotFoundException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public ScriptNotFoundException(Throwable cause) { 17 | super(cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/mock/MockTestService.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.mock; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | 6 | import java.util.Locale; 7 | 8 | @Service 9 | public class MockTestService { 10 | 11 | @Autowired 12 | private MockTestScriptRepository testScriptRepository; 13 | 14 | public String sayHelloWithName(String name, Locale locale) { 15 | return String.format("%s %s", testScriptRepository.sayHello(locale), name); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/mixed/AnnotatedGroovyScript.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.mixed; 2 | 3 | import com.haulmont.scripting.repository.ScriptMethod; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Target(ElementType.METHOD) 12 | @ScriptMethod(providerBeanName = "groovyResourceProvider", evaluatorBeanName = "groovyJsrEvaluator") 13 | public @interface AnnotatedGroovyScript { 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/mixed/MixedConfigurationTestConfig.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.mixed; 2 | 3 | import com.haulmont.scripting.repository.config.EnableScriptRepositories; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @PropertySource("classpath:com/haulmont/scripting/core/test/mixed/mixed-repo.properties") 9 | @EnableScriptRepositories(basePackages = {"com.haulmont.scripting.core.test.mixed"}) 10 | public class MixedConfigurationTestConfig { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/GroovyScript.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * Alias for @{@link ScriptMethod} default annotation. 11 | */ 12 | @Documented 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target({ElementType.METHOD}) 15 | @ScriptMethod 16 | public @interface GroovyScript { 17 | 18 | long timeout() default -1; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/evaluator/TimeoutAware.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.evaluator; 2 | 3 | /** 4 | * Interface to mark classes cancellable - the method is be called on 5 | * exception during execution. You may use it to close DB connections, 6 | * sockets, streams etc. 7 | * It is stronly advised to publish timeout aware bean as a prototype bean to avoid issues 8 | * with multi-threading in singleton beans. 9 | */ 10 | public interface TimeoutAware { 11 | 12 | /** 13 | * This method is called to interrupt script evaluation in case of exception. 14 | */ 15 | void cancel(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/JavaScript.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * Default implementation for JavaScript-backed method execution. 11 | */ 12 | @Documented 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target({ElementType.METHOD}) 15 | @ScriptMethod(providerBeanName = "javaScriptResourceProvider", evaluatorBeanName = "javaScriptJsrEvaluator") 16 | public @interface JavaScript { 17 | 18 | long timeout() default -1; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/ScriptParam.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * We need it to get real argument names in runtime if jar was compiled without debug information. 11 | */ 12 | @Documented 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target(ElementType.PARAMETER) 15 | public @interface ScriptParam { 16 | 17 | /** 18 | * Parameter name that will be used in script. 19 | * @return parameter name. 20 | */ 21 | String value(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/config/ScriptRepositoryNamespaceHandler.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.config; 2 | 3 | import org.springframework.beans.factory.xml.NamespaceHandlerSupport; 4 | 5 | /** 6 | * Registers parser for XML based configuration for script repositories. 7 | */ 8 | public class ScriptRepositoryNamespaceHandler extends NamespaceHandlerSupport { 9 | 10 | public static final String SCRIPT_REPOSITORIES_TAG_NAME = "script-repositories"; 11 | 12 | /** 13 | * @see NamespaceHandlerSupport#init() 14 | */ 15 | @Override 16 | public void init() { 17 | registerBeanDefinitionParser(SCRIPT_REPOSITORIES_TAG_NAME, new ScriptRepositoryConfigurationParser()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/core/test/js/js-test-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/core/test/mock/mock-test-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/ScriptRepository.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * Marker for script repository interface. 11 | */ 12 | @Documented 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target(ElementType.TYPE) 15 | public @interface ScriptRepository { 16 | 17 | /** 18 | * Optional description for script repository. E.g. "Customer manipulation-related scripts". 19 | * @return script repository description. 20 | */ 21 | String description() default ""; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/core/test/files/files-test-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/core/test/timeout/timeout-test-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/database/TestTaxCalculator.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.database; 2 | 3 | import com.haulmont.scripting.repository.ScriptParam; 4 | import com.haulmont.scripting.repository.ScriptRepository; 5 | import com.haulmont.scripting.repository.evaluator.ScriptResult; 6 | 7 | import java.math.BigDecimal; 8 | 9 | @ScriptRepository 10 | public interface TestTaxCalculator { 11 | 12 | @DbGroovyScript 13 | ScriptResult calculateTax(@ScriptParam("amount") BigDecimal amount); 14 | 15 | @WrongScriptAnnotation 16 | BigDecimal calculateVat(@ScriptParam("amount") BigDecimal amount); 17 | 18 | @DbGroovyScript 19 | String notImplementedMethod(); 20 | 21 | @DbGroovyScript (timeout = 500L) 22 | String timeoutError(); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/mixed/MixedConfigScriptRepository.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.mixed; 2 | 3 | import com.haulmont.scripting.core.test.files.Customer; 4 | import com.haulmont.scripting.repository.ScriptParam; 5 | import com.haulmont.scripting.repository.ScriptRepository; 6 | import com.haulmont.scripting.repository.evaluator.ScriptResult; 7 | 8 | import java.util.Date; 9 | import java.util.UUID; 10 | 11 | @ScriptRepository 12 | public interface MixedConfigScriptRepository { 13 | 14 | @XmlGroovyScript 15 | ScriptResult renameCustomer(@ScriptParam("customerId") UUID customerId, @ScriptParam("newName") String newName); 16 | 17 | @AnnotatedGroovyScript 18 | ScriptResult createCustomer(@ScriptParam("name") String name, @ScriptParam("birthDate") Date birthDate); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/provider/ScriptProvider.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.provider; 2 | 3 | import org.springframework.scripting.ScriptSource; 4 | 5 | import java.lang.reflect.Method; 6 | 7 | /** 8 | * Generic interface, use it to implement beans that provide scripts for execution. 9 | */ 10 | public interface ScriptProvider { 11 | 12 | /** 13 | * Gets script text based on method signature. 14 | * Please note that you may want to implement com.haulmont.cuba.security.app.Authenticated from 15 | * core module to get scripts protected by row-level security. 16 | * @param method Script Repository interface method to be executed. 17 | * @return Script text associated with this method. 18 | * @throws ScriptNotFoundException in case script text is not found. 19 | */ 20 | ScriptSource getScript(Method method); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TRACE 8 | 9 | 10 | 11 | %d{HH:mm:ss.SSS} %-5level %logger - %msg%n 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/evaluator/ScriptResult.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.evaluator; 2 | 3 | /** 4 | * Wrapper for script execution result. Contains result object (null in case of execution error), 5 | * execution status and execution error (if any). 6 | * 7 | * @param script execution result type. 8 | */ 9 | public class ScriptResult { 10 | 11 | private final T value; 12 | 13 | private final EvaluationStatus status; 14 | 15 | private final Throwable error; 16 | 17 | public ScriptResult(T value, EvaluationStatus status, Throwable error) { 18 | this.value = value; 19 | this.status = status; 20 | this.error = error; 21 | } 22 | 23 | public T getValue() { 24 | return value; 25 | } 26 | 27 | public EvaluationStatus getStatus() { 28 | return status; 29 | } 30 | 31 | public Throwable getError() { 32 | return error; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/config/EnableScriptRepositories.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.config; 2 | 3 | 4 | import org.springframework.context.annotation.Import; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.ElementType; 8 | import java.lang.annotation.Inherited; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.RetentionPolicy; 11 | import java.lang.annotation.Target; 12 | 13 | /** 14 | * Annotation to enable scripting repositories. 15 | * Will scan packages of the annotated configuration class for scripting repositories. 16 | * 17 | * */ 18 | @Target(ElementType.TYPE) 19 | @Retention(RetentionPolicy.RUNTIME) 20 | @Documented 21 | @Inherited 22 | @Import(ScriptRepositoriesRegistrar.class) 23 | public @interface EnableScriptRepositories { 24 | /** 25 | * Packages to be scanned. 26 | * @return aray of package names. 27 | */ 28 | String[] basePackages(); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/test-scripts/MixedConfigScriptRepository.createCustomer.groovy: -------------------------------------------------------------------------------- 1 | import com.haulmont.scripting.core.test.files.Customer 2 | 3 | class CustomerImplMixed implements Customer { 4 | 5 | private UUID id 6 | private String name 7 | private Date birthDate 8 | 9 | UUID getId() { 10 | return id 11 | } 12 | 13 | void setId(UUID id) { 14 | this.id = id 15 | } 16 | 17 | String getName() { 18 | return name 19 | } 20 | 21 | void setName(String name) { 22 | this.name = name 23 | } 24 | 25 | Date getBirthDate() { 26 | return birthDate 27 | } 28 | 29 | void setBirthDate(Date birthDate) { 30 | this.birthDate = birthDate 31 | } 32 | 33 | @Override 34 | List getMyData() { 35 | return [id.toString(), name, birthDate.toString()] 36 | } 37 | } 38 | 39 | CustomerImplMixed c = new CustomerImplMixed() 40 | c.setId(UUID.randomUUID()) 41 | c.setName(name) 42 | c.setBirthDate(birthDate) 43 | c -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/scripts/CustomerScriptRepository.createCustomer.groovy: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.scripts 2 | 3 | import com.haulmont.scripting.core.test.files.Customer 4 | 5 | class CustomerImpl implements Customer{ 6 | 7 | private UUID id 8 | private String name 9 | private Date birthDate 10 | 11 | UUID getId() { 12 | return id 13 | } 14 | 15 | void setId(UUID id) { 16 | this.id = id 17 | } 18 | 19 | String getName() { 20 | return name 21 | } 22 | 23 | void setName(String name) { 24 | this.name = name 25 | } 26 | 27 | Date getBirthDate() { 28 | return birthDate 29 | } 30 | 31 | void setBirthDate(Date birthDate) { 32 | this.birthDate = birthDate 33 | } 34 | 35 | @Override 36 | List getMyData() { 37 | return [id.toString(), name, birthDate.toString()] 38 | } 39 | } 40 | 41 | CustomerImpl c = new CustomerImpl() 42 | c.setId(UUID.randomUUID()) 43 | c.setName(name) 44 | c.setBirthDate(birthDate) 45 | c -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/files/CustomerScriptRepository.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.files; 2 | 3 | import com.haulmont.scripting.repository.GroovyScript; 4 | import com.haulmont.scripting.repository.ScriptMethod; 5 | import com.haulmont.scripting.repository.ScriptParam; 6 | import com.haulmont.scripting.repository.ScriptRepository; 7 | 8 | import java.util.Date; 9 | import java.util.UUID; 10 | 11 | @ScriptRepository 12 | public interface CustomerScriptRepository { 13 | 14 | @ScriptMethod 15 | String renameCustomer(@ScriptParam("customerId") UUID customerId, @ScriptParam("newName") String newName); 16 | 17 | @GroovyScript 18 | Customer createCustomer(@ScriptParam("name") String name, @ScriptParam("birthDate") Date birthDate); 19 | 20 | @ScriptMethod 21 | default String getDefaultName() { 22 | return "NewCustomer"; 23 | } 24 | 25 | @ScriptMethod 26 | String getDefaultError(); 27 | 28 | @GroovyScript 29 | String sayHello(); 30 | 31 | @GroovyScript 32 | void printData(@ScriptParam("stringToPrint") String stringToPrint); 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/provider/GroovyResourceProvider.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.provider; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.lang.reflect.Method; 7 | 8 | /** 9 | * Provider for groovy scripts stored in application resources. 10 | */ 11 | @Component("groovyResourceProvider") 12 | public class GroovyResourceProvider extends AbstractResourceProvider { 13 | 14 | @Value("${groovy.script.source.root.path}") 15 | private String rootPath; 16 | 17 | /** 18 | * Generates script location path based on rule: rootPath/InterfaceName.methodName.groovy. 19 | * @param method scripted method. 20 | * @return resource path string. 21 | */ 22 | @Override 23 | public String getResourcePath(Method method) { 24 | return rootPath + "/" + method.getDeclaringClass().getSimpleName() + "." + method.getName() + ".groovy"; 25 | } 26 | 27 | public String getRootPath() { 28 | return rootPath; 29 | } 30 | 31 | public void setRootPath(String rootPath) { 32 | this.rootPath = rootPath; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/provider/JavaScriptResourceProvider.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.provider; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.lang.reflect.Method; 7 | 8 | /** 9 | * Provider for groovy scripts stored in application resources. 10 | */ 11 | @Component("javaScriptResourceProvider") 12 | public class JavaScriptResourceProvider extends AbstractResourceProvider { 13 | 14 | @Value("${js.script.source.root.path}") 15 | private String rootPath; 16 | 17 | /** 18 | * Generates script location path based on rule: rootPath/InterfaceName.methodName.js. 19 | * @param method scripted method. 20 | * @return resource path string. 21 | */ 22 | @Override 23 | public String getResourcePath(Method method) { 24 | return rootPath + "/" + method.getDeclaringClass().getSimpleName() + "." + method.getName() + ".js"; 25 | } 26 | 27 | public String getRootPath() { 28 | return rootPath; 29 | } 30 | 31 | public void setRootPath(String rootPath) { 32 | this.rootPath = rootPath; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/mock/MockTest.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.mock; 2 | 3 | import mockit.Expectations; 4 | import mockit.Injectable; 5 | import mockit.Tested; 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.test.context.ContextConfiguration; 10 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 11 | 12 | import java.util.Locale; 13 | 14 | @ContextConfiguration(locations = {"classpath:com/haulmont/scripting/core/test/mock/mock-test-spring.xml"}) 15 | @RunWith(SpringJUnit4ClassRunner.class) 16 | public class MockTest { 17 | 18 | @Tested 19 | private MockTestService testService; 20 | 21 | @Injectable 22 | private MockTestScriptRepository testScriptRepository; 23 | 24 | 25 | @Test 26 | public void invokeMockedMethod() { 27 | 28 | new Expectations() { 29 | {testScriptRepository.sayHello(Locale.ENGLISH); result = "Hi";} 30 | {testScriptRepository.sayHello(Locale.GERMAN); result = "Halo";} 31 | }; 32 | 33 | Assert.assertEquals("Halo John", testService.sayHelloWithName("John", Locale.GERMAN)); 34 | Assert.assertEquals("Hi John", testService.sayHelloWithName("John", Locale.ENGLISH)); 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/core/test/database/db-test-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | com.haulmont.scripting.core.test.database 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/js/JsRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.js; 2 | 3 | import ch.qos.logback.classic.LoggerContext; 4 | import ch.qos.logback.core.util.StatusPrinter; 5 | import org.junit.After; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.test.context.ContextConfiguration; 13 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 14 | 15 | import static org.junit.Assert.assertTrue; 16 | 17 | @ContextConfiguration(locations = {"classpath:com/haulmont/scripting/core/test/js/js-test-spring.xml"}) 18 | @RunWith(SpringJUnit4ClassRunner.class) 19 | public class JsRepositoryTest { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(JsRepositoryTest.class); 22 | 23 | @Autowired 24 | private JsScriptRepository repo; 25 | 26 | 27 | @Before 28 | public void setUp() throws Exception { 29 | LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); 30 | StatusPrinter.print(lc); 31 | } 32 | 33 | @After 34 | public void tearDown() throws Exception { 35 | } 36 | 37 | 38 | @Test 39 | public void testSimpleMath() { 40 | double result = repo.simpleMath(1.1, 2.1); 41 | log.trace("JS result: {}", result); 42 | assertTrue(Math.abs(3.2 - result) < 0.0001); 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/test/resources/com/haulmont/scripting/core/test/mixed/mixed-test-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | com.haulmont.scripting.core.test.mixed 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/timeout/TimeoutTestScriptRepository.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.timeout; 2 | 3 | import com.haulmont.scripting.repository.GroovyScript; 4 | import com.haulmont.scripting.repository.ScriptParam; 5 | import com.haulmont.scripting.repository.ScriptRepository; 6 | import com.haulmont.scripting.repository.evaluator.EvaluationStatus; 7 | import com.haulmont.scripting.repository.evaluator.ScriptResult; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | @ScriptRepository 12 | public interface TimeoutTestScriptRepository { 13 | 14 | Logger log = LoggerFactory.getLogger(TimeoutTestScriptRepository.class); 15 | 16 | 17 | String SUCCESS = "Success!"; 18 | 19 | @GroovyScript (timeout = 1_000L) 20 | default String doLongJob(Long timeInMillis) { 21 | try { 22 | log.info("Will sleep for {} ms", timeInMillis); 23 | Thread.sleep(timeInMillis); 24 | return SUCCESS; 25 | } catch (InterruptedException e) { 26 | throw new RuntimeException(e); 27 | } 28 | } 29 | 30 | 31 | @TestComposedTimeout 32 | default ScriptResult doAnotherLongJob(Long timeInMillis) { 33 | try { 34 | log.info("Will sleep again for {} ms", timeInMillis); 35 | Thread.sleep(timeInMillis); 36 | return new ScriptResult<>(SUCCESS, EvaluationStatus.SUCCESS, null); 37 | } catch (InterruptedException e) { 38 | throw new RuntimeException(e); 39 | } 40 | } 41 | 42 | 43 | @TestComposedTimeout (timeout = 1_000L) 44 | void doThirdLongJob(@ScriptParam("timeMillis") Long timeInMillis); 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/provider/AbstractResourceProvider.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.provider; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.core.io.DefaultResourceLoader; 6 | import org.springframework.core.io.Resource; 7 | import org.springframework.scripting.ScriptSource; 8 | import org.springframework.scripting.support.ResourceScriptSource; 9 | import org.springframework.util.ResourceUtils; 10 | 11 | import java.lang.reflect.Method; 12 | 13 | /** 14 | * Loads script text from application using {@link ResourceUtils#getFile(String)} class. 15 | * Allows you to define your own way of building resource path based on method signature. 16 | */ 17 | public abstract class AbstractResourceProvider implements ScriptProvider { 18 | 19 | private static final Logger log = LoggerFactory.getLogger(AbstractResourceProvider.class); 20 | 21 | private DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); 22 | 23 | /** 24 | * {@inheritDoc} 25 | */ 26 | @Override 27 | public ScriptSource getScript(Method method) { 28 | String path = getResourcePath(method); 29 | log.debug("Getting script from resource {}", path); 30 | Resource res = resourceLoader.getResource(path); 31 | if (res.exists()){ 32 | return new ResourceScriptSource(res); 33 | } else { 34 | throw new ScriptNotFoundException(String.format("Resource %s does not exists", path)); 35 | } 36 | } 37 | 38 | /** 39 | * Creates resource path string based on method signature. 40 | * @param method scripted method. 41 | * @return resource path string. 42 | */ 43 | public abstract String getResourcePath(Method method); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/ScriptMethod.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * Annotation that should be used to mark interface methods as available for scripting. 11 | * Also this annotation can be used as meta-annotation for custom annotations for scripted methods, e.g. 12 | *
13 |  * @ScriptMethod(providerBeanName="sqlScriptProvider", evaluatorBeanName="sqlScriptExecutor")
14 |  * public @interface SqlScriptMethod {...}
15 |  * 
16 | * Then annotation SqlScriptMethod can be used in code. 17 | * 18 | * Bean names are used instead of classes for better flexibility. 19 | */ 20 | @Documented 21 | @Retention(RetentionPolicy.RUNTIME) 22 | @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 23 | public @interface ScriptMethod { 24 | /** 25 | * Spring bean that returns script text. 26 | * @return bean name. 27 | */ 28 | String providerBeanName() default "groovyResourceProvider"; 29 | 30 | /** 31 | * Spring bean name that will execute script returned by provider. 32 | * @return bean name. 33 | */ 34 | String evaluatorBeanName() default "groovyJsrEvaluator"; 35 | 36 | /** 37 | * Method execution timeout in milliseconds, no timeout by default. 38 | * @return execution timeout, negative value if timeout is not set. 39 | */ 40 | long timeout() default -1L; 41 | 42 | /** 43 | * Optional description for script, can be used for auto-generated documentation of all scripted extensions in the application. 44 | * @return description string. 45 | */ 46 | String description() default ""; 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/evaluator/Jsr233Evaluator.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.evaluator; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.scripting.ScriptCompilationException; 6 | import org.springframework.scripting.ScriptEvaluator; 7 | import org.springframework.scripting.ScriptSource; 8 | 9 | import javax.script.ScriptEngine; 10 | import javax.script.ScriptEngineManager; 11 | import javax.script.ScriptException; 12 | import javax.script.SimpleBindings; 13 | import java.io.IOException; 14 | import java.util.Collections; 15 | import java.util.Map; 16 | 17 | 18 | /** 19 | * Evaluates scripts using JSR-223 javax.script API and bindings. 20 | */ 21 | public abstract class Jsr233Evaluator implements ScriptEvaluator { 22 | 23 | private static final Logger log = LoggerFactory.getLogger(Jsr233Evaluator.class); 24 | 25 | private final ScriptEngineManager manager = new ScriptEngineManager(); 26 | 27 | @Override 28 | public Object evaluate(ScriptSource script) throws ScriptCompilationException { 29 | return eval(script, Collections.emptyMap()); 30 | } 31 | 32 | @Override 33 | public Object evaluate(ScriptSource script, Map arguments) throws ScriptCompilationException { 34 | return eval(script, arguments); 35 | } 36 | 37 | private Object eval(ScriptSource script, Map parameters) { 38 | String engineName = getEngineName(); 39 | ScriptEngine scriptEngine = manager.getEngineByName(engineName); 40 | log.trace("Script bindings: {}", parameters); 41 | try { 42 | String scriptAsString = script.getScriptAsString(); 43 | log.trace("Script text ({}): \n {} \n", engineName, scriptAsString); 44 | return scriptEngine.eval(scriptAsString, new SimpleBindings(parameters)); 45 | } catch (IOException | ScriptException e) { 46 | throw new ScriptCompilationException("Error executing script", e); 47 | } 48 | } 49 | 50 | protected abstract String getEngineName(); 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/resources/com/haulmont/scripting/repository/script-repositories.xsd: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/timeout/TimeoutTest.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.timeout; 2 | 3 | import com.haulmont.scripting.repository.evaluator.EvaluationStatus; 4 | import com.haulmont.scripting.repository.evaluator.ScriptEvaluationException; 5 | import com.haulmont.scripting.repository.evaluator.ScriptResult; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.test.context.ContextConfiguration; 10 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 11 | 12 | import java.util.concurrent.TimeoutException; 13 | 14 | import static org.junit.Assert.assertEquals; 15 | import static org.junit.Assert.fail; 16 | 17 | @ContextConfiguration(locations = {"classpath:com/haulmont/scripting/core/test/timeout/timeout-test-spring.xml"}) 18 | @RunWith(SpringJUnit4ClassRunner.class) 19 | public class TimeoutTest { 20 | 21 | @Autowired 22 | private TimeoutTestScriptRepository testScriptRepository; 23 | 24 | @Test//Timeout 1_000L 25 | public void runShortJob() { 26 | String result = testScriptRepository.doLongJob(100L); 27 | assertEquals(testScriptRepository.SUCCESS, result); 28 | } 29 | 30 | @Test(expected = ScriptEvaluationException.class) //Timeout 1_000L 31 | public void runLongJob() { 32 | testScriptRepository.doLongJob(10_000L); 33 | fail("Long-running methods must throw exception if timeout is set"); 34 | } 35 | 36 | 37 | @Test//Timeout 100L 38 | public void runLongJobComposedTimeout() { 39 | ScriptResult result = testScriptRepository.doAnotherLongJob(10_000L); 40 | assertEquals(EvaluationStatus.FAILURE, result.getStatus()); 41 | assertEquals(TimeoutException.class, result.getError().getClass()); 42 | } 43 | 44 | 45 | @Test(expected = ScriptEvaluationException.class) //Timeout 1_000L 46 | public void runLongMethodComposedWithTimeout() { 47 | testScriptRepository.doThirdLongJob(5_000L); 48 | fail("Long-running methods must throw exception if timeout is set"); 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/config/ScriptRepositoriesRegistrar.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.config; 2 | 3 | import com.haulmont.scripting.repository.factory.ScriptRepositoryFactoryBean; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 7 | import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; 8 | import org.springframework.core.annotation.AnnotationAttributes; 9 | import org.springframework.core.type.AnnotationMetadata; 10 | 11 | import java.lang.annotation.Annotation; 12 | import java.util.Arrays; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | /** 18 | * Class that registers script repositories in Spring context if annotation-based configuration is used. 19 | * @see EnableScriptRepositories 20 | */ 21 | public class ScriptRepositoriesRegistrar implements ImportBeanDefinitionRegistrar { 22 | 23 | private static final Logger log = LoggerFactory.getLogger(ScriptRepositoriesRegistrar.class); 24 | 25 | /** 26 | * @see ImportBeanDefinitionRegistrar#registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry) 27 | */ 28 | @Override 29 | public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { 30 | log.info("{}", importingClassMetadata.getAnnotationTypes()); 31 | if (importingClassMetadata.getAnnotationAttributes(getAnnotation().getName()) == null) { 32 | return; 33 | } 34 | 35 | AnnotationAttributes attributes = new AnnotationAttributes(importingClassMetadata.getAnnotationAttributes(getAnnotation().getName())); 36 | 37 | List basePackages = Arrays.asList(attributes.getStringArray("basePackages")); 38 | 39 | //We need it to be not immutable in case XML config parser will add anything 40 | Map, AnnotationConfig> customAnnotationsConfig = new HashMap<>(); 41 | 42 | ScriptRepositoryFactoryBean.registerBean(registry, basePackages, customAnnotationsConfig); 43 | } 44 | 45 | /** 46 | * Method that returns annotation class that will be used to enable spring repositories configuration. 47 | * @return annotation class. 48 | */ 49 | protected Class getAnnotation() { 50 | return EnableScriptRepositories.class; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/database/DatabaseRepositoryTestConfig.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.database; 2 | 3 | import com.haulmont.scripting.repository.evaluator.EvaluationStatus; 4 | import com.haulmont.scripting.repository.evaluator.ScriptResult; 5 | import org.hsqldb.jdbc.JDBCDataSource; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.PropertySource; 11 | 12 | import java.math.BigDecimal; 13 | 14 | @Configuration 15 | @PropertySource({"classpath:com/haulmont/scripting/core/test/database/datasource.properties"}) 16 | public class DatabaseRepositoryTestConfig { 17 | 18 | @Value("${jdbc.url}") 19 | private String dbUrl; 20 | 21 | @Value("${jdbc.user}") 22 | private String user; 23 | 24 | @Value("${jdbc.password}") 25 | private String password; 26 | 27 | @Bean 28 | public JDBCDataSource dataSource(){ 29 | JDBCDataSource dataSource = new JDBCDataSource(); 30 | dataSource.setURL(dbUrl); 31 | dataSource.setUser(user); 32 | dataSource.setPassword(password); 33 | return dataSource; 34 | } 35 | 36 | @Bean 37 | public TestTaxService testTaxService() { 38 | return new TestTaxService(); 39 | } 40 | 41 | /** 42 | * Inner class just to keep all test classes in one place. 43 | */ 44 | public static class TestTaxService { 45 | 46 | @Autowired 47 | private TestTaxCalculator taxCalculator; 48 | 49 | public BigDecimal calculateTaxAmount(BigDecimal sum){ 50 | ScriptResult tax = taxCalculator.calculateTax(sum); 51 | if (tax.getStatus() != EvaluationStatus.FAILURE) { 52 | return tax.getValue(); 53 | } else { 54 | throw new RuntimeException(tax.getError()); 55 | } 56 | } 57 | 58 | public BigDecimal calculateVat(BigDecimal sum) { 59 | return taxCalculator.calculateVat(sum); 60 | } 61 | 62 | public String runNotImplementedMethod() { 63 | return taxCalculator.notImplementedMethod(); 64 | } 65 | 66 | public String runTimeoutError() { 67 | return taxCalculator.timeoutError(); 68 | } 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/mixed/MixedConfigRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.mixed; 2 | 3 | import ch.qos.logback.classic.LoggerContext; 4 | import ch.qos.logback.core.util.StatusPrinter; 5 | import com.haulmont.scripting.core.test.files.Customer; 6 | import org.apache.commons.lang3.RandomStringUtils; 7 | import org.apache.commons.lang3.time.DateUtils; 8 | import org.junit.After; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.test.context.ContextConfiguration; 16 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 17 | 18 | import java.text.ParseException; 19 | import java.util.Date; 20 | import java.util.UUID; 21 | 22 | import static org.junit.Assert.assertNotNull; 23 | import static org.junit.Assert.assertTrue; 24 | import static org.junit.Assert.assertEquals; 25 | 26 | 27 | @ContextConfiguration(locations = {"classpath:com/haulmont/scripting/core/test/mixed/mixed-test-spring.xml"}) 28 | @RunWith(SpringJUnit4ClassRunner.class) 29 | public class MixedConfigRepositoryTest { 30 | 31 | private static final Logger log = LoggerFactory.getLogger(MixedConfigRepositoryTest.class); 32 | 33 | @Autowired 34 | private MixedConfigScriptRepository repo; 35 | 36 | @Before 37 | public void setUp() throws Exception { 38 | LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); 39 | StatusPrinter.print(lc); 40 | } 41 | 42 | @After 43 | public void tearDown() throws Exception { 44 | } 45 | 46 | @Test 47 | public void testRunSimpleScript() { 48 | UUID customerId = UUID.randomUUID(); 49 | String newName = RandomStringUtils.randomAlphabetic(8); 50 | String s = repo.renameCustomer(customerId, newName).getValue(); 51 | log.info("Message: {}", s); 52 | assertNotNull(s); 53 | assertTrue(s.contains(customerId.toString())); 54 | assertTrue(s.contains(newName)); 55 | } 56 | 57 | @Test 58 | public void testCreateObject() throws ParseException { 59 | String newName = RandomStringUtils.randomAlphabetic(8); 60 | Date birthDate = DateUtils.parseDate("1988-12-15", "yyyy-MM-dd"); 61 | Customer c = repo.createCustomer(newName, birthDate).getValue(); 62 | log.info("Customer created in groovy: {}", c); 63 | assertEquals(newName, c.getName()); 64 | assertEquals(birthDate, c.getBirthDate()); 65 | assertNotNull(c.getId()); 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/database/DatabaseRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.database; 2 | 3 | import com.haulmont.scripting.repository.evaluator.ScriptEvaluationException; 4 | import org.hsqldb.jdbc.JDBCDataSource; 5 | import org.junit.After; 6 | import org.junit.Assert; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.test.context.ContextConfiguration; 14 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 15 | 16 | import java.math.BigDecimal; 17 | import java.sql.Connection; 18 | import java.sql.Statement; 19 | 20 | import static org.junit.Assert.fail; 21 | 22 | @ContextConfiguration(locations = {"classpath:com/haulmont/scripting/core/test/database/db-test-spring.xml"}) 23 | @RunWith(SpringJUnit4ClassRunner.class) 24 | public class DatabaseRepositoryTest { 25 | 26 | private static final Logger log = LoggerFactory.getLogger(DatabaseRepositoryTest.class); 27 | 28 | @Autowired 29 | private DatabaseRepositoryTestConfig.TestTaxService taxService; 30 | 31 | @Autowired 32 | private JDBCDataSource dataSource; 33 | 34 | private Connection conn; 35 | 36 | @Before 37 | public void setUp() throws Exception { 38 | conn = dataSource.getConnection(); 39 | try (Statement st = conn.createStatement()){ 40 | st.execute("create table persistent_script (name varchar(255) not null, source_text varchar(1000) not null)"); 41 | st.execute("insert into persistent_script values('TestTaxCalculator.calculateTax', 'return amount*0.13')"); 42 | } 43 | } 44 | 45 | @After 46 | public void tearDown() throws Exception { 47 | try (Statement st = conn.createStatement()){ 48 | st.execute("drop table persistent_script"); 49 | st.execute("shutdown"); 50 | } 51 | conn.close(); 52 | } 53 | 54 | @Test 55 | public void testTaxCalculation () { 56 | BigDecimal taxAmount = taxService.calculateTaxAmount(BigDecimal.TEN); 57 | log.info("Tax amount is: {}", taxAmount); 58 | Assert.assertTrue(BigDecimal.valueOf(1.4).compareTo(taxAmount) > 0); 59 | } 60 | 61 | 62 | @Test (expected = IllegalArgumentException.class) 63 | public void testConfigError() { 64 | taxService.calculateVat(BigDecimal.TEN); 65 | fail("Annotations not mapped in XML should not be handled"); 66 | } 67 | 68 | @Test (expected = ScriptEvaluationException.class) 69 | public void testNotImplementedError() { 70 | taxService.runNotImplementedMethod(); 71 | fail("When running not implemented method it should throw an error"); 72 | } 73 | 74 | @Test (expected = ScriptEvaluationException.class) 75 | public void testDbTimeout() { 76 | taxService.runTimeoutError(); 77 | fail("The method should fail due to timeout"); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/config/AnnotationConfig.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.config; 2 | 3 | import com.haulmont.scripting.repository.ScriptParam; 4 | 5 | import java.io.Serializable; 6 | import java.lang.annotation.Annotation; 7 | import java.lang.reflect.Method; 8 | import java.lang.reflect.Parameter; 9 | import java.util.Arrays; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.function.Function; 13 | 14 | /** 15 | * Struct like class to hold scripted method data taken from annotation. 16 | */ 17 | @SuppressWarnings("serial") 18 | public class AnnotationConfig implements Serializable { 19 | 20 | public final Class scriptAnnotation; 21 | public final String provider; 22 | public final String evaluator; 23 | public final long timeout; 24 | public final String description; 25 | 26 | public AnnotationConfig(Class scriptAnnotation, String provider, String evaluator, long timeout, String description) { 27 | this.scriptAnnotation = scriptAnnotation; 28 | this.provider = provider; 29 | this.evaluator = evaluator; 30 | this.timeout = timeout; 31 | this.description = description; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "AnnotationConfig{" + 37 | "scriptAnnotation=" + scriptAnnotation + 38 | ", provider='" + provider + '\'' + 39 | ", evaluator='" + evaluator + '\'' + 40 | ", timeout=" + timeout + 41 | ", description='" + description + '\'' + 42 | '}'; 43 | } 44 | 45 | /** 46 | * Creates parameters map based on configured parameter names and actual argument values. 47 | * 48 | * @param method called method. 49 | * @param args actual argument values. 50 | * @return parameter name - value maps. 51 | */ 52 | public Map createParameterMap(Method method, Object[] args) { 53 | String[] argNames = Arrays.stream(method.getParameters()) 54 | .map(getParameterName()) 55 | .toArray(String[]::new); 56 | int length = args != null ? args.length : 0; 57 | if (argNames.length != length) { 58 | throw new IllegalArgumentException(String.format("Parameters and args must be the same length. Parameters: %d args: %d", argNames.length, length)); 59 | } 60 | Map paramsMap = new HashMap<>(argNames.length); 61 | for (int i = 0; i < argNames.length; i++) { 62 | paramsMap.put(argNames[i], args[i]); 63 | } 64 | return paramsMap; 65 | } 66 | 67 | /** 68 | * Returns parameter name for a method. 69 | * 70 | * @return parameter name. 71 | */ 72 | private Function getParameterName() { 73 | return p -> p.isAnnotationPresent(ScriptParam.class) 74 | ? p.getAnnotation(ScriptParam.class).value() 75 | : p.getName(); 76 | } 77 | 78 | 79 | } -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/database/GroovyScriptDbProvider.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.database; 2 | 3 | import com.haulmont.scripting.repository.evaluator.TimeoutAware; 4 | import com.haulmont.scripting.repository.provider.ScriptNotFoundException; 5 | import com.haulmont.scripting.repository.provider.ScriptProvider; 6 | import org.hsqldb.jdbc.JDBCDataSource; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.config.ConfigurableBeanFactory; 11 | import org.springframework.context.annotation.Scope; 12 | import org.springframework.scripting.ScriptSource; 13 | import org.springframework.scripting.support.StaticScriptSource; 14 | import org.springframework.stereotype.Component; 15 | 16 | import java.lang.reflect.Method; 17 | import java.sql.Connection; 18 | import java.sql.PreparedStatement; 19 | import java.sql.ResultSet; 20 | import java.sql.SQLException; 21 | 22 | @Component("groovyDbProvider") 23 | @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 24 | public class GroovyScriptDbProvider implements ScriptProvider, TimeoutAware { 25 | 26 | private static final Logger log = LoggerFactory.getLogger(GroovyScriptDbProvider.class); 27 | 28 | @Autowired 29 | private JDBCDataSource dataSource; 30 | 31 | private Connection connection; //This is an execution context for prototype 32 | 33 | private String getScriptTextbyName(String name) throws SQLException, InterruptedException { 34 | String result = null; 35 | try (Connection conn = dataSource.getConnection(); 36 | PreparedStatement st = conn.prepareStatement("select source_text from persistent_script where name = ?")){ 37 | connection = conn; 38 | st.setString(1, name); 39 | 40 | ResultSet rs = st.executeQuery(); 41 | if (rs.next()){ 42 | result = rs.getString(1); 43 | } else { 44 | Thread.sleep(1_000L); 45 | } 46 | rs.close(); 47 | } 48 | return result; 49 | } 50 | 51 | @Override 52 | public ScriptSource getScript(Method method) { 53 | String scriptName = getScriptName(method); 54 | try { 55 | String script = getScriptTextbyName(scriptName); 56 | log.trace("Scripted method name: {} text: {}", scriptName, script); 57 | return new StaticScriptSource(script); 58 | } catch (SQLException e) { 59 | throw new ScriptNotFoundException(e); 60 | } catch (Throwable e) { 61 | throw new RuntimeException(e); 62 | } 63 | } 64 | 65 | private String getScriptName(Method method) { 66 | Class scriptRepositoryClass = method.getDeclaringClass(); 67 | String methodName = method.getName(); 68 | return scriptRepositoryClass.getSimpleName() + "." + methodName; 69 | } 70 | 71 | @Override 72 | public void cancel() { 73 | try { 74 | if ((connection != null) && !connection.isClosed()) { 75 | log.debug("Closing connection: "+connection.getMetaData().getURL()); 76 | connection.close(); 77 | } 78 | } catch (SQLException e) { 79 | log.error("Connection cannot be closed", e); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/config/ScriptRepositoryConfigurationParser.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.config; 2 | 3 | import com.haulmont.scripting.repository.factory.ScriptRepositoryFactoryBean; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.BeanCreationException; 7 | import org.springframework.beans.factory.config.BeanDefinition; 8 | import org.springframework.beans.factory.xml.BeanDefinitionParser; 9 | import org.springframework.beans.factory.xml.ParserContext; 10 | import org.springframework.util.xml.DomUtils; 11 | import org.w3c.dom.Element; 12 | 13 | import java.lang.annotation.Annotation; 14 | import java.util.ArrayList; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * Parses XML configuration for script repositories and registers factory that will produce proxies for 21 | * script execution. 22 | */ 23 | public class ScriptRepositoryConfigurationParser implements BeanDefinitionParser { 24 | 25 | private static final Logger log = LoggerFactory.getLogger(ScriptRepositoryConfigurationParser.class); 26 | 27 | /** 28 | * @see BeanDefinitionParser#parse(Element, ParserContext) 29 | */ 30 | @Override 31 | public BeanDefinition parse(Element element, ParserContext parserContext) throws BeanCreationException { 32 | List basePackages; 33 | Map, AnnotationConfig> customAnnotationsConfig; 34 | try { 35 | basePackages = getPackagesToScan(element); 36 | customAnnotationsConfig = getCustomAnnotationsConfig(element); 37 | } catch (ClassNotFoundException e) { 38 | throw new BeanCreationException(String.format("Error parsing bean definitions: %s", e.getMessage()), e); 39 | } 40 | 41 | return ScriptRepositoryFactoryBean.registerBean(parserContext.getRegistry(), basePackages, customAnnotationsConfig); 42 | } 43 | 44 | /** 45 | * Reads package names form XML configuration. 46 | * @param element XML element to parse. 47 | * @return package names. 48 | */ 49 | private List getPackagesToScan(Element element){ 50 | log.trace("Reading packages to be scanned to find Script Repositories"); 51 | Element basePackagesEl = DomUtils.getChildElementByTagName(element, "base-packages"); 52 | List packageNames = DomUtils.getChildElementsByTagName(basePackagesEl, "base-package"); 53 | int basePackagesCount = packageNames.size(); 54 | List result = new ArrayList<>(basePackagesCount); 55 | for (Element el : packageNames) { 56 | result.add(el.getTextContent()); 57 | } 58 | return result; 59 | } 60 | 61 | /** 62 | * Reads custom annotation configuration in case of XML config for script repositories. 63 | * @param element XML element to parse. 64 | * @return annotation class and bean names for script execution. 65 | * @throws ClassNotFoundException if annotation class was not loaded. 66 | */ 67 | @SuppressWarnings("unchecked")//Unchecked annotation class conversion 68 | private Map, AnnotationConfig> getCustomAnnotationsConfig(Element element) throws ClassNotFoundException { 69 | log.trace("Reading annotations configurations to create script methods later"); 70 | Map, AnnotationConfig> result = new HashMap<>(); 71 | Element annotConfigEl = DomUtils.getChildElementByTagName(element, "annotations-config"); 72 | if (annotConfigEl == null){ 73 | return result; 74 | } 75 | List annotConfig = DomUtils.getChildElementsByTagName(annotConfigEl, "annotation-mapping"); 76 | for (Element el : annotConfig) { 77 | String providerBeanName = el.getAttribute("provider-bean-name"); 78 | String executorBeanName = el.getAttribute("evaluator-bean-name"); 79 | String description = el.getAttribute("description"); 80 | long timeout = Long.parseLong(el.getAttribute("timeout")); 81 | Class annotationClass = (Class)Class.forName(el.getAttribute("annotation-class")); 82 | result.put(annotationClass, new AnnotationConfig(annotationClass, providerBeanName, executorBeanName, timeout, description)); 83 | } 84 | return result; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/com/haulmont/scripting/core/test/files/FileRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.core.test.files; 2 | 3 | import ch.qos.logback.classic.LoggerContext; 4 | import ch.qos.logback.core.util.StatusPrinter; 5 | import com.haulmont.scripting.repository.config.AnnotationConfig; 6 | import com.haulmont.scripting.repository.evaluator.ScriptEvaluationException; 7 | import com.haulmont.scripting.repository.factory.ScriptRepositoryFactoryBean; 8 | import org.apache.commons.lang3.RandomStringUtils; 9 | import org.apache.commons.lang3.time.DateUtils; 10 | import org.junit.After; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.test.context.ContextConfiguration; 18 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 19 | 20 | import java.lang.reflect.Method; 21 | import java.text.ParseException; 22 | import java.util.Arrays; 23 | import java.util.Date; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.UUID; 27 | import java.util.stream.Collectors; 28 | 29 | import static org.junit.Assert.assertEquals; 30 | import static org.junit.Assert.assertNotNull; 31 | import static org.junit.Assert.assertTrue; 32 | import static org.junit.Assert.fail; 33 | 34 | @ContextConfiguration(locations = {"classpath:com/haulmont/scripting/core/test/files/files-test-spring.xml"}) 35 | @RunWith(SpringJUnit4ClassRunner.class) 36 | public class FileRepositoryTest { 37 | 38 | private static final Logger log = LoggerFactory.getLogger(FileRepositoryTest.class); 39 | 40 | @Autowired 41 | private CustomerScriptRepository repo; 42 | 43 | @Autowired 44 | private ScriptRepositoryFactoryBean scriptRepositoryFactoryBean; 45 | 46 | @Before 47 | public void setUp() throws Exception { 48 | LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); 49 | StatusPrinter.print(lc); 50 | } 51 | 52 | @Test 53 | public void testRunSimpleScript() { 54 | UUID customerId = UUID.randomUUID(); 55 | String newName = RandomStringUtils.randomAlphabetic(8); 56 | String s = repo.renameCustomer(customerId, newName); 57 | log.info(s); 58 | assertNotNull(s); 59 | assertTrue(s.contains(customerId.toString())); 60 | assertTrue(s.contains(newName)); 61 | } 62 | 63 | @Test 64 | public void testCreateObject() throws ParseException { 65 | String newName = RandomStringUtils.randomAlphabetic(8); 66 | Date birthDate = DateUtils.parseDate("1988-12-14", "yyyy-MM-dd"); 67 | Customer c = repo.createCustomer(newName, birthDate); 68 | log.info("Customer: {}", c); 69 | assertEquals(newName, c.getName()); 70 | assertEquals(birthDate, c.getBirthDate()); 71 | assertNotNull(c.getId()); 72 | 73 | assertEquals(Arrays.asList(c.getId().toString(), newName, birthDate.toString()), c.getMyData()); 74 | 75 | } 76 | 77 | @Test 78 | public void testScriptMetadata() { 79 | Map scripsMetadata = scriptRepositoryFactoryBean.getMethodInvocationsInfo(); 80 | assertEquals(6, scripsMetadata.size()); 81 | List methods = scripsMetadata.keySet().stream().map(Method::getName).collect(Collectors.toList()); 82 | assertTrue(methods.containsAll( 83 | Arrays.asList("renameCustomer", "createCustomer", "getDefaultName", "getDefaultError", "sayHello", "printData"))); 84 | } 85 | 86 | @Test 87 | public void testDefaultMethodExecution(){ 88 | String defaultName = repo.getDefaultName(); 89 | assertEquals("NewCustomer", defaultName); 90 | } 91 | 92 | 93 | @Test(expected = ScriptEvaluationException.class) 94 | public void testErrorMethodExecution() { 95 | String errorLine = repo.getDefaultError(); 96 | fail("Non-default method without an underlying script must throw an error instead of returning result: "+errorLine); 97 | } 98 | 99 | @Test 100 | public void testZeroArgScript(){ 101 | String hello = repo.sayHello(); 102 | assertEquals("Hello!", hello); 103 | } 104 | 105 | @Test 106 | public void testVoidMethods() { 107 | repo.printData("Test Data "); 108 | } 109 | 110 | @Test 111 | public void testVoidNullParamsMethods() { 112 | repo.printData(null); 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/factory/ScriptRepositoryFactoryBean.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.factory; 2 | 3 | import com.haulmont.scripting.repository.ScriptRepository; 4 | import com.haulmont.scripting.repository.config.AnnotationConfig; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.BeansException; 8 | import org.springframework.beans.factory.BeanCreationException; 9 | import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; 10 | import org.springframework.beans.factory.config.BeanDefinition; 11 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 12 | import org.springframework.beans.factory.support.BeanDefinitionBuilder; 13 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 14 | import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; 15 | import org.springframework.context.ApplicationContext; 16 | import org.springframework.context.ApplicationContextAware; 17 | import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; 18 | import org.springframework.core.type.AnnotationMetadata; 19 | import org.springframework.core.type.filter.AnnotationTypeFilter; 20 | 21 | import java.lang.annotation.Annotation; 22 | import java.lang.reflect.Method; 23 | import java.lang.reflect.Proxy; 24 | import java.util.Collections; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.Set; 28 | import java.util.concurrent.ConcurrentHashMap; 29 | 30 | /** 31 | * Class that creates proxies for script repositories based on configuration data. Proxies will forward script repository interface method 32 | * invocations to get script text from providers and then for evaluation to actual evaluator class. 33 | *

34 | * Factory scans packages and creates script repository proxies when context initialization is finished. 35 | * 36 | * @see BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry(BeanDefinitionRegistry) 37 | */ 38 | public class ScriptRepositoryFactoryBean implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware { 39 | 40 | public static final String NAME = "scriptRepositoryFactory"; 41 | 42 | private static final Logger log = LoggerFactory.getLogger(ScriptRepositoryFactoryBean.class); 43 | 44 | private final List basePackages; 45 | 46 | private Map, AnnotationConfig> customAnnotationsConfig; //global custom annotations config 47 | 48 | private Map methodScriptInvocationMetadata = new ConcurrentHashMap<>(); //global invocation cache 49 | 50 | private ApplicationContext ctx; 51 | 52 | /** 53 | * Creates factory bean definition and registers it in spring context. In case of double configuration XML and annotation 54 | * will merge configuration data - package names and custom annotation configuration. 55 | * 56 | * @param registry Spring bean definition registry 57 | * @param basePackages list of base package names to scan for script repositories. 58 | * @param customAnnotationsConfig configuration for custom annotations. 59 | * @return proxy factory bean definition that was registered in spring context. 60 | */ 61 | @SuppressWarnings("unchecked")//to avoid warnings on constructor argument cast 62 | public static BeanDefinition registerBean(BeanDefinitionRegistry registry, List basePackages, Map, AnnotationConfig> customAnnotationsConfig) { 63 | BeanDefinition beanDefinition; 64 | if (!registry.containsBeanDefinition(ScriptRepositoryFactoryBean.NAME)) { 65 | BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ScriptRepositoryFactoryBean.class); 66 | builder.addConstructorArgValue(basePackages); 67 | builder.addConstructorArgValue(customAnnotationsConfig); 68 | beanDefinition = builder.getBeanDefinition(); 69 | registry.registerBeanDefinition(ScriptRepositoryFactoryBean.NAME, beanDefinition); 70 | log.info("Added script repository factory bean: {}, annotations config {} ", beanDefinition.getBeanClassName(), customAnnotationsConfig); 71 | } else { 72 | beanDefinition = registry.getBeanDefinition(ScriptRepositoryFactoryBean.NAME); 73 | List basePackagesArg = (List) beanDefinition.getConstructorArgumentValues().getArgumentValue(0, List.class).getValue(); 74 | basePackagesArg.addAll(basePackages); 75 | Map, AnnotationConfig> customAnnotationsArg = 76 | (Map, AnnotationConfig>) beanDefinition.getConstructorArgumentValues().getArgumentValue(1, Map.class).getValue(); 77 | customAnnotationsArg.putAll(customAnnotationsConfig); 78 | log.debug("Added configuration to script repository factory bean: {}", customAnnotationsConfig); 79 | } 80 | return beanDefinition; 81 | } 82 | 83 | /** 84 | * Factory constructor. 85 | * 86 | * @param basePackages list of base package names to scan for script repositories. 87 | * @param customAnnotationsConfig configuration for custom annotations. 88 | */ 89 | public ScriptRepositoryFactoryBean(List basePackages, Map, AnnotationConfig> customAnnotationsConfig) { 90 | this.basePackages = basePackages; 91 | this.customAnnotationsConfig = customAnnotationsConfig; 92 | } 93 | 94 | /** 95 | * Register script repository interfaces instances as beans so we can inject them into application. All interface instances 96 | * will be created in a lazy manner using factory method. 97 | * 98 | * @see ScriptRepositoryFactoryBean#createRepository(Class, Map) 99 | * @see BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry(BeanDefinitionRegistry) 100 | */ 101 | @Override 102 | public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { 103 | ClassPathScanningCandidateComponentProvider provider 104 | = new ScriptRepositoryCandidateProvider(); 105 | for (String packageName : basePackages) { 106 | Set candidateComponents = provider.findCandidateComponents(packageName); 107 | try { 108 | for (BeanDefinition definition : candidateComponents) { 109 | definition.setFactoryBeanName(NAME); 110 | definition.setFactoryMethodName("createRepository"); 111 | definition.getConstructorArgumentValues().addGenericArgumentValue(Class.forName(definition.getBeanClassName())); 112 | definition.getConstructorArgumentValues().addGenericArgumentValue(customAnnotationsConfig); 113 | log.info("Registering script repository interface: {}", definition.getBeanClassName()); 114 | registry.registerBeanDefinition(definition.getBeanClassName(), definition); 115 | } 116 | } catch (ClassNotFoundException e) { 117 | throw new BeanCreationException(e.getMessage(), e); 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * Empty method - nothing to do in our case. 124 | * 125 | * @see BeanDefinitionRegistryPostProcessor#postProcessBeanFactory(ConfigurableListableBeanFactory) 126 | */ 127 | @Override 128 | public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { 129 | } 130 | 131 | /** 132 | * Sets application context - we'll need it to get provider and evaluator beans dynamically on method invocation. 133 | * 134 | * @see ApplicationContextAware#setApplicationContext(ApplicationContext) 135 | */ 136 | @Override 137 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 138 | ctx = applicationContext; 139 | } 140 | 141 | /** 142 | * Factory method that creates proxies based on script repository interface and configuration. 143 | * 144 | * @param repositoryClass script repository interface class. 145 | * @param customAnnotationsConfig custom annotation configurations for script execution. 146 | * @param repository class type. 147 | * @return proxy that implements script repository interface. 148 | */ 149 | @SuppressWarnings({"unused", "unchecked"}) 150 | T createRepository(Class repositoryClass, Map, AnnotationConfig> customAnnotationsConfig) { 151 | if (!repositoryClass.isAnnotationPresent(ScriptRepository.class)) { 152 | throw new IllegalArgumentException("Script repositories must be annotated with @ScriptRepository."); 153 | } 154 | 155 | log.debug("Creating proxy for {}", repositoryClass.getName()); 156 | RepositoryMethodsHandler handler = new RepositoryMethodsHandler(repositoryClass, ctx, customAnnotationsConfig); 157 | methodScriptInvocationMetadata.putAll(handler.getMethodScriptInvocationMetadata()); 158 | return (T) Proxy.newProxyInstance(repositoryClass.getClassLoader(), 159 | new Class[]{repositoryClass}, handler); 160 | } 161 | 162 | public Map getMethodInvocationsInfo() { 163 | return Collections.unmodifiableMap(methodScriptInvocationMetadata); 164 | } 165 | 166 | /** 167 | * Custom bean candidate provider that includes only annotated interfaces. 168 | * 169 | * @see ClassPathScanningCandidateComponentProvider 170 | */ 171 | static class ScriptRepositoryCandidateProvider extends ClassPathScanningCandidateComponentProvider { 172 | 173 | ScriptRepositoryCandidateProvider() { 174 | super(false); 175 | addIncludeFilter(new AnnotationTypeFilter(ScriptRepository.class)); 176 | } 177 | 178 | @Override 179 | protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { 180 | AnnotationMetadata metadata = beanDefinition.getMetadata(); 181 | return metadata.isInterface() && metadata.isIndependent(); 182 | } 183 | } 184 | 185 | 186 | } 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Script Repository Interface 2 | Scripting in java applications is not a rare thing. Sometimes you need to extend your current business logic or 3 | add some application management logic. It is very useful since business logic might not be well-defined at the moment 4 | of an application's development, or you need to change it frequently without redeploying the application. 5 | 6 | Scripting adds flexibility to an application, but also it adds some challenges: 7 | 1. Usually scripts are scattered along the application, so it is quite hard to manage numerous ```GroovyShell``` calls. 8 | 2. Scripts usually do not provide any information about input parameters: names and types as well as about output values. 9 | 10 | The purpose of this library is to add some order into scripting extension points. 11 | 12 | The idea behind this library is simple. A developer creates an interface and links its methods to scripts using 13 | annotations. 14 | 15 | This approach adds "type-safety" to the process of passing script parameters and a developer will know what will be a 16 | type of the script evaluation result. 17 | 18 | ## Usage 19 | To start working with the script repositories, you need to do the following: 20 | 21 | 1. Specify repository for library and add it as a dependency in your project build file 22 | ```groovy 23 | repositories { 24 | ... 25 | maven { 26 | url "https://cuba-platform.bintray.com/labs" 27 | } 28 | } 29 | 30 | ... 31 | 32 | dependencies { 33 | ... 34 | compile 'com.haulmont.scripting:spring-script-repositories:0.1.1' 35 | } 36 | ``` 37 | Please note that the library's jar file should be placed near application jar files. 38 | E.g. if you use tomcat, please put this file to deployed application's WEB-INF/lib folder not to tomcat shared libs. 39 | We need it to use a correct classloader for proxy creation. 40 | 41 | 2. Define script repository interfaces 42 | ```java 43 | @ScriptRepository 44 | public interface CustomerScriptRepository { 45 | 46 | @GroovyScript 47 | String renameCustomer(@ScriptParam("customerId") UUID customerId, @ScriptParam("newName") String newName); 48 | 49 | @GroovyScript 50 | Customer createCustomer(@ScriptParam("name") String name, @ScriptParam("birthDate") Date birthDate); 51 | } 52 | ``` 53 | You can use default implementations in repository interfaces if you want to start quickly without writing scripts for methods. 54 | 55 | 3. Define root folder where your scripts will be located by defining ```groovy.script.source.root.path``` property in 56 | ```application.properties``` file: 57 | ```properties 58 | groovy.script.source.root.path=classpath:scripts 59 | ``` 60 | Prefixes ```classpath:```, ```file:```, ```jar:``` can be used. If source root path is not specified, 61 | the library will use default value: ```classpath:com/haulmont/scripting/scripts``` 62 | 63 | 4. Implement scripts that should be executed and save them in script source root folder. By default, they should be named using pattern 64 | ```InterfaceName.methodName.groovy```. So for the example described in p. 2 there will be two files: 65 | 1. ```CustomerScriptRepository.renameCustomer.groovy``` 66 | 2. ```CustomerScriptRepository.createCustomer.groovy``` 67 | 68 | In your scripts you can use parameters defined in interface's method signatures, parameter names should match those 69 | defined in ```@ScriptParam``` annotation. For example, for method ```createCustomer``` script may look like the following: 70 | ```groovy 71 | Customer c = new Customer() 72 | c.setId(UUID.randomUUID()) 73 | c.setName(name) 74 | c.setBirthDate(birthDate) 75 | return c 76 | ``` 77 | Parameters ```name``` and ```birthdate``` will be substituted based on values passed by a caller. 78 | 79 | 5. Enable scripting repositories in your application by adding ```@EnableScriptRepositories``` annotation to your 80 | application configuration and specify path list where your repository interfaces are located. 81 | ```java 82 | @Configuration 83 | @EnableScriptRepositories(basePackages = {"com.example", "com.sample"}) 84 | public class ExampleConfig { 85 | } 86 | ``` 87 | 6. Inject the interface into proper services and use it as "regular" bean. 88 | ```java 89 | public class CustomerService { 90 | 91 | @Autowired 92 | private CustomerScriptRepository customerScriptRepository; 93 | 94 | public Customer createNew(String name, Date birthDate) { 95 | return customerScriptRepository.createCustomer(name, birthDate); 96 | } 97 | } 98 | 99 | ``` 100 | So it should be pretty easy to get started with the library. 101 | By default, it supports Groovy and JavaScript, but it is quite easy to add any scripting language to it. Below is the 102 | explanation of the library's internals and configuration. 103 | 104 | ## Internals 105 | 106 | The library provides the following: 107 | 108 | Marker annotation for script repostitory interfaces. 109 | ```java 110 | public @interface ScriptRepository { 111 | String description() default ""; 112 | } 113 | ``` 114 | 115 | Annotation to link interface method to a script source code. You need to provide bean names for script provider 116 | bean and script evaluator bean. 117 | ```java 118 | public @interface ScriptMethod { 119 | String providerBeanName() default "groovyResourceProvider"; 120 | String evaluatorBeanName() default "groovyJsrEvaluator"; 121 | long timeout() default -1L; 122 | String description() default ""; 123 | } 124 | ``` 125 | Interface for script provider: 126 | ```java 127 | public interface ScriptProvider { 128 | ScriptSource getScript(Method method); 129 | } 130 | ``` 131 | The implementation should be able to find script source text based on scripted method's signature. As an example, the 132 | library provides a default implementation ```GroovyScriptFileProvider``` for a provider that reads text files from a source root. 133 | 134 | Interface for script evaluator - it's a standard Spring Framework class: 135 | ```java 136 | public interface ScriptEvaluator { 137 | Object evaluate(ScriptSource script) throws ScriptCompilationException; 138 | Object evaluate(ScriptSource script, Map arguments) throws ScriptCompilationException; 139 | } 140 | ``` 141 | The implementation just uses script text and invokes it using parameters map. There is a default evaluator implementation 142 | ```GroovyScriptJsrValuator``` that uses JRE's JSR-223 engine to execute Groovy scripts. 143 | 144 | Since parameters names are important and java compiler erase actual parameter names from ```.class``` file (unless you 145 | enable "keep debug information" option during compilation), the library provides annotation for method parameters that 146 | let us to use meaningful parameter names in script instead of "arg0, arg1, etc." 147 | ```java 148 | public @interface ScriptParam { 149 | String value(); 150 | } 151 | ``` 152 | ### More examples 153 | You can find examples in test classes. They include custom script provider and custom annotation configuration. 154 | 155 | ## Implementation 156 | The library creates dynamic proxies for repository interfaces marked with ```@ScriptRepository``` annotation. All methods 157 | in this repository must be marked with ```@ScriptMethod``` (or custom annotation). All interfaces marked with ```@ScriptRepository``` annotation 158 | will be published in Spring's context and can be injected into other spring beans. 159 | 160 | When an interface method is called, the proxy invokes provider to get method script text and then evaluator to evaluate 161 | the result. 162 | 163 | ### Timeout Support 164 | You can specify timeout either in ```@ScriptMethod``` annotation or in custom one to be able to stop script execution 165 | if needed. It is useful if you deal with resources like files or database connections either in your provider or in an evaluator. 166 | If you want to implement such a bean, you need to either: 167 | 1. Publish the bean as a PROTOTYPE 168 | 2. Store a reference to the closeable resource in class member 169 | 3. Implement ```TimeoutAware``` interface and its ```cancel()``` method where all 170 | closeable resources should be closed. 171 | (see ```com.haulmont.scripting.core.test.database.GroovyScriptDbProvider```) as an example. 172 | 173 | Or you can try to use ThreadLocal class members to store a reference to a closeable resource. 174 | 175 | 176 | ## Configuration 177 | 178 | In the project itself you can use two configuration options: annotations and XML. 179 | 180 | ### Annotations configuration 181 | If you plan to use your own implementation for script provider and/or script evaluator (e.g. for JavaScript), you can specify 182 | their spring bean names in ```@ScriptMethod``` annotation: 183 | ```java 184 | @ScriptMethod(providerBeanName = "jsFileProvider", evaluatorBeanName = "jsExecutor") 185 | ``` 186 | To avoid copying and pasting this code across the project you can create your own annotation and use it in your project: 187 | ```java 188 | @Target(ElementType.METHOD) 189 | @ScriptMethod(providerBeanName = "jsFileProvider", evaluatorBeanName = "jsExecutor") 190 | public @interface JsScript { 191 | } 192 | ``` 193 | ### XML Configuration 194 | You can also configure both package scanning and custom annotations using XML in spring configuration file: 195 | ```xml 196 | 197 | 204 | 205 | 206 | 207 | com.example 208 | com.sample 209 | 210 | 211 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | ``` 222 | ### Mixed configuration 223 | In case of mixed configuration - Annotations+XML, config parameters will be merged, therefore it is not recommended 224 | to configure the same custom annotation in two places because one of the configuration will override another. 225 | 226 | ### References and thanks 227 | There is a good [article](https://zeroturnaround.com/rebellabs/scripting-your-java-application-with-groovy/) by [Anton Arhipov](https://github.com/antonarhipov) that helped us a lot with implementation of this library. 228 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/com/haulmont/scripting/repository/factory/RepositoryMethodsHandler.java: -------------------------------------------------------------------------------- 1 | package com.haulmont.scripting.repository.factory; 2 | 3 | import com.haulmont.scripting.repository.ScriptMethod; 4 | import com.haulmont.scripting.repository.config.AnnotationConfig; 5 | import com.haulmont.scripting.repository.evaluator.EvaluationStatus; 6 | import com.haulmont.scripting.repository.evaluator.ScriptEvaluationException; 7 | import com.haulmont.scripting.repository.evaluator.ScriptResult; 8 | import com.haulmont.scripting.repository.evaluator.TimeoutAware; 9 | import com.haulmont.scripting.repository.provider.ScriptNotFoundException; 10 | import com.haulmont.scripting.repository.provider.ScriptProvider; 11 | import org.joor.Reflect; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.context.ApplicationContext; 15 | import org.springframework.core.annotation.AnnotationUtils; 16 | import org.springframework.scripting.ScriptEvaluator; 17 | import org.springframework.scripting.ScriptSource; 18 | 19 | import java.io.Serializable; 20 | import java.lang.annotation.Annotation; 21 | import java.lang.reflect.InvocationHandler; 22 | import java.lang.reflect.InvocationTargetException; 23 | import java.lang.reflect.Method; 24 | import java.util.Arrays; 25 | import java.util.Collections; 26 | import java.util.List; 27 | import java.util.Map; 28 | import java.util.Set; 29 | import java.util.concurrent.CompletableFuture; 30 | import java.util.concurrent.ConcurrentHashMap; 31 | import java.util.concurrent.TimeUnit; 32 | import java.util.stream.Collectors; 33 | 34 | /** 35 | * Class that process all repository invocations. All method invocations that are not specific to script 36 | * repository interface (equals, hashcode, etc.) will be redirected to Object class instance created within the class. 37 | * Scripted method invocation configuration (method, provider bean instance and evaluator bean instance) are cached. 38 | */ 39 | @SuppressWarnings("serial") 40 | class RepositoryMethodsHandler implements InvocationHandler, Serializable { 41 | 42 | private final Logger log = LoggerFactory.getLogger(RepositoryMethodsHandler.class); 43 | 44 | private final Object defaultDelegate = new Object(); 45 | 46 | private final Class repositoryClass; 47 | 48 | private final Map, AnnotationConfig> customAnnotationsConfig; 49 | 50 | private final ApplicationContext ctx; 51 | 52 | private Map methodScriptInvocationMetadata = new ConcurrentHashMap<>(); //local invocation metadata cache 53 | 54 | 55 | RepositoryMethodsHandler(Class repositoryClass, ApplicationContext ctx, Map, AnnotationConfig> customAnnotationsConfig) { 56 | this.ctx = ctx; 57 | this.repositoryClass = repositoryClass; 58 | this.customAnnotationsConfig = customAnnotationsConfig; 59 | List scriptedMethods = Arrays.stream(repositoryClass.getMethods()) 60 | .filter(this::isScriptedMethod) 61 | .collect(Collectors.toList()); 62 | scriptedMethods.forEach(method -> methodScriptInvocationMetadata.computeIfAbsent(method, this::getAnnotationConfig)); 63 | } 64 | 65 | /** 66 | * Main method that process script repository methods invocations. 67 | * On the first stage it checks if the method is scripted by checking annotation 68 | * (either ScriptMethod or pre-configured one) presence. If the method is not scripted, its invocation 69 | * is delegated to an Object instance. Otherwise we get script provider, script evaluator and 70 | * let them do their work. 71 | * 72 | * @see InvocationHandler#invoke(Object, Method, Object[]) 73 | */ 74 | @Override 75 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 76 | log.trace("Class: {}, Proxy: {}, Method: {}, Args: {}", 77 | method.getDeclaringClass().getName(), proxy.getClass(), method.getName(), args); 78 | 79 | if (!isScriptedMethod(method)) { 80 | return method.invoke(defaultDelegate, args); 81 | } 82 | 83 | AnnotationConfig invocationInfo = methodScriptInvocationMetadata.get(method); 84 | 85 | long timeout = invocationInfo.timeout; 86 | 87 | log.trace("Submitting task for execution, timeout: {} method: {}", timeout, method); 88 | 89 | CompletableFuture scriptExecutionChain = null; 90 | ScriptProvider provider = (ScriptProvider) ctx.getBean(invocationInfo.provider); 91 | ScriptEvaluator evaluator = (ScriptEvaluator) ctx.getBean(invocationInfo.evaluator); 92 | Map binds = invocationInfo.createParameterMap(method, args); 93 | 94 | try { 95 | scriptExecutionChain = CompletableFuture 96 | .supplyAsync(() -> provider.getScript(method)) 97 | .thenApply(scriptSource -> executeScriptedMethod(scriptSource, method, binds, evaluator)) 98 | .exceptionally(throwable -> tryDefaultMethod(throwable, method, args, repositoryClass)); 99 | 100 | if (timeout > 0) { 101 | return scriptExecutionChain.get(timeout, TimeUnit.MILLISECONDS); 102 | } else { 103 | return scriptExecutionChain.get(); 104 | } 105 | } catch (Throwable ex) { 106 | log.error("Error during script evaluation", ex); 107 | if (scriptExecutionChain != null && scriptExecutionChain.isCompletedExceptionally()) { 108 | cancelExecution(invocationInfo, provider, evaluator); 109 | } 110 | if (shouldWrapResult(method)) { 111 | return new ScriptResult<>(null, EvaluationStatus.FAILURE, ex); 112 | } else 113 | //Wrapping into RuntimeException to avoid confusing UndeclaredThrowableException throw 114 | throw new ScriptEvaluationException( 115 | String.format("Error during script evaluation: %s", ex.getClass().getSimpleName()), ex); 116 | } 117 | } 118 | 119 | private void cancelExecution(AnnotationConfig invocationInfo, ScriptProvider provider, ScriptEvaluator evaluator) { 120 | if (provider instanceof TimeoutAware) { 121 | log.trace("Cancelling provider {} ", invocationInfo.provider); 122 | ((TimeoutAware)provider).cancel(); 123 | } 124 | if (evaluator instanceof TimeoutAware) { 125 | log.trace("Cancelling evaluator {} ", invocationInfo.provider); 126 | ((TimeoutAware)evaluator).cancel(); 127 | } 128 | } 129 | 130 | private Object executeScriptedMethod(ScriptSource script, Method method, Map binds, ScriptEvaluator evaluator) { 131 | Object scriptResult = evaluator.evaluate(script, binds); 132 | if (shouldWrapResult(method)) { 133 | return new ScriptResult<>(scriptResult, EvaluationStatus.SUCCESS, null); 134 | } else { 135 | return scriptResult; 136 | } 137 | } 138 | 139 | 140 | /** 141 | * Checks whether or not this method is scripted. It should be either annotated with 142 | * ScriptMethod annotation or annotation annotated with ScriptMethod or custom annotation should be configured in XML. 143 | * 144 | * @param method method to be checked. 145 | * @return true if method should be executed as scripted method. 146 | */ 147 | private boolean isScriptedMethod(Method method) { 148 | Annotation[] methodAnnotations = method.getAnnotations(); 149 | Set> annotClasses = Arrays.stream(methodAnnotations) 150 | .map(Annotation::annotationType) 151 | .collect(Collectors.toSet()); 152 | boolean match = customAnnotationsConfig.keySet().stream() 153 | .anyMatch(annotClasses::contains); 154 | ScriptMethod annot = AnnotationUtils.getAnnotation(method, ScriptMethod.class); 155 | return annot != null || match; 156 | } 157 | 158 | /** 159 | * Creates method invocation data - annotation and provider and evaluator names. 160 | * 161 | * @param method method that should be processed. 162 | * @return scripted method configuration. 163 | * @throws IllegalStateException if method is neither annotated nor configured. 164 | */ 165 | private AnnotationConfig getAnnotationConfig(Method method) { 166 | 167 | //Getting timeout from method's direct (1st level) annotations 168 | //Timeout specified at 1st level annotations will prevail over indirect one, 169 | //In absence of direct timeout we need to check 170 | // if we have indirect timeout set in @ScriptMethod or in XML config 171 | Long methodTimeout = getTimeout(method); 172 | 173 | ScriptMethod annotation = AnnotationUtils.getAnnotation(method, ScriptMethod.class); 174 | 175 | if (annotation == null) { //Annotation is configured in XML 176 | Annotation[] methodAnnotations = method.getAnnotations(); 177 | 178 | Class annotationInXml = Arrays 179 | .stream(methodAnnotations) 180 | .map(Annotation::annotationType) 181 | .filter(customAnnotationsConfig.keySet()::contains) 182 | .findAny() 183 | .orElseThrow(() -> new IllegalStateException ( 184 | String.format("A method %s is not a scripted method. Annotation is not configured in XML" 185 | , method.getName()))); 186 | 187 | AnnotationConfig annotationXmlConfig = customAnnotationsConfig.get(annotationInXml); 188 | 189 | Long timeout = methodTimeout != null ? methodTimeout : annotationXmlConfig.timeout; 190 | 191 | return new AnnotationConfig(ScriptMethod.class, 192 | annotationXmlConfig.provider, 193 | annotationXmlConfig.evaluator, 194 | timeout, 195 | annotationXmlConfig.description); 196 | } else { //If method is configured with custom annotation annotated with ScriptMethod 197 | 198 | Long timeout = methodTimeout != null ? methodTimeout : annotation.timeout(); 199 | 200 | return new AnnotationConfig(ScriptMethod.class, 201 | annotation.providerBeanName(), 202 | annotation.evaluatorBeanName(), 203 | timeout, 204 | annotation.description()); 205 | } 206 | } 207 | 208 | private Long getTimeout(Method method) { 209 | return Arrays.stream(AnnotationUtils.getAnnotations(method)) 210 | .filter(ann -> AnnotationUtils.getAnnotationAttributes(ann).containsKey("timeout")) 211 | .map(ann -> Long.parseLong(String.valueOf(AnnotationUtils.getValue(ann, "timeout")))) 212 | .filter(value -> value > 0L) 213 | .min(Long::compareTo) 214 | .orElse(null); 215 | } 216 | 217 | /** 218 | * Default interface method invocation. 219 | * @param cause Why this method was called. 220 | * @param method Interface default method to be invoked. 221 | * @param args Method's arguments. 222 | * @param interfaceClass interface which default method to be invoked. 223 | * @return Default interface method invocation result. 224 | * @throws UnsupportedOperationException in case default method is not found. 225 | * @link https://blog.jooq.org/2018/03/28/correct-reflective-access-to-interface-default-methods-in-java-8-9-10/ 226 | */ 227 | private Object tryDefaultMethod(Throwable cause, Method method, Object[] args, Class interfaceClass) { 228 | 229 | if (!(cause instanceof ScriptNotFoundException || cause.getCause() instanceof ScriptNotFoundException)) { 230 | throw new UnsupportedOperationException( 231 | String.format("Error executing default method %s", method), cause); 232 | } 233 | 234 | if (!method.isDefault()) { 235 | throw new UnsupportedOperationException( 236 | String.format("Method %s should have either script implementation or be default", method)); 237 | } 238 | 239 | try { 240 | Object typedProxyWithDefaultMethod = Reflect.on(new Object()).as(interfaceClass); 241 | Method defaultMethod = interfaceClass. 242 | getMethod(method.getName(), method.getParameterTypes()); 243 | return defaultMethod.invoke(typedProxyWithDefaultMethod, args); 244 | } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { 245 | throw new UnsupportedOperationException(String.format("Default method %s cannot be invoked on %s: %s" 246 | , method.getName(), interfaceClass.getName(), e.getMessage()) 247 | , e); 248 | } 249 | } 250 | 251 | private boolean shouldWrapResult(Method method) { 252 | Class returnType = method.getReturnType(); 253 | return ScriptResult.class.isAssignableFrom(returnType); 254 | } 255 | 256 | 257 | Map getMethodScriptInvocationMetadata() { 258 | return Collections.unmodifiableMap(methodScriptInvocationMetadata); 259 | } 260 | } 261 | --------------------------------------------------------------------------------