001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.camel.builder.script;
018    
019    import java.io.File;
020    import java.io.IOException;
021    import java.io.InputStreamReader;
022    import java.net.URL;
023    
024    import javax.script.Compilable;
025    import javax.script.CompiledScript;
026    import javax.script.ScriptContext;
027    import javax.script.ScriptEngine;
028    import javax.script.ScriptEngineManager;
029    import javax.script.ScriptException;
030    
031    import org.apache.camel.Exchange;
032    import org.apache.camel.Expression;
033    import org.apache.camel.Predicate;
034    import org.apache.camel.Processor;
035    import org.apache.camel.converter.ObjectConverter;
036    import org.apache.commons.logging.Log;
037    import org.apache.commons.logging.LogFactory;
038    import org.springframework.core.io.FileSystemResource;
039    import org.springframework.core.io.Resource;
040    import org.springframework.core.io.UrlResource;
041    
042    /**
043     * A builder class for creating {@link Processor}, {@link Expression} and
044     * {@link Predicate} objects using the JSR 223 scripting engine.
045     *
046     * @version $Revision: 769448 $
047     */
048    public class ScriptBuilder implements Expression, Predicate, Processor {
049        private static final transient Log LOG = LogFactory.getLog(ScriptBuilder.class);
050    
051        private String scriptEngineName;
052        private Resource scriptResource;
053        private String scriptText;
054        private ScriptEngine engine;
055        private CompiledScript compiledScript;
056    
057        public ScriptBuilder(String scriptEngineName) {
058            this.scriptEngineName = scriptEngineName;
059        }
060    
061        public ScriptBuilder(String scriptEngineName, String scriptText) {
062            this(scriptEngineName);
063            this.scriptText = scriptText;
064        }
065    
066        public ScriptBuilder(String scriptEngineName, Resource scriptResource) {
067            this(scriptEngineName);
068            this.scriptResource = scriptResource;
069        }
070    
071        @Override
072        public String toString() {
073            return getScriptDescription();
074        }
075    
076        public Object evaluate(Exchange exchange) {
077            return evaluateScript(exchange);
078        }
079    
080        public <T> T evaluate(Exchange exchange, Class<T> type) {
081            Object result = evaluate(exchange);
082            return exchange.getContext().getTypeConverter().convertTo(type, result);
083        }
084    
085        public boolean matches(Exchange exchange) {
086            Object scriptValue = evaluateScript(exchange);
087            return matches(exchange, scriptValue);
088        }
089    
090        public void assertMatches(String text, Exchange exchange) throws AssertionError {
091            Object scriptValue = evaluateScript(exchange);
092            if (!matches(exchange, scriptValue)) {
093                throw new AssertionError(this + " failed on " + exchange + " as script returned <" + scriptValue + ">");
094            }
095        }
096    
097        public void process(Exchange exchange) {
098            evaluateScript(exchange);
099        }
100    
101        // Builder API
102        // -------------------------------------------------------------------------
103    
104        /**
105         * Sets the attribute on the context so that it is available to the script
106         * as a variable in the {@link ScriptContext#ENGINE_SCOPE}
107         *
108         * @param name the name of the attribute
109         * @param value the attribute value
110         * @return this builder
111         */
112        public ScriptBuilder attribute(String name, Object value) {
113            getScriptContext().setAttribute(name, value, ScriptContext.ENGINE_SCOPE);
114            return this;
115        }
116    
117        // Create any scripting language builder recognised by JSR 223
118        // -------------------------------------------------------------------------
119    
120        /**
121         * Creates a script builder for the named language and script contents
122         *
123         * @param language the language to use for the script
124         * @param scriptText the script text to be evaluated
125         * @return the builder
126         */
127        public static ScriptBuilder script(String language, String scriptText) {
128            return new ScriptBuilder(language, scriptText);
129        }
130    
131        /**
132         * Creates a script builder for the named language and script {@link Resource}
133         *
134         * @param language the language to use for the script
135         * @param scriptResource the resource used to load the script
136         * @return the builder
137         */
138        public static ScriptBuilder script(String language, Resource scriptResource) {
139            return new ScriptBuilder(language, scriptResource);
140        }
141    
142        /**
143         * Creates a script builder for the named language and script {@link File}
144         *
145         * @param language the language to use for the script
146         * @param scriptFile the file used to load the script
147         * @return the builder
148         */
149        public static ScriptBuilder script(String language, File scriptFile) {
150            return new ScriptBuilder(language, new FileSystemResource(scriptFile));
151        }
152    
153        /**
154         * Creates a script builder for the named language and script {@link URL}
155         *
156         * @param language the language to use for the script
157         * @param scriptURL the URL used to load the script
158         * @return the builder
159         */
160        public static ScriptBuilder script(String language, URL scriptURL) {
161            return new ScriptBuilder(language, new UrlResource(scriptURL));
162        }
163    
164        // Groovy
165        // -------------------------------------------------------------------------
166    
167        /**
168         * Creates a script builder for the groovy script contents
169         *
170         * @param scriptText the script text to be evaluated
171         * @return the builder
172         */
173        public static ScriptBuilder groovy(String scriptText) {
174            return new ScriptBuilder("groovy", scriptText);
175        }
176    
177        /**
178         * Creates a script builder for the groovy script {@link Resource}
179         *
180         * @param scriptResource the resource used to load the script
181         * @return the builder
182         */
183        public static ScriptBuilder groovy(Resource scriptResource) {
184            return new ScriptBuilder("groovy", scriptResource);
185        }
186    
187        /**
188         * Creates a script builder for the groovy script {@link File}
189         *
190         * @param scriptFile the file used to load the script
191         * @return the builder
192         */
193        public static ScriptBuilder groovy(File scriptFile) {
194            return new ScriptBuilder("groovy", new FileSystemResource(scriptFile));
195        }
196    
197        /**
198         * Creates a script builder for the groovy script {@link URL}
199         *
200         * @param scriptURL the URL used to load the script
201         * @return the builder
202         */
203        public static ScriptBuilder groovy(URL scriptURL) {
204            return new ScriptBuilder("groovy", new UrlResource(scriptURL));
205        }
206    
207        // JavaScript
208        // -------------------------------------------------------------------------
209    
210        /**
211         * Creates a script builder for the JavaScript/ECMAScript script contents
212         *
213         * @param scriptText the script text to be evaluated
214         * @return the builder
215         */
216        public static ScriptBuilder javaScript(String scriptText) {
217            return new ScriptBuilder("js", scriptText);
218        }
219    
220        /**
221         * Creates a script builder for the JavaScript/ECMAScript script
222         *
223         * @{link Resource}
224         * @param scriptResource the resource used to load the script
225         * @return the builder
226         */
227        public static ScriptBuilder javaScript(Resource scriptResource) {
228            return new ScriptBuilder("js", scriptResource);
229        }
230    
231        /**
232         * Creates a script builder for the JavaScript/ECMAScript script {@link File}
233         *
234         * @param scriptFile the file used to load the script
235         * @return the builder
236         */
237        public static ScriptBuilder javaScript(File scriptFile) {
238            return new ScriptBuilder("js", new FileSystemResource(scriptFile));
239        }
240    
241        /**
242         * Creates a script builder for the JavaScript/ECMAScript script {@link URL}
243         *
244         * @param scriptURL the URL used to load the script
245         * @return the builder
246         */
247        public static ScriptBuilder javaScript(URL scriptURL) {
248            return new ScriptBuilder("js", new UrlResource(scriptURL));
249        }
250    
251        // PHP
252        // -------------------------------------------------------------------------
253    
254        /**
255         * Creates a script builder for the PHP script contents
256         *
257         * @param scriptText the script text to be evaluated
258         * @return the builder
259         */
260        public static ScriptBuilder php(String scriptText) {
261            return new ScriptBuilder("php", scriptText);
262        }
263    
264        /**
265         * Creates a script builder for the PHP script {@link Resource}
266         *
267         * @param scriptResource the resource used to load the script
268         * @return the builder
269         */
270        public static ScriptBuilder php(Resource scriptResource) {
271            return new ScriptBuilder("php", scriptResource);
272        }
273    
274        /**
275         * Creates a script builder for the PHP script {@link File}
276         *
277         * @param scriptFile the file used to load the script
278         * @return the builder
279         */
280        public static ScriptBuilder php(File scriptFile) {
281            return new ScriptBuilder("php", new FileSystemResource(scriptFile));
282        }
283    
284        /**
285         * Creates a script builder for the PHP script {@link URL}
286         *
287         * @param scriptURL the URL used to load the script
288         * @return the builder
289         */
290        public static ScriptBuilder php(URL scriptURL) {
291            return new ScriptBuilder("php", new UrlResource(scriptURL));
292        }
293    
294        // Python
295        // -------------------------------------------------------------------------
296    
297        /**
298         * Creates a script builder for the Python script contents
299         *
300         * @param scriptText the script text to be evaluated
301         * @return the builder
302         */
303        public static ScriptBuilder python(String scriptText) {
304            return new ScriptBuilder("python", scriptText);
305        }
306    
307        /**
308         * Creates a script builder for the Python script {@link Resource}
309         *
310         * @param scriptResource the resource used to load the script
311         * @return the builder
312         */
313        public static ScriptBuilder python(Resource scriptResource) {
314            return new ScriptBuilder("python", scriptResource);
315        }
316    
317        /**
318         * Creates a script builder for the Python script {@link File}
319         *
320         * @param scriptFile the file used to load the script
321         * @return the builder
322         */
323        public static ScriptBuilder python(File scriptFile) {
324            return new ScriptBuilder("python", new FileSystemResource(scriptFile));
325        }
326    
327        /**
328         * Creates a script builder for the Python script {@link URL}
329         *
330         * @param scriptURL the URL used to load the script
331         * @return the builder
332         */
333        public static ScriptBuilder python(URL scriptURL) {
334            return new ScriptBuilder("python", new UrlResource(scriptURL));
335        }
336    
337        // Ruby/JRuby
338        // -------------------------------------------------------------------------
339    
340        /**
341         * Creates a script builder for the Ruby/JRuby script contents
342         *
343         * @param scriptText the script text to be evaluated
344         * @return the builder
345         */
346        public static ScriptBuilder ruby(String scriptText) {
347            return new ScriptBuilder("jruby", scriptText);
348        }
349    
350        /**
351         * Creates a script builder for the Ruby/JRuby script {@link Resource}
352         *
353         * @param scriptResource the resource used to load the script
354         * @return the builder
355         */
356        public static ScriptBuilder ruby(Resource scriptResource) {
357            return new ScriptBuilder("jruby", scriptResource);
358        }
359    
360        /**
361         * Creates a script builder for the Ruby/JRuby script {@link File}
362         *
363         * @param scriptFile the file used to load the script
364         * @return the builder
365         */
366        public static ScriptBuilder ruby(File scriptFile) {
367            return new ScriptBuilder("jruby", new FileSystemResource(scriptFile));
368        }
369    
370        /**
371         * Creates a script builder for the Ruby/JRuby script {@link URL}
372         *
373         * @param scriptURL the URL used to load the script
374         * @return the builder
375         */
376        public static ScriptBuilder ruby(URL scriptURL) {
377            return new ScriptBuilder("jruby", new UrlResource(scriptURL));
378        }
379    
380        // Properties
381        // -------------------------------------------------------------------------
382        public ScriptEngine getEngine() {
383            checkInitialised();
384            if (engine == null) {
385                throw new IllegalArgumentException("No script engine could be created for: " + getScriptEngineName());
386            }
387            return engine;
388        }
389    
390        public CompiledScript getCompiledScript() {
391            return compiledScript;
392        }
393    
394        public String getScriptText() {
395            return scriptText;
396        }
397    
398        public void setScriptText(String scriptText) {
399            this.scriptText = scriptText;
400        }
401    
402        public String getScriptEngineName() {
403            return scriptEngineName;
404        }
405    
406        /**
407         * Returns a description of the script
408         *
409         * @return the script description
410         */
411        public String getScriptDescription() {
412            if (scriptText != null) {
413                return scriptEngineName + ": " + scriptText;
414            } else if (scriptResource != null) {
415                return scriptEngineName + ": " + scriptResource.getDescription();
416            } else {
417                return scriptEngineName + ": null script";
418            }
419        }
420    
421        /**
422         * Access the script context so that it can be configured such as adding
423         * attributes
424         */
425        public ScriptContext getScriptContext() {
426            return getEngine().getContext();
427        }
428    
429        /**
430         * Sets the context to use by the script
431         */
432        public void setScriptContext(ScriptContext scriptContext) {
433            getEngine().setContext(scriptContext);
434        }
435    
436        public Resource getScriptResource() {
437            return scriptResource;
438        }
439    
440        public void setScriptResource(Resource scriptResource) {
441            this.scriptResource = scriptResource;
442        }
443    
444        // Implementation methods
445        // -------------------------------------------------------------------------
446        protected void checkInitialised() {
447            if (scriptText == null && scriptResource == null) {
448                throw new IllegalArgumentException("Neither scriptText or scriptResource are specified");
449            }
450            if (engine == null) {
451                engine = createScriptEngine();
452            }
453            if (compiledScript == null) {
454                if (engine instanceof Compilable) {
455                    compileScript((Compilable)engine);
456                }
457            }
458        }
459    
460        protected boolean matches(Exchange exchange, Object scriptValue) {
461            return ObjectConverter.toBool(scriptValue);
462        }
463    
464        protected ScriptEngine createScriptEngine() {
465            ScriptEngineManager manager = new ScriptEngineManager();
466            try {
467                engine = manager.getEngineByName(scriptEngineName);
468            } catch (NoClassDefFoundError ex) {
469                LOG.error("Can't load the scriptEngine for " + scriptEngineName + ", the exception is " + ex
470                          + ", please check the scriptEngine needs jars.");
471            }
472            if (engine == null) {
473                throw new IllegalArgumentException("No script engine could be created for: " + getScriptEngineName());
474            }
475            if (isPython()) {
476                ScriptContext context = engine.getContext();
477                context.setAttribute("com.sun.script.jython.comp.mode", "eval", ScriptContext.ENGINE_SCOPE);
478            }
479            return engine;
480        }
481    
482        protected void compileScript(Compilable compilable) {
483            try {
484                if (scriptText != null) {
485                    compiledScript = compilable.compile(scriptText);
486                } else if (scriptResource != null) {
487                    compiledScript = compilable.compile(createScriptReader());
488                }
489            } catch (ScriptException e) {
490                if (LOG.isDebugEnabled()) {
491                    LOG.debug("Script compile failed: " + e, e);
492                }
493                throw createScriptCompileException(e);
494            } catch (IOException e) {
495                throw createScriptCompileException(e);
496            }
497        }
498    
499        protected synchronized Object evaluateScript(Exchange exchange) {
500            try {
501                getScriptContext();
502                populateBindings(getEngine(), exchange);
503                Object result = runScript();
504                if (LOG.isDebugEnabled()) {
505                    LOG.debug("The script evaluation result is: " + result);
506                }
507                return result;
508            } catch (ScriptException e) {
509                if (LOG.isDebugEnabled()) {
510                    LOG.debug("Script evaluation failed: " + e, e);
511                }
512                throw createScriptEvaluationException(e.getCause());
513            } catch (IOException e) {
514                throw createScriptEvaluationException(e);
515            }
516        }
517    
518        protected Object runScript() throws ScriptException, IOException {
519            checkInitialised();
520            Object result = null;
521            if (compiledScript != null) {
522                result = compiledScript.eval();
523            } else {
524                if (scriptText != null) {
525                    result = getEngine().eval(scriptText);
526                } else {
527                    result = getEngine().eval(createScriptReader());
528                }
529            }
530            return result;
531        }
532    
533        protected void populateBindings(ScriptEngine engine, Exchange exchange) {
534            ScriptContext context = engine.getContext();
535            int scope = ScriptContext.ENGINE_SCOPE;
536            context.setAttribute("context", exchange.getContext(), scope);
537            context.setAttribute("exchange", exchange, scope);
538            context.setAttribute("request", exchange.getIn(), scope);
539            if (exchange.hasOut()) {
540                context.setAttribute("response", exchange.getOut(), scope);
541            }
542        }
543    
544        protected InputStreamReader createScriptReader() throws IOException {
545            // TODO consider character sets?
546            return new InputStreamReader(scriptResource.getInputStream());
547        }
548    
549        protected ScriptEvaluationException createScriptCompileException(Exception e) {
550            return new ScriptEvaluationException("Failed to compile: " + getScriptDescription() + ". Cause: " + e, e);
551        }
552    
553        protected ScriptEvaluationException createScriptEvaluationException(Throwable e) {
554            if (e.getClass().getName().equals("org.jruby.exceptions.RaiseException")) {
555                // Only the nested exception has the specific problem
556                try {
557                    Object ex = e.getClass().getMethod("getException").invoke(e);
558                    return new ScriptEvaluationException("Failed to evaluate: " + getScriptDescription() + ".  Error: " + ex + ". Cause: " + e, e);
559                } catch (Exception e1) {
560                    // do nothing here
561                }
562            }
563            return new ScriptEvaluationException("Failed to evaluate: " + getScriptDescription() + ". Cause: " + e, e);
564        }
565    
566        protected boolean isPython() {
567            return "python".equals(scriptEngineName) || "jython".equals(scriptEngineName);
568        }
569    
570    }