All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dellroad.jct.jshell.LocalContextExecutionControlProvider Maven / Gradle / Ivy

There is a newer version: 1.1.0
Show newest version

/*
 * Copyright (C) 2023 Archie L. Cobbs. All rights reserved.
 */

package org.dellroad.jct.jshell;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.regex.Pattern;

import jdk.jshell.execution.LocalExecutionControl;
import jdk.jshell.execution.LocalExecutionControlProvider;
import jdk.jshell.spi.ExecutionControl;
import jdk.jshell.spi.ExecutionControlProvider;
import jdk.jshell.spi.ExecutionEnv;

import org.dellroad.stuff.java.MemoryClassLoader;

/**
 * Same as {@link LocalExecutionControlProvider}, but with additional support for handling certain
 * class path and class loading issues.
 *
 * 

* When executing JShell locally, it is often desirable that any classes visible to the thread * that starts JShell should also be visible to any scripts and command inputs given to that JShell * instance. * *

* Unfortunately, this doesn't always happen automatically when using the standard {@code "local"} * execution provided by {@link LocalExecutionControl}. * *

* When executing JShell locally, there are two class path/loading issues to worry about: *

    *
  • When JShell compiles source snippets, what classes are available on the "source class path"? * That is, what class names can you refer to by name in your scripts or snippets? *
  • When a compiled script or snippet is loaded as a Java class file by the execution engine, what * classes are available on the "binary class path" when resolving symbolic references? *
* *

* The standard {@link LocalExecutionControl} requires non-standard classes to be explicitly added * via the {@code --class-path} command line flag. Moreover, the {@link ClassLoader} that is uses delegates * to the system class loader, which means that in certain more complex class loading scenarios (for example, * when running as a servlet in the Tomcat web container), compiled snippets classes will fail to load * due to resolution errors. * *

* This class tries to workaround these issues as follows: *

    *
  • To address the "binary class path", this class uses a {@link ClassLoader} that delegates * to the current thread's context class loader instead of the system class loader. This should * fix linking problems in complex class loading scenarios. *
  • To address the "souce class path", this class introspects the current thread's context class * loader and its parents (recursively), attempting to glean what's on the class path. This works * for any {@link ClassLoader} that are instances of {@link URLClassLoader}. However, Java's standard * application class loader is not, so items on the JVM application class path are missed by this * strategy. To include the application class loader, hacky instrospection relying on illegal accesses * is attempted (failures are silently ignored). To ensure these efforts succeed, the following flag * must be added to JVM startup: {@code --add-opens=java.base/jdk.internal.loader=ALL-UNNAMED}. * Note there is also another ugly workaround, which is to write JShell code that only accesses classes * on the application class path via reflection. *
* *

* To utilize this class, include the flags returned by {@link #modifyJShellFlags modifyJShellFlags()} * as parameters to JShell startup. * *

* This provider uses a {@link MemoryLoaderDelegate}. * * @see JDK-8314327 */ public class LocalContextExecutionControlProvider implements ExecutionControlProvider { public static final String NAME = "localContext"; // Public Methods /** * Modify a list of JShell tool command line flags to enable use of this class. * * @param loader loader to copy from, or null for the current thread's context class loader * @param flags modifiable list of command line flags * @throws IllegalArgumentException if {@code flags} is null */ public static void modifyJShellFlags(ClassLoader loader, List flags) { // Sanity check if (flags == null) throw new IllegalArgumentException("null flags"); if (loader == null) loader = Thread.currentThread().getContextClassLoader(); // Use our local execution engine, unless another is specified if (LocalContextExecutionControlProvider.getExecutionFlag(flags) == null) LocalContextExecutionControlProvider.setExecutionFlag(flags, NAME); // Start by grabbing anything from the "java.class.path" system property final LinkedHashSet classpath = new LinkedHashSet<>(); final String cp = System.getProperty("java.class.path"); if (cp != null) { for (String item : cp.split(Pattern.quote(File.pathSeparator))) { if (!item.isEmpty()) classpath.add(item); } } // Visit class loader hierarchy and try to infer application classpath for ( ; loader != null; loader = loader.getParent()) { // Extract classpath URLs from this loader final URL[] urls; if (loader instanceof URLClassLoader) urls = ((URLClassLoader)loader).getURLs(); else { try { // Ugly hack; we are trying to do this: "urls = loader.ucp.getURLs();" final Field field = loader.getClass().getDeclaredField("ucp"); field.setAccessible(true); final Object ucp = field.get(loader); final Method method = ucp.getClass().getMethod("getURLs"); urls = (URL[])method.invoke(ucp); } catch (ReflectiveOperationException | SecurityException | InaccessibleObjectException e) { continue; } } // Pass these URLs to JShell by adding to our "--class-path" flag for (URL url : urls) { final URI uri; try { uri = url.toURI(); } catch (URISyntaxException e) { continue; } final File file; try { file = Paths.get(uri).toFile(); } catch (IllegalArgumentException | FileSystemNotFoundException e) { continue; } classpath.add(file.toString()); } } // Augment "--class-path" command line flag LocalContextExecutionControlProvider.addToClassPath(flags, new ArrayList<>(classpath)); } /** * Get the execution provider name specified via the {@code --execution} flag, if any. * * @param flags list of command line flags * @return execution provider name configured via {@code --execution} flag, or null if none * @throws IllegalArgumentException if {@code flags} is null * @see ExecutionControlProvider#name */ public static String getExecutionFlag(List flags) { final int index = flags.indexOf("--execution"); return index >= 0 && index < flags.size() - 1 ? flags.get(index + 1) : null; } /** * Modify a list of JShell tool command line flags to force the use of the named * execution provider, overriding any previous. * * @param flags modifiable list of command line flags * @param providerName execution provider name * @throws IllegalArgumentException if {@code flags} is null * @see ExecutionControlProvider#name */ public static void setExecutionFlag(List flags, String providerName) { // Sanity check if (flags == null) throw new IllegalArgumentException("null flags"); if (providerName == null) throw new IllegalArgumentException("null providerName"); // Use specified execution engine final int index = flags.indexOf("--execution"); if (index >= 0) { if (index < flags.size() - 1) flags.set(index + 1, providerName); else flags.add(providerName); // weird, there was a bogus trailing "--execution" flag } else { flags.add("--execution"); flags.add(providerName); } } /** * Utility method to modify the given JShell command line flags to add/augment the {@code --class-path} * flag(s) to (also) include the given classpath components. * * @param commandLine jshell command line, possibly including exisiting {@code --class-path} flag(s) * @param components new classpath components to add to {@code commandLine} * @throws IllegalArgumentException if either parameter is null */ public static void addToClassPath(List commandLine, List components) { // Sanity check if (commandLine == null) throw new IllegalArgumentException("null commandLine"); if (components == null) throw new IllegalArgumentException("null components"); // Prepare new classpath final StringBuilder classPath = new StringBuilder(); final String pathSeparator = System.getProperty("path.separator", ":"); final Consumer pathAdder = component -> { if (classPath.length() > 0) classPath.append(pathSeparator); classPath.append(component); }; // Copy any existing classpath components and remove them from the command line int i = 0; while (i < commandLine.size()) { final String flag = commandLine.get(i); if (flag.startsWith("--class-path=")) { commandLine.remove(i); pathAdder.accept(flag.substring("--class-path=".length())); } else if (flag.equals("--class-path")) { commandLine.remove(i); if (i < commandLine.size()) pathAdder.accept(commandLine.remove(i)); } else i++; } // Append new classpath components components.forEach(pathAdder); // Add back combined classpath to the command line (only one "--class-path" flag allowed) if (classPath.length() > 0) { commandLine.add("--class-path"); commandLine.add(classPath.toString()); } } // ExecutionControlProvider @Override public String name() { return NAME; } @Override public Map defaultParameters() { return ExecutionControlProvider.super.defaultParameters(); } @Override public ExecutionControl generate(ExecutionEnv env, Map params) { // Create our class loader final MemoryClassLoader memoryLoader = this.createMemoryClassLoader(); // Set our class loader as the context loader for the current thread. // Note: this action gets undone in JShellShellSession.doExecute(). Thread.currentThread().setContextClassLoader(memoryLoader); // Create our delegate thingie final MemoryLoaderDelegate delegate = this.createMemoryLoaderDelegate(memoryLoader); // Create local ExecutionControl using delegate return this.createLocalExecutionControl(delegate); } // Subclass Methods protected MemoryLoaderDelegate createMemoryLoaderDelegate(MemoryClassLoader memoryLoader) { return new MemoryLoaderDelegate(memoryLoader); } protected MemoryClassLoader createMemoryClassLoader() { return new MemoryClassLoader(); } /** * Build a new {@link LocalExecutionControl} using the given delegate. * *

* The implementation in {@link LocalContextExecutionControlProvider} returns a new {@link LocalContextExecutionControl}. * * @param delegate the delegate to handle loading classes * @return new {@link LocalExecutionControl} instance */ protected LocalExecutionControl createLocalExecutionControl(MemoryLoaderDelegate delegate) { return new LocalContextExecutionControl(delegate); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy