001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2015
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.ByteArrayOutputStream;
032
033import org.python.core.PyModule;
034import org.python.core.PyStringMap;
035import org.python.core.PySystemState;
036import org.python.core.imp;
037import org.python.util.InteractiveInterpreter;
038
039public 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.isEmpty()) {
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.isEmpty()) {
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.isEmpty() ? 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}