From 49c86e6b0800f0dee2fab9fc6de87d55ecc88d21 Mon Sep 17 00:00:00 2001 From: zml Date: Sat, 24 Aug 2024 16:14:42 -0700 Subject: [PATCH] feat: Add isolating classloader for sharing across projects Closes GH-55 --- .../kyori/mammoth/IsolatingClassLoader.java | 86 +++++++++++++ .../mammoth/IsolatingClassLoaderImpl.java | 114 ++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 src/main/java/net/kyori/mammoth/IsolatingClassLoader.java create mode 100644 src/main/java/net/kyori/mammoth/IsolatingClassLoaderImpl.java diff --git a/src/main/java/net/kyori/mammoth/IsolatingClassLoader.java b/src/main/java/net/kyori/mammoth/IsolatingClassLoader.java new file mode 100644 index 0000000..28f681d --- /dev/null +++ b/src/main/java/net/kyori/mammoth/IsolatingClassLoader.java @@ -0,0 +1,86 @@ +/* + * This file is part of mammoth, licensed under the MIT License. + * + * Copyright (c) 2024 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.mammoth; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Iterator; +import java.util.Set; +import org.gradle.api.file.FileCollection; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A factory for classloaders that will load classes from themselves rather than its parent where possible. + * + *

This can be used to create isolated environments for working with Gradle plugins, or providing an alternative to workers.

+ * + *

The returned loader from any of these factory methods will be registered as parallel capable.

+ * + * @since 1.4.0 + */ +public final class IsolatingClassLoader { + private IsolatingClassLoader() { + } + + /** + * Create a new loader based on a provided set of URLs. + * + * @param parent the parent loader + * @param urls the urls + * @return the newly created loader + * @since 1.4.0 + */ + public static @NotNull URLClassLoader isolatingClassLoader(final @Nullable ClassLoader parent, final @NotNull URL @NotNull... urls) { + return new IsolatingClassLoaderImpl(urls, parent); + } + + /** + * Create a new loader based on a provided file collection. + * + * @param parent the parent loader + * @param files the file collection + * @return the newly created loader + * @since 1.4.0 + */ + public static @NotNull URLClassLoader isolatingClassLoader(final @Nullable ClassLoader parent, final @NotNull FileCollection files) { + final Set unwrapped = files.getFiles(); + final URL[] urls = new URL[unwrapped.size()]; + final Iterator it = files.iterator(); + int idx = 0; + File file; + while (it.hasNext()) { + file = it.next(); + try { + urls[idx++] = file.toURI().toURL(); + } catch (final MalformedURLException ex) { + throw new IllegalArgumentException("Unable to include file " + file + " in classpath"); + } + } + + return new IsolatingClassLoaderImpl(urls, parent); + } +} diff --git a/src/main/java/net/kyori/mammoth/IsolatingClassLoaderImpl.java b/src/main/java/net/kyori/mammoth/IsolatingClassLoaderImpl.java new file mode 100644 index 0000000..6352c59 --- /dev/null +++ b/src/main/java/net/kyori/mammoth/IsolatingClassLoaderImpl.java @@ -0,0 +1,114 @@ +/* + * This file is part of mammoth, licensed under the MIT License. + * + * Copyright (c) 2024 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.mammoth; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Enumeration; +import java.util.NoSuchElementException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class IsolatingClassLoaderImpl extends URLClassLoader { + static { + ClassLoader.registerAsParallelCapable(); + } + + private final ClassLoader parent; + + // todo: maybe add transformer function (UnaryOperator)? just for fun + // todo: add a filter + IsolatingClassLoaderImpl(final URL[] urls, final ClassLoader parent) { + super(urls, parent); + this.parent = parent; + } + + @Override + protected @Nullable Class loadClass(final @NotNull String name, final boolean resolve) throws ClassNotFoundException { + synchronized (this.getClassLoadingLock(name)) { + Class result = this.findLoadedClass(name); + if (result == null) { + try { + result = this.findClass(name); + } catch (final ClassNotFoundException ex) { + // ignore, delegate to parent + } + } + + if (result == null) { + return super.loadClass(name, resolve); + } + + if (resolve) { + this.resolveClass(result); + } + return result; + } + } + + @Override + public @Nullable URL getResource(final String name) { + @Nullable URL result = this.findResource(name); + if (result == null) { + result = super.getResource(name); + } + return result; + } + + @Override + public @NotNull Enumeration getResources(final String name) throws IOException { + return new Enumeration() { + @Nullable Enumeration active = IsolatingClassLoaderImpl.this.findResources(name); + @Nullable Enumeration staged = IsolatingClassLoaderImpl.this.parent == null + ? ClassLoader.getSystemClassLoader().getResources(name) + : IsolatingClassLoaderImpl.this.parent.getResources(name); + + private @Nullable Enumeration nextComponent() { + if (this.active == null) { + return null; + } else if (!this.active.hasMoreElements()) { + this.active = this.staged; + this.staged = null; + } + return this.active; + } + + @Override + public boolean hasMoreElements() { + final @Nullable Enumeration component = this.nextComponent(); + return component != null && component.hasMoreElements(); + } + + @Override + public @NotNull URL nextElement() { + final @Nullable Enumeration component = this.nextComponent(); + if (component == null) { + throw new NoSuchElementException(); + } + return component.nextElement(); + } + }; + } +}