001    /*
002     * $Id: Interpreter.java,v 1.14 2012/02/19 17:35:47 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
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    
031    package edu.wisc.ssec.mcidasv.jython;
032    
033    import java.io.ByteArrayOutputStream;
034    
035    import org.python.core.PyModule;
036    import org.python.core.PyStringMap;
037    import org.python.core.PySystemState;
038    import org.python.core.imp;
039    import org.python.util.InteractiveInterpreter;
040    
041    public 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    }