001/* An actor that wraps (an instance of) a Java class.
002
003 Copyright (c) 2001-2014 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 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028package ptolemy.actor.lib;
029
030import java.lang.reflect.Constructor;
031import java.lang.reflect.InvocationTargetException;
032import java.lang.reflect.Method;
033import java.lang.reflect.Modifier;
034import java.util.Hashtable;
035import java.util.Iterator;
036
037import ptolemy.actor.IOPort;
038import ptolemy.actor.TypedAtomicActor;
039import ptolemy.data.BooleanToken;
040import ptolemy.data.ComplexToken;
041import ptolemy.data.DoubleToken;
042import ptolemy.data.FixToken;
043import ptolemy.data.IntToken;
044import ptolemy.data.LongToken;
045import ptolemy.data.RecordToken;
046import ptolemy.data.StringToken;
047import ptolemy.data.Token;
048import ptolemy.data.expr.ConversionUtilities;
049import ptolemy.kernel.CompositeEntity;
050import ptolemy.kernel.util.IllegalActionException;
051import ptolemy.kernel.util.NameDuplicationException;
052import ptolemy.kernel.util.StringAttribute;
053import ptolemy.math.Complex;
054import ptolemy.math.FixPoint;
055
056///////////////////////////////////////////////////////////////////
057//// ClassWrapper
058
059/**
060 This actor wraps (an instance of) a Java class specified by the
061 <i>className</i> parameter. The actor has no ports when created.
062 If an input port is added to the actor, the name of the port is
063 interpreted as the name of a method of the Java class. When the
064 actor is fired and a token is received from this input port, the
065 value of the token is treated as the argument(s) for invoking
066 the method. If the method has a return value and the actor has
067 an output port named <i>methodName</i>Result, the return value
068 is wrapped in a token that is sent to the output port.
069 <p>
070 For example, suppose the specified class has a method named foo
071 and the actor has an input port of the same name. If method foo
072 takes no argument, the token received from port <i>foo</i> is
073 treated as the trigger for invoking the method, and its content
074 is ignored. If method foo takes arguments, the input token
075 should be a record token whose field values are used as the
076 arguments. The field labels of the record token should be "arg1",
077 "arg2", etc. For example, if method foo takes two double arguments,
078 the record token "{arg1 = 0.0, arg2 = 1.0}" can be the input.
079
080 A special case is when method foo takes one argument, the token
081 containing the argument value can be input directly, and does not
082 need to be put into a record token.
083 <p>
084 FIXME: Need to set type constraints appropriately.
085 Need (and how) to handle overloaded methods.
086
087 @author Xiaojun Liu
088 @version $Id$
089 @since Ptolemy II 2.0
090 @Pt.ProposedRating Red (liuxj)
091 @Pt.AcceptedRating Red (liuxj)
092 */
093public class ClassWrapper extends TypedAtomicActor {
094    /** Construct an actor with the given container and name.
095     *  In addition to invoking the base class constructor, create
096     *  the <i>className</i> parameter.
097     *  @param container The container of this actor.
098     *  @param name The name of this actor.
099     *  @exception IllegalActionException If the actor cannot be contained
100     *   by the proposed container.
101     *  @exception NameDuplicationException If the container already has an
102     *   actor with the specified name.
103     */
104    public ClassWrapper(CompositeEntity container, String name)
105            throws NameDuplicationException, IllegalActionException {
106        super(container, name);
107        className = new StringAttribute(this, "className");
108    }
109
110    ///////////////////////////////////////////////////////////////////
111    ////                     ports and parameters                  ////
112
113    /** The name of the Java class.
114     */
115    public StringAttribute className;
116
117    ///////////////////////////////////////////////////////////////////
118    ////                         public methods                    ////
119
120    /** Read at most one token from each input port. If an input port
121     *  has a token, the content of the token is used as argument(s)
122     *  for invoking (on the wrapped instance or class) the method of
123     *  the same name as the port. If the method has a return value,
124     *  the value is wrapped in a token, and is sent to the output port
125     *  named <i>methodName</i>Result.
126     *  @exception IllegalActionException If the method invocation fails.
127     */
128    @Override
129    public void fire() throws IllegalActionException {
130        super.fire();
131        Iterator inPorts = inputPortList().iterator();
132
133        while (inPorts.hasNext()) {
134            IOPort inPort = (IOPort) inPorts.next();
135
136            if (inPort.hasToken(0)) {
137                _invoke(inPort, inPort.get(0));
138            }
139        }
140    }
141
142    /** Get the Class object of the specified class. Gather method
143     *  invocation information corresponding to each input port. If
144     *  at least one method corresponding to a port is not static,
145     *  create an instance of the specified class.
146     *  @exception IllegalActionException If the specified class cannot
147     *   be loaded, or there is no method of the same name as an input
148     *   port, or an instance of the class cannot be created.
149     */
150    @Override
151    public void preinitialize() throws IllegalActionException {
152        super.preinitialize();
153        try {
154            _class = Class.forName(className.getExpression());
155        } catch (ClassNotFoundException ex) {
156            throw new IllegalActionException(this, ex, "Cannot find specified "
157                    + "class " + className.getExpression());
158        }
159
160        _methodTable = new Hashtable();
161
162        Method[] methods = _class.getMethods();
163        Iterator inPorts = inputPortList().iterator();
164        boolean needInstance = false;
165
166        while (inPorts.hasNext()) {
167            IOPort inPort = (IOPort) inPorts.next();
168            String portName = inPort.getName();
169            Method m = null;
170
171            for (int i = 0; i < methods.length; ++i) {
172                if (methods[i].getName().equals(portName)) {
173                    m = methods[i];
174                    break;
175                }
176            }
177
178            if (m == null) {
179                throw new IllegalActionException(this, "The specified class "
180                        + "does not have a method of the same name as input "
181                        + "port " + portName);
182            }
183
184            Object[] methodInfo = new Object[3];
185            methodInfo[0] = m;
186            methodInfo[1] = m.getParameterTypes();
187
188            IOPort outPort = (IOPort) getPort(portName + "Result");
189
190            if (outPort != null && outPort.isOutput()) {
191                methodInfo[2] = outPort;
192            } else {
193                methodInfo[2] = null;
194            }
195
196            _methodTable.put(inPort, methodInfo);
197
198            if (!Modifier.isStatic(m.getModifiers())) {
199                needInstance = true;
200            }
201        }
202
203        _instance = null;
204
205        if (needInstance) {
206            try {
207                // FIXME: here only try to use a constructor with no argument
208                Constructor constructor = _class.getConstructor(new Class[0]);
209                _instance = constructor.newInstance(new Object[0]);
210            } catch (Exception ex) {
211                throw new IllegalActionException(this, ex, "Cannot create an "
212                        + "instance of the specified class");
213            }
214        }
215    }
216
217    ///////////////////////////////////////////////////////////////////
218    ////                         private methods                   ////
219    // Invoke on the wrapped instance the method with the same name as the
220    // specified port, treating argv as the arguments.
221    // NOTE: Should this method return a boolean, false when the invocation
222    // fails? The actor may have an error output port, and send a token
223    // to this port when invocation fails.
224    private void _invoke(IOPort port, Token argv)
225            throws IllegalActionException {
226        // assert port.isInput()
227        Object[] methodInfo = (Object[]) _methodTable.get(port);
228
229        // when _methodTable is built, an entry for each input port is
230        // guaranteed
231        Method m = (Method) methodInfo[0];
232        Class[] argTypes = (Class[]) methodInfo[1];
233        int args = argTypes.length;
234        IOPort outPort = (IOPort) methodInfo[2];
235
236        // The following code is mostly copied from data.expr.ASTPtFunctionNode
237        Object[] argValues = new Object[args];
238
239        if (args > 0) {
240            RecordToken argRecord = null;
241
242            if (argv instanceof RecordToken) {
243                argRecord = (RecordToken) argv;
244            } else if (args > 1) {
245                throw new IllegalActionException(this, "cannot convert "
246                        + "input token to method call arguments.");
247            }
248
249            for (int i = 0; i < args; ++i) {
250                Token arg = null;
251
252                if (argRecord != null) {
253                    arg = argRecord.get("arg" + (i + 1));
254                } else {
255                    // this is the case when the method takes one argument
256                    // and the input token is not a record token
257                    arg = argv;
258                }
259
260                if (argTypes[i].isAssignableFrom(arg.getClass())) {
261                    argValues[i] = arg;
262                } else {
263                    argValues[i] = ConversionUtilities
264                            .convertTokenToJavaType(arg);
265                }
266            }
267        }
268
269        Object result = null;
270
271        try {
272            result = m.invoke(_instance, argValues);
273        } catch (InvocationTargetException ex) {
274            // get the exception produced by the invoked function
275            ex.getTargetException().printStackTrace();
276            throw new IllegalActionException(this, ex.getTargetException(),
277                    "Error invoking method " + m.getName());
278        } catch (Exception ex) {
279            throw new IllegalActionException(this, ex,
280                    "Error invoking method " + m.getName());
281        }
282
283        Token resultToken = null;
284
285        if (result == null) {
286            // the method does not return value
287            return;
288        } else if (result instanceof Token) {
289            resultToken = (Token) result;
290        } else if (result instanceof Double) {
291            resultToken = new DoubleToken(((Double) result).doubleValue());
292        } else if (result instanceof Integer) {
293            resultToken = new IntToken(((Integer) result).intValue());
294        } else if (result instanceof Long) {
295            resultToken = new LongToken(((Long) result).longValue());
296        } else if (result instanceof String) {
297            resultToken = new StringToken((String) result);
298        } else if (result instanceof Boolean) {
299            resultToken = new BooleanToken(((Boolean) result).booleanValue());
300        } else if (result instanceof Complex) {
301            resultToken = new ComplexToken((Complex) result);
302        } else if (result instanceof FixPoint) {
303            resultToken = new FixToken((FixPoint) result);
304        } else {
305            throw new IllegalActionException(this, "Result of method call "
306                    + port.getName() + " is not a supported type: boolean, "
307                    + "complex, fixpoint, double, int, long  and String, "
308                    + "or a Token.");
309        }
310
311        if (outPort != null) {
312            outPort.send(0, resultToken);
313        }
314    }
315
316    ///////////////////////////////////////////////////////////////////
317    ////                         private variables                 ////
318    // A hash table containing the method invocation information for the
319    // input ports. For each input port, the entry in the hash table is
320    // an array of three objects. The first is the Method object of the
321    // method to be invoked. The second is the array of argument types
322    // of the method. The third is the output port to which the return
323    // value of the method is sent.
324    private Hashtable _methodTable = null;
325
326    // The instance of the specified class.
327    private Object _instance = null;
328
329    // The Class object of the specified class.
330    private Class _class = null;
331}