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}