From 67fd4a5a60d920a6c6c86cf82badac810f33507c Mon Sep 17 00:00:00 2001 From: David Turanski Date: Mon, 10 Jun 2013 10:40:44 -0400 Subject: [PATCH] INT-3051 Derive Script Language from Extension Make 'lang' attribute optional on a script element and attempt to derive the language from the file extension. Change return value on ScriptParser.deriveLanguageFromExtension to the language name Polishing (PR Comments) --- .../config/AbstractScriptParser.java | 70 ++++++++------- .../scripting/config/jsr223/ScriptParser.java | 59 +++++++++++-- .../spring-integration-scripting-3.0.xsd | 6 +- ...riveLanguageFromExtensionTests-context.xml | 12 +++ ...nguageFromExtensionTests-fail1-context.xml | 9 ++ ...nguageFromExtensionTests-fail2-context.xml | 9 ++ .../DeriveLanguageFromExtensionTests.java | 87 +++++++++++++++++++ 7 files changed, 212 insertions(+), 40 deletions(-) create mode 100644 spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-context.xml create mode 100644 spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-fail1-context.xml create mode 100644 spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-fail2-context.xml create mode 100644 spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests.java diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/AbstractScriptParser.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/AbstractScriptParser.java index 286386386f..8e02a81da2 100644 --- a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/AbstractScriptParser.java +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/AbstractScriptParser.java @@ -1,11 +1,11 @@ /* - * Copyright 2002-2011 the original author or authors. - * + * Copyright 2002-2013 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. @@ -14,6 +14,8 @@ package org.springframework.integration.scripting.config; import java.util.List; +import org.w3c.dom.Element; + import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; @@ -22,11 +24,11 @@ import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.beans.factory.xml.XmlReaderContext; import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.scripting.RefreshableResourceScriptSource; import org.springframework.scripting.support.StaticScriptSource; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; -import org.w3c.dom.Element; /** * @author David Turanski @@ -36,60 +38,68 @@ public abstract class AbstractScriptParser extends AbstractSingleBeanDefinitionP protected static final String LOCATION_ATTRIBUTE = "location"; protected static final String REFRESH_CHECK_DELAY_ATTRIBUTE = "refresh-check-delay"; - + + @Override protected boolean shouldGenerateIdAsFallback() { return true; } - + + @Override protected abstract String getBeanClassName(Element element); - + protected abstract String getScriptSourceClassName(); - + @Override protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { String scriptLocation = element.getAttribute(LOCATION_ATTRIBUTE); String scriptText = DomUtils.getTextValue(element); if (!(StringUtils.hasText(scriptLocation) ^ StringUtils.hasText(scriptText))) { - parserContext.getReaderContext().error("Either the 'location' attribute or inline script text must be provided, but not both.", element); + parserContext.getReaderContext().error( + "Either the 'location' attribute or inline script text must be provided, but not both.", element); return; } List variableElements = DomUtils.getChildElementsByTagName(element, "variable"); String scriptVariableGeneratorName = element.getAttribute("script-variable-generator"); - if (StringUtils.hasText(scriptText) && (variableElements.size() > 0 || StringUtils.hasText(scriptVariableGeneratorName))) { - parserContext.getReaderContext().error("Variable bindings or custom ScriptVariableGenerator are not allowed when using an inline groovy script. " + - "Specify location of the script via 'location' attribute instead", element); + if (StringUtils.hasText(scriptText) + && (variableElements.size() > 0 || StringUtils.hasText(scriptVariableGeneratorName))) { + parserContext.getReaderContext().error( + "Variable bindings or custom ScriptVariableGenerator are not allowed when using an inline groovy script. " + + "Specify location of the script via 'location' attribute instead", element); return; } - + if (StringUtils.hasText(scriptVariableGeneratorName) && variableElements.size() > 0) { - parserContext.getReaderContext().error("'script-variable-generator' and 'variable' sub-elements are mutually exclusive.", element); + parserContext.getReaderContext().error( + "'script-variable-generator' and 'variable' sub-elements are mutually exclusive.", element); return; } - + if (StringUtils.hasText(scriptLocation)) { - builder.addConstructorArgValue(this.resolveScriptLocation(element, parserContext.getReaderContext(), scriptLocation)); + builder.addConstructorArgValue(this.resolveScriptLocation(element, parserContext.getReaderContext(), + scriptLocation)); } else { - if (getScriptSourceClassName() != null){ + if (getScriptSourceClassName() != null) { builder.addConstructorArgValue(new StaticScriptSource(scriptText, getScriptSourceClassName())); - } else { + } + else { builder.addConstructorArgValue(new StaticScriptSource(scriptText)); } } if (!StringUtils.hasText(scriptVariableGeneratorName)) { - BeanDefinitionBuilder scriptVariableGeneratorBuilder = - BeanDefinitionBuilder.genericBeanDefinition("org.springframework.integration.scripting.DefaultScriptVariableGenerator"); + BeanDefinitionBuilder scriptVariableGeneratorBuilder = BeanDefinitionBuilder + .genericBeanDefinition("org.springframework.integration.scripting.DefaultScriptVariableGenerator"); ManagedMap variableMap = new ManagedMap(); for (Element childElement : variableElements) { String variableName = childElement.getAttribute("name"); String variableValue = childElement.getAttribute("value"); String variableRef = childElement.getAttribute("ref"); if (!(StringUtils.hasText(variableValue) ^ StringUtils.hasText(variableRef))) { - parserContext.getReaderContext().error("Exactly one of the 'ref' attribute or 'value' attribute, " + - " is required for element " + - IntegrationNamespaceUtils.createElementDescription(element) + ".", element); + parserContext.getReaderContext().error( + "Exactly one of the 'ref' attribute or 'value' attribute, " + " is required for element " + + IntegrationNamespaceUtils.createElementDescription(element) + ".", element); } if (StringUtils.hasText(variableValue)) { variableMap.put(variableName, variableValue); @@ -107,7 +117,7 @@ public abstract class AbstractScriptParser extends AbstractSingleBeanDefinitionP builder.addConstructorArgReference(scriptVariableGeneratorName); postProcess(builder, element, parserContext); } - + /** * Subclasses may override this no-op method to provide additional configuration. */ @@ -115,19 +125,17 @@ public abstract class AbstractScriptParser extends AbstractSingleBeanDefinitionP } private Object resolveScriptLocation(Element element, XmlReaderContext readerContext, String scriptLocation) { - String refreshDelayText = element.getAttribute(REFRESH_CHECK_DELAY_ATTRIBUTE); - String beanClassName = "org.springframework.integration.scripting.RefreshableResourceScriptSource"; - BeanDefinitionBuilder resourceScriptSourceBuilder = - BeanDefinitionBuilder.genericBeanDefinition(beanClassName); + String refreshDelayText = element.getAttribute(REFRESH_CHECK_DELAY_ATTRIBUTE); + String beanClassName = RefreshableResourceScriptSource.class.getName(); + BeanDefinitionBuilder resourceScriptSourceBuilder = BeanDefinitionBuilder.genericBeanDefinition(beanClassName); resourceScriptSourceBuilder.addConstructorArgValue(scriptLocation); if (StringUtils.hasText(refreshDelayText)) { resourceScriptSourceBuilder.addConstructorArgValue(refreshDelayText); } else { - resourceScriptSourceBuilder.addConstructorArgValue(-1L); + resourceScriptSourceBuilder.addConstructorArgValue(-1L); } return resourceScriptSourceBuilder.getBeanDefinition(); } - } diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/jsr223/ScriptParser.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/jsr223/ScriptParser.java index f5fde7a4e6..cea39cde61 100644 --- a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/jsr223/ScriptParser.java +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/jsr223/ScriptParser.java @@ -16,26 +16,30 @@ package org.springframework.integration.scripting.config.jsr223; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; + +import org.w3c.dom.Element; + import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.integration.scripting.config.AbstractScriptParser; import org.springframework.integration.scripting.jsr223.ScriptExecutorFactory; -import org.springframework.util.Assert; -import org.w3c.dom.Element; +import org.springframework.util.StringUtils; /** * @author David Turanski * @since 2.1 */ public class ScriptParser extends AbstractScriptParser { - + private static final String LANGUAGE_ATTRIBUTE = "lang"; @Override protected String getBeanClassName(Element element) { return "org.springframework.integration.scripting.jsr223.ScriptExecutingMessageProcessor"; } - + /* (non-Javadoc) * @see org.springframework.integration.config.xml.AbstractScriptParser#getScriptSourceClassName() */ @@ -44,11 +48,52 @@ public class ScriptParser extends AbstractScriptParser { return null; } - protected void postProcess(BeanDefinitionBuilder builder, Element element, ParserContext parserContext){ + @Override + protected void postProcess(BeanDefinitionBuilder builder, Element element, ParserContext parserContext) { String language = element.getAttribute(LANGUAGE_ATTRIBUTE); - Assert.hasLength(language, "Attribute " + LANGUAGE_ATTRIBUTE + " is required"); + String scriptLocation = element.getAttribute(LOCATION_ATTRIBUTE); + if (!StringUtils.hasText(language)) { + if (!StringUtils.hasText(scriptLocation)) { + parserContext.getReaderContext().error( + "An inline script requires the '" + LANGUAGE_ATTRIBUTE + "' attribute.", element); + return; + } + else { + language = getLanguageFromFileExtension(scriptLocation, parserContext, element); + if (language == null) { + parserContext.getReaderContext().error( + "Unable to determine language for script '" + scriptLocation + "'", element); + return; + } + } + } + builder.addConstructorArgValue(ScriptExecutorFactory.getScriptExecutor(language)); } - + /** + * @param scriptLocation + * @param parserContext + * @param element + * @return the language + */ + private String getLanguageFromFileExtension(String scriptLocation, ParserContext parserContext, Element element) { + ScriptEngineManager engineManager = new ScriptEngineManager(); + ScriptEngine engine = null; + + int index = scriptLocation.lastIndexOf(".") + 1; + if (index < 1) { + return null; + } + String extension = scriptLocation.substring(index); + + engine = engineManager.getEngineByExtension(extension); + + if (engine == null) { + parserContext.getReaderContext().error( + "No suitable scripting engine found for extension '" + extension + "'", element); + } + + return engine.getFactory().getLanguageName(); + } } diff --git a/spring-integration-scripting/src/main/resources/org/springframework/integration/scripting/config/spring-integration-scripting-3.0.xsd b/spring-integration-scripting/src/main/resources/org/springframework/integration/scripting/config/spring-integration-scripting-3.0.xsd index 68b4668640..d07dd7e107 100644 --- a/spring-integration-scripting/src/main/resources/org/springframework/integration/scripting/config/spring-integration-scripting-3.0.xsd +++ b/spring-integration-scripting/src/main/resources/org/springframework/integration/scripting/config/spring-integration-scripting-3.0.xsd @@ -19,10 +19,12 @@ - + - The script language or JSR 223 scripting engine name + The script language or JSR 223 scripting engine name. Required only for inline scripts. If a script location is + referenced, the language may be derived from the file extension + (.rb: ruby, .groovy, js: javascript (ECMAScript), .py: python). diff --git a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-context.xml b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-context.xml new file mode 100644 index 0000000000..1d68861b05 --- /dev/null +++ b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-context.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-fail1-context.xml b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-fail1-context.xml new file mode 100644 index 0000000000..01d6bf6ba5 --- /dev/null +++ b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-fail1-context.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-fail2-context.xml b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-fail2-context.xml new file mode 100644 index 0000000000..7caf1a79ef --- /dev/null +++ b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-fail2-context.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests.java b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests.java new file mode 100644 index 0000000000..18c7d87305 --- /dev/null +++ b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2013 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.jsr223; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author David Turanski + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class DeriveLanguageFromExtensionTests { + @Autowired + private ApplicationContext ctx; + + @Test + public void testParseLanguage() { + String langs[] = { "ruby", "Groovy", "ECMAScript", "python" }; + Class executors[] = { + RubyScriptExecutor.class, + DefaultScriptExecutor.class, + DefaultScriptExecutor.class, + PythonScriptExecutor.class + }; + + Map scriptProcessors = ctx + .getBeansOfType(ScriptExecutingMessageProcessor.class); + assertEquals(4, scriptProcessors.size()); + + for (int i = 0; i < 4; i++) { + + ScriptExecutingMessageProcessor processor = ctx.getBean( + "org.springframework.integration.scripting.jsr223.ScriptExecutingMessageProcessor#" + i, + ScriptExecutingMessageProcessor.class); + + AbstractScriptExecutor executor = (AbstractScriptExecutor) TestUtils.getPropertyValue(processor, + "scriptExecutor"); + assertEquals(langs[i], executor.language); + assertEquals(executors[i], executor.getClass()); + } + } + + @Test + public void testBadExtension() { + try { + new ClassPathXmlApplicationContext(this.getClass().getSimpleName() + "-fail1-context.xml", this.getClass()); + } + catch (Exception e) { + assertTrue(e.getMessage().contains("No suitable scripting engine found for extension 'xx'")); + } + } + + @Test + public void testNoExtension() { + try { + new ClassPathXmlApplicationContext(this.getClass().getSimpleName() + "-fail2-context.xml", this.getClass()); + } + catch (Exception e) { + assertTrue(e.getMessage().contains("Unable to determine language for script 'foo'")); + } + } + +}