001/* An actor whose execution is specified by a script. 002 003 Copyright (c) 2013 The Regents of the University of California. 004 All rights reserved. 005 Permission is hereby granted, without written agreement and without 006 license or royalty fees, to use, copy, modify, and distribute this 007 software and its documentation for any purpose, provided that the above 008 copyright notice and the following two paragraphs appear in all copies 009 of this software. 010 011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 015 SUCH DAMAGE. 016 017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 022 ENHANCEMENTS, OR MODIFICATIONS. 023 024 */ 025package org.kepler.scriptengine; 026 027import java.lang.reflect.InvocationTargetException; 028 029import javax.script.Invocable; 030import javax.script.ScriptEngine; 031import javax.script.ScriptEngineManager; 032import javax.script.ScriptException; 033 034import org.kepler.sms.SemanticType; 035 036import ptolemy.actor.TypedAtomicActor; 037import ptolemy.actor.TypedIOPort; 038import ptolemy.actor.parameters.ParameterPort; 039import ptolemy.actor.process.TerminateProcessException; 040import ptolemy.data.expr.Parameter; 041import ptolemy.data.expr.StringParameter; 042import ptolemy.kernel.CompositeEntity; 043import ptolemy.kernel.util.Attribute; 044import ptolemy.kernel.util.IllegalActionException; 045import ptolemy.kernel.util.NameDuplicationException; 046import ptolemy.kernel.util.Settable; 047import ptolemy.kernel.util.StringAttribute; 048import ptolemy.kernel.util.Workspace; 049import ptolemy.util.MessageHandler; 050import ptolemy.vergil.toolbox.TextEditorConfigureFactory; 051 052/** An actor whose execution is specified by a script. The Java 053 * ScriptEngine API is used to invoke methods in the script. 054 * 055 * @author Daniel Crawl 056 * @version $Id: ScriptEngineActor.java 34142 2015-10-28 21:16:20Z crawl $ 057 */ 058public abstract class ScriptEngineActor extends TypedAtomicActor { 059 060 /** Construct a new ScriptEngineActor for a specified workspace. */ 061 public ScriptEngineActor(Workspace workspace) { 062 super(workspace); 063 } 064 065 /** Construct a new ScriptEngineActor with the given container and name. */ 066 public ScriptEngineActor(CompositeEntity container, String name) 067 throws IllegalActionException, NameDuplicationException { 068 super(container, name); 069 070 _manager = new ScriptEngineManager(); 071 _registerEngineFactory(); 072 073 actorClassName = new StringParameter(this, "actorClassName"); 074 actorClassName.setToken("Actor"); 075 actorClassName.setVisibility(Settable.EXPERT); 076 077 language = new StringParameter(this, "language"); 078 language.setVisibility(Settable.EXPERT); 079 080 script = new StringAttribute(this, "script"); 081 082 _editorFactory = new TextEditorConfigureFactory(this, "_editorFactory"); 083 _editorFactory.attributeName.setExpression("script"); 084 _editorFactory.setPersistent(false); 085 } 086 087 /** React to an attribute change. When the language changes, this 088 * method loads the script engine for that language. */ 089 @Override 090 public void attributeChanged(Attribute attribute) throws IllegalActionException { 091 092 if(attribute == language) { 093 String val = language.stringValue(); 094 if(!val.equals(_languageStr)) { 095 096 // in java 8, "javascript" retrieves nashorn, 097 // but we want rhino. 098 if(val.equals("javascript")) { 099 val = "rhino-nonjdk"; 100 } 101 102 final ScriptEngine engine = _manager.getEngineByName(val); 103 if(engine == null) { 104 throw new IllegalActionException(this, 105 "Could not find script engine for " + val); 106 } 107 108 if(!(engine instanceof Invocable)) { 109 throw new IllegalActionException(this, 110 "Script engine for " + _languageStr + " is not invocable."); 111 } 112 113 _languageStr = val; 114 _engine = engine; 115 116 } 117 } else if(attribute == script) { 118 if(_engine != null) { 119 _initializeScript(); 120 } 121 } else { 122 super.attributeChanged(attribute); 123 } 124 } 125 126 /** Clone the actor into the specified workspace. */ 127 @Override 128 public Object clone(Workspace workspace) throws CloneNotSupportedException { 129 final ScriptEngineActor newObject = (ScriptEngineActor) super.clone(workspace); 130 newObject._actorObject = null; 131 newObject._editorFactory = (TextEditorConfigureFactory) newObject.getAttribute("_editorFactory"); 132 newObject._engine = null; 133 newObject._evaluatedScriptObject = null; 134 newObject._languageStr = ""; 135 newObject._missingMethods = new boolean[MethodType.values().length]; 136 137 newObject._manager = new ScriptEngineManager(); 138 // allow the child class to register an engine factory with the 139 // manager before setting the language. this is necessary when 140 // the engine factory is not loaded in a jar. 141 newObject._registerEngineFactory(); 142 143 // set the language in the clone so that the engine is initialized. 144 try { 145 newObject.language.setToken(_languageStr); 146 } catch (IllegalActionException e) { 147 throw new CloneNotSupportedException("Error setting language in clone: " + 148 e.getMessage()); 149 } 150 151 return newObject; 152 } 153 154 /** Send the message to all registered debug listeners. 155 * @param message The debug message. 156 */ 157 public void debug(String message) { 158 if (_debugging) { 159 _debug("From script: ", message); 160 } 161 } 162 163 /** Set an error message that will be thrown in an exception after the 164 * current or next method is invoked in the script. 165 * @param message The debug message. 166 */ 167 public void error(String message) { 168 _errorMessage = message; 169 } 170 171 /** Invoke the fire() method in the script. */ 172 @Override 173 public void fire() throws IllegalActionException { 174 175 super.fire(); 176 177 if(!_missingMethods[MethodType.fire.ordinal()]) { 178 _invokeMethod(MethodType.fire); 179 } 180 } 181 182 /** Invoke the initialize() method in the script. */ 183 @Override 184 public void initialize() throws IllegalActionException { 185 super.initialize(); 186 if(!_missingMethods[MethodType.initialize.ordinal()]) { 187 _invokeMethod(MethodType.initialize); 188 } 189 } 190 191 /** Invoke the postfire() method in the script. 192 * @return If the postfire method is present in the script, 193 * returns the return value from executing this method. Otherwise, 194 * returns the value from the parent class's postfire(). 195 */ 196 @Override 197 public boolean postfire() throws IllegalActionException { 198 199 if(!_missingMethods[MethodType.postfire.ordinal()]) { 200 final InvocationType type = _invokeMethod(MethodType.postfire); 201 202 // see if the method returned false or true 203 if(type == InvocationType.ReturnedFalse) { 204 return false; 205 } else if(type == InvocationType.ReturnedTrue) { 206 return true; 207 } 208 } 209 210 // postfire method not implemented so use the super class postfire 211 return super.postfire(); 212 } 213 214 /** Invoke the prefire() method in the script. 215 * @return If the prefire method is present in the script, 216 * returns the return value from executing this method. Otherwise, 217 * returns the value from the parent class's prefire(). 218 */ 219 @Override 220 public boolean prefire() throws IllegalActionException { 221 222 if(!_missingMethods[MethodType.prefire.ordinal()]) { 223 final InvocationType type = _invokeMethod(MethodType.prefire); 224 225 // see if the method returned false or true 226 if(type == InvocationType.ReturnedFalse) { 227 return false; 228 } else if(type == InvocationType.ReturnedTrue) { 229 return true; 230 } 231 } 232 233 // prefire method not implemented so use the super class prefire 234 return super.prefire(); 235 } 236 237 238 /** Invoke the preinitialize() method of the script. */ 239 @Override 240 public void preinitialize() throws IllegalActionException { 241 242 super.preinitialize(); 243 244 // make sure a language was chosen and engine is not null 245 if(_languageStr.isEmpty()) { 246 throw new IllegalActionException(this, "Chose a scripting language."); 247 } 248 249 _initializeScript(); 250 251 // NOTE: we don't check if the preinitialize method exists, since 252 // initializeScript() clears missingMethods[]. 253 254 _invokeMethod(MethodType.preinitialize); 255 } 256 257 /** Invoke the stop() method in the script. */ 258 @Override 259 public void stop() { 260 super.stop(); 261 if(!_missingMethods[MethodType.stop.ordinal()]) { 262 try { 263 _invokeMethod(MethodType.stop); 264 } catch (IllegalActionException e) { 265 MessageHandler.error("Error invoking stop().", e); 266 } 267 } 268 } 269 270 /** Invoke the stopFire() method in the script. */ 271 @Override 272 public void stopFire() { 273 super.stopFire(); 274 if(_engine != null) { 275 if(!_missingMethods[MethodType.stopFire.ordinal()]) { 276 try { 277 _invokeMethod(MethodType.stopFire); 278 } catch (IllegalActionException e) { 279 MessageHandler.error("Error invoking stopFire().", e); 280 } 281 } 282 } 283 } 284 285 /** Invoke the terminate() method in the script. */ 286 @Override 287 public void terminate() { 288 super.terminate(); 289 if(!_missingMethods[MethodType.terminate.ordinal()]) { 290 try { 291 _invokeMethod(MethodType.terminate); 292 } catch (IllegalActionException e) { 293 MessageHandler.error("Error invoking terminate().", e); 294 } 295 } 296 _clearScriptObjects(); 297 } 298 299 /** Invoke the wrapup() method in the script. */ 300 @Override 301 public void wrapup() throws IllegalActionException { 302 super.wrapup(); 303 if(_engine != null) { 304 if(!_missingMethods[MethodType.wrapup.ordinal()]) { 305 _invokeMethod(MethodType.wrapup); 306 } 307 _clearScriptObjects(); 308 } 309 } 310 311 /////////////////////////////////////////////////////////////////// 312 //// public variables //// 313 314 /** The name of the class in the script defining the execution methods. */ 315 public StringParameter actorClassName; 316 317 /** The script language. */ 318 public StringParameter language; 319 320 /** The contents of the script. */ 321 public StringAttribute script; 322 323 /////////////////////////////////////////////////////////////////// 324 //// protected methods //// 325 326 /** Create an instance of the actor object in the script. 327 * @return the actor instance. 328 */ 329 protected Object _createActorInstance(String actorClassNameStr) throws ScriptException { 330 _engine.eval(_ACTOR_INSTANCE_NAME + " = new " + actorClassNameStr + "()"); 331 return _engine.get(_ACTOR_INSTANCE_NAME); 332 } 333 334 /** Evaluate the script. 335 * @return Returns the object resulting from evaluating the script. 336 */ 337 protected Object _evaluateScript() throws ScriptException, IllegalActionException { 338 return _engine.eval(script.getExpression()); 339 } 340 341 /** Put the given object to the actor instance in the script. 342 * @param name the name of the object to put. 343 * @param object the object to put. 344 */ 345 protected void _putObjectToActorInstance(String name, Object object) throws ScriptException { 346 String globalName = "_yyy_" + name; 347 _engine.put(globalName, object); 348 _engine.eval(_ACTOR_INSTANCE_NAME + "." + name + " = " + globalName); 349 //System.out.println("put " + object + " as " + globalName); 350 //System.out.println(_ACTOR_INSTANCE_NAME + "." + name + " = " + globalName); 351 } 352 353 /** Register an script engine factory with the scrip engine manager. 354 * In this class, does nothing. 355 */ 356 protected void _registerEngineFactory() { 357 358 } 359 360 /////////////////////////////////////////////////////////////////// 361 //// protected variables //// 362 363 /** The engine to parse and execute scripts. */ 364 protected ScriptEngine _engine; 365 366 /** The name of the actor object in the script. */ 367 protected final static String _ACTOR_INSTANCE_NAME = "_TOP_ACTOR_INSTANCE"; 368 369 /** The actor object in the script. */ 370 protected Object _actorObject; 371 372 /** If true, use reflection to invoke the method. Otherwise, use the 373 * Invocable interface of the ScriptEngine to invoke the method. 374 */ 375 protected boolean _invokeMethodWithReflection = false; 376 377 /** The Object resulting from evaluating the script. */ 378 protected Object _evaluatedScriptObject; 379 380 /** An object to load script engines. */ 381 protected ScriptEngineManager _manager; 382 383 /** Text editor. */ 384 protected TextEditorConfigureFactory _editorFactory; 385 386 /////////////////////////////////////////////////////////////////// 387 //// private methods //// 388 389 /** Remove all java objects, e.g., ports and parameters, added to the script. */ 390 private void _clearScriptObjects() { 391 //Bindings bindings = _engine.getBindings(ScriptContext.ENGINE_SCOPE); 392 //bindings.clear(); 393 //_engine.setBindings(_engine.createBindings(), ScriptContext.ENGINE_SCOPE); 394 _engine = null; 395 _errorMessage = null; 396 } 397 398 /** Clear any existing objects put to the existing script, get a new 399 * script engine based on the value in the language parameter, create 400 * the actor instance in the engine, and puts ports and parameters 401 * to the actor instance. 402 */ 403 private void _initializeScript() throws IllegalActionException { 404 405 // remove any ports and parameters we previously put to the script 406 _clearScriptObjects(); 407 408 try { 409 410 if(_engine == null) { 411 _engine = _manager.getEngineByName(_languageStr); 412 } 413 414 if(_engine == null) { 415 throw new IllegalActionException(this, 416 "Scripting engine not found for " + _languageStr); 417 } 418 419 _evaluatedScriptObject = _evaluateScript(); 420 421 //System.out.println("script changed:\n" + script.getExpression()); 422 423 String actorClassNameStr = actorClassName.stringValue(); 424 if(!actorClassNameStr.isEmpty()) { 425 _actorObject = _createActorInstance(actorClassNameStr); 426 } 427 428 //System.out.println("actor object = " + _actorObject); 429 430 _putObjectsToEngine(); 431 432 // initialize the error string 433 // _engine.put("error", null); 434 435 // initialize the set of missing methods since the methods could 436 // have been added/removed since last execution. 437 for(int i = 0; i < _missingMethods.length; i++) { 438 _missingMethods[i] = false; 439 } 440 441 } catch (ScriptException e) { 442 throw new IllegalActionException(this, e, "Error evaluating script."); 443 } 444 } 445 446 /** Attempt to invoke the specified method in the script. If the method is defined 447 * in the script, the method is invoked and the object returned by the method is 448 * returned. If the method does not exist, this returns InvocationType.MethodNotFound. 449 * If invoking the method in the script causes an exception, the exception is wrapped 450 * in an IllegalActionException, which is then thrown by this method. If the 451 * method in the script successfully finishes, and if the error message has been 452 * (by calling error()), then the error message is wrapped in an IllegalActionException 453 * and thrown. 454 */ 455 private InvocationType _invokeMethod(MethodType method) throws IllegalActionException { 456 457 //System.out.println("in invokeMethod for " + method); 458 459 // see if we already know the method is not implemented in the script 460 if(_missingMethods[method.ordinal()]) { 461 return InvocationType.MethodNotFound; 462 } 463 464 Object retval = null; 465 466 final String methodName = method.toString(); 467 468 // try invoking the method 469 try { 470 //System.out.println("going to invoke " + methodName); 471 472 // see if the actor object was found in the script. 473 // if it was found, invoke the method on that object, 474 // otherwise invoke the function with the same name as the method. 475 if(_actorObject != null) { 476 if(_invokeMethodWithReflection) { 477 retval = _actorObject.getClass().getMethod(methodName).invoke(_actorObject); 478 } else { 479 retval = ((Invocable)_engine).invokeMethod(_actorObject, methodName); 480 } 481 } else { 482 retval = ((Invocable)_engine).invokeFunction(methodName); 483 } 484 } catch (ScriptException e) { 485 486 // check if exception cause, or cause's cause is TerminateProcessException 487 // thrown when PN is done. 488 Throwable cause = e.getCause(); 489 if(cause != null) { 490 if(cause instanceof TerminateProcessException) { 491 throw (TerminateProcessException) cause; 492 } 493 Throwable innerCause = cause.getCause(); 494 if(innerCause != null && (innerCause instanceof TerminateProcessException)) { 495 throw (TerminateProcessException) innerCause; 496 } 497 } else { 498 // check the exception message. 499 // when a TerminateProcessException occurs, javascript throws a ScriptException 500 // whose cause is null, but message something line: 501 // sun.org.mozilla.javascript.internal.WrappedException: Wrapped ptolemy.actor.process.TerminateProcessException: 502 // 503 final String message = e.getMessage(); 504 if(message.contains("ptolemy.actor.process.TerminateProcessException")) { 505 throw new TerminateProcessException(""); 506 } 507 } 508 throw new IllegalActionException(this, e, "Error excuting " + methodName + " in script."); 509 } catch (NoSuchMethodException e) { 510 // the method was not implemented, so update the array 511 _missingMethods[method.ordinal()] = true; 512 //System.out.println("does not have " + methodName); 513 return InvocationType.MethodNotFound; 514 } catch(InvocationTargetException e) { 515 Throwable target = e.getTargetException(); 516 // for Java actor, target is TerminateProcessException 517 if(target instanceof TerminateProcessException) { 518 throw (TerminateProcessException)target; 519 } 520 if(target != null) { 521 Throwable cause = target.getCause(); 522 if(cause instanceof TerminateProcessException) { 523 throw (TerminateProcessException)cause; 524 } else if(cause != null) { 525 throw new IllegalActionException(this, cause, "Error executing " + methodName + " in script."); 526 } else { 527 throw new IllegalActionException(this, target, "Error executing " + methodName + " in script."); 528 } 529 } 530 throw new IllegalActionException(this, e, "Error executing " + methodName + " in script."); 531 } catch(Exception e) { 532 throw new IllegalActionException(this, e, "Error executing " + methodName + " in script."); 533 } 534 535 // the method was implemented in the script and executed 536 537 // check for an error 538 if(_errorMessage != null) { 539 throw new IllegalActionException(this, _errorMessage); 540 } 541 542 /* 543 final Object error = _engine.get("error"); 544 if(error != null) { 545 throw new IllegalActionException(this, error.toString()); 546 } 547 */ 548 549 // check for a return type 550 if(retval == null) { 551 return InvocationType.ReturnedNothing; 552 } else if(retval == Boolean.FALSE) { 553 return InvocationType.ReturnedFalse; 554 } else if(retval == Boolean.TRUE) { 555 return InvocationType.ReturnedTrue; 556 } else if(retval instanceof Boolean) { 557 if(((Boolean)retval).booleanValue()) { 558 return InvocationType.ReturnedTrue; 559 } else { 560 return InvocationType.ReturnedFalse; 561 } 562 } 563 564 // the method returned something unexpected: not true or false. 565 System.out.println("WARNING: unknown return type: " + retval); 566 return InvocationType.ReturnedNothing; 567 568 } 569 570 /** Put the ports, parameters, etc. of this actor to the script's actor 571 * object instance. If the script does not define an actor class, 572 * put the ports and parameters as global variables. 573 */ 574 private void _putObjectsToEngine() throws ScriptException, IllegalActionException { 575 576 // put all the ports and parameters to the script 577 for(TypedIOPort port : portList()) { 578 579 // do not put ParameterPorts since the connected PortParameters are there 580 if(port instanceof ParameterPort) { 581 continue; 582 } 583 584 //System.out.println("putting port " + port.getName()); 585 final String portName = port.getName(); 586 if(_actorObject == null) { 587 _engine.put(portName, port); 588 } else { 589 _putObjectToActorInstance(portName, port); 590 } 591 } 592 593 for(Attribute attribute : attributeList(Parameter.class)) { 594 final String attributeName = attribute.getName(); 595 if(attribute == actorClassName || 596 attribute == language || 597 attributeName.equals("class") || 598 attributeName.startsWith("_") || 599 attributeName.isEmpty() || 600 (attribute instanceof SemanticType)) { 601 continue; 602 } 603 604 //System.out.println("putting attribute " + attribute.getName()); 605 if(_actorObject == null) { 606 _engine.put(attributeName, attribute); 607 } else { 608 _putObjectToActorInstance(attributeName, attribute); 609 } 610 } 611 612 // put the java actor (this object) to the actor instance 613 if(_actorObject == null) { 614 _engine.put("actor", this); 615 } else { 616 _putObjectToActorInstance("actor", this); 617 } 618 619 // put the script engine object to the actor instance. 620 // in the script, engine.eval() can be used to load libraries. 621 if(_actorObject == null) { 622 _engine.put("engine", _engine); 623 } else { 624 _putObjectToActorInstance("engine", _engine); 625 } 626 } 627 628 /////////////////////////////////////////////////////////////////// 629 //// private variables //// 630 631 /** The value of language parameter. */ 632 private String _languageStr = ""; 633 634 /** An array denoting if a method is implemented by the script. */ 635 private boolean[] _missingMethods = new boolean[MethodType.values().length]; 636 637 /** A string containing an error message set by the script. */ 638 private String _errorMessage; 639 640 /** Possible outcomes when calling invoking a method. */ 641 private enum InvocationType { 642 MethodNotFound, 643 ReturnedFalse, 644 ReturnedNothing, 645 ReturnedTrue, 646 }; 647 648 /** The types of methods that can be implemented by the script. */ 649 private enum MethodType { 650 fire, 651 initialize, 652 prefire, 653 preinitialize, 654 postfire, 655 stop, 656 stopFire, 657 terminate, 658 wrapup 659 }; 660}