001    /*
002     * $Id: Command.java,v 1.21 2012/02/19 17:35:46 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
007     * Space Science and Engineering Center (SSEC)
008     * University of Wisconsin - Madison
009     * 1225 W. Dayton Street, Madison, WI 53706, USA
010     * https://www.ssec.wisc.edu/mcidas
011     * 
012     * All Rights Reserved
013     * 
014     * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015     * some McIDAS-V source code is based on IDV and VisAD source code.  
016     * 
017     * McIDAS-V is free software; you can redistribute it and/or modify
018     * it under the terms of the GNU Lesser Public License as published by
019     * the Free Software Foundation; either version 3 of the License, or
020     * (at your option) any later version.
021     * 
022     * McIDAS-V is distributed in the hope that it will be useful,
023     * but WITHOUT ANY WARRANTY; without even the implied warranty of
024     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025     * GNU Lesser Public License for more details.
026     * 
027     * You should have received a copy of the GNU Lesser Public License
028     * along with this program.  If not, see http://www.gnu.org/licenses.
029     */
030    
031    package edu.wisc.ssec.mcidasv.jython;
032    
033    import java.io.File;
034    import java.io.InputStream;
035    import java.net.URL;
036    import java.util.ArrayList;
037    import java.util.List;
038    
039    import org.python.core.Py;
040    import org.python.core.PyObject;
041    import org.python.core.PyString;
042    import org.python.core.PyStringMap;
043    
044    /**
045     * A {@code Command} is an action that can alter the state of an 
046     * {@link Interpreter}.
047     */
048    public abstract class Command {
049        /** Console that created this command. */
050        protected Console console;
051    
052        /**
053         * Creates a command.
054         * 
055         * @param console Console that created this command.
056         */
057        public Command(final Console console) {
058            this.console = console;
059        }
060    
061        /**
062         * Hook to provide various implementations of command execution.
063         * 
064         * @param interpreter Jython interpreter that will execute the command.
065         * 
066         * @throws Exception An error was encountered executing the command. Jython
067         * will catch three standard Python exceptions: SyntaxError, ValueError, 
068         * and OverflowError. Other exceptions are thrown.
069         */
070        public abstract void execute(final Interpreter interpreter)
071            throws Exception;
072    
073        /**
074         * Creates a {@link InputStream} using {@code path}. It's here entirely for
075         * convenience.
076         * 
077         * @param path Path to the desired file.
078         * 
079         * @return {code InputStream} for {@code path}.
080         * 
081         * @throws Exception if there was badness.
082         */
083        protected InputStream getInputStream(final String path) throws Exception {
084            File f = new File(path);
085            if (f.exists()) {
086                return f.toURI().toURL().openStream();
087            }
088            URL url = getClass().getResource(path);
089            if (url != null) { 
090                return url.openStream();
091            }
092            return null;
093        }
094    }
095    
096    /**
097     * This class is a type of {@link Command} that represents a line of Jython. 
098     * These sorts of commands are only created by user input in a {@link Console}.
099     */
100    class LineCommand extends Command {
101        /** The line of jython that needs to be passed to the interpreter */
102        private String command;
103    
104        /**
105         * Creates a command based upon the contents of {@code command}.
106         * 
107         * @param console Console where the specified text came from.
108         * @param command Text that will be passed to an {@link Interpreter} for
109         * execution.
110         */
111        public LineCommand(final Console console, final String command) {
112            super(console);
113            this.command = command;
114        }
115    
116        /**
117         * Attempts to execute a line of Jython. Displays the appropriate prompt
118         * on {@link Command#console}, depending upon whether Jython requires more
119         * input.
120         * 
121         * @param interpreter Interpreter that will execute this command.
122         * 
123         * @throws Exception See {@link Command#execute(Interpreter)}.
124         */
125        public void execute(final Interpreter interpreter) throws Exception {
126            if (!interpreter.push(console, command)) {
127                interpreter.handleStreams(console, command);
128                console.prompt();
129            } else {
130                console.moreInput();
131            }
132        }
133    
134        @Override public String toString() {
135            return "[LineCommand@" + Integer.toHexString(hashCode()) +
136                ": command=" + command + "]";
137        }
138    }
139    
140    /**
141     * This class represents a {@link Command} that injects a standard Java 
142     * variable into the local namespace of an {@link Interpreter}. This is useful
143     * for allowing Jython to manipulate objects created by the IDV or McIDAS-V.
144     */
145    //class InjectCommand extends Command {
146    //    /** Name Jython will use to refer to {@link #pyObject}. */
147    //    private String name;
148    //
149    //    /** Wrapper around the Java object that is being injected. */
150    //    private PyObject pyObject;
151    //
152    //    /**
153    //     * Creates an injection command based upon the specified name and object.
154    //     * 
155    //     * @param console Likely not required in this context!
156    //     * @param name Name Jython will use to refer to {@code pyObject}.
157    //     * @param pyObject Wrapper around the Java object that is being injected.
158    //     */
159    //    public InjectCommand(final Console console, final String name, 
160    //        final PyObject pyObject) 
161    //    {
162    //        super(console);
163    //        this.name = name;
164    //        this.pyObject = pyObject;
165    //    }
166    //
167    //    /**
168    //     * Attempts to inject a variable created in Java into the local namespace 
169    //     * of {@code interpreter}.
170    //     * 
171    //     * @param interpreter Interpreter that will execute this command.
172    //     * 
173    //     * @throws Exception if {@link Interpreter#set(String, PyObject)} had 
174    //     * problems.
175    //     */
176    //    public void execute(final Interpreter interpreter) throws Exception {
177    //        interpreter.set(name, pyObject);
178    //    }
179    //
180    //    @Override public String toString() {
181    //        return "[InjectCommand@" + Integer.toHexString(hashCode()) + 
182    //            ": name=" + name + ", pyObject=" + pyObject + "]";
183    //    }
184    //}
185    class InjectCommand extends Command {
186        /** Name Jython will use to refer to {@link #object}. */
187        private String name;
188    
189        /** Wrapper around the Java object that is being injected. */
190        private Object object;
191    
192        /**
193         * Creates an injection command based upon the specified name and object.
194         * 
195         * @param console Likely not required in this context!
196         * @param name Name Jython will use to refer to {@code object}.
197         * @param object Wrapper around the Java object that is being injected.
198         */
199        public InjectCommand(final Console console, final String name, 
200            final Object object) 
201        {
202            super(console);
203            this.name = name;
204            this.object = object;
205        }
206    
207        /**
208         * Attempts to inject a variable created in Java into the local namespace 
209         * of {@code interpreter}.
210         * 
211         * @param interpreter Interpreter that will execute this command.
212         * 
213         * @throws Exception if {@link Interpreter#set(String, PyObject)} had 
214         * problems.
215         */
216        public void execute(final Interpreter interpreter) throws Exception {
217            interpreter.set(name, object);
218        }
219    
220        @Override public String toString() {
221            return "[InjectCommand@" + Integer.toHexString(hashCode()) + 
222                ": name=" + name + ", object=" + object + "]";
223        }
224    }
225    
226    /**
227     * This class represents a {@link Command} that removes an object from the 
228     * local namespace of an {@link Interpreter}. These commands can remove any 
229     * Jython objects, while {@link InjectCommand} may only inject Java objects.
230     */
231    class EjectCommand extends Command {
232        /** Name of the Jython object to remove. */
233        private String name;
234    
235        /**
236         * Creates an ejection command for {@code name}.
237         * 
238         * @param console Console that requested {@code name}'s removal.
239         * @param name Name of the Jython object that needs removin'.
240         */
241        public EjectCommand(final Console console, final String name) {
242            super(console);
243            this.name = name;
244        }
245    
246        /**
247         * Attempts to remove whatever Jython knows as {@code name} from the local
248         * namespace of {@code interpreter}.
249         * 
250         * @param interpreter Interpreter whose local namespace is required.
251         * 
252         * @throws Exception if {@link PyObject#__delitem__(PyObject)} had some
253         * second thoughts about ejection.
254         */
255        public void execute(final Interpreter interpreter) throws Exception {
256            interpreter.getLocals().__delitem__(name);
257        }
258    
259        @Override public String toString() {
260            return String.format("[EjectCommand@%x: name=%s]", hashCode(), name);
261        }
262    }
263    
264    // TODO(jon): when documenting this, make sure to note that the commands appear
265    // in the console as "normal" user input.
266    class BatchCommand extends Command {
267        private final String bufferSource;
268        private final List<String> commandBuffer;
269    
270        public BatchCommand(final Console console, final String bufferSource,
271            final List<String> buffer) 
272        {
273            super(console);
274            this.bufferSource = bufferSource;
275            this.commandBuffer = new ArrayList<String>(buffer);
276        }
277    
278        public void execute(final Interpreter interpreter) throws Exception {
279            PyStringMap locals = (PyStringMap)interpreter.getLocals();
280            PyObject currentName = locals.__getitem__(new PyString("__name__"));
281            locals.__setitem__("__name__", new PyString("__main__"));
282    
283            for (String command : commandBuffer) {
284                console.insert(Console.TXT_NORMAL, command);
285                if (!interpreter.push(console, command)) {
286                    interpreter.handleStreams(console, command);
287                    console.prompt();
288                } else {
289                    console.moreInput();
290                }
291            }
292            locals.__setitem__("__name__", currentName);
293            commandBuffer.clear();
294        }
295    
296        @Override public String toString() {
297            return String.format("[BatchCommand@%x: bufferSource=%s, commandBuffer=%s]",
298                hashCode(), bufferSource, commandBuffer);
299        }
300    }
301    
302    class RegisterCallbackCommand extends Command {
303        private final ConsoleCallback callback;
304        public RegisterCallbackCommand(final Console console, final ConsoleCallback callback) {
305            super(console);
306            this.callback = callback;
307        }
308    
309        public void execute(final Interpreter interpreter) throws Exception {
310            if (interpreter == null) {
311                throw new NullPointerException("Interpreter is null!");
312            }
313            interpreter.setCallbackHandler(callback);
314        }
315    }
316    
317    /**
318     * This class is a type of {@link Command} that represents a request to use
319     * Jython to run a file containing Jython statements. This is conceptually a 
320     * bit similar to importing a module, but the loading is done behind the scenes
321     * and you may specify whatever namespace you like (be careful!).
322     */
323    class LoadFileCommand extends Command {
324        /** Namespace to use when executing {@link #path}. */
325        private String name;
326    
327        /** Path to the Jython file awaiting execution. */
328        private String path;
329    
330        /**
331         * Creates a command that will attempt to execute a Jython file in the 
332         * namespace given by {@code name}.
333         * 
334         * @param console Originating console.
335         * @param name Namespace to use when executing {@code path}.
336         * @param path Path to a Jython file.
337         */
338        public LoadFileCommand(final Console console, final String name, 
339            final String path) 
340        {
341            super(console);
342            this.name = name;
343            this.path = path;
344        }
345    
346        /**
347         * Tries to load the file specified by {@code path} using {@code moduleName}
348         * for the {@code __name__} attribute. Note that this command does not
349         * currently display any results in the originating {@link Console}.
350         * 
351         * <p>If {@code moduleName} is not {@code __main__}, this command is 
352         * basically the same thing as doing {@code from moduleName import *}.
353         * 
354         * <p>If {@code moduleName} <b>is</b> {@code __main__}, then this command
355         * will work for {@code if __name__ == '__main__'} and will run main 
356         * functions as expected.
357         * 
358         * @param interpreter Interpreter to use to load the specified file.
359         * 
360         * @throws Exception if Jython has a problem with running {@code path}.
361         */
362        public void execute(final Interpreter interpreter) throws Exception {
363            InputStream stream = getInputStream(path);
364            if (stream == null) {
365                return;
366            }
367            PyStringMap locals = (PyStringMap)interpreter.getLocals();
368            PyObject currentName = locals.__getitem__(new PyString("__name__"));
369            locals.__setitem__("__name__", new PyString(name));
370            interpreter.execfile(stream, path);
371            locals.__setitem__("__name__", currentName);
372    
373            Py.getSystemState().stdout.invoke("flush");
374            Py.getSystemState().stderr.invoke("flush");
375    //        interpreter.handleStreams(console, " ");
376    //        console.prompt();
377        }
378    
379        @Override public String toString() {
380            return "[LoadFileCommand@" + Integer.toHexString(hashCode()) + 
381                ": path=" + path + "]";
382        }
383    }