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 }