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 }