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