001/*
002 * $Id: Interpreter.java,v 1.13 2011/03/24 16:06:33 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
007 * Space Science and Engineering Center (SSEC)
008 * University of Wisconsin - Madison
009 * 1225 W. Dayton Street, Madison, WI 53706, USA
010 * https://www.ssec.wisc.edu/mcidas
011 * 
012 * All Rights Reserved
013 * 
014 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015 * some McIDAS-V source code is based on IDV and VisAD source code.  
016 * 
017 * McIDAS-V is free software; you can redistribute it and/or modify
018 * it under the terms of the GNU Lesser Public License as published by
019 * the Free Software Foundation; either version 3 of the License, or
020 * (at your option) any later version.
021 * 
022 * McIDAS-V is distributed in the hope that it will be useful,
023 * but WITHOUT ANY WARRANTY; without even the implied warranty of
024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025 * GNU Lesser Public License for more details.
026 * 
027 * You should have received a copy of the GNU Lesser Public License
028 * along with this program.  If not, see http://www.gnu.org/licenses.
029 */
030
031package edu.wisc.ssec.mcidasv.jython;
032
033import java.io.ByteArrayOutputStream;
034
035import org.python.core.PyModule;
036import org.python.core.PyStringMap;
037import org.python.core.PySystemState;
038import org.python.core.imp;
039import org.python.util.InteractiveInterpreter;
040
041public class Interpreter extends InteractiveInterpreter {
042    /** Dummy filename for the interactive interpreter. */
043    private static final String CONSOLE_FILENAME = "<console>";
044
045    /** Stream used for error output. */
046    private ByteArrayOutputStream stderr;
047
048    /** Stream used for normal output. */
049    private ByteArrayOutputStream stdout;
050
051    /** Whether or not jython needs more input to run something. */
052    private boolean moreInput;
053
054    /** A hook that allows external classes to respond to events. */
055    private ConsoleCallback callback;
056
057    /** Whether or not Jython is working on something */
058    private boolean thinking;
059
060    /**
061     * Creates a Jython interpreter based upon the specified system state and
062     * whose output streams are mapped to the specified byte streams.
063     * 
064     * <p>Additionally, the {@literal "__main__"} module is imported by 
065     * default so that the locals namespace makes sense.
066     * 
067     * @param state The system state you want to use with the interpreter.
068     * @param stdout The stream Jython will use for standard output.
069     * @param stderr The stream Jython will use for error output.
070     */
071    public Interpreter(final PySystemState state, 
072        final ByteArrayOutputStream stdout, 
073        final ByteArrayOutputStream stderr) 
074    {
075        super(null, state);
076        this.stdout = stdout;
077        this.stderr = stderr;
078        this.callback = new DummyCallbackHandler();
079        this.moreInput = false;
080        this.thinking = false;
081
082        setOut(stdout);
083        setErr(stderr);
084
085        PyModule mod = imp.addModule("__main__");
086        PyStringMap locals = ((PyStringMap)mod.__dict__).copy();
087        setLocals(locals);
088    }
089
090    /**
091     * Registers a new callback handler with the interpreter. This mechanism
092     * allows external code to easily react to events taking place in the
093     * interpreter.
094     * 
095     * @param newCallback The new callback handler.
096     */
097    protected void setCallbackHandler(final ConsoleCallback newCallback) {
098        callback = newCallback;
099    }
100
101    /**
102     * Here's the magic! Basically just accumulates a buffer that gets passed
103     * off to jython-land until it can run.
104     * 
105     * @param line A Jython command.
106     * @return False if Jython did something. True if more input is needed.
107     */
108    public boolean push(Console console, final String line) {
109        if (buffer.length() > 0) {
110            buffer.append('\n');
111        }
112
113        thinking = true;
114        buffer.append(line);
115        moreInput = runsource(buffer.toString(), CONSOLE_FILENAME);
116        if (!moreInput) {
117            String bufferCopy = new String(buffer);
118            resetbuffer();
119            callback.ranBlock(bufferCopy);
120        }
121
122        thinking = false;
123        return moreInput;
124    }
125
126    /**
127     * Determines whether or not Jython is busy.
128     * 
129     * @return {@code true} if busy, {@code false} otherwise.
130     */
131    public boolean isBusy() {
132        return thinking;
133    }
134
135    /**
136     * 
137     * 
138     * @return Whether or not Jython needs more input to run something.
139     */
140    public boolean needMoreInput() {
141        return moreInput;
142    }
143
144    /**
145     * Sends the contents of {@link #stdout} and {@link #stderr} on their 
146     * merry way. Both streams are emptied as a result.
147     * 
148     * @param console Console where the command originated.
149     * @param command The command that was executed. Null values are permitted,
150     * as they signify that no command was entered for any generated output.
151     */
152    public void handleStreams(final Console console, final String command) {
153        String output = clearStream(command, stdout);
154        if (output.length() != 0) {
155            if (command != null) {
156                console.result(output);
157            } else {
158                console.generatedOutput(output);
159            }
160        }
161
162        String error = clearStream(command, stderr);
163        if (error.length() != 0) {
164            if (command != null) {
165                console.error(error);
166            } else {
167                console.generatedError(error);
168            }
169        }
170    }
171
172    /**
173     * Removes and returns all existing text from {@code stream}.
174     * 
175     * @param command Command that was executed. Null values are permitted and
176     * imply that no command is {@literal "associated"} with text in 
177     * {@code stream}.
178     * @param stream Stream to be cleared out.
179     * 
180     * @return The contents of {@code stream} before it was reset.
181     * @see #handleStreams(Console, String)
182     */
183    private static String clearStream(final String command, final ByteArrayOutputStream stream) {
184        String output = "";
185        if (command == null) {
186            output = stream.toString();
187        } else if (stream.size() > 1) {
188            String text = stream.toString();
189            int end = text.length() - ((command.length() == 0) ? 0 : 1);
190            output = text.substring(0, end);
191        }
192        stream.reset();
193        return output;
194    }
195
196    /**
197     * Sends error information to the specified console.
198     * 
199     * @param console The console that caused the exception.
200     * @param e The exception!
201     */
202    public void handleException(final Console console, final Throwable e) {
203        handleStreams(console, " ");
204        console.error(e.toString());
205        console.prompt();
206    }
207}