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