diff --git a/build.gradle b/build.gradle index a9ab43a122..44a60dd7af 100644 --- a/build.gradle +++ b/build.gradle @@ -104,7 +104,7 @@ subprojects { subproject -> jmsApiVersion = '2.0.1' jpa21ApiVersion = '1.0.0.Final' jpaApiVersion = '2.1.1' - jrubyVersion = '1.7.23' + jrubyVersion = '9.1.5.0' jschVersion = '0.1.54' jsonpathVersion = '2.2.0' junitVersion = '4.12' diff --git a/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyAwareScriptExecutingProcessorFactory.java b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyAwareScriptExecutingProcessorFactory.java new file mode 100644 index 0000000000..5cb46dce97 --- /dev/null +++ b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyAwareScriptExecutingProcessorFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.groovy.config; + +import org.springframework.integration.groovy.GroovyScriptExecutingMessageProcessor; +import org.springframework.integration.scripting.AbstractScriptExecutingMessageProcessor; +import org.springframework.integration.scripting.ScriptVariableGenerator; +import org.springframework.integration.scripting.config.ScriptExecutingProcessorFactory; +import org.springframework.scripting.ScriptSource; + +/** + * The factory to create {@link GroovyScriptExecutingMessageProcessor} instances + * if provided {@code language == "groovy"}, otherwise delegates to the super class. + * + * @author Artem Bilan + * @since 5.0 + */ +public class GroovyAwareScriptExecutingProcessorFactory extends ScriptExecutingProcessorFactory { + + @Override + public AbstractScriptExecutingMessageProcessor createMessageProcessor(String language, + ScriptSource scriptSource, ScriptVariableGenerator scriptVariableGenerator) { + if ("groovy".equals(language.toLowerCase())) { + return new GroovyScriptExecutingMessageProcessor(scriptSource, scriptVariableGenerator); + } + else { + return super.createMessageProcessor(language, scriptSource, scriptVariableGenerator); + } + } + +} diff --git a/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyIntegrationConfigurationInitializer.java b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyIntegrationConfigurationInitializer.java new file mode 100644 index 0000000000..72b16208e9 --- /dev/null +++ b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyIntegrationConfigurationInitializer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.groovy.config; + + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.integration.config.IntegrationConfigurationInitializer; +import org.springframework.integration.scripting.config.ScriptExecutingProcessorFactory; + +/** + * The Groovy Module Integration infrastructure {@code beanFactory} initializer. + * + * @author Artem Bilan + * @since 5.0 + */ +public class GroovyIntegrationConfigurationInitializer implements IntegrationConfigurationInitializer { + + private static final Log logger = LogFactory.getLog(GroovyIntegrationConfigurationInitializer.class); + + @Override + public void initialize(ConfigurableListableBeanFactory beanFactory) throws BeansException { + if (beanFactory instanceof BeanDefinitionRegistry) { + registerScriptExecutorProviderIfNecessary((BeanDefinitionRegistry) beanFactory); + } + else { + logger.warn("The 'ScriptExecutingProcessorFactory' isn't registered because 'beanFactory'" + + " isn't an instance of `BeanDefinitionRegistry`."); + } + } + + protected void registerScriptExecutorProviderIfNecessary(BeanDefinitionRegistry registry) { + if (!registry.containsBeanDefinition(ScriptExecutingProcessorFactory.BEAN_NAME)) { + registry.registerBeanDefinition(ScriptExecutingProcessorFactory.BEAN_NAME, + new RootBeanDefinition(GroovyAwareScriptExecutingProcessorFactory.class)); + } + } + +} diff --git a/spring-integration-groovy/src/main/resources/META-INF/spring.factories b/spring-integration-groovy/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..008eced746 --- /dev/null +++ b/spring-integration-groovy/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.integration.config.IntegrationConfigurationInitializer=\ +org.springframework.integration.groovy.config.GroovyIntegrationConfigurationInitializer diff --git a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/ScriptsFactoryTests.java b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/ScriptsFactoryTests.java new file mode 100644 index 0000000000..52c6c26d78 --- /dev/null +++ b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/ScriptsFactoryTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.groovy; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.handler.MessageProcessor; +import org.springframework.integration.scripting.dsl.Scripts; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.PollableChannel; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author Artem Bilan + * @since 5.0 + */ +@RunWith(SpringRunner.class) +@DirtiesContext +public class ScriptsFactoryTests { + + @Autowired + @Qualifier("scriptFilter.input") + private MessageChannel filterInput; + + @Autowired + private PollableChannel discardChannel; + + @Autowired + private PollableChannel results; + + @Autowired + private MessageProcessor scriptMessageProcessor; + + @Test + public void filterTest() { + Message message1 = MessageBuilder.withPayload("bad").setHeader("type", "bad").build(); + Message message2 = MessageBuilder.withPayload("good").setHeader("type", "good").build(); + this.filterInput.send(message1); + this.filterInput.send(message2); + assertEquals("good", this.results.receive(10000).getPayload()); + assertNull(this.results.receive(0)); + assertEquals("bad", this.discardChannel.receive(10000).getPayload()); + assertNull(this.discardChannel.receive(0)); + + MessageProcessor delegate = TestUtils.getPropertyValue(this.scriptMessageProcessor, "delegate", + MessageProcessor.class); + + assertThat(delegate, instanceOf(GroovyScriptExecutingMessageProcessor.class)); + } + + + @Configuration + @EnableIntegration + public static class ContextConfiguration { + + @Bean + public PollableChannel discardChannel() { + return new QueueChannel(); + } + + @Bean + public Resource scriptResource() { + return new ByteArrayResource("headers.type == 'good'".getBytes()); + } + + @Bean + public IntegrationFlow scriptFilter() { + return f -> f + .filter(Scripts.processor(scriptResource()) + .lang("groovy"), + e -> e.discardChannel("discardChannel")) + .channel(c -> c.queue("results")); + } + + } + +} diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/ScriptExecutingProcessorFactory.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/ScriptExecutingProcessorFactory.java new file mode 100644 index 0000000000..f49d9274aa --- /dev/null +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/ScriptExecutingProcessorFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.scripting.config; + +import org.springframework.integration.scripting.AbstractScriptExecutingMessageProcessor; +import org.springframework.integration.scripting.ScriptExecutor; +import org.springframework.integration.scripting.ScriptVariableGenerator; +import org.springframework.integration.scripting.jsr223.ScriptExecutingMessageProcessor; +import org.springframework.integration.scripting.jsr223.ScriptExecutorFactory; +import org.springframework.scripting.ScriptSource; + +/** + * The factory to create {@link AbstractScriptExecutingMessageProcessor} instances for provided arguments. + * + * @author Artem Bilan + * @since 5.0 + */ +public class ScriptExecutingProcessorFactory { + + public static final String BEAN_NAME = "integrationScriptExecutingProcessorFactory"; + + public AbstractScriptExecutingMessageProcessor createMessageProcessor(String language, + ScriptSource scriptSource, ScriptVariableGenerator scriptVariableGenerator) { + ScriptExecutor scriptExecutor = ScriptExecutorFactory.getScriptExecutor(language); + return new ScriptExecutingMessageProcessor(scriptSource, scriptVariableGenerator, scriptExecutor); + } + +} diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/DslScriptExecutingMessageProcessor.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/DslScriptExecutingMessageProcessor.java new file mode 100644 index 0000000000..5a012e5400 --- /dev/null +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/DslScriptExecutingMessageProcessor.java @@ -0,0 +1,126 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.scripting.dsl; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.io.Resource; +import org.springframework.integration.handler.MessageProcessor; +import org.springframework.integration.scripting.AbstractScriptExecutingMessageProcessor; +import org.springframework.integration.scripting.RefreshableResourceScriptSource; +import org.springframework.integration.scripting.ScriptVariableGenerator; +import org.springframework.integration.scripting.config.ScriptExecutingProcessorFactory; +import org.springframework.integration.scripting.jsr223.ScriptExecutingMessageProcessor; +import org.springframework.integration.scripting.jsr223.ScriptExecutorFactory; +import org.springframework.messaging.Message; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.StringUtils; + +/** + * The adapter {@link MessageProcessor} around {@link AbstractScriptExecutingMessageProcessor}. + * Delegates to the {@code GroovyScriptExecutingMessageProcessor}, if provided {@link #lang} + * matches to {@code groovy} string and {@code spring-integration-groovy} jar is in classpath. + * Otherwise to the {@link ScriptExecutingMessageProcessor}. + * + * @author Artem Bilan + * + * @since 5.0 + */ +class DslScriptExecutingMessageProcessor + implements MessageProcessor, InitializingBean, ApplicationContextAware { + + private Resource script; + + private String location; + + private String lang; + + private long refreshCheckDelay = -1; + + private ScriptVariableGenerator variableGenerator; + + private ApplicationContext applicationContext; + + private AbstractScriptExecutingMessageProcessor delegate; + + DslScriptExecutingMessageProcessor(Resource script) { + this.script = script; + } + + DslScriptExecutingMessageProcessor(String location) { + this.location = location; + } + + public void setLang(String lang) { + this.lang = lang; + } + + public void setRefreshCheckDelay(Long refreshCheckDelay) { + this.refreshCheckDelay = refreshCheckDelay; + } + + public void setVariableGenerator(ScriptVariableGenerator variableGenerator) { + this.variableGenerator = variableGenerator; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (StringUtils.hasText(this.location)) { + this.script = this.applicationContext.getResource(this.location); + } + + ScriptSource scriptSource = new RefreshableResourceScriptSource(this.script, this.refreshCheckDelay); + + if (!StringUtils.hasText(this.lang)) { + String filename = this.script.getFilename(); + int index = filename.lastIndexOf(".") + 1; + if (index < 1) { + throw new BeanCreationException("'lang' isn't provided and there is 'file extension' for script " + + "resource: " + this.script); + } + this.lang = filename.substring(index); + } + + if (this.applicationContext.containsBean(ScriptExecutingProcessorFactory.BEAN_NAME)) { + ScriptExecutingProcessorFactory processorFactory = + this.applicationContext.getBean(ScriptExecutingProcessorFactory.BEAN_NAME, + ScriptExecutingProcessorFactory.class); + this.delegate = processorFactory.createMessageProcessor(this.lang, scriptSource, this.variableGenerator); + } + else { + this.delegate = new ScriptExecutingMessageProcessor(scriptSource, this.variableGenerator, + ScriptExecutorFactory.getScriptExecutor(this.lang)); + } + + this.delegate.setBeanFactory(this.applicationContext); + this.delegate.setBeanClassLoader(this.applicationContext.getClassLoader()); + } + + @Override + public Object processMessage(Message message) { + return this.delegate.processMessage(message); + } + +} diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/ScriptMessageSourceSpec.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/ScriptMessageSourceSpec.java new file mode 100644 index 0000000000..4d16b0b057 --- /dev/null +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/ScriptMessageSourceSpec.java @@ -0,0 +1,133 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.scripting.dsl; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.springframework.core.io.Resource; +import org.springframework.integration.core.MessageSource; +import org.springframework.integration.dsl.ComponentsRegistration; +import org.springframework.integration.dsl.MessageSourceSpec; +import org.springframework.integration.endpoint.MessageProcessorMessageSource; +import org.springframework.integration.scripting.ScriptVariableGenerator; +import org.springframework.integration.support.MapBuilder; + +/** + * The {@link MessageSourceSpec} for Dynamic Language Scripts. + * Delegates configuration options to the {@link ScriptSpec}. + * Produces {@link MessageProcessorMessageSource}. + * * + * @author Artem Bilan + * + * @since 5.0 + * + * @see ScriptSpec + * @see MessageProcessorMessageSource + */ +public class ScriptMessageSourceSpec extends MessageSourceSpec> + implements ComponentsRegistration { + + private final ScriptSpec delegate; + + public ScriptMessageSourceSpec(Resource scriptResource) { + this.delegate = new ScriptSpec(scriptResource); + } + + public ScriptMessageSourceSpec(String scriptLocation) { + this.delegate = new ScriptSpec(scriptLocation); + } + + /** + * The script lang (Groovy, ruby, python etc.). + * @param lang the script lang + * @return the current spec + * @see ScriptSpec#lang + */ + public ScriptMessageSourceSpec lang(String lang) { + this.delegate.lang(lang); + return this; + } + + /** + * The {@link ScriptVariableGenerator} to use. + * @param variableGenerator the {@link ScriptVariableGenerator} + * @return the current spec + * @see ScriptSpec#variableGenerator + */ + public ScriptMessageSourceSpec variableGenerator(ScriptVariableGenerator variableGenerator) { + this.delegate.variableGenerator(variableGenerator); + return this; + } + + /** + * The script variables to use. + * @param variables the script variables + * @return the current spec + * @see ScriptSpec#variables(MapBuilder) + */ + public ScriptMessageSourceSpec variables(MapBuilder variables) { + this.delegate.variables(variables); + return this; + } + + /** + * The script variables to use. + * @param variables the script variables + * @return the current spec + * @see ScriptSpec#variables(Map) + */ + public ScriptMessageSourceSpec variables(Map variables) { + this.delegate.variables(variables); + return this; + } + + /** + * The script variable to use. + * @param name the name of variable + * @param value the value of variable + * @return the current spec + * @see ScriptSpec#variable + */ + public ScriptMessageSourceSpec variable(String name, Object value) { + this.delegate.variable(name, value); + return this; + } + + /** + * The refreshCheckDelay in milliseconds for refreshable script resource. + * @param refreshCheckDelay the refresh check delay milliseconds + * @return the current spec + * @see ScriptSpec#refreshCheckDelay + */ + public ScriptMessageSourceSpec refreshCheckDelay(long refreshCheckDelay) { + this.delegate.refreshCheckDelay(refreshCheckDelay); + return this; + } + + @Override + protected MessageSource doGet() { + return new MessageProcessorMessageSource(this.delegate.get()); + } + + @Override + public Collection getComponentsToRegister() { + return Collections.singletonList(this.delegate.get()); + } + +} diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/ScriptSpec.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/ScriptSpec.java new file mode 100644 index 0000000000..cb18193593 --- /dev/null +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/ScriptSpec.java @@ -0,0 +1,138 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.scripting.dsl; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.core.io.Resource; +import org.springframework.integration.dsl.MessageProcessorSpec; +import org.springframework.integration.handler.MessageProcessor; +import org.springframework.integration.scripting.DefaultScriptVariableGenerator; +import org.springframework.integration.scripting.ScriptVariableGenerator; +import org.springframework.integration.support.MapBuilder; +import org.springframework.util.Assert; + +/** + * The {@link MessageProcessorSpec} implementation for the {@link DslScriptExecutingMessageProcessor}. + * + * @author Artem Bilan + * + * @since 5.0 + */ +public class ScriptSpec extends MessageProcessorSpec { + + private final DslScriptExecutingMessageProcessor processor; + + private ScriptVariableGenerator variableGenerator; + + private Map variables = new HashMap(); + + ScriptSpec(Resource scriptResource) { + Assert.notNull(scriptResource); + this.processor = new DslScriptExecutingMessageProcessor(scriptResource); + } + + ScriptSpec(String scriptLocation) { + Assert.hasText(scriptLocation); + this.processor = new DslScriptExecutingMessageProcessor(scriptLocation); + } + + /** + * The script lang (Groovy, ruby, python etc.). + * @param lang the script lang + * @return the current spec + * @see DslScriptExecutingMessageProcessor#setLang + */ + public ScriptSpec lang(String lang) { + Assert.hasText(lang); + this.processor.setLang(lang); + return this; + } + + /** + * The refreshCheckDelay in milliseconds for refreshable script resource. + * @param refreshCheckDelay the refresh check delay milliseconds + * @return the current spec + * @see org.springframework.integration.scripting.RefreshableResourceScriptSource + */ + public ScriptSpec refreshCheckDelay(long refreshCheckDelay) { + this.processor.setRefreshCheckDelay(refreshCheckDelay); + return this; + } + + /** + * The {@link ScriptVariableGenerator} to use. + * @param variableGenerator the {@link ScriptVariableGenerator} + * @return the current spec + * @see org.springframework.integration.scripting.AbstractScriptExecutingMessageProcessor + */ + public ScriptSpec variableGenerator(ScriptVariableGenerator variableGenerator) { + Assert.notNull(variableGenerator); + Assert.state(this.variables.isEmpty(), "'variableGenerator' and 'variables' are mutually exclusive"); + this.variableGenerator = variableGenerator; + return this; + } + + /** + * The script variables to use. + * @param variables the script variables {@link MapBuilder} + * @return the current spec + * @see DefaultScriptVariableGenerator + */ + public ScriptSpec variables(MapBuilder variables) { + return variables(variables.get()); + } + + /** + * The script variables to use + * @param variables the script variables {@link Map} + * @return the current spec + * @see DefaultScriptVariableGenerator + */ + public ScriptSpec variables(Map variables) { + Assert.notEmpty(variables); + Assert.state(this.variableGenerator == null, "'variableGenerator' and 'variables' are mutually exclusive"); + this.variables.putAll(variables); + return this; + } + + /** + * The script variable to use. + * @param name the name of variable + * @param value the value of variable + * @return the current spec + * @see DefaultScriptVariableGenerator + */ + public ScriptSpec variable(String name, Object value) { + Assert.hasText(name); + Assert.state(this.variableGenerator == null, "'variableGenerator' and 'variables' are mutually exclusive"); + this.variables.put(name, value); + return this; + } + + + @Override + protected MessageProcessor doGet() { + if (this.variableGenerator == null) { + this.variableGenerator = new DefaultScriptVariableGenerator(this.variables); + } + this.processor.setVariableGenerator(this.variableGenerator); + return this.processor; + } + +} diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/Scripts.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/Scripts.java new file mode 100644 index 0000000000..e0d6270ecb --- /dev/null +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/Scripts.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.scripting.dsl; + +import org.springframework.core.io.Resource; + +/** + * The factory for Dynamic Language Scripts (Groovy, Ruby, Python, JavaScript etc.). + * + * @author Artem Bilan + * + * @since 5.0 + */ +public final class Scripts { + + /** + * The factory method to produce {@link ScriptSpec} based on the {@link Resource}. + * The {@link Resource} must represent the real file and can be injected like: + *
+	 *  @Value("com/my/project/scripts/FilterScript.groovy")
+	 *  private Resource filterScript;
+	 * 
+ * @param scriptResource the script file {@link Resource} + * @return the ScriptSpec instance + */ + public static ScriptSpec processor(Resource scriptResource) { + return new ScriptSpec(scriptResource); + } + + /** + * The factory method to produce {@link ScriptSpec} based on the script file location. + * @param scriptLocation the path to the script file. + * {@code file:}, {@code ftp:}, {@code s3:} etc. + * The {@code classpath:} can be omitted. + * @return the ScriptSpec instance + */ + public static ScriptSpec processor(String scriptLocation) { + return new ScriptSpec(scriptLocation); + } + + /** + * Factory for the {@link ScriptMessageSourceSpec} based on the {@link Resource}. + * The {@link Resource} must represent the real file and can be injected like: + *
+	 *  @Value("com/my/project/scripts/FilterScript.groovy")
+	 *  private Resource filterScript;
+	 * 
+ * @param scriptResource the script {@link Resource} + * @return the {@link ScriptMessageSourceSpec} + */ + public static ScriptMessageSourceSpec messageSource(Resource scriptResource) { + return new ScriptMessageSourceSpec(scriptResource); + } + + /** + * Factory for the {@link ScriptMessageSourceSpec} based on the script location. + * @param scriptLocation the path to the script file. + * {@code file:}, {@code ftp:}, {@code s3:} etc. + * The {@code classpath:} can be omitted. + * @return the {@link ScriptMessageSourceSpec} + */ + public static ScriptMessageSourceSpec messageSource(String scriptLocation) { + return new ScriptMessageSourceSpec(scriptLocation); + } + + private Scripts() { + } + +} diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/package-info.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/package-info.java new file mode 100644 index 0000000000..625b5db41f --- /dev/null +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides Scripting Components support for Spring Integration Java DSL. + */ +package org.springframework.integration.scripting.dsl; diff --git a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/dsl/ScriptsTests.java b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/dsl/ScriptsTests.java new file mode 100644 index 0000000000..5d1980b60b --- /dev/null +++ b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/dsl/ScriptsTests.java @@ -0,0 +1,262 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.scripting.dsl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.Date; + +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.channel.QueueChannelOperations; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.dsl.IntegrationFlows; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.PollableChannel; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.FileCopyUtils; + +/** + * @author Artem Bilan + * + * @since 5.0 + */ +@RunWith(SpringRunner.class) +@DirtiesContext +public class ScriptsTests { + + @ClassRule + public static final TemporaryFolder FOLDER = new TemporaryFolder(); + + private static File SCRIPT_FILE; + + @Autowired + @Qualifier("scriptSplitter.input") + private MessageChannel splitterInput; + + @Autowired + @Qualifier("scriptTransformer.input") + private MessageChannel transformerInput; + + @Autowired + @Qualifier("scriptFilter.input") + private MessageChannel filterInput; + + @Autowired + private PollableChannel discardChannel; + + @Autowired + @Qualifier("scriptService.input") + private MessageChannel scriptServiceInput; + + @Autowired + @Qualifier("scriptRouter.input") + private MessageChannel scriptRouterInput; + + @Autowired + private PollableChannel longStrings; + + @Autowired + private PollableChannel shortStrings; + + @Autowired + private PollableChannel results; + + @Autowired + private PollableChannel messageSourceChannel; + + @BeforeClass + public static void setup() throws IOException { + SCRIPT_FILE = FOLDER.newFile("script.jython"); + FileCopyUtils.copy("1".getBytes(), SCRIPT_FILE); + } + + @After + public void clear() { + ((QueueChannelOperations) this.results).clear(); + } + + @Test + public void splitterTest() { + this.splitterInput.send(new GenericMessage<>("x,y,z")); + assertEquals("x", this.results.receive(10000).getPayload()); + assertEquals("y", this.results.receive(10000).getPayload()); + assertEquals("z", this.results.receive(10000).getPayload()); + } + + @Test + public void transformerTest() { + for (int i = 1; i <= 3; i++) { + this.transformerInput.send(new GenericMessage<>("test-" + i)); + } + assertEquals("ruby-test-1-bar", this.results.receive(10000).getPayload()); + assertEquals("ruby-test-2-bar", this.results.receive(10000).getPayload()); + assertEquals("ruby-test-3-bar", this.results.receive(10000).getPayload()); + } + + @Test + public void filterTest() { + Message message1 = MessageBuilder.withPayload("bad").setHeader("type", "bad").build(); + Message message2 = MessageBuilder.withPayload("good").setHeader("type", "good").build(); + this.filterInput.send(message1); + this.filterInput.send(message2); + assertEquals("good", this.results.receive(10000).getPayload()); + assertNull(this.results.receive(0)); + assertEquals("bad", this.discardChannel.receive(10000).getPayload()); + assertNull(this.discardChannel.receive(0)); + } + + @Test + public void serviceWithRefreshCheckDelayTest() throws IOException { + this.scriptServiceInput.send(new GenericMessage("test")); + assertEquals(1, this.results.receive(10000).getPayload()); + + FileCopyUtils.copy("2".getBytes(), SCRIPT_FILE); + SCRIPT_FILE.setLastModified(System.currentTimeMillis() + 10000); // force refresh + + this.scriptServiceInput.send(new GenericMessage("test")); + assertEquals(2, this.results.receive(10000).getPayload()); + } + + @Test + public void routerTest() throws IOException { + this.scriptRouterInput.send(new GenericMessage<>("aardvark")); + this.scriptRouterInput.send(new GenericMessage<>("bear")); + this.scriptRouterInput.send(new GenericMessage<>("cat")); + this.scriptRouterInput.send(new GenericMessage<>("dog")); + this.scriptRouterInput.send(new GenericMessage<>("elephant")); + assertEquals("bear", this.shortStrings.receive(10000).getPayload()); + assertEquals("cat", this.shortStrings.receive(10000).getPayload()); + assertEquals("dog", this.shortStrings.receive(10000).getPayload()); + assertEquals("aardvark", this.longStrings.receive(10000).getPayload()); + assertEquals("elephant", this.longStrings.receive(10000).getPayload()); + } + + @Test + public void messageSourceTest() throws IOException, InterruptedException { + Message message = this.messageSourceChannel.receive(20000); + assertNotNull(message); + Object payload = message.getPayload(); + assertThat(payload, Matchers.instanceOf(Date.class)); + + // Some time window to avoid dates collision + Thread.sleep(500); + + assertTrue(((Date) payload).before(new Date())); + + assertNotNull(this.messageSourceChannel.receive(20000)); + + assertNull(this.messageSourceChannel.receive(10)); + } + + @Configuration + @EnableIntegration + public static class ContextConfiguration { + + @Value("scripts/TesSplitterScript.groovy") + private Resource splitterScript; + + @Value("scripts/TestFilterScript.groovy") + private Resource filterScript; + + @Bean + public PollableChannel results() { + return new QueueChannel(); + } + + @Bean + public IntegrationFlow scriptSplitter() { + return f -> f.split(Scripts.processor(this.splitterScript)).channel(results()); + } + + @Bean + public IntegrationFlow scriptTransformer() { + return f -> f + .transform(Scripts.processor("scripts/TestTransformerScript.rb") + .lang("ruby") + .variable("foo", "bar")) + .channel(results()); + } + + @Bean + public PollableChannel discardChannel() { + return new QueueChannel(); + } + + @Bean + public IntegrationFlow scriptFilter() { + return f -> f.filter(Scripts.processor(this.filterScript), e -> e.discardChannel("discardChannel")) + .channel(results()); + } + + @Bean + public IntegrationFlow scriptService() { + return f -> f.handle(Scripts.processor("file:" + SCRIPT_FILE.getAbsolutePath()).refreshCheckDelay(0)) + .channel(results()); + } + + @Bean + public IntegrationFlow scriptRouter() { + return f -> f.route(Scripts.processor("scripts/TestRouterScript.js")); + } + + @Bean + public PollableChannel longStrings() { + return new QueueChannel(); + } + + @Bean + public PollableChannel shortStrings() { + return new QueueChannel(); + } + + @Bean + public IntegrationFlow scriptPollingAdapter() { + return IntegrationFlows + .from(Scripts.messageSource("scripts/TestMessageSourceScript.ruby"), + e -> e.poller(p -> p.fixedDelay(100))) + .channel(c -> c.queue("messageSourceChannel")) + .get(); + } + + } + +} diff --git a/spring-integration-scripting/src/test/resources/scripts/TesSplitterScript.groovy b/spring-integration-scripting/src/test/resources/scripts/TesSplitterScript.groovy new file mode 100644 index 0000000000..803d916e18 --- /dev/null +++ b/spring-integration-scripting/src/test/resources/scripts/TesSplitterScript.groovy @@ -0,0 +1 @@ +payload.split(',') diff --git a/spring-integration-scripting/src/test/resources/scripts/TestFilterScript.groovy b/spring-integration-scripting/src/test/resources/scripts/TestFilterScript.groovy new file mode 100644 index 0000000000..06a775dfb2 --- /dev/null +++ b/spring-integration-scripting/src/test/resources/scripts/TestFilterScript.groovy @@ -0,0 +1 @@ +headers.type == 'good' diff --git a/spring-integration-scripting/src/test/resources/scripts/TestMessageSourceScript.ruby b/spring-integration-scripting/src/test/resources/scripts/TestMessageSourceScript.ruby new file mode 100644 index 0000000000..5f532006ce --- /dev/null +++ b/spring-integration-scripting/src/test/resources/scripts/TestMessageSourceScript.ruby @@ -0,0 +1 @@ +java.util.Date.new diff --git a/spring-integration-scripting/src/test/resources/scripts/TestRouterScript.js b/spring-integration-scripting/src/test/resources/scripts/TestRouterScript.js new file mode 100644 index 0000000000..80a8fac8d7 --- /dev/null +++ b/spring-integration-scripting/src/test/resources/scripts/TestRouterScript.js @@ -0,0 +1,3 @@ +(function(){ + return payload.length > 5 ? "longStrings" : "shortStrings"; +})(); diff --git a/spring-integration-scripting/src/test/resources/scripts/TestTransformerScript.rb b/spring-integration-scripting/src/test/resources/scripts/TestTransformerScript.rb new file mode 100644 index 0000000000..e6ed08edeb --- /dev/null +++ b/spring-integration-scripting/src/test/resources/scripts/TestTransformerScript.rb @@ -0,0 +1,7 @@ +class Transformer + def transform(payload, arg) + "ruby-#{payload}-#{arg}" + end +end + +Transformer.new.transform(payload, foo)