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.ByteArrayOutputStream;
032
033 import org.python.core.PyModule;
034 import org.python.core.PyStringMap;
035 import org.python.core.PySystemState;
036 import org.python.core.imp;
037 import org.python.util.InteractiveInterpreter;
038
039 public class Interpreter extends InteractiveInterpreter {
040 /** Dummy filename for the interactive interpreter. */
041 private static final String CONSOLE_FILENAME = "<console>";
042
043 /** Stream used for error output. */
044 private ByteArrayOutputStream stderr;
045
046 /** Stream used for normal output. */
047 private ByteArrayOutputStream stdout;
048
049 /** Whether or not jython needs more input to run something. */
050 private boolean moreInput;
051
052 /** A hook that allows external classes to respond to events. */
053 private ConsoleCallback callback;
054
055 /** Whether or not Jython is working on something */
056 private boolean thinking;
057
058 /**
059 * Creates a Jython interpreter based upon the specified system state and
060 * whose output streams are mapped to the specified byte streams.
061 *
062 * <p>Additionally, the {@literal "__main__"} module is imported by
063 * default so that the locals namespace makes sense.
064 *
065 * @param state The system state you want to use with the interpreter.
066 * @param stdout The stream Jython will use for standard output.
067 * @param stderr The stream Jython will use for error output.
068 */
069 public Interpreter(final PySystemState state,
070 final ByteArrayOutputStream stdout,
071 final ByteArrayOutputStream stderr)
072 {
073 super(null, state);
074 this.stdout = stdout;
075 this.stderr = stderr;
076 this.callback = new DummyCallbackHandler();
077 this.moreInput = false;
078 this.thinking = false;
079
080 setOut(stdout);
081 setErr(stderr);
082
083 PyModule mod = imp.addModule("__main__");
084 PyStringMap locals = ((PyStringMap)mod.__dict__).copy();
085 setLocals(locals);
086 }
087
088 /**
089 * Registers a new callback handler with the interpreter. This mechanism
090 * allows external code to easily react to events taking place in the
091 * interpreter.
092 *
093 * @param newCallback The new callback handler.
094 */
095 protected void setCallbackHandler(final ConsoleCallback newCallback) {
096 callback = newCallback;
097 }
098
099 /**
100 * Here's the magic! Basically just accumulates a buffer that gets passed
101 * off to jython-land until it can run.
102 *
103 * @param line A Jython command.
104 * @return False if Jython did something. True if more input is needed.
105 */
106 public boolean push(Console console, final String line) {
107 if (buffer.length() > 0) {
108 buffer.append('\n');
109 }
110
111 thinking = true;
112 buffer.append(line);
113 moreInput = runsource(buffer.toString(), CONSOLE_FILENAME);
114 if (!moreInput) {
115 String bufferCopy = new String(buffer);
116 resetbuffer();
117 callback.ranBlock(bufferCopy);
118 }
119
120 thinking = false;
121 return moreInput;
122 }
123
124 /**
125 * Determines whether or not Jython is busy.
126 *
127 * @return {@code true} if busy, {@code false} otherwise.
128 */
129 public boolean isBusy() {
130 return thinking;
131 }
132
133 /**
134 *
135 *
136 * @return Whether or not Jython needs more input to run something.
137 */
138 public boolean needMoreInput() {
139 return moreInput;
140 }
141
142 /**
143 * Sends the contents of {@link #stdout} and {@link #stderr} on their
144 * merry way. Both streams are emptied as a result.
145 *
146 * @param console Console where the command originated.
147 * @param command The command that was executed. Null values are permitted,
148 * as they signify that no command was entered for any generated output.
149 */
150 public void handleStreams(final Console console, final String command) {
151 String output = clearStream(command, stdout);
152 if (output.length() != 0) {
153 if (command != null) {
154 console.result(output);
155 } else {
156 console.generatedOutput(output);
157 }
158 }
159
160 String error = clearStream(command, stderr);
161 if (error.length() != 0) {
162 if (command != null) {
163 console.error(error);
164 } else {
165 console.generatedError(error);
166 }
167 }
168 }
169
170 /**
171 * Removes and returns all existing text from {@code stream}.
172 *
173 * @param command Command that was executed. Null values are permitted and
174 * imply that no command is {@literal "associated"} with text in
175 * {@code stream}.
176 * @param stream Stream to be cleared out.
177 *
178 * @return The contents of {@code stream} before it was reset.
179 * @see #handleStreams(Console, String)
180 */
181 private static String clearStream(final String command, final ByteArrayOutputStream stream) {
182 String output = "";
183 if (command == null) {
184 output = stream.toString();
185 } else if (stream.size() > 1) {
186 String text = stream.toString();
187 int end = text.length() - ((command.length() == 0) ? 0 : 1);
188 output = text.substring(0, end);
189 }
190 stream.reset();
191 return output;
192 }
193
194 /**
195 * Sends error information to the specified console.
196 *
197 * @param console The console that caused the exception.
198 * @param e The exception!
199 */
200 public void handleException(final Console console, final Throwable e) {
201 handleStreams(console, " ");
202 console.error(e.toString());
203 console.prompt();
204 }
205 }