001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2015
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
029package edu.wisc.ssec.mcidasv.jython;
030
031import java.io.File;
032import java.io.InputStream;
033import java.net.URL;
034import java.util.ArrayList;
035import java.util.List;
036
037import org.python.core.Py;
038import org.python.core.PyObject;
039import org.python.core.PyString;
040import org.python.core.PyStringMap;
041
042/**
043 * A {@code Command} is an action that can alter the state of an 
044 * {@link Interpreter}.
045 */
046public 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 */
098class 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//}
183class 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 */
229class 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.
264class 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<>(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
300class 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 */
321class 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}