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