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    }