001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2016 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 048 /** Console that created this command. */ 049 protected Console console; 050 051 /** 052 * Creates a command. 053 * 054 * @param console Console that created this command. 055 */ 056 public Command(final Console console) { 057 this.console = console; 058 } 059 060 /** 061 * Hook to provide various implementations of command execution. 062 * 063 * @param interpreter Jython interpreter that will execute the command. 064 * 065 * @throws Exception An error was encountered executing the command. Jython 066 * will catch three standard Python exceptions: SyntaxError, ValueError, 067 * and OverflowError. Other exceptions are thrown. 068 */ 069 public abstract void execute(final Interpreter interpreter) 070 throws Exception; 071 072 /** 073 * Creates a {@link InputStream} using {@code path}. It's here entirely for 074 * convenience. 075 * 076 * @param path Path to the desired file. 077 * 078 * @return {code InputStream} for {@code path}. 079 * 080 * @throws Exception if there was badness. 081 */ 082 protected InputStream getInputStream(final String path) throws Exception { 083 File f = new File(path); 084 if (f.exists()) { 085 return f.toURI().toURL().openStream(); 086 } 087 URL url = getClass().getResource(path); 088 if (url != null) { 089 return url.openStream(); 090 } 091 return null; 092 } 093} 094 095/** 096 * This class is a type of {@link Command} that represents a line of Jython. 097 * These sorts of commands are only created by user input in a {@link Console}. 098 */ 099class LineCommand extends Command { 100 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 187 /** Name Jython will use to refer to {@link #object}. */ 188 private String name; 189 190 /** Wrapper around the Java object that is being injected. */ 191 private Object object; 192 193 /** 194 * Creates an injection command based upon the specified name and object. 195 * 196 * @param console Likely not required in this context! 197 * @param name Name Jython will use to refer to {@code object}. 198 * @param object Wrapper around the Java object that is being injected. 199 */ 200 public InjectCommand(final Console console, final String name, 201 final Object object) 202 { 203 super(console); 204 this.name = name; 205 this.object = object; 206 } 207 208 /** 209 * Attempts to inject a variable created in Java into the local namespace 210 * of {@code interpreter}. 211 * 212 * @param interpreter Interpreter that will execute this command. 213 * 214 * @throws Exception if {@link Interpreter#set(String, PyObject)} had 215 * problems. 216 */ 217 public void execute(final Interpreter interpreter) throws Exception { 218 interpreter.set(name, object); 219 } 220 221 @Override public String toString() { 222 return "[InjectCommand@" + Integer.toHexString(hashCode()) + 223 ": name=" + name + ", object=" + object + "]"; 224 } 225} 226 227/** 228 * This class represents a {@link Command} that removes an object from the 229 * local namespace of an {@link Interpreter}. These commands can remove any 230 * Jython objects, while {@link InjectCommand} may only inject Java objects. 231 */ 232class EjectCommand extends Command { 233 234 /** Name of the Jython object to remove. */ 235 private String name; 236 237 /** 238 * Creates an ejection command for {@code name}. 239 * 240 * @param console Console that requested {@code name}'s removal. 241 * @param name Name of the Jython object that needs removin'. 242 */ 243 public EjectCommand(final Console console, final String name) { 244 super(console); 245 this.name = name; 246 } 247 248 /** 249 * Attempts to remove whatever Jython knows as {@code name} from the local 250 * namespace of {@code interpreter}. 251 * 252 * @param interpreter Interpreter whose local namespace is required. 253 * 254 * @throws Exception if {@link PyObject#__delitem__(PyObject)} had some 255 * second thoughts about ejection. 256 */ 257 public void execute(final Interpreter interpreter) throws Exception { 258 interpreter.getLocals().__delitem__(name); 259 } 260 261 @Override public String toString() { 262 return String.format("[EjectCommand@%x: name=%s]", hashCode(), name); 263 } 264} 265 266// TODO(jon): when documenting this, make sure to note that the commands appear 267// in the console as "normal" user input. 268class BatchCommand extends Command { 269 270 private final String bufferSource; 271 272 private final List<String> commandBuffer; 273 274 public BatchCommand(final Console console, final String bufferSource, 275 final List<String> buffer) 276 { 277 super(console); 278 this.bufferSource = bufferSource; 279 this.commandBuffer = new ArrayList<>(buffer); 280 } 281 282 public void execute(final Interpreter interpreter) throws Exception { 283 PyStringMap locals = (PyStringMap)interpreter.getLocals(); 284 PyObject currentName = locals.__getitem__(new PyString("__name__")); 285 locals.__setitem__("__name__", new PyString("__main__")); 286 287 for (String command : commandBuffer) { 288 console.insert(Console.TXT_NORMAL, command); 289 if (!interpreter.push(console, command)) { 290 interpreter.handleStreams(console, command); 291 console.prompt(); 292 } else { 293 console.moreInput(); 294 } 295 } 296 locals.__setitem__("__name__", currentName); 297 commandBuffer.clear(); 298 } 299 300 @Override public String toString() { 301 return String.format("[BatchCommand@%x: bufferSource=%s, commandBuffer=%s]", 302 hashCode(), bufferSource, commandBuffer); 303 } 304} 305 306class RegisterCallbackCommand extends Command { 307 308 private final ConsoleCallback callback; 309 310 public RegisterCallbackCommand(final Console console, final ConsoleCallback callback) { 311 super(console); 312 this.callback = callback; 313 } 314 315 public void execute(final Interpreter interpreter) throws Exception { 316 if (interpreter == null) { 317 throw new NullPointerException("Interpreter is null!"); 318 } 319 interpreter.setCallbackHandler(callback); 320 } 321} 322 323/** 324 * This class is a type of {@link Command} that represents a request to use 325 * Jython to run a file containing Jython statements. This is conceptually a 326 * bit similar to importing a module, but the loading is done behind the scenes 327 * and you may specify whatever namespace you like (be careful!). 328 */ 329class LoadFileCommand extends Command { 330 331 /** Namespace to use when executing {@link #path}. */ 332 private String name; 333 334 /** Path to the Jython file awaiting execution. */ 335 private String path; 336 337 /** 338 * Creates a command that will attempt to execute a Jython file in the 339 * namespace given by {@code name}. 340 * 341 * @param console Originating console. 342 * @param name Namespace to use when executing {@code path}. 343 * @param path Path to a Jython file. 344 */ 345 public LoadFileCommand(final Console console, final String name, 346 final String path) 347 { 348 super(console); 349 this.name = name; 350 this.path = path; 351 } 352 353 /** 354 * Tries to load the file specified by {@code path} using {@code moduleName} 355 * for the {@code __name__} attribute. Note that this command does not 356 * currently display any results in the originating {@link Console}. 357 * 358 * <p>If {@code moduleName} is not {@code __main__}, this command is 359 * basically the same thing as doing {@code from moduleName import *}. 360 * 361 * <p>If {@code moduleName} <b>is</b> {@code __main__}, then this command 362 * will work for {@code if __name__ == '__main__'} and will run main 363 * functions as expected. 364 * 365 * @param interpreter Interpreter to use to load the specified file. 366 * 367 * @throws Exception if Jython has a problem with running {@code path}. 368 */ 369 public void execute(final Interpreter interpreter) throws Exception { 370 InputStream stream = getInputStream(path); 371 if (stream == null) { 372 return; 373 } 374 PyStringMap locals = (PyStringMap)interpreter.getLocals(); 375 PyObject currentName = locals.__getitem__(new PyString("__name__")); 376 locals.__setitem__("__name__", new PyString(name)); 377 interpreter.execfile(stream, path); 378 locals.__setitem__("__name__", currentName); 379 380 Py.getSystemState().stdout.invoke("flush"); 381 Py.getSystemState().stderr.invoke("flush"); 382// interpreter.handleStreams(console, " "); 383// console.prompt(); 384 } 385 386 @Override public String toString() { 387 return "[LoadFileCommand@" + Integer.toHexString(hashCode()) + 388 ": path=" + path + "]"; 389 } 390}