001/*
002 * Copyright 2002-2016 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.scripting.bsh;
018
019import java.io.IOException;
020
021import bsh.EvalError;
022
023import org.springframework.beans.factory.BeanClassLoaderAware;
024import org.springframework.scripting.ScriptCompilationException;
025import org.springframework.scripting.ScriptFactory;
026import org.springframework.scripting.ScriptSource;
027import org.springframework.util.Assert;
028import org.springframework.util.ClassUtils;
029
030/**
031 * {@link org.springframework.scripting.ScriptFactory} implementation
032 * for a BeanShell script.
033 *
034 * <p>Typically used in combination with a
035 * {@link org.springframework.scripting.support.ScriptFactoryPostProcessor};
036 * see the latter's javadoc for a configuration example.
037 *
038 * @author Juergen Hoeller
039 * @author Rob Harrop
040 * @since 2.0
041 * @see BshScriptUtils
042 * @see org.springframework.scripting.support.ScriptFactoryPostProcessor
043 */
044public class BshScriptFactory implements ScriptFactory, BeanClassLoaderAware {
045
046        private final String scriptSourceLocator;
047
048        private final Class<?>[] scriptInterfaces;
049
050        private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
051
052        private Class<?> scriptClass;
053
054        private final Object scriptClassMonitor = new Object();
055
056        private boolean wasModifiedForTypeCheck = false;
057
058
059        /**
060         * Create a new BshScriptFactory for the given script source.
061         * <p>With this {@code BshScriptFactory} variant, the script needs to
062         * declare a full class or return an actual instance of the scripted object.
063         * @param scriptSourceLocator a locator that points to the source of the script.
064         * Interpreted by the post-processor that actually creates the script.
065         */
066        public BshScriptFactory(String scriptSourceLocator) {
067                Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty");
068                this.scriptSourceLocator = scriptSourceLocator;
069                this.scriptInterfaces = null;
070        }
071
072        /**
073         * Create a new BshScriptFactory for the given script source.
074         * <p>The script may either be a simple script that needs a corresponding proxy
075         * generated (implementing the specified interfaces), or declare a full class
076         * or return an actual instance of the scripted object (in which case the
077         * specified interfaces, if any, need to be implemented by that class/instance).
078         * @param scriptSourceLocator a locator that points to the source of the script.
079         * Interpreted by the post-processor that actually creates the script.
080         * @param scriptInterfaces the Java interfaces that the scripted object
081         * is supposed to implement (may be {@code null})
082         */
083        public BshScriptFactory(String scriptSourceLocator, Class<?>... scriptInterfaces) {
084                Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty");
085                this.scriptSourceLocator = scriptSourceLocator;
086                this.scriptInterfaces = scriptInterfaces;
087        }
088
089
090        @Override
091        public void setBeanClassLoader(ClassLoader classLoader) {
092                this.beanClassLoader = classLoader;
093        }
094
095
096        @Override
097        public String getScriptSourceLocator() {
098                return this.scriptSourceLocator;
099        }
100
101        @Override
102        public Class<?>[] getScriptInterfaces() {
103                return this.scriptInterfaces;
104        }
105
106        /**
107         * BeanShell scripts do require a config interface.
108         */
109        @Override
110        public boolean requiresConfigInterface() {
111                return true;
112        }
113
114        /**
115         * Load and parse the BeanShell script via {@link BshScriptUtils}.
116         * @see BshScriptUtils#createBshObject(String, Class[], ClassLoader)
117         */
118        @Override
119        public Object getScriptedObject(ScriptSource scriptSource, Class<?>... actualInterfaces)
120                        throws IOException, ScriptCompilationException {
121
122                Class<?> clazz;
123
124                try {
125                        synchronized (this.scriptClassMonitor) {
126                                boolean requiresScriptEvaluation = (this.wasModifiedForTypeCheck && this.scriptClass == null);
127                                this.wasModifiedForTypeCheck = false;
128
129                                if (scriptSource.isModified() || requiresScriptEvaluation) {
130                                        // New script content: Let's check whether it evaluates to a Class.
131                                        Object result = BshScriptUtils.evaluateBshScript(
132                                                        scriptSource.getScriptAsString(), actualInterfaces, this.beanClassLoader);
133                                        if (result instanceof Class) {
134                                                // A Class: We'll cache the Class here and create an instance
135                                                // outside of the synchronized block.
136                                                this.scriptClass = (Class<?>) result;
137                                        }
138                                        else {
139                                                // Not a Class: OK, we'll simply create BeanShell objects
140                                                // through evaluating the script for every call later on.
141                                                // For this first-time check, let's simply return the
142                                                // already evaluated object.
143                                                return result;
144                                        }
145                                }
146                                clazz = this.scriptClass;
147                        }
148                }
149                catch (EvalError ex) {
150                        this.scriptClass = null;
151                        throw new ScriptCompilationException(scriptSource, ex);
152                }
153
154                if (clazz != null) {
155                        // A Class: We need to create an instance for every call.
156                        try {
157                                return clazz.newInstance();
158                        }
159                        catch (Throwable ex) {
160                                throw new ScriptCompilationException(
161                                                scriptSource, "Could not instantiate script class: " + clazz.getName(), ex);
162                        }
163                }
164                else {
165                        // Not a Class: We need to evaluate the script for every call.
166                        try {
167                                return BshScriptUtils.createBshObject(
168                                                scriptSource.getScriptAsString(), actualInterfaces, this.beanClassLoader);
169                        }
170                        catch (EvalError ex) {
171                                throw new ScriptCompilationException(scriptSource, ex);
172                        }
173                }
174        }
175
176        @Override
177        public Class<?> getScriptedObjectType(ScriptSource scriptSource)
178                        throws IOException, ScriptCompilationException {
179
180                synchronized (this.scriptClassMonitor) {
181                        try {
182                                if (scriptSource.isModified()) {
183                                        // New script content: Let's check whether it evaluates to a Class.
184                                        this.wasModifiedForTypeCheck = true;
185                                        this.scriptClass = BshScriptUtils.determineBshObjectType(
186                                                        scriptSource.getScriptAsString(), this.beanClassLoader);
187                                }
188                                return this.scriptClass;
189                        }
190                        catch (EvalError ex) {
191                                this.scriptClass = null;
192                                throw new ScriptCompilationException(scriptSource, ex);
193                        }
194                }
195        }
196
197        @Override
198        public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) {
199                synchronized (this.scriptClassMonitor) {
200                        return (scriptSource.isModified() || this.wasModifiedForTypeCheck);
201                }
202        }
203
204
205        @Override
206        public String toString() {
207                return "BshScriptFactory: script source locator [" + this.scriptSourceLocator + "]";
208        }
209
210}