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 }