diff --git a/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyScriptParser.java b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyScriptParser.java index 8f811ba512..af6d656f09 100644 --- a/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyScriptParser.java +++ b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyScriptParser.java @@ -33,6 +33,7 @@ import org.springframework.util.xml.DomUtils; public class GroovyScriptParser extends AbstractSingleBeanDefinitionParser { private static final String LOCATION_ATTRIBUTE = "location"; + private static final String REFRESH_CHECK_DELAY_ATTRIBUTE = "refresh-check-delay"; @Override @@ -53,9 +54,16 @@ public class GroovyScriptParser extends AbstractSingleBeanDefinitionParser { return null; } else if (hasScriptLocation) { + String refreshDelayText = element.getAttribute(REFRESH_CHECK_DELAY_ATTRIBUTE); + String beanClassName = "org.springframework.integration.groovy.config.RefreshableResourceScriptSource"; BeanDefinitionBuilder resourceScriptSourceBuilder = - BeanDefinitionBuilder.genericBeanDefinition("org.springframework.scripting.support.ResourceScriptSource"); + BeanDefinitionBuilder.genericBeanDefinition(beanClassName); resourceScriptSourceBuilder.addConstructorArgValue(element.getAttribute(LOCATION_ATTRIBUTE)); + if (StringUtils.hasText(refreshDelayText)) { + resourceScriptSourceBuilder.addConstructorArgValue(refreshDelayText); + } else { + resourceScriptSourceBuilder.addConstructorArgValue(-1L); + } return resourceScriptSourceBuilder.getBeanDefinition(); } return new StaticScriptSource(scriptText, "groovy.lang.Script"); diff --git a/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/RefreshableResourceScriptSource.java b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/RefreshableResourceScriptSource.java new file mode 100644 index 0000000000..0cf7f7e669 --- /dev/null +++ b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/RefreshableResourceScriptSource.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2010 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 java.io.IOException; +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.core.io.Resource; +import org.springframework.scripting.ScriptSource; +import org.springframework.scripting.support.ResourceScriptSource; + +/** + * @author Dave Syer + * + * @since 2.0 + * + */ +public class RefreshableResourceScriptSource implements ScriptSource { + + private final long refreshDelay; + + private final ResourceScriptSource source; + + private AtomicLong lastModifiedChecked = new AtomicLong(System.currentTimeMillis()); + + private String script; + + public RefreshableResourceScriptSource(Resource resource, long refreshDelay) { + this.refreshDelay = refreshDelay; + this.source = new ResourceScriptSource(resource); + try { + this.script = source.getScriptAsString(); + } + catch (IOException e) { + lastModifiedChecked.set(0); + } + } + + public String getScriptAsString() throws IOException { + if (isModified()) { + this.script = source.getScriptAsString(); + } + return script; + } + + public boolean isModified() { + if (refreshDelay < 0) { + return false; + } + long time = System.currentTimeMillis(); + if (refreshDelay == 0 || (time - lastModifiedChecked.get()) > refreshDelay) { + lastModifiedChecked.set(time); + return source.isModified(); + } + return false; + } + + public String suggestedClassName() { + return source.suggestedClassName(); + } + +} diff --git a/spring-integration-groovy/src/main/resources/org/springframework/integration/groovy/config/spring-integration-groovy-2.0.xsd b/spring-integration-groovy/src/main/resources/org/springframework/integration/groovy/config/spring-integration-groovy-2.0.xsd index 1898d8a6b6..78e9f9c967 100644 --- a/spring-integration-groovy/src/main/resources/org/springframework/integration/groovy/config/spring-integration-groovy-2.0.xsd +++ b/spring-integration-groovy/src/main/resources/org/springframework/integration/groovy/config/spring-integration-groovy-2.0.xsd @@ -1,7 +1,6 @@ + schemaLocation="http://www.springframework.org/schema/integration/spring-integration-2.0.xsd" /> - Configures an inner bean that will generate a Groovy Script. - + Configures an inner bean that will generate a Groovy Script. + @@ -23,8 +22,16 @@ - Resource location path for the Script. Either this or an inline script - as body text should be provided, but not both. + Resource location path for the Script. Either this or an inline script + as body text should be provided, but not both. + + + + + + + Refresh delay for the script contents if specified as a resource + location (defaults to never refresh). diff --git a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyScriptExecutingMessageProcessorTests.java b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyScriptExecutingMessageProcessorTests.java index 233d8bf462..e365eb2e9f 100644 --- a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyScriptExecutingMessageProcessorTests.java +++ b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyScriptExecutingMessageProcessorTests.java @@ -17,15 +17,16 @@ package org.springframework.integration.groovy; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import org.junit.Test; - import org.springframework.core.io.AbstractResource; import org.springframework.integration.Message; +import org.springframework.integration.groovy.config.RefreshableResourceScriptSource; import org.springframework.integration.handler.MessageProcessor; import org.springframework.integration.support.MessageBuilder; import org.springframework.scripting.ScriptSource; @@ -38,12 +39,60 @@ import org.springframework.scripting.support.ResourceScriptSource; public class GroovyScriptExecutingMessageProcessorTests { @Test - public void simpleTest() throws Exception { + public void testSimpleExecution() throws Exception { String script = "return \"payload is $payload, header is $headers.testHeader\""; Message message = MessageBuilder.withPayload("foo").setHeader("testHeader", "bar").build(); TestResource resource = new TestResource(script, "simpleTest"); ScriptSource scriptSource = new ResourceScriptSource(resource); - MessageProcessor processor = new GroovyScriptExecutingMessageProcessor(scriptSource); + MessageProcessor processor = new GroovyScriptExecutingMessageProcessor(scriptSource); + Object result = processor.processMessage(message); + assertEquals("payload is foo, header is bar", result.toString()); + } + + @Test + public void testLastModified() throws Exception { + String script = "return \"payload is $payload, header is $headers.testHeader\""; + TestResource resource = new TestResource(script, "simpleTest"); + long lastModified = resource.lastModified(); + Thread.sleep(20L); + resource.setScript("foo"); + assertFalse("Expected last modified to change: "+lastModified+"=="+resource.lastModified(), lastModified==resource.lastModified()); + } + + @Test + public void testModifiedScriptExecution() throws Exception { + String script = "return \"payload is $payload, header is $headers.testHeader\""; + Message message = MessageBuilder.withPayload("foo").setHeader("testHeader", "bar").build(); + TestResource resource = new TestResource(script, "simpleTest"); + ScriptSource scriptSource = new ResourceScriptSource(resource); + MessageProcessor processor = new GroovyScriptExecutingMessageProcessor(scriptSource); + Thread.sleep(20L); + resource.setScript("return \"payload is $payload\""); + Object result = processor.processMessage(message); + assertEquals("payload is foo", result.toString()); + } + + @Test + public void testRefreshableScriptExecution() throws Exception { + String script = "return \"payload is $payload, header is $headers.testHeader\""; + Message message = MessageBuilder.withPayload("foo").setHeader("testHeader", "bar").build(); + TestResource resource = new TestResource(script, "simpleTest"); + ScriptSource scriptSource = new RefreshableResourceScriptSource(resource, 1000L); + MessageProcessor processor = new GroovyScriptExecutingMessageProcessor(scriptSource); + Thread.sleep(20L); + resource.setScript("return \"payload is $payload\""); + Object result = processor.processMessage(message); + assertEquals("payload is foo, header is bar", result.toString()); + } + + @Test + public void testRefreshableScriptExecutionWithInfiniteDelay() throws Exception { + String script = "return \"payload is $payload, header is $headers.testHeader\""; + Message message = MessageBuilder.withPayload("foo").setHeader("testHeader", "bar").build(); + TestResource resource = new TestResource(script, "simpleTest"); + ScriptSource scriptSource = new RefreshableResourceScriptSource(resource, -1L); + MessageProcessor processor = new GroovyScriptExecutingMessageProcessor(scriptSource); + resource.setScript("return \"payload is $payload\""); Object result = processor.processMessage(message); assertEquals("payload is foo, header is bar", result.toString()); } @@ -51,15 +100,26 @@ public class GroovyScriptExecutingMessageProcessorTests { private static class TestResource extends AbstractResource { - private final String scriptString; + private String script; private final String filename; - private TestResource(String scriptString, String filename) { - this.scriptString = scriptString; + private long lastModified; + + private TestResource(String script, String filename) { + setScript(script); this.filename = filename; } + public long lastModified() throws IOException { + return lastModified; + } + + public void setScript(String script) { + this.lastModified = System.currentTimeMillis(); + this.script = script; + } + public String getDescription() { return "test"; } @@ -70,7 +130,7 @@ public class GroovyScriptExecutingMessageProcessorTests { } public InputStream getInputStream() throws IOException { - return new ByteArrayInputStream(scriptString.getBytes("UTF-8")); + return new ByteArrayInputStream(script.getBytes("UTF-8")); } } diff --git a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/GroovyRefreshTests-context.xml b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/GroovyRefreshTests-context.xml new file mode 100644 index 0000000000..7c53be1a95 --- /dev/null +++ b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/GroovyRefreshTests-context.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + diff --git a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/GroovyRefreshTests.java b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/GroovyRefreshTests.java new file mode 100644 index 0000000000..48e162977c --- /dev/null +++ b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/GroovyRefreshTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2010 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.beans.PropertyEditorSupport; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.AbstractResource; +import org.springframework.integration.MessageChannel; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Mark Fisher + * @since 2.0 + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class GroovyRefreshTests { + + @Autowired + private MessageChannel referencedScriptInput; + + @Test + public void referencedScript() throws Exception { + QueueChannel replyChannel = new QueueChannel(); + replyChannel.setBeanName("returnAddress"); + this.referencedScriptInput.send(MessageBuilder.withPayload("test").setReplyChannel(replyChannel).build()); + assertEquals("groovy-test-1", replyChannel.receive(0).getPayload()); + this.referencedScriptInput.send(MessageBuilder.withPayload("test").setReplyChannel(replyChannel).build()); + assertEquals("groovy-test-0", replyChannel.receive(0).getPayload()); + assertNull(replyChannel.receive(0)); + } + + public static class ResourceEditor extends PropertyEditorSupport { + @Override + public void setAsText(String text) throws IllegalArgumentException { + super.setValue(new CycleResource()); + } + } + + private static class CycleResource extends AbstractResource { + + private int count = -1; + private String[] scripts = {"\"groovy-$payload-0\"", "\"groovy-$payload-1\""}; + + public String getDescription() { + return "CycleResource"; + } + + @Override + public String getFilename() throws IllegalStateException { + return "CycleResource"; + } + + @Override + public long lastModified() throws IOException { + return -1; + } + + public InputStream getInputStream() throws IOException { + if (++count>scripts.length-1) { + count = 0; + } + return new ByteArrayInputStream(scripts[count].getBytes()); + } + + } +} diff --git a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/RefreshableResourceScriptSourceTests.java b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/RefreshableResourceScriptSourceTests.java new file mode 100644 index 0000000000..778192e4f5 --- /dev/null +++ b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/RefreshableResourceScriptSourceTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2010 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 static org.junit.Assert.assertEquals; + +import java.io.IOException; + +import org.junit.Test; +import org.springframework.core.io.ByteArrayResource; + +/** + * @author Dave Syer + * + */ +public class RefreshableResourceScriptSourceTests { + + @Test + public void testGetScriptAsString() throws Exception { + RefreshableResourceScriptSource source = new RefreshableResourceScriptSource(new ByteArrayResource("foo".getBytes()), 1000); + assertEquals("foo", source.getScriptAsString()); + } + + @Test + public void testIsModified() throws Exception { + RefreshableResourceScriptSource source = new RefreshableResourceScriptSource(new ByteArrayResource("foo".getBytes()), 1000); + assertEquals(false, source.isModified()); + } + + @Test + public void testIsModifiedZeroDelay() throws Exception { + RefreshableResourceScriptSource source = new RefreshableResourceScriptSource(new ByteArrayResource("foo".getBytes()) { + @Override + public long lastModified() throws IOException { + return System.currentTimeMillis(); + } + }, 0); + assertEquals(false, source.isModified()); + Thread.sleep(20L); + assertEquals(true, source.isModified()); + assertEquals("foo", source.getScriptAsString()); + } + + @Test + public void testIsModifiedInfiniteDelay() throws Exception { + RefreshableResourceScriptSource source = new RefreshableResourceScriptSource(new ByteArrayResource("foo".getBytes()) { + @Override + public long lastModified() throws IOException { + return System.currentTimeMillis(); + } + }, -1); + assertEquals(false, source.isModified()); + assertEquals("foo", source.getScriptAsString()); + } + + @Test + public void testSuggestedClassName() throws Exception { + RefreshableResourceScriptSource source = new RefreshableResourceScriptSource(new ByteArrayResource("foo".getBytes()) { + @Override + public String getFilename() throws IllegalStateException { + return "Foo"; + } + }, 1000); + assertEquals("Foo", source.suggestedClassName()); + } + +}