diff --git a/java/src/processing/mode/java/pdex/util/RuntimePathBuilder.java b/java/src/processing/mode/java/pdex/util/RuntimePathBuilder.java
new file mode 100644
index 000000000..f3bc609e0
--- /dev/null
+++ b/java/src/processing/mode/java/pdex/util/RuntimePathBuilder.java
@@ -0,0 +1,685 @@
+/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
+
+/*
+Part of the Processing project - http://processing.org
+Copyright (c) 2012-19 The Processing Foundation
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program 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 for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software Foundation, Inc.
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+package processing.mode.java.pdex.util;
+
+import com.google.classpath.ClassPathFactory;
+import processing.app.*;
+import processing.mode.java.JavaMode;
+import processing.mode.java.pdex.ImportStatement;
+import processing.mode.java.pdex.PreprocessedSketch;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+
+/**
+ * Builder which generates runtime paths using a series of caches.
+ *
+ *
+ * The runtime path is dependent on the java runtime, libraries, code folder contents and is used
+ * both to determine classpath in sketch execution and to determine import suggestions within the
+ * editor. This builder determines those paths using a mixture of inputs (modules, jars, etc)
+ * and manages when those paths need to be recalculated (like with addition of a new library).
+ * Note that this is a wrapper around com.google.classpath.ClassPathFactory which is where the
+ * classpath is actually determined.
+ *
+ */
+public class RuntimePathBuilder {
+
+ /*
+ * ==============================================================
+ * === List of modules and jars part of standard distribution ===
+ * ==============================================================
+ *
+ * List of modules and to be included as part of a "standard" distribution which, at this point,
+ * contains the standard library and JFX.
+ */
+
+ /**
+ * The modules comprising the Java standard modules.
+ */
+ protected static final String[] STANDARD_MODULES = {
+ "java.base.jmod",
+ "java.compiler.jmod",
+ "java.datatransfer.jmod",
+ "java.desktop.jmod",
+ "java.instrument.jmod",
+ "java.logging.jmod",
+ "java.management.jmod",
+ "java.management.rmi.jmod",
+ "java.naming.jmod",
+ "java.net.http.jmod",
+ "java.prefs.jmod",
+ "java.rmi.jmod",
+ "java.scripting.jmod",
+ "java.se.jmod",
+ "java.security.jgss.jmod",
+ "java.security.sasl.jmod",
+ "java.smartcardio.jmod",
+ "java.sql.jmod",
+ "java.sql.rowset.jmod",
+ "java.transaction.xa.jmod",
+ "java.xml.crypto.jmod",
+ "java.xml.jmod",
+ "jdk.accessibility.jmod",
+ "jdk.aot.jmod",
+ "jdk.attach.jmod",
+ "jdk.charsets.jmod",
+ "jdk.compiler.jmod",
+ "jdk.crypto.cryptoki.jmod",
+ "jdk.crypto.ec.jmod",
+ "jdk.dynalink.jmod",
+ "jdk.editpad.jmod",
+ "jdk.hotspot.agent.jmod",
+ "jdk.httpserver.jmod",
+ "jdk.internal.ed.jmod",
+ "jdk.internal.jvmstat.jmod",
+ "jdk.internal.le.jmod",
+ "jdk.internal.opt.jmod",
+ "jdk.internal.vm.ci.jmod",
+ "jdk.internal.vm.compiler.jmod",
+ "jdk.internal.vm.compiler.management.jmod",
+ "jdk.jartool.jmod",
+ "jdk.javadoc.jmod",
+ "jdk.jcmd.jmod",
+ "jdk.jconsole.jmod",
+ "jdk.jdeps.jmod",
+ "jdk.jdi.jmod",
+ "jdk.jdwp.agent.jmod",
+ "jdk.jfr.jmod",
+ "jdk.jlink.jmod",
+ "jdk.jshell.jmod",
+ "jdk.jsobject.jmod",
+ "jdk.jstatd.jmod",
+ "jdk.localedata.jmod",
+ "jdk.management.agent.jmod",
+ "jdk.management.jfr.jmod",
+ "jdk.management.jmod",
+ "jdk.naming.dns.jmod",
+ "jdk.naming.rmi.jmod",
+ "jdk.net.jmod",
+ "jdk.pack.jmod",
+ "jdk.rmic.jmod",
+ "jdk.scripting.nashorn.jmod",
+ "jdk.scripting.nashorn.shell.jmod",
+ "jdk.sctp.jmod",
+ "jdk.security.auth.jmod",
+ "jdk.security.jgss.jmod",
+ "jdk.unsupported.desktop.jmod",
+ "jdk.unsupported.jmod",
+ "jdk.xml.dom.jmod",
+ "jdk.zipfs.jmod"
+ };
+
+ /**
+ * The jars required for OpenJFX.
+ */
+ protected static final String[] JAVA_FX_JARS = {
+ "javafx-swt.jar",
+ "javafx.base.jar",
+ "javafx.controls.jar",
+ "javafx.fxml.jar",
+ "javafx.graphics.jar",
+ "javafx.media.jar",
+ "javafx.swing.jar",
+ "javafx.web.jar"
+ };
+
+ /*
+ * ======================
+ * === Path factories ===
+ * ======================
+ *
+ * There are multiple types of paths that are used in different contexts (sketch class path and
+ * import recommendations) and that are required to be re-calculated due to different events.
+ *
+ * The following collections determine which types of paths apply in each and are assigned in the
+ * constructor. Note that often factories are included in more than one of these collections
+ * and are cached independently as their values are invalidated at different events.
+ */
+
+ // Path caches that are invalidated by one or more events within processing.
+ private final List libraryDependentCaches;
+ private final List libraryImportsDependentCaches;
+ private final List codeFolderDependentCaches;
+
+ // Path factories involved in determining sketch class path
+ private final List sketchClassPathStrategies;
+
+ // Path factories involved in determining sketch search path (import recommendations)
+ private final List searchClassPathStrategies;
+
+ // Inner class path factory
+ private final ClassPathFactory classPathFactory;
+
+ /*
+ * ======================
+ * === Public Methods ===
+ * ======================
+ */
+
+ /**
+ * Create a new runtime path builder with empty caches.
+ */
+ public RuntimePathBuilder() {
+
+ // Create the inner classpath factory.
+ classPathFactory = new ClassPathFactory();
+
+ // Build caches
+ CachedRuntimePathFactory javaRuntimePathFactory = new CachedRuntimePathFactory(
+ this::buildJavaRuntimePath
+ );
+ CachedRuntimePathFactory javaFxRuntimePathFactory = new CachedRuntimePathFactory(
+ this::buildJavaFxRuntimePath
+ );
+ CachedRuntimePathFactory modeSketchPathFactory = new CachedRuntimePathFactory(
+ this::buildModeSketchPath
+ );
+ CachedRuntimePathFactory modeSearchPathFactory = new CachedRuntimePathFactory(
+ this::buildModeSearchPath
+ );
+ CachedRuntimePathFactory librarySketchPathFactory = new CachedRuntimePathFactory(
+ this::buildLibrarySketchPath
+ );
+ CachedRuntimePathFactory librarySearchPathFactory = new CachedRuntimePathFactory(
+ this::buildLibrarySearchPath
+ );
+ CachedRuntimePathFactory coreLibraryPathFactory = new CachedRuntimePathFactory(
+ this::buildCoreLibraryPath
+ );
+ CachedRuntimePathFactory codeFolderPathFactory = new CachedRuntimePathFactory(
+ this::buildCodeFolderPath
+ );
+
+ // Create collections for strategies
+ sketchClassPathStrategies = new ArrayList<>();
+ searchClassPathStrategies = new ArrayList<>();
+ libraryDependentCaches = new ArrayList<>();
+ libraryImportsDependentCaches = new ArrayList<>();
+ codeFolderDependentCaches = new ArrayList<>();
+
+ // Strategies required for the sketch class path at sketch execution
+ sketchClassPathStrategies.add(javaRuntimePathFactory);
+ sketchClassPathStrategies.add(javaFxRuntimePathFactory);
+ sketchClassPathStrategies.add(modeSketchPathFactory);
+ sketchClassPathStrategies.add(librarySketchPathFactory);
+ sketchClassPathStrategies.add(coreLibraryPathFactory);
+ sketchClassPathStrategies.add(codeFolderPathFactory);
+
+ // Strategies required for import suggestions
+ searchClassPathStrategies.add(javaRuntimePathFactory);
+ searchClassPathStrategies.add(javaFxRuntimePathFactory);
+ searchClassPathStrategies.add(modeSearchPathFactory);
+ searchClassPathStrategies.add(librarySearchPathFactory);
+ searchClassPathStrategies.add(coreLibraryPathFactory);
+ searchClassPathStrategies.add(codeFolderPathFactory);
+
+ // Assign strategies to collections for cache invalidation on library events.
+ libraryDependentCaches.add(coreLibraryPathFactory);
+ libraryImportsDependentCaches.add(librarySketchPathFactory);
+ libraryImportsDependentCaches.add(librarySearchPathFactory);
+
+ // Assign strategies to collections for cache invalidation on code folder changes.
+ codeFolderDependentCaches.add(codeFolderPathFactory);
+ }
+
+ /**
+ * Invalidate all of the runtime path caches associated with sketch libraries.
+ */
+ public void markLibrariesChanged() {
+ invalidateAll(libraryDependentCaches);
+ }
+
+ /**
+ * Invalidate all of the runtime path caches associated with sketch library imports.
+ */
+ public void markLibraryImportsChanged() {
+ invalidateAll(libraryImportsDependentCaches);
+ }
+
+ /**
+ * Invalidate all of the runtime path caches associated with the code folder having changed.
+ */
+ public void markCodeFolderChanged() {
+ invalidateAll(codeFolderDependentCaches);
+ }
+
+ /**
+ * Generate a classpath and inject it into a {PreprocessedSketch.Builder}.
+ *
+ * @param result The {PreprocessedSketch.Builder} into which the classpath should be inserted.
+ * @param mode The {JavaMode} for which the classpath should be generated.
+ */
+ public void prepareClassPath(PreprocessedSketch.Builder result, JavaMode mode) {
+ List programImports = result.programImports;
+ Sketch sketch = result.sketch;
+
+ prepareSketchClassPath(result, mode, programImports, sketch);
+ prepareSearchClassPath(result, mode, programImports, sketch);
+ }
+
+ /**
+ * Invalidate all of the caches in a provided collection.
+ *
+ * @param caches The caches to invalidate so that, when their value is requested again, the value
+ * is generated again.
+ */
+ private void invalidateAll(List caches) {
+ for (CachedRuntimePathFactory cache : caches) {
+ cache.invalidateCache();
+ }
+ }
+
+ /**
+ * Prepare the classpath required for the sketch's execution.
+ *
+ * @param result The PreprocessedSketch builder into which the classpath and class loader should
+ * be injected.
+ * @param mode The JavaMode for which a sketch classpath should be generated.
+ * @param programImports The imports listed by the sketch (user imports).
+ * @param sketch The sketch for which the classpath is being generated.
+ */
+ private void prepareSketchClassPath(PreprocessedSketch.Builder result, JavaMode mode,
+ List programImports, Sketch sketch) {
+
+ Stream sketchClassPath = sketchClassPathStrategies.stream()
+ .flatMap((x) -> x.buildClasspath(mode, programImports, sketch).stream());
+
+ String[] classPathArray = sketchClassPath.toArray(String[]::new);
+ URL[] urlArray = Arrays.stream(classPathArray)
+ .map(path -> {
+ try {
+ return Paths.get(path).toUri().toURL();
+ } catch (MalformedURLException e) {
+ Messages.loge("malformed URL when preparing sketch classloader", e);
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .toArray(URL[]::new);
+
+ result.classLoader = new URLClassLoader(urlArray, null);
+ result.classPath = classPathFactory.createFromPaths(classPathArray);
+ result.classPathArray = classPathArray;
+ }
+
+ /**
+ * Prepare the classpath for searching in case of import suggestions.
+ *
+ * @param result The PreprocessedSketch builder into which the search classpath should be
+ * injected.
+ * @param mode The JavaMode for which a sketch classpath should be generated.
+ * @param programImports The imports listed by the sketch (user imports).
+ * @param sketch The sketch for which the classpath is being generated.
+ */
+ private void prepareSearchClassPath(PreprocessedSketch.Builder result, JavaMode mode,
+ List programImports, Sketch sketch) {
+
+ Stream searchClassPath = searchClassPathStrategies.stream()
+ .flatMap((x) -> x.buildClasspath(mode, programImports, sketch).stream());
+
+ result.searchClassPathArray = searchClassPath.toArray(String[]::new);
+ }
+
+ /*
+ * ===============================================================
+ * ====== Methods for determining different kinds of paths. ======
+ * ===============================================================
+ *
+ * Methods which help determine different paths for different types of classpath entries. Note
+ * that these are protected so that they can be tested.
+ */
+
+ /**
+ * Enumerate the modules as part of the java runtime.
+ *
+ * @param mode The mode engaged by the user like JavaMode.
+ * @param imports The program imports imposed by the user within their sketch.
+ * @param sketch The sketch provided by the user.
+ * @return List of classpath and/or module path entries.
+ */
+ protected List buildJavaRuntimePath(JavaMode mode, List imports,
+ Sketch sketch) {
+
+ return Arrays.stream(STANDARD_MODULES)
+ .map(this::buildForModule)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Enumerate the modules as part of the java runtime.
+ *
+ * @param mode The mode engaged by the user like JavaMode.
+ * @param imports The program imports imposed by the user within their sketch.
+ * @param sketch The sketch provided by the user.
+ * @return List of classpath and/or module path entries.
+ */
+ protected List buildJavaFxRuntimePath(JavaMode mode, List imports,
+ Sketch sketch) {
+
+ return Arrays.stream(JAVA_FX_JARS)
+ .map(this::findFullyQualifiedJarName)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Enumerate paths for resources like jars within the sketch code folder.
+ *
+ * @param mode The mode engaged by the user like JavaMode.
+ * @param imports The program imports imposed by the user within their sketch.
+ * @param sketch The sketch provided by the user.
+ * @return List of classpath and/or module path entries.
+ */
+ protected List buildCodeFolderPath(JavaMode mode, List imports,
+ Sketch sketch) {
+
+ StringBuilder classPath = new StringBuilder();
+
+ // Code folder
+ if (sketch.hasCodeFolder()) {
+ File codeFolder = sketch.getCodeFolder();
+ String codeFolderClassPath = Util.contentsToClassPath(codeFolder);
+ classPath.append(codeFolderClassPath);
+ }
+
+ return sanitizeClassPath(classPath.toString());
+ }
+
+ /**
+ * Determine paths for libraries part of the processing mode (like {JavaMode}).
+ *
+ * @param mode The mode engaged by the user like JavaMode.
+ * @param imports The program imports imposed by the user within their sketch.
+ * @param sketch The sketch provided by the user.
+ * @return List of classpath and/or module path entries.
+ */
+ protected List buildCoreLibraryPath(JavaMode mode, List imports,
+ Sketch sketch) {
+
+ StringBuilder classPath = new StringBuilder();
+
+ for (Library lib : mode.coreLibraries) {
+ classPath.append(File.pathSeparator).append(lib.getClassPath());
+ }
+
+ return sanitizeClassPath(classPath.toString());
+ }
+
+ /**
+ * Generate classpath entries for third party libraries that are required for running the sketch.
+ *
+ * @param mode The mode engaged by the user like JavaMode.
+ * @param imports The program imports imposed by the user within their sketch.
+ * @param sketch The sketch provided by the user.
+ * @return List of classpath and/or module path entries.
+ */
+ protected List buildLibrarySketchPath(JavaMode mode, List imports,
+ Sketch sketch) {
+
+ StringJoiner classPathBuilder = new StringJoiner(File.pathSeparator);
+
+ imports.stream()
+ .map(ImportStatement::getPackageName)
+ .filter(pckg -> !isIgnorableForSketchPath(pckg))
+ .map(pckg -> {
+ try {
+ return mode.getLibrary(pckg);
+ } catch (SketchException e) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .map(Library::getClassPath)
+ .forEach(classPathBuilder::add);
+
+ return sanitizeClassPath(classPathBuilder.toString());
+ }
+
+ /**
+ * Generate classpath entries for third party libraries that are used when determining import
+ * recommendations.
+ *
+ * @param mode The mode engaged by the user like JavaMode.
+ * @param imports The program imports imposed by the user within their sketch.
+ * @param sketch The sketch provided by the user.
+ * @return List of classpath and/or module path entries.
+ */
+ protected List buildLibrarySearchPath(JavaMode mode, List imports,
+ Sketch sketch) {
+
+ StringJoiner classPathBuilder = new StringJoiner(File.pathSeparator);
+
+ for (Library lib : mode.contribLibraries) {
+ classPathBuilder.add(lib.getClassPath());
+ }
+
+ return sanitizeClassPath(classPathBuilder.toString());
+ }
+
+ /**
+ * Generate classpath entries for the processing mode (like {JavaMode}) used when making import
+ * recommendations.
+ *
+ * @param mode The mode engaged by the user like JavaMode.
+ * @param imports The program imports imposed by the user within their sketch.
+ * @param sketch The sketch provided by the user.
+ * @return List of classpath and/or module path entries.
+ */
+ protected List buildModeSearchPath(JavaMode mode, List imports,
+ Sketch sketch) {
+
+ String searchClassPath = mode.getSearchPath();
+
+ if (searchClassPath != null) {
+ return sanitizeClassPath(searchClassPath);
+ } else {
+ return new ArrayList<>();
+ }
+ }
+
+ /**
+ * Generate classpath entries required by the processing mode like {JavaMode}.
+ *
+ * @param mode The mode engaged by the user like JavaMode.
+ * @param imports The program imports imposed by the user within their sketch.
+ * @param sketch The sketch provided by the user.
+ * @return List of classpath and/or module path entries.
+ */
+ protected List buildModeSketchPath(JavaMode mode, List imports,
+ Sketch sketch) {
+
+ Library coreLibrary = mode.getCoreLibrary();
+ String coreClassPath = coreLibrary != null ?
+ coreLibrary.getClassPath() : mode.getSearchPath();
+ if (coreClassPath != null) {
+ return sanitizeClassPath(coreClassPath);
+ } else {
+ return new ArrayList<>();
+ }
+ }
+
+ /*
+ * ===============================================================
+ * === Helper functions for the path generation methods above. ===
+ * ===============================================================
+ *
+ * Note that these are made protected so that they can be tested.
+ */
+
+ /**
+ * Remove invalid entries in a classpath string.
+ *
+ * @param classPathString The classpath to clean.
+ * @return The cleaned classpath entries without invalid entries.
+ */
+ protected List sanitizeClassPath(String classPathString) {
+ // Make sure class path does not contain empty string (home dir)
+ return Arrays.stream(classPathString.split(File.pathSeparator))
+ .filter(p -> p != null && !p.trim().isEmpty())
+ .distinct()
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Determine if a package is ignorable because it is standard.
+ *
+ * Determine if a package is ignorable on the sketch path because it is standard. This is
+ * different than being ignorable in imports recommendations.
+ *
+ * @param packageName The name of the package to evaluate.
+ * @return True if the package is part of standard Java (like java.lang.*). False otherwise.
+ */
+ protected boolean isIgnorableForSketchPath(String packageName) {
+ return (packageName.startsWith("java.") || packageName.startsWith("javax."));
+ }
+
+ /**
+ * Find a fully qualified jar name.
+ *
+ * @param jarName The jar name like "javafx.base.jar" for which a fully qualified entry should be
+ * created.
+ * @return The fully qualified classpath entry like ".../Processing.app/Contents/PlugIns/
+ * adoptopenjdk-11.0.1.jdk/Contents/Home/lib/javafx.base.jar"
+ */
+ protected String findFullyQualifiedJarName(String jarName) {
+ StringJoiner joiner = new StringJoiner(File.separator);
+ joiner.add(System.getProperty("java.home"));
+ joiner.add("lib");
+ joiner.add(jarName);
+
+ return joiner.toString();
+ }
+
+ /**
+ * Build a classpath entry for a module.
+ *
+ * @param moduleName The name of the module like "java.base.jmod".
+ * @return The fully qualified classpath entry like ".../Processing.app/Contents/PlugIns/
+ * adoptopenjdk-11.0.1.jdk/Contents/Home/jmods/java.base.jmod"
+ */
+ protected String buildForModule(String moduleName) {
+ StringJoiner jmodPathJoiner = new StringJoiner(File.separator);
+ jmodPathJoiner.add(System.getProperty("java.home"));
+ jmodPathJoiner.add("jmods");
+ jmodPathJoiner.add(moduleName);
+ return jmodPathJoiner.toString();
+ }
+
+ /*
+ * ============================================
+ * === Interface definitions and utilities. ===
+ * ============================================
+ *
+ * Note that these are protected so that they can be tested. The interface below defines a
+ * strategy for determining path elements. An optional caching object which allows for path
+ * invalidatino is also defined below.
+ */
+
+ /**
+ * Strategy which generates part of the classpath and/or module path.
+ *
+ *
+ * Strategy for factories each of which generate part of the classpath and/or module path required
+ * by a sketch through user supplied requirements, mode (as in JavaMode) requirements, or
+ * transitive requirements imposed by third party libraries.
+ *
+ */
+ protected interface RuntimePathFactoryStrategy {
+
+ /**
+ * Create classpath and/or module path entries.
+ *
+ * @param mode The mode engaged by the user like JavaMode.
+ * @param programImports The program imports imposed by the user within their sketch.
+ * @param sketch The sketch provided by the user.
+ * @return List of classpath and/or module path entries.
+ */
+ List buildClasspath(JavaMode mode, List programImports, Sketch sketch);
+
+ }
+
+ /**
+ * Runtime path factory which caches the results of another runtime path factory.
+ *
+ *
+ * Runtime path factory which decorates another {RuntimePathFactoryStrategy} that caches the
+ * results of another runtime path factory. This is a lazy cached getter so the value will not be
+ * resolved until it is requested.
+ *
+ */
+ protected static class CachedRuntimePathFactory implements RuntimePathFactoryStrategy {
+
+ private AtomicReference> cachedResult;
+ private RuntimePathFactoryStrategy innerStrategy;
+
+ /**
+ * Create a new cache around {RuntimePathFactoryStrategy}.
+ *
+ * @param newInnerStrategy The strategy to cache.
+ */
+ public CachedRuntimePathFactory(RuntimePathFactoryStrategy newInnerStrategy) {
+ cachedResult = new AtomicReference<>(null);
+ innerStrategy = newInnerStrategy;
+ }
+
+ /**
+ * Invalidate the cached path so that, when requested next time, it will be rebuilt from
+ * scratch.
+ */
+ public void invalidateCache() {
+ cachedResult.set(null);
+ }
+
+ /**
+ * Return the cached classpath or, if not cached, build a classpath using the inner strategy.
+ *
+ *
+ * Return the cached classpath or, if not cached, build a classpath using the inner strategy.
+ * Note that this getter will not check to see if mode, imports, or sketch have changed. If a
+ * cached value is available, it will be returned without examining the identity of the
+ * parameters.
+ *
+ *
+ * @param mode The {JavaMode} for which the classpath should be built.
+ * @param imports The sketch (user) imports.
+ * @param sketch The sketch for which a classpath is to be returned.
+ * @return Newly generated classpath.
+ */
+ @Override
+ public List buildClasspath(JavaMode mode, List imports,
+ Sketch sketch) {
+
+ return cachedResult.updateAndGet((cachedValue) ->
+ cachedValue == null ? innerStrategy.buildClasspath(mode, imports, sketch) : cachedValue
+ );
+ }
+
+ }
+
+}
diff --git a/java/test/processing/mode/java/pdex/util/RuntimePathBuilderTest.java b/java/test/processing/mode/java/pdex/util/RuntimePathBuilderTest.java
new file mode 100644
index 000000000..19fc76b80
--- /dev/null
+++ b/java/test/processing/mode/java/pdex/util/RuntimePathBuilderTest.java
@@ -0,0 +1,156 @@
+/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
+
+/*
+Part of the Processing project - http://processing.org
+Copyright (c) 2019 The Processing Foundation
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program 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 for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software Foundation, Inc.
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+package processing.mode.java.pdex.util;
+
+import org.junit.Before;
+import org.junit.Test;
+import processing.app.Sketch;
+import processing.mode.java.JavaMode;
+import processing.mode.java.pdex.ImportStatement;
+import processing.mode.java.pdex.PreprocessedSketch;
+import processing.mode.java.pdex.util.RuntimePathFactoryTestUtil;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.StringJoiner;
+
+import static org.junit.Assert.*;
+
+
+public class RuntimePathBuilderTest {
+
+ private RuntimePathBuilder builder;
+ private JavaMode testMode;
+ private List testImports;
+ private Sketch testSketch;
+ private PreprocessedSketch.Builder result;
+
+ @Before
+ public void setUp() throws Exception {
+ builder = new RuntimePathBuilder();
+ testMode = RuntimePathFactoryTestUtil.createTestJavaMode();
+ testImports = RuntimePathFactoryTestUtil.createTestImports();
+ testSketch = RuntimePathFactoryTestUtil.createTestSketch();
+
+ result = new PreprocessedSketch.Builder();
+ result.programImports.addAll(testImports);
+ result.sketch = testSketch;
+
+ builder.prepareClassPath(result, testMode);
+ }
+
+ @Test
+ public void testClassPathLoader() {
+ assertNotNull(result.classLoader);
+ }
+
+ @Test
+ public void testClassPathObj() {
+ assertNotNull(result.classPath);
+ }
+
+ @Test
+ public void testSketchClassPathStrategiesJava() {
+ checkPresent(result.classPathArray, "java.base.jmod");
+ }
+
+ @Test
+ public void testSketchClassPathStrategiesLibrary() {
+ checkPresent(result.classPathArray, "library3");
+ }
+
+ @Test
+ public void testSketchClassPathStrategiesCore() {
+ checkPresent(result.classPathArray, "library3");
+ }
+
+ @Test
+ public void testSketchClassPathStrategiesMode() {
+ checkPresent(result.classPathArray, "library6");
+ }
+
+ @Test
+ public void testSketchClassPathStrategiesCodeFolder() {
+ checkPresent(result.classPathArray, "file1.jar");
+ }
+
+ @Test
+ public void testSearchClassPathStrategiesCodeJava() {
+ checkPresent(result.searchClassPathArray, "java.base.jmod");
+ }
+
+ @Test
+ public void testSearchClassPathStrategiesCodeMode() {
+ checkPresent(result.classPathArray, "library6");
+ }
+
+ @Test
+ public void testSearchClassPathStrategiesCodeLibrary() {
+ checkPresent(result.classPathArray, "library3");
+ }
+
+ @Test
+ public void testSearchClassPathStrategiesCodeCore() {
+ checkPresent(result.classPathArray, "library1");
+ }
+
+ @Test
+ public void testSearchClassPathStrategiesCodeCodeFolder() {
+ checkPresent(result.classPathArray, "file3.zip");
+ }
+
+ private void checkPresent(String[] classPathArray, String target) {
+ long count = Arrays.stream(classPathArray)
+ .filter((x) -> x.contains(target))
+ .count();
+
+ assertTrue(count > 0);
+ }
+
+ @Test
+ public void sanitizeClassPath() {
+ StringJoiner testStrJoiner = new StringJoiner(File.pathSeparator);
+ testStrJoiner.add("test1");
+ testStrJoiner.add("");
+ testStrJoiner.add("test2");
+
+ List classPath = builder.sanitizeClassPath(testStrJoiner.toString());
+ assertEquals(2, classPath.size());
+ assertEquals("test1", classPath.get(0));
+ assertEquals("test2", classPath.get(1));
+ }
+
+ @Test
+ public void sanitizeClassPathNoDuplicate() {
+ StringJoiner testStrJoiner = new StringJoiner(File.pathSeparator);
+ testStrJoiner.add("test1");
+ testStrJoiner.add("");
+ testStrJoiner.add("test2");
+ testStrJoiner.add("test2");
+
+ List classPath = builder.sanitizeClassPath(testStrJoiner.toString());
+ assertEquals(2, classPath.size());
+ assertEquals("test1", classPath.get(0));
+ assertEquals("test2", classPath.get(1));
+ }
+
+}