JShell.java revision 3062:15bdc18525ff
1/* 2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26package jdk.jshell; 27 28import java.io.ByteArrayInputStream; 29import java.io.IOException; 30import java.io.InputStream; 31import java.io.PrintStream; 32import java.util.ArrayList; 33import java.util.Collections; 34import java.util.HashMap; 35import java.util.List; 36import java.util.Map; 37import java.util.Objects; 38import java.util.function.BiFunction; 39import java.util.function.Consumer; 40 41import java.util.function.Supplier; 42import jdk.internal.jshell.debug.InternalDebugControl; 43import static java.util.stream.Collectors.collectingAndThen; 44import static java.util.stream.Collectors.toList; 45import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT; 46import static jdk.jshell.Util.expunge; 47import jdk.jshell.Snippet.Status; 48 49/** 50 * The JShell evaluation state engine. This is the central class in the JShell 51 * API. A <code>JShell</code> instance holds the evolving compilation and 52 * execution state. The state is changed with the instance methods 53 * {@link jdk.jshell.JShell#eval(java.lang.String) eval(String)}, 54 * {@link jdk.jshell.JShell#drop(jdk.jshell.PersistentSnippet) drop(PersistentSnippet)} and 55 * {@link jdk.jshell.JShell#addToClasspath(java.lang.String) addToClasspath(String)}. 56 * The majority of methods query the state. 57 * A <code>JShell</code> instance also allows registering for events with 58 * {@link jdk.jshell.JShell#onSnippetEvent(java.util.function.Consumer) onSnippetEvent(Consumer)} 59 * and {@link jdk.jshell.JShell#onShutdown(java.util.function.Consumer) onShutdown(Consumer)}, which 60 * are unregistered with 61 * {@link jdk.jshell.JShell#unsubscribe(jdk.jshell.JShell.Subscription) unsubscribe(Subscription)}. 62 * Access to the source analysis utilities is via 63 * {@link jdk.jshell.JShell#sourceCodeAnalysis()}. 64 * When complete the instance should be closed to free resources -- 65 * {@link jdk.jshell.JShell#close()}. 66 * <p> 67 * An instance of <code>JShell</code> is created with 68 * <code>JShell.create()</code>. 69 * <p> 70 * This class is not thread safe, except as noted, all access should be through 71 * a single thread. 72 * @see jdk.jshell 73 * @author Robert Field 74 */ 75public class JShell implements AutoCloseable { 76 77 final SnippetMaps maps; 78 final KeyMap keyMap; 79 final TaskFactory taskFactory; 80 final InputStream in; 81 final PrintStream out; 82 final PrintStream err; 83 final Supplier<String> tempVariableNameGenerator; 84 final BiFunction<Snippet, Integer, String> idGenerator; 85 86 private int nextKeyIndex = 1; 87 88 final Eval eval; 89 final ClassTracker classTracker; 90 private final Map<Subscription, Consumer<JShell>> shutdownListeners = new HashMap<>(); 91 private final Map<Subscription, Consumer<SnippetEvent>> keyStatusListeners = new HashMap<>(); 92 private boolean closed = false; 93 94 95 private ExecutionControl executionControl = null; 96 private SourceCodeAnalysis sourceCodeAnalysis = null; 97 98 99 JShell(Builder b) { 100 this.in = b.in; 101 this.out = b.out; 102 this.err = b.err; 103 this.tempVariableNameGenerator = b.tempVariableNameGenerator; 104 this.idGenerator = b.idGenerator; 105 106 this.maps = new SnippetMaps(this); 107 maps.setPackageName("REPL"); 108 this.keyMap = new KeyMap(this); 109 this.taskFactory = new TaskFactory(this); 110 this.eval = new Eval(this); 111 this.classTracker = new ClassTracker(this); 112 } 113 114 /** 115 * Builder for <code>JShell</code> instances. 116 * Create custom instances of <code>JShell</code> by using the setter 117 * methods on this class. After zero or more of these, use the 118 * {@link #build()} method to create a <code>JShell</code> instance. 119 * These can all be chained. For example, setting the remote output and 120 * error streams: 121 * <pre> 122 * <code> 123 * JShell myShell = 124 * JShell.builder() 125 * .out(myOutStream) 126 * .err(myErrStream) 127 * .build(); </code> </pre> 128 * If no special set-up is needed, just use 129 * <code>JShell.builder().build()</code> or the short-cut equivalent 130 * <code>JShell.create()</code>. 131 */ 132 public static class Builder { 133 134 InputStream in = new ByteArrayInputStream(new byte[0]); 135 PrintStream out = System.out; 136 PrintStream err = System.err; 137 Supplier<String> tempVariableNameGenerator = null; 138 BiFunction<Snippet, Integer, String> idGenerator = null; 139 140 Builder() { } 141 142 /** 143 * Input for the running evaluation (it's <code>System.in</code>). Note: 144 * applications that use <code>System.in</code> for snippet or other 145 * user input cannot use <code>System.in</code> as the input stream for 146 * the remote process. 147 * <p> 148 * The default, if this is not set, is to provide an empty input stream 149 * -- <code>new ByteArrayInputStream(new byte[0])</code>. 150 * 151 * @param in the <code>InputStream</code> to be channelled to 152 * <code>System.in</code> in the remote execution process. 153 * @return the <code>Builder</code> instance (for use in chained 154 * initialization). 155 */ 156 public Builder in(InputStream in) { 157 this.in = in; 158 return this; 159 } 160 161 /** 162 * Output for the running evaluation (it's <code>System.out</code>). 163 * The controlling process and 164 * the remote process can share <code>System.out</code>. 165 * <p> 166 * The default, if this is not set, is <code>System.out</code>. 167 * 168 * @param out the <code>PrintStream</code> to be channelled to 169 * <code>System.out</code> in the remote execution process. 170 * @return the <code>Builder</code> instance (for use in chained 171 * initialization). 172 */ 173 public Builder out(PrintStream out) { 174 this.out = out; 175 return this; 176 } 177 178 /** 179 * Error output for the running evaluation (it's 180 * <code>System.err</code>). The controlling process and the remote 181 * process can share <code>System.err</code>. 182 * <p> 183 * The default, if this is not set, is <code>System.err</code>. 184 * 185 * @param err the <code>PrintStream</code> to be channelled to 186 * <code>System.err</code> in the remote execution process. 187 * @return the <code>Builder</code> instance (for use in chained 188 * initialization). 189 */ 190 public Builder err(PrintStream err) { 191 this.err = err; 192 return this; 193 } 194 195 /** 196 * Set a generator of temp variable names for 197 * {@link jdk.jshell.VarSnippet} of 198 * {@link jdk.jshell.Snippet.SubKind#TEMP_VAR_EXPRESSION_SUBKIND}. 199 * <p> 200 * Do not use this method unless you have explicit need for it. 201 * <p> 202 * The generator will be used for newly created VarSnippet 203 * instances. The name of a variable is queried with 204 * {@link jdk.jshell.VarSnippet#name()}. 205 * <p> 206 * The callback is sent during the processing of the snippet, the 207 * JShell state is not stable. No calls whatsoever on the 208 * <code>JShell</code> instance may be made from the callback. 209 * <p> 210 * The generated name must be unique within active snippets. 211 * <p> 212 * The default behavior (if this is not set or <code>generator</code> 213 * is null) is to generate the name as a sequential number with a 214 * prefixing dollar sign ("$"). 215 * 216 * @param generator the <code>Supplier</code> to generate the temporary 217 * variable name string or <code>null</code>. 218 * @return the <code>Builder</code> instance (for use in chained 219 * initialization). 220 */ 221 public Builder tempVariableNameGenerator(Supplier<String> generator) { 222 this.tempVariableNameGenerator = generator; 223 return this; 224 } 225 226 /** 227 * Set the generator of identifying names for Snippets. 228 * <p> 229 * Do not use this method unless you have explicit need for it. 230 * <p> 231 * The generator will be used for newly created Snippet instances. The 232 * identifying name (id) is accessed with 233 * {@link jdk.jshell.Snippet#id()} and can be seen in the 234 * <code>StackTraceElement.getFileName()</code> for a 235 * {@link jdk.jshell.EvalException} and 236 * {@link jdk.jshell.UnresolvedReferenceException}. 237 * <p> 238 * The inputs to the generator are the {@link jdk.jshell.Snippet} and an 239 * integer. The integer will be the same for two Snippets which would 240 * overwrite one-another, but otherwise is unique. 241 * <p> 242 * The callback is sent during the processing of the snippet and the 243 * Snippet and the state as a whole are not stable. No calls to change 244 * system state (including Snippet state) should be made. Queries of 245 * Snippet may be made except to {@link jdk.jshell.Snippet#id()}. No 246 * calls on the <code>JShell</code> instance may be made from the 247 * callback, except to 248 * {@link #status(jdk.jshell.Snippet) status(Snippet)}. 249 * <p> 250 * The default behavior (if this is not set or <code>generator</code> 251 * is null) is to generate the id as the integer converted to a string. 252 * 253 * @param generator the <code>BiFunction</code> to generate the id 254 * string or <code>null</code>. 255 * @return the <code>Builder</code> instance (for use in chained 256 * initialization). 257 */ 258 public Builder idGenerator(BiFunction<Snippet, Integer, String> generator) { 259 this.idGenerator = generator; 260 return this; 261 } 262 263 /** 264 * Build a JShell state engine. This is the entry-point to all JShell 265 * functionality. This creates a remote process for execution. It is 266 * thus important to close the returned instance. 267 * 268 * @return the state engine. 269 */ 270 public JShell build() { 271 return new JShell(this); 272 } 273 } 274 275 // --- public API --- 276 277 /** 278 * Create a new JShell state engine. 279 * That is, create an instance of <code>JShell</code>. 280 * <p> 281 * Equivalent to {@link JShell#builder() JShell.builder()}{@link JShell.Builder#build() .build()}. 282 * @return an instance of <code>JShell</code>. 283 */ 284 public static JShell create() { 285 return builder().build(); 286 } 287 288 /** 289 * Factory method for <code>JShell.Builder</code> which, in-turn, is used 290 * for creating instances of <code>JShell</code>. 291 * Create a default instance of <code>JShell</code> with 292 * <code>JShell.builder().build()</code>. For more construction options 293 * see {@link jdk.jshell.JShell.Builder}. 294 * @return an instance of <code>Builder</code>. 295 * @see jdk.jshell.JShell.Builder 296 */ 297 public static Builder builder() { 298 return new Builder(); 299 } 300 301 /** 302 * Access to source code analysis functionality. 303 * An instance of <code>JShell</code> will always return the same 304 * <code>SourceCodeAnalysis</code> instance from 305 * <code>sourceCodeAnalysis()</code>. 306 * @return an instance of {@link SourceCodeAnalysis SourceCodeAnalysis} 307 * which can be used for source analysis such as completion detection and 308 * completion suggestions. 309 */ 310 public SourceCodeAnalysis sourceCodeAnalysis() { 311 if (sourceCodeAnalysis == null) { 312 sourceCodeAnalysis = new SourceCodeAnalysisImpl(this); 313 } 314 return sourceCodeAnalysis; 315 } 316 317 /** 318 * Evaluate the input String, including definition and/or execution, if 319 * applicable. The input is checked for errors, unless the errors can be 320 * deferred (as is the case with some unresolvedDependencies references), 321 * errors will abort evaluation. The input should be 322 * exactly one complete snippet of source code, that is, one expression, 323 * statement, variable declaration, method declaration, class declaration, 324 * or import. 325 * To break arbitrary input into individual complete snippets, use 326 * {@link SourceCodeAnalysis#analyzeCompletion(String)}. 327 * <p> 328 * For imports, the import is added. Classes, interfaces. methods, 329 * and variables are defined. The initializer of variables, statements, 330 * and expressions are executed. 331 * The modifiers public, protected, private, static, and final are not 332 * allowed on op-level declarations and are ignored with a warning. 333 * Synchronized, native, abstract, and default top-level methods are not 334 * allowed and are errors. 335 * If a previous definition of a declaration is overwritten then there will 336 * be an event showing its status changed to OVERWRITTEN, this will not 337 * occur for dropped, rejected, or already overwritten declarations. 338 * <p> 339 * The execution environment is out of process. If the evaluated code 340 * causes the execution environment to terminate, this <code>JShell</code> 341 * instance will be closed but the calling process and VM remain valid. 342 * @param input The input String to evaluate 343 * @return the list of events directly or indirectly caused by this evaluation. 344 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 345 * @see SourceCodeAnalysis#analyzeCompletion(String) 346 * @see JShell#onShutdown(java.util.function.Consumer) 347 */ 348 public List<SnippetEvent> eval(String input) throws IllegalStateException { 349 checkIfAlive(); 350 List<SnippetEvent> events = eval.eval(input); 351 events.forEach(this::notifyKeyStatusEvent); 352 return Collections.unmodifiableList(events); 353 } 354 355 /** 356 * Remove a declaration from the state. 357 * @param snippet The snippet to remove 358 * @return The list of events from updating declarations dependent on the 359 * dropped snippet. 360 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 361 * @throws IllegalArgumentException if the snippet is not associated with 362 * this <code>JShell</code> instance. 363 */ 364 public List<SnippetEvent> drop(PersistentSnippet snippet) throws IllegalStateException { 365 checkIfAlive(); 366 checkValidSnippet(snippet); 367 List<SnippetEvent> events = eval.drop(snippet); 368 events.forEach(this::notifyKeyStatusEvent); 369 return Collections.unmodifiableList(events); 370 } 371 372 /** 373 * The specified path is added to the end of the classpath used in eval(). 374 * Note that the unnamed package is not accessible from the package in which 375 * {@link JShell#eval()} code is placed. 376 * @param path the path to add to the classpath. 377 */ 378 public void addToClasspath(String path) { 379 taskFactory.addToClasspath(path); // Compiler 380 executionControl().commandAddToClasspath(path); // Runtime 381 } 382 383 /** 384 * Attempt to stop currently running evaluation. When called while 385 * the {@link #eval(java.lang.String) } method is running and the 386 * user's code being executed, an attempt will be made to stop user's code. 387 * Note that typically this method needs to be called from a different thread 388 * than the one running the {@code eval} method. 389 * <p> 390 * If the {@link #eval(java.lang.String) } method is not running, does nothing. 391 * <p> 392 * The attempt to stop the user's code may fail in some case, which may include 393 * when the execution is blocked on an I/O operation, or when the user's code is 394 * catching the {@link ThreadDeath} exception. 395 */ 396 public void stop() { 397 if (executionControl != null) 398 executionControl.commandStop(); 399 } 400 401 /** 402 * Close this state engine. Frees resources. Should be called when this 403 * state engine is no longer needed. 404 */ 405 @Override 406 public void close() { 407 if (!closed) { 408 closeDown(); 409 executionControl().commandExit(); 410 } 411 } 412 413 /** 414 * Return all snippets. 415 * @return the snippets for all current snippets in id order. 416 * @throws IllegalStateException if this JShell instance is closed. 417 */ 418 public List<Snippet> snippets() throws IllegalStateException { 419 checkIfAlive(); 420 return Collections.unmodifiableList(maps.snippetList()); 421 } 422 423 /** 424 * Returns the active variable snippets. 425 * This convenience method is equivalent to <code>snippets()</code> filtered for 426 * {@link jdk.jshell.Snippet.Status#isActive status(snippet).isActive} 427 * <code>&& snippet.kind() == Kind.VARIABLE</code> 428 * and cast to <code>VarSnippet</code>. 429 * @return the active declared variables. 430 * @throws IllegalStateException if this JShell instance is closed. 431 */ 432 public List<VarSnippet> variables() throws IllegalStateException { 433 return snippets().stream() 434 .filter(sn -> status(sn).isActive && sn.kind() == Snippet.Kind.VAR) 435 .map(sn -> (VarSnippet) sn) 436 .collect(collectingAndThen(toList(), Collections::unmodifiableList)); 437 } 438 439 /** 440 * Returns the active method snippets. 441 * This convenience method is equivalent to <code>snippets()</code> filtered for 442 * {@link jdk.jshell.Snippet.Status#isActive status(snippet).isActive} 443 * <code>&& snippet.kind() == Kind.METHOD</code> 444 * and cast to MethodSnippet. 445 * @return the active declared methods. 446 * @throws IllegalStateException if this JShell instance is closed. 447 */ 448 public List<MethodSnippet> methods() throws IllegalStateException { 449 return snippets().stream() 450 .filter(sn -> status(sn).isActive && sn.kind() == Snippet.Kind.METHOD) 451 .map(sn -> (MethodSnippet)sn) 452 .collect(collectingAndThen(toList(), Collections::unmodifiableList)); 453 } 454 455 /** 456 * Returns the active type declaration (class, interface, annotation type, and enum) snippets. 457 * This convenience method is equivalent to <code>snippets()</code> filtered for 458 * {@link jdk.jshell.Snippet.Status#isActive status(snippet).isActive} 459 * <code>&& snippet.kind() == Kind.TYPE_DECL</code> 460 * and cast to TypeDeclSnippet. 461 * @return the active declared type declarations. 462 * @throws IllegalStateException if this JShell instance is closed. 463 */ 464 public List<TypeDeclSnippet> types() throws IllegalStateException { 465 return snippets().stream() 466 .filter(sn -> status(sn).isActive && sn.kind() == Snippet.Kind.TYPE_DECL) 467 .map(sn -> (TypeDeclSnippet) sn) 468 .collect(collectingAndThen(toList(), Collections::unmodifiableList)); 469 } 470 471 /** 472 * Return the status of the snippet. 473 * This is updated either because of an explicit <code>eval()</code> call or 474 * an automatic update triggered by a dependency. 475 * @param snippet the <code>Snippet</code> to look up 476 * @return the status corresponding to this snippet 477 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 478 * @throws IllegalArgumentException if the snippet is not associated with 479 * this <code>JShell</code> instance. 480 */ 481 public Status status(Snippet snippet) { 482 return checkValidSnippet(snippet).status(); 483 } 484 485 /** 486 * Return the diagnostics of the most recent evaluation of the snippet. 487 * The evaluation can either because of an explicit <code>eval()</code> call or 488 * an automatic update triggered by a dependency. 489 * @param snippet the <code>Snippet</code> to look up 490 * @return the diagnostics corresponding to this snippet. This does not 491 * include unresolvedDependencies references reported in <code>unresolvedDependencies()</code>. 492 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 493 * @throws IllegalArgumentException if the snippet is not associated with 494 * this <code>JShell</code> instance. 495 */ 496 public List<Diag> diagnostics(Snippet snippet) { 497 return Collections.unmodifiableList(checkValidSnippet(snippet).diagnostics()); 498 } 499 500 /** 501 * For {@link jdk.jshell.Snippet.Status#RECOVERABLE_DEFINED RECOVERABLE_DEFINED} or 502 * {@link jdk.jshell.Snippet.Status#RECOVERABLE_NOT_DEFINED RECOVERABLE_NOT_DEFINED} 503 * declarations, the names of current unresolved dependencies for 504 * the snippet. 505 * The returned value of this method, for a given method may change when an 506 * <code>eval()</code> or <code>drop()</code> of another snippet causes 507 * an update of a dependency. 508 * @param snippet the declaration <code>Snippet</code> to look up 509 * @return the list of symbol names that are currently unresolvedDependencies. 510 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 511 * @throws IllegalArgumentException if the snippet is not associated with 512 * this <code>JShell</code> instance. 513 */ 514 public List<String> unresolvedDependencies(DeclarationSnippet snippet) { 515 return Collections.unmodifiableList(checkValidSnippet(snippet).unresolved()); 516 } 517 518 /** 519 * Get the current value of a variable. 520 * @param snippet the variable Snippet whose value is queried. 521 * @return the current value of the variable referenced by snippet. 522 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 523 * @throws IllegalArgumentException if the snippet is not associated with 524 * this <code>JShell</code> instance. 525 * @throws IllegalArgumentException if the variable's status is anything but 526 * {@link jdk.jshell.Snippet.Status#VALID}. 527 */ 528 public String varValue(VarSnippet snippet) throws IllegalStateException { 529 checkIfAlive(); 530 checkValidSnippet(snippet); 531 if (snippet.status() != Status.VALID) { 532 throw new IllegalArgumentException("Snippet parameter of varValue() '" + 533 snippet + "' must be VALID, it is: " + snippet.status()); 534 } 535 String value = executionControl().commandVarValue(maps.classFullName(snippet), snippet.name()); 536 return expunge(value); 537 } 538 539 /** 540 * Register a callback to be called when the Status of a snippet changes. 541 * Each call adds a new subscription. 542 * @param listener Action to perform when the Status changes. 543 * @return A token which can be used to {@linkplain JShell#unsubscribe unsubscribe} this subscription. 544 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 545 */ 546 public Subscription onSnippetEvent(Consumer<SnippetEvent> listener) 547 throws IllegalStateException { 548 return onX(keyStatusListeners, listener); 549 } 550 551 /** 552 * Register a callback to be called when this JShell instance terminates. 553 * This occurs either because the client process has ended (e.g. called System.exit(0)) 554 * or the connection has been shutdown, as by close(). 555 * Each call adds a new subscription. 556 * @param listener Action to perform when the state terminates. 557 * @return A token which can be used to {@linkplain JShell#unsubscribe unsubscribe} this subscription. 558 * @throws IllegalStateException if this JShell instance is closed 559 */ 560 public Subscription onShutdown(Consumer<JShell> listener) 561 throws IllegalStateException { 562 return onX(shutdownListeners, listener); 563 } 564 565 /** 566 * Cancel a callback subscription. 567 * @param token The token corresponding to the subscription to be unsubscribed. 568 */ 569 public void unsubscribe(Subscription token) { 570 synchronized (this) { 571 token.remover.accept(token); 572 } 573 } 574 575 /** 576 * Subscription is a token for referring to subscriptions so they can 577 * be {@linkplain JShell#unsubscribe unsubscribed}. 578 */ 579 public class Subscription { 580 581 Consumer<Subscription> remover; 582 583 Subscription(Consumer<Subscription> remover) { 584 this.remover = remover; 585 } 586 } 587 588 // --- private / package-private implementation support --- 589 590 ExecutionControl executionControl() { 591 if (executionControl == null) { 592 this.executionControl = new ExecutionControl(new JDIEnv(this), maps, this); 593 try { 594 executionControl.launch(); 595 } catch (IOException ex) { 596 throw new InternalError("Launching JDI execution engine threw: " + ex.getMessage(), ex); 597 } 598 } 599 return executionControl; 600 } 601 602 void debug(int flags, String format, Object... args) { 603 if (InternalDebugControl.debugEnabled(this, flags)) { 604 err.printf(format, args); 605 } 606 } 607 608 void debug(Exception ex, String where) { 609 if (InternalDebugControl.debugEnabled(this, 0xFFFFFFFF)) { 610 err.printf("Fatal error: %s: %s\n", where, ex.getMessage()); 611 ex.printStackTrace(err); 612 } 613 } 614 615 /** 616 * Generate the next key index, indicating a unique snippet signature. 617 * @return the next key index 618 */ 619 int nextKeyIndex() { 620 return nextKeyIndex++; 621 } 622 623 private synchronized <T> Subscription onX(Map<Subscription, Consumer<T>> map, Consumer<T> listener) 624 throws IllegalStateException { 625 Objects.requireNonNull(listener); 626 checkIfAlive(); 627 Subscription token = new Subscription(map::remove); 628 map.put(token, listener); 629 return token; 630 } 631 632 private synchronized void notifyKeyStatusEvent(SnippetEvent event) { 633 keyStatusListeners.values().forEach(l -> l.accept(event)); 634 } 635 636 private synchronized void notifyShutdownEvent(JShell state) { 637 shutdownListeners.values().forEach(l -> l.accept(state)); 638 } 639 640 void closeDown() { 641 if (!closed) { 642 // Send only once 643 closed = true; 644 notifyShutdownEvent(this); 645 } 646 } 647 648 /** 649 * Check if this JShell has been closed 650 * @throws IllegalStateException if it is closed 651 */ 652 private void checkIfAlive() throws IllegalStateException { 653 if (closed) { 654 throw new IllegalStateException("JShell (" + this + ") has been closed."); 655 } 656 } 657 658 /** 659 * Check a Snippet parameter coming from the API user 660 * @param sn the Snippet to check 661 * @throws NullPointerException if Snippet parameter is null 662 * @throws IllegalArgumentException if Snippet is not from this JShell 663 * @return the input Snippet (for chained calls) 664 */ 665 private Snippet checkValidSnippet(Snippet sn) { 666 if (sn == null) { 667 throw new NullPointerException("Snippet must not be null"); 668 } else { 669 if (sn.key().state() != this) { 670 throw new IllegalArgumentException("Snippet not from this JShell"); 671 } 672 return sn; 673 } 674 } 675 676} 677