/* * Copyright 2002-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 java.security.AccessController; import java.security.PrivilegedAction; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.integration.scripting.AbstractScriptExecutingMessageProcessor; import org.springframework.integration.scripting.ScriptVariableGenerator; import org.springframework.messaging.Message; import org.springframework.scripting.ScriptCompilationException; import org.springframework.scripting.ScriptSource; import org.springframework.scripting.groovy.GroovyObjectCustomizer; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import groovy.lang.Binding; import groovy.lang.GString; import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyObject; import groovy.lang.MetaClass; import groovy.lang.MissingPropertyException; import groovy.lang.Script; import groovy.transform.CompileStatic; /** * The {@link org.springframework.integration.handler.MessageProcessor} implementation * to evaluate Groovy scripts. * * @author Dave Syer * @author Mark Fisher * @author Oleg Zhurakousky * @author Stefan Reuter * @author Artem Bilan * @author Gary Russell * @since 2.0 */ public class GroovyScriptExecutingMessageProcessor extends AbstractScriptExecutingMessageProcessor<Object> implements InitializingBean { private final VariableBindingGroovyObjectCustomizerDecorator customizerDecorator = new VariableBindingGroovyObjectCustomizerDecorator(); private final Lock scriptLock = new ReentrantLock(); private volatile ScriptSource scriptSource; private volatile GroovyClassLoader groovyClassLoader = AccessController.doPrivileged( (PrivilegedAction<GroovyClassLoader>) () -> new GroovyClassLoader(ClassUtils.getDefaultClassLoader())); private volatile Class<?> scriptClass; private boolean compileStatic; private CompilerConfiguration compilerConfiguration; /** * Create a processor for the given {@link ScriptSource} that will use a * DefaultScriptVariableGenerator. * @param scriptSource The script source. */ public GroovyScriptExecutingMessageProcessor(ScriptSource scriptSource) { super(); this.scriptSource = scriptSource; } /** * Create a processor for the given {@link ScriptSource} that will use the provided * ScriptVariableGenerator. * @param scriptSource The script source. * @param scriptVariableGenerator The variable generator. */ public GroovyScriptExecutingMessageProcessor(ScriptSource scriptSource, ScriptVariableGenerator scriptVariableGenerator) { super(scriptVariableGenerator); this.scriptSource = scriptSource; } /** * Sets a {@link GroovyObjectCustomizer} for this processor. * @param customizer The customizer. */ public void setCustomizer(GroovyObjectCustomizer customizer) { this.customizerDecorator.setCustomizer(customizer); } /** * Specify the {@code boolean} flag to indicate if the {@link GroovyClassLoader}'s compiler * should be customised for the {@link CompileStatic} hint for the provided script. * <p> More compiler options can be provided via {@link #setCompilerConfiguration(CompilerConfiguration)} * overriding this flag. * @param compileStatic the compile static {@code boolean} flag. * @since 4.3 * @see CompileStatic */ public void setCompileStatic(boolean compileStatic) { this.compileStatic = compileStatic; } /** * Specify the {@link CompilerConfiguration} options to customize the Groovy script compilation. * For example the {@link CompileStatic} and {@link org.codehaus.groovy.control.customizers.ImportCustomizer} * are the most popular options. * @param compilerConfiguration the Groovy script compiler options to use. * @since 4.3 * @see CompileStatic * @see GroovyClassLoader */ public void setCompilerConfiguration(CompilerConfiguration compilerConfiguration) { this.compilerConfiguration = compilerConfiguration; } @Override protected ScriptSource getScriptSource(Message<?> message) { return this.scriptSource; } @Override public void afterPropertiesSet() throws Exception { if (this.beanFactory != null && this.beanFactory instanceof ConfigurableListableBeanFactory) { ((ConfigurableListableBeanFactory) this.beanFactory).ignoreDependencyType(MetaClass.class); } CompilerConfiguration compilerConfiguration = this.compilerConfiguration; if (compilerConfiguration == null && this.compileStatic) { compilerConfiguration = new CompilerConfiguration(); compilerConfiguration.addCompilationCustomizers(new ASTTransformationCustomizer(CompileStatic.class)); } this.groovyClassLoader = new GroovyClassLoader(this.beanClassLoader, compilerConfiguration); } @Override protected Object executeScript(ScriptSource scriptSource, Map<String, Object> variables) throws Exception { Assert.notNull(scriptSource, "scriptSource must not be null"); this.parseScriptIfNecessary(scriptSource); Object result = this.execute(variables); return (result instanceof GString) ? result.toString() : result; } private void parseScriptIfNecessary(ScriptSource scriptSource) throws Exception { if (this.scriptClass == null || scriptSource.isModified()) { this.scriptLock.lockInterruptibly(); try { // synchronized double check if (this.scriptClass == null || scriptSource.isModified()) { String className = scriptSource.suggestedClassName(); if (StringUtils.hasText(className)) { this.scriptClass = this.groovyClassLoader.parseClass(scriptSource.getScriptAsString(), className); } else { this.scriptClass = this.groovyClassLoader.parseClass(scriptSource.getScriptAsString()); } } } finally { this.scriptLock.unlock(); } } } private Object execute(Map<String, Object> variables) throws ScriptCompilationException { try { GroovyObject goo = (GroovyObject) this.scriptClass.newInstance(); VariableBindingGroovyObjectCustomizerDecorator groovyObjectCustomizer = new BindingOverwriteGroovyObjectCustomizerDecorator(new BeanFactoryFallbackBinding(variables)); groovyObjectCustomizer.setCustomizer(this.customizerDecorator); if (goo instanceof Script) { // Allow metaclass and other customization. groovyObjectCustomizer.customize(goo); // A Groovy script, probably creating an instance: let's execute it. return ((Script) goo).run(); } else { // An instance of the scripted class: let's return it as-is. return goo; } } catch (InstantiationException ex) { throw new ScriptCompilationException( this.scriptSource, "Could not instantiate Groovy script class: " + this.scriptClass.getName(), ex); } catch (IllegalAccessException ex) { throw new ScriptCompilationException( this.scriptSource, "Could not access Groovy script constructor: " + this.scriptClass.getName(), ex); } } private final class BeanFactoryFallbackBinding extends Binding { private BeanFactoryFallbackBinding(Map<?, ?> variables) { super(variables); } @Override public Object getVariable(String name) { try { return super.getVariable(name); } catch (MissingPropertyException e) { // Original {@link Binding} doesn't have 'variable' for the given 'name'. // Try to resolve it as 'bean' from the given <code>beanFactory</code>. } if (GroovyScriptExecutingMessageProcessor.this.beanFactory == null) { throw new MissingPropertyException(name, this.getClass()); } try { return GroovyScriptExecutingMessageProcessor.this.beanFactory.getBean(name); } catch (NoSuchBeanDefinitionException e) { throw new MissingPropertyException(name, this.getClass(), e); } } } }