001/*
002 * $Id: Command.java,v 1.19 2011/03/24 18:13:11 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
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
031package edu.wisc.ssec.mcidasv.jython;
032
033import java.io.File;
034import java.io.InputStream;
035import java.net.URL;
036import java.util.ArrayList;
037import java.util.List;
038
039import org.python.core.Py;
040import org.python.core.PyObject;
041import org.python.core.PyString;
042import org.python.core.PyStringMap;
043
044/**
045 * A {@code Command} is an action that can alter the state of an 
046 * {@link Interpreter}.
047 */
048public 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 */
100class 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//}
185class InjectCommand extends Command {
186    /** Name Jython will use to refer to {@link #pyObject}. */
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 pyObject}.
197     * @param pyObject 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 */
231class 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.
266class 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
302class 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 */
323class 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}