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: 700514 $
047     */
048    public class ScriptBuilder<E extends Exchange> implements Expression<E>, Predicate<E>, 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(E exchange) {
077            return evaluateScript(exchange);
078        }
079    
080        public boolean matches(E exchange) {
081            Object scriptValue = evaluateScript(exchange);
082            return matches(exchange, scriptValue);
083        }
084    
085        public void assertMatches(String text, E exchange) throws AssertionError {
086            Object scriptValue = evaluateScript(exchange);
087            if (!matches(exchange, scriptValue)) {
088                throw new AssertionError(this + " failed on " + exchange + " as script returned <" + scriptValue + ">");
089            }
090        }
091    
092        public void process(Exchange exchange) {
093            evaluateScript(exchange);
094        }
095    
096        // Builder API
097        // -------------------------------------------------------------------------
098    
099        /**
100         * Sets the attribute on the context so that it is available to the script
101         * as a variable in the {@link ScriptContext#ENGINE_SCOPE}
102         *
103         * @param name the name of the attribute
104         * @param value the attribute value
105         * @return this builder
106         */
107        public ScriptBuilder attribute(String name, Object value) {
108            getScriptContext().setAttribute(name, value, ScriptContext.ENGINE_SCOPE);
109            return this;
110        }
111    
112        // Create any scripting language builder recognised by JSR 223
113        // -------------------------------------------------------------------------
114    
115        /**
116         * Creates a script builder for the named language and script contents
117         *
118         * @param language the language to use for the script
119         * @param scriptText the script text to be evaluted
120         * @return the builder
121         */
122        public static ScriptBuilder script(String language, String scriptText) {
123            return new ScriptBuilder(language, scriptText);
124        }
125    
126        /**
127         * Creates a script builder for the named language and script {@link Resource}
128         *
129         * @param language the language to use for the script
130         * @param scriptResource the resource used to load the script
131         * @return the builder
132         */
133        public static ScriptBuilder script(String language, Resource scriptResource) {
134            return new ScriptBuilder(language, scriptResource);
135        }
136    
137        /**
138         * Creates a script builder for the named language and script {@link File}
139         *
140         * @param language the language to use for the script
141         * @param scriptFile the file used to load the script
142         * @return the builder
143         */
144        public static ScriptBuilder script(String language, File scriptFile) {
145            return new ScriptBuilder(language, new FileSystemResource(scriptFile));
146        }
147    
148        /**
149         * Creates a script builder for the named language and script {@link URL}
150         *
151         * @param language the language to use for the script
152         * @param scriptURL the URL used to load the script
153         * @return the builder
154         */
155        public static ScriptBuilder script(String language, URL scriptURL) {
156            return new ScriptBuilder(language, new UrlResource(scriptURL));
157        }
158    
159        // Beanshell
160        // -------------------------------------------------------------------------
161    
162        /**
163         * Creates a script builder for the BeanShell script contents
164         *
165         * @param scriptText the script text to be evaluted
166         * @return the builder
167         * @deprecated will be removed in Camel 2.0
168         */
169        public static ScriptBuilder beanShell(String scriptText) {
170            return new ScriptBuilder("beanshell", scriptText);
171        }
172    
173        /**
174         * Creates a script builder for the BeanShell script {@link Resource}
175         *
176         * @param scriptResource the resource used to load the script
177         * @return the builder
178         * @deprecated will be removed in Camel 2.0
179         */
180        public static ScriptBuilder beanShell(Resource scriptResource) {
181            return new ScriptBuilder("beanshell", scriptResource);
182        }
183    
184        /**
185         * Creates a script builder for the BeanShell script {@link File}
186         *
187         * @param scriptFile the file used to load the script
188         * @return the builder
189         * @deprecated will be removed in Camel 2.0
190         */
191        public static ScriptBuilder beanShell(File scriptFile) {
192            return new ScriptBuilder("beanshell", new FileSystemResource(scriptFile));
193        }
194    
195        /**
196         * Creates a script builder for the BeanShell script {@link URL}
197         *
198         * @param scriptURL the URL used to load the script
199         * @return the builder
200         * @deprecated will be removed in Camel 2.0
201         */
202        public static ScriptBuilder beanShell(URL scriptURL) {
203            return new ScriptBuilder("beanshell", new UrlResource(scriptURL));
204        }
205    
206        // Groovy
207        // -------------------------------------------------------------------------
208    
209        /**
210         * Creates a script builder for the groovy script contents
211         *
212         * @param scriptText the script text to be evaluted
213         * @return the builder
214         */
215        public static ScriptBuilder groovy(String scriptText) {
216            return new ScriptBuilder("groovy", scriptText);
217        }
218    
219        /**
220         * Creates a script builder for the groovy script {@link Resource}
221         *
222         * @param scriptResource the resource used to load the script
223         * @return the builder
224         */
225        public static ScriptBuilder groovy(Resource scriptResource) {
226            return new ScriptBuilder("groovy", scriptResource);
227        }
228    
229        /**
230         * Creates a script builder for the groovy script {@link File}
231         *
232         * @param scriptFile the file used to load the script
233         * @return the builder
234         */
235        public static ScriptBuilder groovy(File scriptFile) {
236            return new ScriptBuilder("groovy", new FileSystemResource(scriptFile));
237        }
238    
239        /**
240         * Creates a script builder for the groovy script {@link URL}
241         *
242         * @param scriptURL the URL used to load the script
243         * @return the builder
244         */
245        public static ScriptBuilder groovy(URL scriptURL) {
246            return new ScriptBuilder("groovy", new UrlResource(scriptURL));
247        }
248    
249        // JavaScript
250        // -------------------------------------------------------------------------
251    
252        /**
253         * Creates a script builder for the JavaScript/ECMAScript script contents
254         *
255         * @param scriptText the script text to be evaluted
256         * @return the builder
257         */
258        public static ScriptBuilder javaScript(String scriptText) {
259            return new ScriptBuilder("js", scriptText);
260        }
261    
262        /**
263         * Creates a script builder for the JavaScript/ECMAScript script
264         *
265         * @{link Resource}
266         * @param scriptResource the resource used to load the script
267         * @return the builder
268         */
269        public static ScriptBuilder javaScript(Resource scriptResource) {
270            return new ScriptBuilder("js", scriptResource);
271        }
272    
273        /**
274         * Creates a script builder for the JavaScript/ECMAScript script {@link File}
275         *
276         * @param scriptFile the file used to load the script
277         * @return the builder
278         */
279        public static ScriptBuilder javaScript(File scriptFile) {
280            return new ScriptBuilder("js", new FileSystemResource(scriptFile));
281        }
282    
283        /**
284         * Creates a script builder for the JavaScript/ECMAScript script {@link URL}
285         *
286         * @param scriptURL the URL used to load the script
287         * @return the builder
288         */
289        public static ScriptBuilder javaScript(URL scriptURL) {
290            return new ScriptBuilder("js", new UrlResource(scriptURL));
291        }
292    
293        // PHP
294        // -------------------------------------------------------------------------
295    
296        /**
297         * Creates a script builder for the PHP script contents
298         *
299         * @param scriptText the script text to be evaluted
300         * @return the builder
301         */
302        public static ScriptBuilder php(String scriptText) {
303            return new ScriptBuilder("php", scriptText);
304        }
305    
306        /**
307         * Creates a script builder for the PHP script {@link Resource}
308         *
309         * @param scriptResource the resource used to load the script
310         * @return the builder
311         */
312        public static ScriptBuilder php(Resource scriptResource) {
313            return new ScriptBuilder("php", scriptResource);
314        }
315    
316        /**
317         * Creates a script builder for the PHP script {@link File}
318         *
319         * @param scriptFile the file used to load the script
320         * @return the builder
321         */
322        public static ScriptBuilder php(File scriptFile) {
323            return new ScriptBuilder("php", new FileSystemResource(scriptFile));
324        }
325    
326        /**
327         * Creates a script builder for the PHP script {@link URL}
328         *
329         * @param scriptURL the URL used to load the script
330         * @return the builder
331         */
332        public static ScriptBuilder php(URL scriptURL) {
333            return new ScriptBuilder("php", new UrlResource(scriptURL));
334        }
335    
336        // Python
337        // -------------------------------------------------------------------------
338    
339        /**
340         * Creates a script builder for the Python script contents
341         *
342         * @param scriptText the script text to be evaluted
343         * @return the builder
344         */
345        public static ScriptBuilder python(String scriptText) {
346            return new ScriptBuilder("python", scriptText);
347        }
348    
349        /**
350         * Creates a script builder for the Python script {@link Resource}
351         *
352         * @param scriptResource the resource used to load the script
353         * @return the builder
354         */
355        public static ScriptBuilder python(Resource scriptResource) {
356            return new ScriptBuilder("python", scriptResource);
357        }
358    
359        /**
360         * Creates a script builder for the Python script {@link File}
361         *
362         * @param scriptFile the file used to load the script
363         * @return the builder
364         */
365        public static ScriptBuilder python(File scriptFile) {
366            return new ScriptBuilder("python", new FileSystemResource(scriptFile));
367        }
368    
369        /**
370         * Creates a script builder for the Python script {@link URL}
371         *
372         * @param scriptURL the URL used to load the script
373         * @return the builder
374         */
375        public static ScriptBuilder python(URL scriptURL) {
376            return new ScriptBuilder("python", new UrlResource(scriptURL));
377        }
378    
379        // Ruby/JRuby
380        // -------------------------------------------------------------------------
381    
382        /**
383         * Creates a script builder for the Ruby/JRuby script contents
384         *
385         * @param scriptText the script text to be evaluted
386         * @return the builder
387         */
388        public static ScriptBuilder ruby(String scriptText) {
389            return new ScriptBuilder("jruby", scriptText);
390        }
391    
392        /**
393         * Creates a script builder for the Ruby/JRuby script {@link Resource}
394         *
395         * @param scriptResource the resource used to load the script
396         * @return the builder
397         */
398        public static ScriptBuilder ruby(Resource scriptResource) {
399            return new ScriptBuilder("jruby", scriptResource);
400        }
401    
402        /**
403         * Creates a script builder for the Ruby/JRuby script {@link File}
404         *
405         * @param scriptFile the file used to load the script
406         * @return the builder
407         */
408        public static ScriptBuilder ruby(File scriptFile) {
409            return new ScriptBuilder("jruby", new FileSystemResource(scriptFile));
410        }
411    
412        /**
413         * Creates a script builder for the Ruby/JRuby script {@link URL}
414         *
415         * @param scriptURL the URL used to load the script
416         * @return the builder
417         */
418        public static ScriptBuilder ruby(URL scriptURL) {
419            return new ScriptBuilder("jruby", new UrlResource(scriptURL));
420        }
421    
422        // Properties
423        // -------------------------------------------------------------------------
424        public ScriptEngine getEngine() {
425            checkInitialised();
426            if (engine == null) {
427                throw new IllegalArgumentException("No script engine could be created for: " + getScriptEngineName());
428            }
429            return engine;
430        }
431    
432        public CompiledScript getCompiledScript() {
433            return compiledScript;
434        }
435    
436        public String getScriptText() {
437            return scriptText;
438        }
439    
440        public void setScriptText(String scriptText) {
441            this.scriptText = scriptText;
442        }
443    
444        public String getScriptEngineName() {
445            return scriptEngineName;
446        }
447    
448        /**
449         * Returns a description of the script
450         *
451         * @return the script description
452         */
453        public String getScriptDescription() {
454            if (scriptText != null) {
455                return scriptEngineName + ": " + scriptText;
456            } else if (scriptResource != null) {
457                return scriptEngineName + ": " + scriptResource.getDescription();
458            } else {
459                return scriptEngineName + ": null script";
460            }
461        }
462    
463        /**
464         * Access the script context so that it can be configured such as adding
465         * attributes
466         */
467        public ScriptContext getScriptContext() {
468            return getEngine().getContext();
469        }
470    
471        /**
472         * Sets the context to use by the script
473         */
474        public void setScriptContext(ScriptContext scriptContext) {
475            getEngine().setContext(scriptContext);
476        }
477    
478        public Resource getScriptResource() {
479            return scriptResource;
480        }
481    
482        public void setScriptResource(Resource scriptResource) {
483            this.scriptResource = scriptResource;
484        }
485    
486        // Implementation methods
487        // -------------------------------------------------------------------------
488        protected void checkInitialised() {
489            if (scriptText == null && scriptResource == null) {
490                throw new IllegalArgumentException("Neither scriptText or scriptResource are specified");
491            }
492            if (engine == null) {
493                engine = createScriptEngine();
494            }
495            if (compiledScript == null) {
496                if (engine instanceof Compilable) {
497                    compileScript((Compilable)engine);
498                }
499            }
500        }
501    
502        protected boolean matches(E exchange, Object scriptValue) {
503            return ObjectConverter.toBool(scriptValue);
504        }
505    
506        protected ScriptEngine createScriptEngine() {
507            ScriptEngineManager manager = new ScriptEngineManager();
508            try {
509                engine = manager.getEngineByName(scriptEngineName);
510            } catch (NoClassDefFoundError ex) {
511                LOG.error("Can't load the scriptEngine for " + scriptEngineName + ", the exception is " + ex
512                          + ", please check the scriptEngine needs jars.");
513            }
514            if (engine == null) {
515                throw new IllegalArgumentException("No script engine could be created for: " + getScriptEngineName());
516            }
517            if (isPython()) {
518                ScriptContext context = engine.getContext();
519                context.setAttribute("com.sun.script.jython.comp.mode", "eval", ScriptContext.ENGINE_SCOPE);
520            }
521            return engine;
522        }
523    
524        protected void compileScript(Compilable compilable) {
525            try {
526                if (scriptText != null) {
527                    compiledScript = compilable.compile(scriptText);
528                } else if (scriptResource != null) {
529                    compiledScript = compilable.compile(createScriptReader());
530                }
531            } catch (ScriptException e) {
532                if (LOG.isDebugEnabled()) {
533                    LOG.debug("Script compile failed: " + e, e);
534                }
535                throw createScriptCompileException(e);
536            } catch (IOException e) {
537                throw createScriptCompileException(e);
538            }
539        }
540    
541        protected synchronized Object evaluateScript(Exchange exchange) {
542            try {
543                getScriptContext();
544                populateBindings(getEngine(), exchange);
545                Object result = runScript();
546                if (LOG.isDebugEnabled()) {
547                    LOG.debug("The script evaluation result is: " + result);
548                }
549                return result;
550            } catch (ScriptException e) {
551                if (LOG.isDebugEnabled()) {
552                    LOG.debug("Script evaluation failed: " + e, e);
553                }
554                throw createScriptEvaluationException(e.getCause());
555            } catch (IOException e) {
556                throw createScriptEvaluationException(e);
557            }
558        }
559    
560        protected Object runScript() throws ScriptException, IOException {
561            checkInitialised();
562            Object result = null;
563            if (compiledScript != null) {
564                result = compiledScript.eval();
565            } else {
566                if (scriptText != null) {
567                    result = getEngine().eval(scriptText);
568                } else {
569                    result = getEngine().eval(createScriptReader());
570                }
571            }
572            return result;
573        }
574    
575        protected void populateBindings(ScriptEngine engine, Exchange exchange) {
576            ScriptContext context = engine.getContext();
577            int scope = ScriptContext.ENGINE_SCOPE;
578            context.setAttribute("context", exchange.getContext(), scope);
579            context.setAttribute("exchange", exchange, scope);
580            context.setAttribute("request", exchange.getIn(), scope);
581            context.setAttribute("response", exchange.getOut(false), scope);
582        }
583    
584        protected InputStreamReader createScriptReader() throws IOException {
585            // TODO consider character sets?
586            return new InputStreamReader(scriptResource.getInputStream());
587        }
588    
589        protected ScriptEvaluationException createScriptCompileException(Exception e) {
590            return new ScriptEvaluationException("Failed to compile: " + getScriptDescription() + ". Cause: " + e, e);
591        }
592    
593        protected ScriptEvaluationException createScriptEvaluationException(Throwable e) {
594            if (e.getClass().getName().equals("org.jruby.exceptions.RaiseException")) {
595                // Only the nested exception has the specific problem
596                try {
597                    Object ex = e.getClass().getMethod("getException").invoke(e);
598                    return new ScriptEvaluationException("Failed to evaluate: " + getScriptDescription() + ".  Error: " + ex + ". Cause: " + e, e);
599                } catch (Exception e1) {
600                    // do nothing here
601                }
602            }
603            return new ScriptEvaluationException("Failed to evaluate: " + getScriptDescription() + ". Cause: " + e, e);
604        }
605    
606        protected boolean isPython() {
607            return "python".equals(scriptEngineName) || "jython".equals(scriptEngineName);
608        }
609    
610    }