| /* | |
| * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. Oracle designates this | |
| * particular file as subject to the "Classpath" exception as provided | |
| * by Oracle in the LICENSE file that accompanied this code. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| package jdk.jshell; | |
| import jdk.jshell.spi.ExecutionControl; | |
| import java.io.ByteArrayInputStream; | |
| import java.io.InputStream; | |
| import java.io.InterruptedIOException; | |
| import java.io.PrintStream; | |
| import java.net.InetAddress; | |
| import java.text.MessageFormat; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.MissingResourceException; | |
| import java.util.Objects; | |
| import java.util.Optional; | |
| import java.util.ResourceBundle; | |
| import java.util.function.BiFunction; | |
| import java.util.function.Consumer; | |
| import java.util.function.Function; | |
| import java.util.function.Supplier; | |
| import java.util.stream.Stream; | |
| import javax.tools.StandardJavaFileManager; | |
| import jdk.internal.jshell.debug.InternalDebugControl; | |
| import jdk.jshell.Snippet.Status; | |
| import jdk.jshell.spi.ExecutionControl.EngineTerminationException; | |
| import jdk.jshell.spi.ExecutionControl.ExecutionControlException; | |
| import jdk.jshell.spi.ExecutionControlProvider; | |
| import jdk.jshell.spi.ExecutionEnv; | |
| import static jdk.jshell.Util.expunge; | |
| /** | |
| * The JShell evaluation state engine. This is the central class in the JShell | |
| * API. A {@code JShell} instance holds the evolving compilation and | |
| * execution state. The state is changed with the instance methods | |
| * {@link jdk.jshell.JShell#eval(java.lang.String) eval(String)}, | |
| * {@link jdk.jshell.JShell#drop(jdk.jshell.Snippet) drop(Snippet)} and | |
| * {@link jdk.jshell.JShell#addToClasspath(java.lang.String) addToClasspath(String)}. | |
| * The majority of methods query the state. | |
| * A {@code JShell} instance also allows registering for events with | |
| * {@link jdk.jshell.JShell#onSnippetEvent(java.util.function.Consumer) onSnippetEvent(Consumer)} | |
| * and {@link jdk.jshell.JShell#onShutdown(java.util.function.Consumer) onShutdown(Consumer)}, which | |
| * are unregistered with | |
| * {@link jdk.jshell.JShell#unsubscribe(jdk.jshell.JShell.Subscription) unsubscribe(Subscription)}. | |
| * Access to the source analysis utilities is via | |
| * {@link jdk.jshell.JShell#sourceCodeAnalysis()}. | |
| * When complete the instance should be closed to free resources -- | |
| * {@link jdk.jshell.JShell#close()}. | |
| * <p> | |
| * An instance of {@code JShell} is created with | |
| * {@code JShell.create()}. | |
| * <p> | |
| * This class is not thread safe, except as noted, all access should be through | |
| * a single thread. | |
| * | |
| * @author Robert Field | |
| * @since 9 | |
| */ | |
| public class JShell implements AutoCloseable { | |
| final SnippetMaps maps; | |
| final KeyMap keyMap; | |
| final OuterWrapMap outerMap; | |
| final TaskFactory taskFactory; | |
| final InputStream in; | |
| final PrintStream out; | |
| final PrintStream err; | |
| final Optional<JShellConsole> console; | |
| final Supplier<String> tempVariableNameGenerator; | |
| final BiFunction<Snippet, Integer, String> idGenerator; | |
| final List<String> extraRemoteVMOptions; | |
| final List<String> extraCompilerOptions; | |
| final Function<StandardJavaFileManager, StandardJavaFileManager> fileManagerMapping; | |
| private int nextKeyIndex = 1; | |
| final Eval eval; | |
| final ClassTracker classTracker; | |
| private final Map<Subscription, Consumer<JShell>> shutdownListeners = new HashMap<>(); | |
| private final Map<Subscription, Consumer<SnippetEvent>> keyStatusListeners = new HashMap<>(); | |
| private boolean closed = false; | |
| private final ExecutionControl executionControl; | |
| private SourceCodeAnalysisImpl sourceCodeAnalysis = null; | |
| private static final String L10N_RB_NAME = "jdk.jshell.resources.l10n"; | |
| private static ResourceBundle outputRB = null; | |
| JShell(Builder b) throws IllegalStateException { | |
| this.in = b.in; | |
| this.out = b.out; | |
| this.err = b.err; | |
| this.console = Optional.ofNullable(b.console); | |
| this.tempVariableNameGenerator = b.tempVariableNameGenerator; | |
| this.idGenerator = b.idGenerator; | |
| this.extraRemoteVMOptions = b.extraRemoteVMOptions; | |
| this.extraCompilerOptions = b.extraCompilerOptions; | |
| this.fileManagerMapping = b.fileManagerMapping; | |
| try { | |
| if (b.executionControlProvider != null) { | |
| executionControl = b.executionControlProvider.generate(new ExecutionEnvImpl(), | |
| b.executionControlParameters == null | |
| ? b.executionControlProvider.defaultParameters() | |
| : b.executionControlParameters); | |
| } else { | |
| String loopback = InetAddress.getLoopbackAddress().getHostAddress(); | |
| String spec = b.executionControlSpec == null | |
| ? "failover:0(jdi:hostname(" + loopback + "))," | |
| + "1(jdi:launch(true)), 2(jdi)" | |
| : b.executionControlSpec; | |
| executionControl = ExecutionControl.generate(new ExecutionEnvImpl(), spec); | |
| } | |
| } catch (Throwable ex) { | |
| throw new IllegalStateException("Launching JShell execution engine threw: " + ex.getMessage(), ex); | |
| } | |
| this.maps = new SnippetMaps(this); | |
| this.keyMap = new KeyMap(this); | |
| this.outerMap = new OuterWrapMap(this); | |
| this.taskFactory = new TaskFactory(this); | |
| this.eval = new Eval(this); | |
| this.classTracker = new ClassTracker(); | |
| } | |
| /** | |
| * Builder for {@code JShell} instances. | |
| * Create custom instances of {@code JShell} by using the setter | |
| * methods on this class. After zero or more of these, use the | |
| * {@link #build()} method to create a {@code JShell} instance. | |
| * These can all be chained. For example, setting the remote output and | |
| * error streams: | |
| * <pre> | |
| * {@code | |
| * JShell myShell = | |
| * JShell.builder() | |
| * .out(myOutStream) | |
| * .err(myErrStream) | |
| * .build(); } </pre> | |
| * If no special set-up is needed, just use | |
| * {@code JShell.builder().build()} or the short-cut equivalent | |
| * {@code JShell.create()}. | |
| */ | |
| public static class Builder { | |
| InputStream in = new ByteArrayInputStream(new byte[0]); | |
| PrintStream out = System.out; | |
| PrintStream err = System.err; | |
| JShellConsole console = null; | |
| Supplier<String> tempVariableNameGenerator = null; | |
| BiFunction<Snippet, Integer, String> idGenerator = null; | |
| List<String> extraRemoteVMOptions = new ArrayList<>(); | |
| List<String> extraCompilerOptions = new ArrayList<>(); | |
| ExecutionControlProvider executionControlProvider; | |
| Map<String,String> executionControlParameters; | |
| String executionControlSpec; | |
| Function<StandardJavaFileManager, StandardJavaFileManager> fileManagerMapping; | |
| Builder() { } | |
| /** | |
| * Sets the input for the running evaluation (it's {@code System.in}). Note: | |
| * applications that use {@code System.in} for snippet or other | |
| * user input cannot use {@code System.in} as the input stream for | |
| * the remote process. | |
| * <p> | |
| * The {@code read} method of the {@code InputStream} may throw the {@link InterruptedIOException} | |
| * to signal the user canceled the input. The currently running snippet will be automatically | |
| * {@link JShell#stop() stopped}. | |
| * <p> | |
| * The default, if this is not set, is to provide an empty input stream | |
| * -- {@code new ByteArrayInputStream(new byte[0])}. | |
| * | |
| * @param in the {@code InputStream} to be channelled to | |
| * {@code System.in} in the remote execution process | |
| * @return the {@code Builder} instance (for use in chained | |
| * initialization) | |
| */ | |
| public Builder in(InputStream in) { | |
| this.in = in; | |
| return this; | |
| } | |
| /** | |
| * Sets the output for the running evaluation (it's {@code System.out}). | |
| * The controlling process and | |
| * the remote process can share {@code System.out}. | |
| * <p> | |
| * The default, if this is not set, is {@code System.out}. | |
| * | |
| * @param out the {@code PrintStream} to be channelled to | |
| * {@code System.out} in the remote execution process | |
| * @return the {@code Builder} instance (for use in chained | |
| * initialization) | |
| */ | |
| public Builder out(PrintStream out) { | |
| this.out = out; | |
| return this; | |
| } | |
| /** | |
| * Sets the error output for the running evaluation (it's | |
| * {@code System.err}). The controlling process and the remote | |
| * process can share {@code System.err}. | |
| * <p> | |
| * The default, if this is not set, is {@code System.err}. | |
| * | |
| * @param err the {@code PrintStream} to be channelled to | |
| * {@code System.err} in the remote execution process | |
| * @return the {@code Builder} instance (for use in chained | |
| * initialization) | |
| */ | |
| public Builder err(PrintStream err) { | |
| this.err = err; | |
| return this; | |
| } | |
| /** | |
| * Sets the console for the running evalution. | |
| * <p> | |
| * The default, if this is not set, is no console ({@code System.console()} | |
| * will return {@code null} while running a snippet). | |
| * | |
| * @param console console to use while a snippet is run | |
| * @return the {@code Builder} instance (for use in chained | |
| * initialization) | |
| * @since 21 | |
| */ | |
| public Builder console(JShellConsole console) { | |
| this.console = console; | |
| return this; | |
| } | |
| /** | |
| * Sets a generator of temp variable names for | |
| * {@link jdk.jshell.VarSnippet} of | |
| * {@link jdk.jshell.Snippet.SubKind#TEMP_VAR_EXPRESSION_SUBKIND}. | |
| * <p> | |
| * Do not use this method unless you have explicit need for it. | |
| * <p> | |
| * The generator will be used for newly created VarSnippet | |
| * instances. The name of a variable is queried with | |
| * {@link jdk.jshell.VarSnippet#name()}. | |
| * <p> | |
| * The callback is sent during the processing of the snippet, the | |
| * JShell state is not stable. No calls whatsoever on the | |
| * {@code JShell} instance may be made from the callback. | |
| * <p> | |
| * The generated name must be unique within active snippets. | |
| * <p> | |
| * The default behavior (if this is not set or {@code generator} | |
| * is null) is to generate the name as a sequential number with a | |
| * prefixing dollar sign ("$"). | |
| * | |
| * @param generator the {@code Supplier} to generate the temporary | |
| * variable name string or {@code null} | |
| * @return the {@code Builder} instance (for use in chained | |
| * initialization) | |
| */ | |
| public Builder tempVariableNameGenerator(Supplier<String> generator) { | |
| this.tempVariableNameGenerator = generator; | |
| return this; | |
| } | |
| /** | |
| * Sets the generator of identifying names for Snippets. | |
| * <p> | |
| * Do not use this method unless you have explicit need for it. | |
| * <p> | |
| * The generator will be used for newly created Snippet instances. The | |
| * identifying name (id) is accessed with | |
| * {@link jdk.jshell.Snippet#id()} and can be seen in the | |
| * {@code StackTraceElement.getFileName()} for a | |
| * {@link jdk.jshell.EvalException} and | |
| * {@link jdk.jshell.UnresolvedReferenceException}. | |
| * <p> | |
| * The inputs to the generator are the {@link jdk.jshell.Snippet} and an | |
| * integer. The integer will be the same for two Snippets which would | |
| * overwrite one-another, but otherwise is unique. | |
| * <p> | |
| * The callback is sent during the processing of the snippet and the | |
| * Snippet and the state as a whole are not stable. No calls to change | |
| * system state (including Snippet state) should be made. Queries of | |
| * Snippet may be made except to {@link jdk.jshell.Snippet#id()}. No | |
| * calls on the {@code JShell} instance may be made from the | |
| * callback, except to | |
| * {@link #status(jdk.jshell.Snippet) status(Snippet)}. | |
| * <p> | |
| * The default behavior (if this is not set or {@code generator} | |
| * is null) is to generate the id as the integer converted to a string. | |
| * | |
| * @param generator the {@code BiFunction} to generate the id | |
| * string or {@code null} | |
| * @return the {@code Builder} instance (for use in chained | |
| * initialization) | |
| */ | |
| public Builder idGenerator(BiFunction<Snippet, Integer, String> generator) { | |
| this.idGenerator = generator; | |
| return this; | |
| } | |
| /** | |
| * Sets additional VM options for launching the VM. | |
| * | |
| * @param options The options for the remote VM | |
| * @return the {@code Builder} instance (for use in chained | |
| * initialization) | |
| */ | |
| public Builder remoteVMOptions(String... options) { | |
| this.extraRemoteVMOptions.addAll(Arrays.asList(options)); | |
| return this; | |
| } | |
| /** | |
| * Adds compiler options. These additional options will be used on | |
| * parsing, analysis, and code generation calls to the compiler. | |
| * Options which interfere with results are not supported and have | |
| * undefined effects on JShell's operation. | |
| * | |
| * @param options the addition options for compiler invocations | |
| * @return the {@code Builder} instance (for use in chained | |
| * initialization) | |
| */ | |
| public Builder compilerOptions(String... options) { | |
| this.extraCompilerOptions.addAll(Arrays.asList(options)); | |
| return this; | |
| } | |
| /** | |
| * Sets the custom engine for execution. Snippet execution will be | |
| * provided by the {@link ExecutionControl} instance selected by the | |
| * specified execution control spec. | |
| * Use, at most, one of these overloaded {@code executionEngine} builder | |
| * methods. | |
| * | |
| * @param executionControlSpec the execution control spec, | |
| * which is documented in the {@link jdk.jshell.spi} | |
| * package documentation. | |
| * @return the {@code Builder} instance (for use in chained | |
| * initialization) | |
| */ | |
| public Builder executionEngine(String executionControlSpec) { | |
| this.executionControlSpec = executionControlSpec; | |
| return this; | |
| } | |
| /** | |
| * Sets the custom engine for execution. Snippet execution will be | |
| * provided by the specified {@link ExecutionControl} instance. | |
| * Use, at most, one of these overloaded {@code executionEngine} builder | |
| * methods. | |
| * | |
| * @param executionControlProvider the provider to supply the execution | |
| * engine | |
| * @param executionControlParameters the parameters to the provider, or | |
| * {@code null} for default parameters | |
| * @return the {@code Builder} instance (for use in chained | |
| * initialization) | |
| */ | |
| public Builder executionEngine(ExecutionControlProvider executionControlProvider, | |
| Map<String,String> executionControlParameters) { | |
| this.executionControlProvider = executionControlProvider; | |
| this.executionControlParameters = executionControlParameters; | |
| return this; | |
| } | |
| /** | |
| * Configure the {@code FileManager} to be used by compilation and | |
| * source analysis. | |
| * If not set or passed null, the compiler's standard file manager will | |
| * be used (identity mapping). | |
| * For use in special applications where the compiler's normal file | |
| * handling needs to be overridden. See the file manager APIs for more | |
| * information. | |
| * The file manager input enables forwarding file managers, if this | |
| * is not needed, the incoming file manager can be ignored (constant | |
| * function). | |
| * | |
| * @param mapping a function that given the compiler's standard file | |
| * manager, returns a file manager to use | |
| * @return the {@code Builder} instance (for use in chained | |
| * initialization) | |
| */ | |
| public Builder fileManager(Function<StandardJavaFileManager, StandardJavaFileManager> mapping) { | |
| this.fileManagerMapping = mapping; | |
| return this; | |
| } | |
| /** | |
| * Builds a JShell state engine. This is the entry-point to all JShell | |
| * functionality. This creates a remote process for execution. It is | |
| * thus important to close the returned instance. | |
| * | |
| * @throws IllegalStateException if the {@code JShell} instance could not be created. | |
| * @return the state engine | |
| */ | |
| public JShell build() throws IllegalStateException { | |
| return new JShell(this); | |
| } | |
| } | |
| // --- public API --- | |
| /** | |
| * Create a new JShell state engine. | |
| * That is, create an instance of {@code JShell}. | |
| * <p> | |
| * Equivalent to {@link JShell#builder() JShell.builder()}{@link JShell.Builder#build() .build()}. | |
| * @throws IllegalStateException if the {@code JShell} instance could not be created. | |
| * @return an instance of {@code JShell}. | |
| */ | |
| public static JShell create() throws IllegalStateException { | |
| return builder().build(); | |
| } | |
| /** | |
| * Factory method for {@code JShell.Builder} which, in-turn, is used | |
| * for creating instances of {@code JShell}. | |
| * Create a default instance of {@code JShell} with | |
| * {@code JShell.builder().build()}. For more construction options | |
| * see {@link jdk.jshell.JShell.Builder}. | |
| * @return an instance of {@code Builder}. | |
| * @see jdk.jshell.JShell.Builder | |
| */ | |
| public static Builder builder() { | |
| return new Builder(); | |
| } | |
| /** | |
| * Access to source code analysis functionality. | |
| * An instance of {@code JShell} will always return the same | |
| * {@code SourceCodeAnalysis} instance from | |
| * {@code sourceCodeAnalysis()}. | |
| * @return an instance of {@link SourceCodeAnalysis SourceCodeAnalysis} | |
| * which can be used for source analysis such as completion detection and | |
| * completion suggestions. | |
| */ | |
| public SourceCodeAnalysis sourceCodeAnalysis() { | |
| if (sourceCodeAnalysis == null) { | |
| sourceCodeAnalysis = new SourceCodeAnalysisImpl(this); | |
| } | |
| return sourceCodeAnalysis; | |
| } | |
| /** | |
| * Evaluate the input String, including definition and/or execution, if | |
| * applicable. The input is checked for errors, unless the errors can be | |
| * deferred (as is the case with some unresolvedDependencies references), | |
| * errors will abort evaluation. | |
| * <p> | |
| * The input should be | |
| * exactly one complete snippet of source code, that is, one expression, | |
| * statement, variable declaration, method declaration, class declaration, | |
| * or import. | |
| * To break arbitrary input into individual complete snippets, use | |
| * {@link SourceCodeAnalysis#analyzeCompletion(String)}. | |
| * <p> | |
| * For imports, the import is added. Classes, interfaces. methods, | |
| * and variables are defined. The initializer of variables, statements, | |
| * and expressions are executed. | |
| * The modifiers public, protected, private, static, and final are not | |
| * allowed on op-level declarations and are ignored with a warning. | |
| * Synchronized, native, abstract, and default top-level methods are not | |
| * allowed and are errors. | |
| * If a previous definition of a declaration is overwritten then there will | |
| * be an event showing its status changed to OVERWRITTEN, this will not | |
| * occur for dropped, rejected, or already overwritten declarations. | |
| * <p> | |
| * If execution environment is out of process, as is the default case, then | |
| * if the evaluated code | |
| * causes the execution environment to terminate, this {@code JShell} | |
| * instance will be closed but the calling process and VM remain valid. | |
| * @param input The input String to evaluate | |
| * @return the list of events directly or indirectly caused by this evaluation. | |
| * @throws IllegalStateException if this {@code JShell} instance is closed. | |
| * @see SourceCodeAnalysis#analyzeCompletion(String) | |
| * @see JShell#onShutdown(java.util.function.Consumer) | |
| */ | |
| public List<SnippetEvent> eval(String input) throws IllegalStateException { | |
| SourceCodeAnalysisImpl a = sourceCodeAnalysis; | |
| if (a != null) { | |
| a.suspendIndexing(); | |
| } | |
| try { | |
| checkIfAlive(); | |
| List<SnippetEvent> events = eval.eval(input); | |
| events.forEach(this::notifyKeyStatusEvent); | |
| return Collections.unmodifiableList(events); | |
| } finally { | |
| if (a != null) { | |
| a.resumeIndexing(); | |
| } | |
| } | |
| } | |
| /** | |
| * Remove a declaration from the state. That is, if the snippet is an | |
| * {@linkplain jdk.jshell.Snippet.Status#isActive() active} | |
| * {@linkplain jdk.jshell.PersistentSnippet persistent} snippet, remove the | |
| * snippet and update the JShell evaluation state accordingly. | |
| * For all active snippets, change the {@linkplain #status status} to | |
| * {@link jdk.jshell.Snippet.Status#DROPPED DROPPED}. | |
| * @param snippet The snippet to remove | |
| * @return The list of events from updating declarations dependent on the | |
| * dropped snippet. | |
| * @throws IllegalStateException if this {@code JShell} instance is closed. | |
| * @throws IllegalArgumentException if the snippet is not associated with | |
| * this {@code JShell} instance. | |
| */ | |
| public List<SnippetEvent> drop(Snippet snippet) throws IllegalStateException { | |
| checkIfAlive(); | |
| checkValidSnippet(snippet); | |
| List<SnippetEvent> events = eval.drop(snippet); | |
| events.forEach(this::notifyKeyStatusEvent); | |
| return Collections.unmodifiableList(events); | |
| } | |
| /** | |
| * The specified path is added to the end of the classpath used in eval(). | |
| * Note that the unnamed package is not accessible from the package in which | |
| * {@link JShell#eval(String)} code is placed. | |
| * @param path the path to add to the classpath. | |
| * @throws IllegalStateException if this {@code JShell} instance is closed. | |
| */ | |
| public void addToClasspath(String path) { | |
| checkIfAlive(); | |
| // Compiler | |
| taskFactory.addToClasspath(path); | |
| // Runtime | |
| try { | |
| executionControl().addToClasspath(path); | |
| } catch (ExecutionControlException ex) { | |
| debug(ex, "on addToClasspath(" + path + ")"); | |
| } | |
| if (sourceCodeAnalysis != null) { | |
| sourceCodeAnalysis.classpathChanged(); | |
| } | |
| } | |
| /** | |
| * Attempt to stop currently running evaluation. When called while | |
| * the {@link #eval(java.lang.String) } method is running and the | |
| * user's code being executed, an attempt will be made to stop user's code. | |
| * Note that typically this method needs to be called from a different thread | |
| * than the one running the {@code eval} method. | |
| * <p> | |
| * If the {@link #eval(java.lang.String) } method is not running, does nothing. | |
| * <p> | |
| * The attempt to stop the user's code may fail in some case, which may include | |
| * when the execution is blocked on an I/O operation, or when the user's code is | |
| * catching the {@link ThreadDeath} exception. | |
| */ | |
| public void stop() { | |
| if (executionControl != null) { | |
| try { | |
| executionControl.stop(); | |
| } catch (ExecutionControlException ex) { | |
| debug(ex, "on stop()"); | |
| } | |
| } | |
| } | |
| /** | |
| * Close this state engine. Frees resources. Should be called when this | |
| * state engine is no longer needed. | |
| */ | |
| @Override | |
| public void close() { | |
| closeDown(); | |
| } | |
| /** | |
| * Return all snippets. | |
| * @return the snippets for all current snippets in id order. | |
| */ | |
| public Stream<Snippet> snippets() { | |
| return maps.snippetList().stream(); | |
| } | |
| /** | |
| * Returns the active variable snippets. | |
| * This convenience method is equivalent to {@code snippets()} filtered for | |
| * {@link jdk.jshell.Snippet.Status#isActive() status(snippet).isActive()} | |
| * {@code && snippet.kind() == Kind.VARIABLE} | |
| * and cast to {@code VarSnippet}. | |
| * @return the active declared variables. | |
| */ | |
| public Stream<VarSnippet> variables() { | |
| return snippets() | |
| .filter(sn -> status(sn).isActive() && sn.kind() == Snippet.Kind.VAR) | |
| .map(sn -> (VarSnippet) sn); | |
| } | |
| /** | |
| * Returns the active method snippets. | |
| * This convenience method is equivalent to {@code snippets()} filtered for | |
| * {@link jdk.jshell.Snippet.Status#isActive() status(snippet).isActive()} | |
| * {@code && snippet.kind() == Kind.METHOD} | |
| * and cast to MethodSnippet. | |
| * @return the active declared methods. | |
| */ | |
| public Stream<MethodSnippet> methods() { | |
| return snippets() | |
| .filter(sn -> status(sn).isActive() && sn.kind() == Snippet.Kind.METHOD) | |
| .map(sn -> (MethodSnippet)sn); | |
| } | |
| /** | |
| * Returns the active type declaration (class, interface, annotation type, and enum) snippets. | |
| * This convenience method is equivalent to {@code snippets()} filtered for | |
| * {@link jdk.jshell.Snippet.Status#isActive() status(snippet).isActive()} | |
| * {@code && snippet.kind() == Kind.TYPE_DECL} | |
| * and cast to TypeDeclSnippet. | |
| * @return the active declared type declarations. | |
| */ | |
| public Stream<TypeDeclSnippet> types() { | |
| return snippets() | |
| .filter(sn -> status(sn).isActive() && sn.kind() == Snippet.Kind.TYPE_DECL) | |
| .map(sn -> (TypeDeclSnippet) sn); | |
| } | |
| /** | |
| * Returns the active import snippets. | |
| * This convenience method is equivalent to {@code snippets()} filtered for | |
| * {@link jdk.jshell.Snippet.Status#isActive() status(snippet).isActive()} | |
| * {@code && snippet.kind() == Kind.IMPORT} | |
| * and cast to ImportSnippet. | |
| * @return the active declared import declarations. | |
| */ | |
| public Stream<ImportSnippet> imports() { | |
| return snippets() | |
| .filter(sn -> status(sn).isActive() && sn.kind() == Snippet.Kind.IMPORT) | |
| .map(sn -> (ImportSnippet) sn); | |
| } | |
| /** | |
| * Return the status of the snippet. | |
| * This is updated either because of an explicit {@code eval()} call or | |
| * an automatic update triggered by a dependency. | |
| * @param snippet the {@code Snippet} to look up | |
| * @return the status corresponding to this snippet | |
| * @throws IllegalStateException if this {@code JShell} instance is closed. | |
| * @throws IllegalArgumentException if the snippet is not associated with | |
| * this {@code JShell} instance. | |
| */ | |
| public Status status(Snippet snippet) { | |
| return checkValidSnippet(snippet).status(); | |
| } | |
| /** | |
| * Return the diagnostics of the most recent evaluation of the snippet. | |
| * The evaluation can either because of an explicit {@code eval()} call or | |
| * an automatic update triggered by a dependency. | |
| * | |
| * <p>This method will return best-effort diagnostics for snippets returned | |
| * from {@link SourceCodeAnalysis#sourceToSnippets(java.lang.String) }. The | |
| * diagnostics returned for such snippets may differ from diagnostics provided | |
| * after the snippet is {@link #eval(java.lang.String) }-ed. | |
| * | |
| * @param snippet the {@code Snippet} to look up | |
| * @return the diagnostics corresponding to this snippet. This does not | |
| * include unresolvedDependencies references reported in {@code unresolvedDependencies()}. | |
| * @throws IllegalStateException if this {@code JShell} instance is closed. | |
| * @throws IllegalArgumentException if the snippet is not associated with | |
| * this {@code JShell} instance. | |
| */ | |
| public Stream<Diag> diagnostics(Snippet snippet) { | |
| return checkValidSnippet(snippet, true).diagnostics().stream(); | |
| } | |
| /** | |
| * For {@link jdk.jshell.Snippet.Status#RECOVERABLE_DEFINED RECOVERABLE_DEFINED} or | |
| * {@link jdk.jshell.Snippet.Status#RECOVERABLE_NOT_DEFINED RECOVERABLE_NOT_DEFINED} | |
| * declarations, the names of current unresolved dependencies for | |
| * the snippet. | |
| * The returned value of this method, for a given method may change when an | |
| * {@code eval()} or {@code drop()} of another snippet causes | |
| * an update of a dependency. | |
| * @param snippet the declaration {@code Snippet} to look up | |
| * @return a stream of symbol names that are currently unresolvedDependencies. | |
| * @throws IllegalStateException if this {@code JShell} instance is closed. | |
| * @throws IllegalArgumentException if the snippet is not associated with | |
| * this {@code JShell} instance. | |
| */ | |
| public Stream<String> unresolvedDependencies(DeclarationSnippet snippet) { | |
| return checkValidSnippet(snippet).unresolved().stream(); | |
| } | |
| /** | |
| * Get the current value of a variable. | |
| * @param snippet the variable Snippet whose value is queried. | |
| * @return the current value of the variable referenced by snippet. | |
| * @throws IllegalStateException if this {@code JShell} instance is closed. | |
| * @throws IllegalArgumentException if the snippet is not associated with | |
| * this {@code JShell} instance. | |
| * @throws IllegalArgumentException if the variable's status is anything but | |
| * {@link jdk.jshell.Snippet.Status#VALID}. | |
| */ | |
| public String varValue(VarSnippet snippet) throws IllegalStateException { | |
| checkIfAlive(); | |
| checkValidSnippet(snippet); | |
| if (snippet.status() != Status.VALID) { | |
| throw new IllegalArgumentException( | |
| messageFormat("jshell.exc.var.not.valid", snippet, snippet.status())); | |
| } | |
| String value; | |
| try { | |
| value = executionControl().varValue(snippet.classFullName(), snippet.fieldName()); | |
| } catch (EngineTerminationException ex) { | |
| throw new IllegalStateException(ex.getMessage()); | |
| } catch (ExecutionControlException ex) { | |
| debug(ex, "In varValue()"); | |
| return "[" + ex.getMessage() + "]"; | |
| } | |
| return expunge(value); | |
| } | |
| /** | |
| * Register a callback to be called when the Status of a snippet changes. | |
| * Each call adds a new subscription. | |
| * @param listener Action to perform when the Status changes. | |
| * @return A token which can be used to {@linkplain JShell#unsubscribe unsubscribe} this subscription. | |
| * @throws IllegalStateException if this {@code JShell} instance is closed. | |
| */ | |
| public Subscription onSnippetEvent(Consumer<SnippetEvent> listener) | |
| throws IllegalStateException { | |
| return onX(keyStatusListeners, listener); | |
| } | |
| /** | |
| * Register a callback to be called when this JShell instance terminates. | |
| * This occurs either because the client process has ended (e.g. called System.exit(0)) | |
| * or the connection has been shutdown, as by close(). | |
| * Each call adds a new subscription. | |
| * @param listener Action to perform when the state terminates. | |
| * @return A token which can be used to {@linkplain JShell#unsubscribe unsubscribe} this subscription. | |
| * @throws IllegalStateException if this JShell instance is closed | |
| */ | |
| public Subscription onShutdown(Consumer<JShell> listener) | |
| throws IllegalStateException { | |
| return onX(shutdownListeners, listener); | |
| } | |
| /** | |
| * Cancel a callback subscription. | |
| * @param token The token corresponding to the subscription to be unsubscribed. | |
| */ | |
| public void unsubscribe(Subscription token) { | |
| synchronized (this) { | |
| token.remover.accept(token); | |
| } | |
| } | |
| /** | |
| * Subscription is a token for referring to subscriptions so they can | |
| * be {@linkplain JShell#unsubscribe unsubscribed}. | |
| */ | |
| public class Subscription { | |
| Consumer<Subscription> remover; | |
| Subscription(Consumer<Subscription> remover) { | |
| this.remover = remover; | |
| } | |
| } | |
| /** | |
| * Provide the environment for a execution engine. | |
| */ | |
| class ExecutionEnvImpl implements ExecutionEnv { | |
| @Override | |
| public InputStream userIn() { | |
| return in; | |
| } | |
| @Override | |
| public PrintStream userOut() { | |
| return out; | |
| } | |
| @Override | |
| public PrintStream userErr() { | |
| return err; | |
| } | |
| @Override | |
| public List<String> extraRemoteVMOptions() { | |
| return extraRemoteVMOptions; | |
| } | |
| @Override | |
| public void closeDown() { | |
| JShell.this.closeDown(); | |
| } | |
| @Override | |
| public Optional<JShellConsole> console() { | |
| return console; | |
| } | |
| } | |
| // --- private / package-private implementation support --- | |
| ExecutionControl executionControl() { | |
| return executionControl; | |
| } | |
| void debug(int flags, String format, Object... args) { | |
| InternalDebugControl.debug(this, err, flags, format, args); | |
| } | |
| void debug(Throwable ex, String where) { | |
| InternalDebugControl.debug(this, err, ex, where); | |
| } | |
| /** | |
| * Generate the next key index, indicating a unique snippet signature. | |
| * | |
| * @return the next key index | |
| */ | |
| int nextKeyIndex() { | |
| return nextKeyIndex++; | |
| } | |
| private synchronized <T> Subscription onX(Map<Subscription, Consumer<T>> map, Consumer<T> listener) | |
| throws IllegalStateException { | |
| Objects.requireNonNull(listener); | |
| checkIfAlive(); | |
| Subscription token = new Subscription(map::remove); | |
| map.put(token, listener); | |
| return token; | |
| } | |
| private synchronized void notifyKeyStatusEvent(SnippetEvent event) { | |
| keyStatusListeners.values().forEach(l -> l.accept(event)); | |
| } | |
| private synchronized void notifyShutdownEvent(JShell state) { | |
| shutdownListeners.values().forEach(l -> l.accept(state)); | |
| } | |
| void closeDown() { | |
| if (!closed) { | |
| // Send only once | |
| closed = true; | |
| try { | |
| notifyShutdownEvent(this); | |
| } catch (Throwable thr) { | |
| // Don't care about dying exceptions | |
| } | |
| try { | |
| executionControl().close(); | |
| } catch (Throwable ex) { | |
| // don't care about exceptions on close | |
| } | |
| if (sourceCodeAnalysis != null) { | |
| sourceCodeAnalysis.close(); | |
| } | |
| InternalDebugControl.release(this); | |
| } | |
| } | |
| /** | |
| * Check if this JShell has been closed | |
| * @throws IllegalStateException if it is closed | |
| */ | |
| void checkIfAlive() throws IllegalStateException { | |
| if (closed) { | |
| throw new IllegalStateException(messageFormat("jshell.exc.closed", this)); | |
| } | |
| } | |
| /** | |
| * Check a Snippet parameter coming from the API user | |
| * @param sn the Snippet to check | |
| * @throws NullPointerException if Snippet parameter is null | |
| * @throws IllegalArgumentException if Snippet is not from this JShell | |
| * @return the input Snippet (for chained calls) | |
| */ | |
| private Snippet checkValidSnippet(Snippet sn) { | |
| return checkValidSnippet(sn, false); | |
| } | |
| /** | |
| * Check a Snippet parameter coming from the API user | |
| * @param sn the Snippet to check | |
| * @param acceptUnassociated accept snippets that are unassociated | |
| * @throws NullPointerException if Snippet parameter is null | |
| * @throws IllegalArgumentException if Snippet is not from this JShell | |
| * @return the input Snippet (for chained calls) | |
| */ | |
| private Snippet checkValidSnippet(Snippet sn, boolean acceptUnassociated) { | |
| if (sn == null) { | |
| throw new NullPointerException(messageFormat("jshell.exc.null")); | |
| } else { | |
| if (sn.key().state() != this || (!acceptUnassociated && sn.id() == Snippet.UNASSOCIATED_ID)) { | |
| throw new IllegalArgumentException(messageFormat("jshell.exc.alien", sn.toString())); | |
| } | |
| return sn; | |
| } | |
| } | |
| /** | |
| * Format using resource bundle look-up using MessageFormat | |
| * | |
| * @param key the resource key | |
| * @param args | |
| */ | |
| String messageFormat(String key, Object... args) { | |
| if (outputRB == null) { | |
| try { | |
| outputRB = ResourceBundle.getBundle(L10N_RB_NAME); | |
| } catch (MissingResourceException mre) { | |
| throw new InternalError("Cannot find ResourceBundle: " + L10N_RB_NAME); | |
| } | |
| } | |
| String s; | |
| try { | |
| s = outputRB.getString(key); | |
| } catch (MissingResourceException mre) { | |
| throw new InternalError("Missing resource: " + key + " in " + L10N_RB_NAME); | |
| } | |
| return MessageFormat.format(s, args); | |
| } | |
| } |