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 }