diff --git a/stage2/modlauncher8/src/main/java/net/minecraftforge/fml/loading/SortedLanguageLoadingProvider.java b/stage2/modlauncher8/src/main/java/net/minecraftforge/fml/loading/SortedLanguageLoadingProvider.java index 866ddccb..6159bd90 100644 --- a/stage2/modlauncher8/src/main/java/net/minecraftforge/fml/loading/SortedLanguageLoadingProvider.java +++ b/stage2/modlauncher8/src/main/java/net/minecraftforge/fml/loading/SortedLanguageLoadingProvider.java @@ -6,6 +6,7 @@ import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import java.nio.file.Path; +import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; @@ -36,7 +37,7 @@ public class SortedLanguageLoadingProvider extends LanguageLoadingProvider { @Override public void addAdditionalLanguages(List modFiles) { if (modFiles == null) { - return; + modFiles = Collections.emptyList(); } Set visited = new HashSet<>(); diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java index fa71e717..94e7d7ee 100644 --- a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java @@ -5,6 +5,7 @@ import cpw.mods.modlauncher.api.*; import gg.essential.loader.stage2.modlauncher.CompatibilityLayer; import gg.essential.loader.stage2.modlauncher.EssentialModLocator; +import gg.essential.loader.stage2.util.AugmentedJarOrPathList; import gg.essential.loader.stage2.util.KFFMerger; import gg.essential.loader.stage2.util.SortedJarOrPathList; import org.apache.logging.log4j.LogManager; @@ -19,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import static gg.essential.loader.stage2.Utils.hasClass; @@ -160,12 +162,25 @@ public void onLoad(IEnvironment env, Set otherServices) { * By default, if there are multiple jars declaring the same module in a layer, ModLauncher will simply pick * whichever was registered first (see JarModuleFinder). Registration order is effectively random (HashMap iteration * order), so it effectively picks a random version, which is no good. - * To work around this behavior, we replace the list which holds all jars in a layer with one that automatically - * sorts by version. + * To work around this behavior, we sort all jars in a layer by their version before the layer is loaded. * This may fail if ModLauncher internals change but there isn't much we can do about it. In such case, we will * simply fall back to the old, unstable behavior. */ - private void configureLayerToBeSortedByVersion(IModuleLayerManager.Layer layer) { + private Consumer> sortedByVersion() { + return new SortedJarOrPathList(compatibilityLayer); + } + + /** + * Configures the given layer to have the give augmentations applied to its classpath before it is loaded. + */ + @SafeVarargs + private void configureLayerAugmentation(IModuleLayerManager.Layer layer, Consumer>...augmentations) { + Consumer> combinedAugmentations = list -> { + for (Consumer> augmentation : augmentations) { + augmentation.accept(list); + } + }; + try { IModuleLayerManager layerManager = Launcher.INSTANCE.findLayerManager().orElseThrow(); Field layersField = layerManager.getClass().getDeclaredField("layers"); @@ -175,14 +190,13 @@ private void configureLayerToBeSortedByVersion(IModuleLayerManager.Layer layer) (Map>) layersField.get(layerManager); layers.compute(layer, (__, list) -> { - SortedJarOrPathList sortedList = new SortedJarOrPathList(compatibilityLayer, kffMerger::maybeMergeInto); - if (list != null) { - sortedList.addAll(list); + if (list == null) { + list = new ArrayList<>(); } - return sortedList; + return new AugmentedJarOrPathList(list, combinedAugmentations); }); } catch (Throwable t) { - LOGGER.error("Failed to replace mod list of " + layer + " with sorted list:", t); + LOGGER.error("Failed to replace mod list of " + layer + " with augmented list:", t); } } @@ -244,7 +258,7 @@ public List beginScanning(IEnvironment environment) { if (injectMods()) { modsInjected = true; } - configureLayerToBeSortedByVersion(IModuleLayerManager.Layer.PLUGIN); + configureLayerAugmentation(IModuleLayerManager.Layer.PLUGIN, kffMerger::apply, sortedByVersion()); return List.of(new Resource(IModuleLayerManager.Layer.PLUGIN, this.pluginJars)); } @@ -256,7 +270,7 @@ public List completeScan(IModuleLayerManager layerManager) { if (!modsInjected) { LOGGER.error("Failed to inject Essential into Forge mod list, falling back to Mixin-only operation. " + "Mod will not be listed in Forge's mod list."); - configureLayerToBeSortedByVersion(IModuleLayerManager.Layer.GAME); + configureLayerAugmentation(IModuleLayerManager.Layer.GAME, sortedByVersion()); return Collections.singletonList(new Resource(IModuleLayerManager.Layer.GAME, this.gameJars)); } return List.of(); diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/SelfRenamingJarMetadata.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/SelfRenamingJarMetadata.java index cf3e6f4d..7d556d51 100644 --- a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/SelfRenamingJarMetadata.java +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/SelfRenamingJarMetadata.java @@ -7,7 +7,7 @@ import gg.essential.loader.stage2.modlauncher.CompatibilityLayer; import gg.essential.loader.stage2.util.DelegatingJarMetadata; import gg.essential.loader.stage2.util.Lazy; -import gg.essential.loader.stage2.util.SortedJarOrPathList; +import gg.essential.loader.stage2.util.PathOrJarAccessor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -102,7 +102,7 @@ private List getLayerElements() throws Throwable { private List getLayerJars() throws Throwable { List jars = new ArrayList<>(); for (Object pathOrJar : getLayerElements()) { - SecureJar jar = SortedJarOrPathList.getJar(pathOrJar); + SecureJar jar = PathOrJarAccessor.getJar(pathOrJar); if (jar != null) { jars.add(jar); } diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/AugmentedJarOrPathList.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/AugmentedJarOrPathList.java new file mode 100644 index 00000000..73af8dcb --- /dev/null +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/AugmentedJarOrPathList.java @@ -0,0 +1,31 @@ +package gg.essential.loader.stage2.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; + +public class AugmentedJarOrPathList extends DelegatingList { + private final Consumer> augmentation; + + public AugmentedJarOrPathList(List inner, Consumer> augmentation) { + super(inner); + this.augmentation = augmentation; + } + + // Called by ModLauncher when finalizing the layer + @Override + public @NotNull Stream stream() { + augmentation.accept(this); + return super.stream(); + } + + // MinecraftForge's ModLauncher fork uses this instead: + // https://github.com/MinecraftForge/ModLauncher/commit/1c5695789fd26bef41328900bb82898c14760629#diff-19fd94ffbddfd7c1d3967bc8152bf96d12ce47c23e7f9e83f734198175c428e1 + @Override + public T1 @NotNull [] toArray(T1 @NotNull [] a) { + augmentation.accept(this); + return super.toArray(a); + } +} diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/DelegatingList.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/DelegatingList.java new file mode 100644 index 00000000..a1def8b3 --- /dev/null +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/DelegatingList.java @@ -0,0 +1,62 @@ +package gg.essential.loader.stage2.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.AbstractList; +import java.util.List; +import java.util.stream.Stream; + +public class DelegatingList extends AbstractList { + private final List delegate; + + public DelegatingList(List delegate) { + this.delegate = delegate; + } + + @Override + public T get(int index) { + return delegate.get(index); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public T set(int index, T element) { + return delegate.set(index, element); + } + + @Override + public void add(int index, T element) { + delegate.add(index, element); + } + + @Override + public T remove(int index) { + return delegate.remove(index); + } + + // + // These methods are called by ModLauncher, so we'll forward them directly in case + // another mod also replaces the list and relies on them. + // + + @Override + public boolean add(T t) { + return delegate.add(t); + } + + @Override + public @NotNull Stream stream() { + return delegate.stream(); + } + + // Used by MinecraftForge's ModLauncher fork: + // https://github.com/MinecraftForge/ModLauncher/commit/1c5695789fd26bef41328900bb82898c14760629#diff-19fd94ffbddfd7c1d3967bc8152bf96d12ce47c23e7f9e83f734198175c428e1 + @Override + public T1 @NotNull [] toArray(T1 @NotNull [] a) { + return delegate.toArray(a); + } +} diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/JarMetadataAccessor.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/JarMetadataAccessor.java new file mode 100644 index 00000000..e2a940b2 --- /dev/null +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/JarMetadataAccessor.java @@ -0,0 +1,39 @@ +package gg.essential.loader.stage2.util; + +import cpw.mods.jarhandling.JarMetadata; +import cpw.mods.jarhandling.SecureJar; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +public class JarMetadataAccessor { + private static final Logger LOGGER = LogManager.getLogger(); + + private static final Map, Function> metadataGetters = new HashMap<>(); + + public static JarMetadata getMetadata(SecureJar jar) { + Class implClass = jar.getClass(); + Function metadataGetter = metadataGetters.get(implClass); + if (metadataGetter == null) { + try { + String implName = implClass.getName(); + switch (implName) { + case "org.sinytra.connector.service.DummyVirtualJar": + // Used for the dummy fabric-loader mod as well as for code generated by ManninghamMills + metadataGetter = __ -> null; + break; + default: // probably the main ModLauncher-internal impl + metadataGetter = UnsafeHacks.makeGetter(implClass.getDeclaredField("metadata")); + } + } catch (Throwable t) { + LOGGER.error("Failed to get metadata from " + implClass + ":", t); + metadataGetter = __ -> null; + } + metadataGetters.put(implClass, metadataGetter); + } + return metadataGetter.apply(jar); + } +} diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/KFFMerger.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/KFFMerger.java index 29fb09f6..12faf860 100644 --- a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/KFFMerger.java +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/KFFMerger.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.ListIterator; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -108,25 +109,35 @@ public boolean addKotlinJar(Path sourceFile, SecureJar secureJar) { return true; } - public List maybeMergeInto(SecureJar secureJar) { + public void apply(List pathOrJarList) { // Nothing to merge (older Essential version), nothing to do if (ourCoreJars.jars.isEmpty()) { - return null; + return; + } + + boolean didMergeIntoExistingMod = false; + ListIterator iter = pathOrJarList.listIterator(); + while (iter.hasNext()) { + SecureJar jar = PathOrJarAccessor.getJar(iter.next()); + if (jar == null) continue; + + SecureJar newJar = maybeMergeInto(jar); + if (newJar != null) { + iter.set(PathOrJarAccessor.from(newJar)); + didMergeIntoExistingMod = true; + // continue, in case there's multiple KFF versions + } } - // If this is a KotlinForForge version which uses JarJar to bundle Kotlin (KFF 5+, potentially 4.12), - // we no longer need to merge the Kotlin jars into it, we merely need to add our Kotlin jars to the to-be-loaded - // list too. - if (isJarJarKff(secureJar)) { - LOGGER.info("Looks like KotlinForForge is using JarJar now, all good to go: {}", secureJar); - List allJars = new ArrayList<>(); - allJars.add(secureJar); // keep user-installed jar - allJars.addAll(ourCoreJars.jars); - allJars.addAll(ourCoroutinesJars.jars); - allJars.addAll(ourSerializationJars.jars); - return allJars; + if (didMergeIntoExistingMod) { + return; } + // No KFF (or KFF 5+ which is using JarJar), so we can simply add all our Kotlin libs to the classpath directly. + addKotlinLibsToPathOrJarList(pathOrJarList); + } + + public SecureJar maybeMergeInto(SecureJar secureJar) { // Only care about a jar if it contains a Kotlin we can overwrite if (!compatibilityLayer.getPackages(secureJar).contains("kotlin")) { return null; @@ -147,7 +158,7 @@ public List maybeMergeInto(SecureJar secureJar) { // If the jar contains only core but not coroutine libs, then it's not the fat KFF jar but rather KFF is // using JarJar, and we should be able to find that one later. if (theirCoreVersion != 0 && theirCoroutinesVersion == 0) { - LOGGER.info("Looks like a standalone Kotlin jar. Keeping as is, we should be finding a JarJar KFF jar."); + LOGGER.info("Looks like a standalone Kotlin jar. Keeping as is."); return null; } @@ -200,19 +211,33 @@ public List maybeMergeInto(SecureJar secureJar) { } } - return List.of(compatibilityLayer.newSecureJarWithCustomMetadata((__, newMeta) -> new DescriptorRewritingJarMetadata(orgMeta, newMeta) { + return compatibilityLayer.newSecureJarWithCustomMetadata((__, newMeta) -> new DescriptorRewritingJarMetadata(orgMeta, newMeta) { @Override public String name() { // Call the original name from the original SecureJar to allow SelfRenamingJarMetadata to function return secureJar.name(); } - }, tmpFile)); + }, tmpFile); } catch (Throwable t) { LOGGER.fatal("Failed to merge updated Kotlin into " + secureJar + ":", t); return null; // oh well, guess we'll give it a try as is } } + private void addKotlinLibsToPathOrJarList(List pathOrJarList) { + addToPathOrJarList(pathOrJarList, ourCoreJars.jars); + addToPathOrJarList(pathOrJarList, ourCoroutinesJars.jars); + addToPathOrJarList(pathOrJarList, ourSerializationJars.jars); + } + + private void addToPathOrJarList(List pathOrJarList, List jarsToBeAdded) { + for (SecureJar jar : jarsToBeAdded) { + Object pathOrJar = PathOrJarAccessor.from(jar); + if (pathOrJar == null) continue; + pathOrJarList.add(pathOrJar); + } + } + public static boolean isJarJarKff(SecureJar jar) { try { Path jarjarPath = jar.getRootPath().resolve("META-INF").resolve("jarjar").resolve("metadata.json"); @@ -264,6 +289,13 @@ private int detectKotlinCoroutinesVersion(SecureJar jar, Path root) { try { Path versionFile = root.resolve("META-INF").resolve("kotlinx_coroutines_core.version"); if (Files.notExists(versionFile)) { + // Coroutines prior to 1.6.0 (e.g. as in KFF 2.2.0) did not yet have the version file: + // https://github.com/Kotlin/kotlinx.coroutines/commit/5c75e7ad58337dcce68b00f5f7e40bfc0108dce7 + // There's no way to know their exact version, but we don't really need to either because it'll always + // be older than what we ship, so any old non-zero version will do. + if (Files.exists(root.resolve("kotlinx").resolve("coroutines").resolve("Job.class"))) { + return version(1, 0, 0); + } return 0; // this is one of our slim jars, always consider it outdated } return version(Files.readString(versionFile)); diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/PathOrJarAccessor.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/PathOrJarAccessor.java new file mode 100644 index 00000000..f4f2a984 --- /dev/null +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/PathOrJarAccessor.java @@ -0,0 +1,91 @@ +package gg.essential.loader.stage2.util; + +import cpw.mods.jarhandling.SecureJar; +import cpw.mods.modlauncher.Launcher; +import cpw.mods.modlauncher.api.IModuleLayerManager; +import cpw.mods.modlauncher.api.NamedPath; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Accessor for ModLauncher's private `PathOrJar` class used by the `layers` map. + * That class wraps either a `NamedPath` or a `SecureJar`. + *
+ * Prior to the introduction of `PathOrJar`, `SecureJar`s were used directly. + * On these older versions, we simply passes through the `SecureJars`, + * allowing our code to just always act as if `PathOrJar` exists, even if it doesn't yet. + */ +public class PathOrJarAccessor { + private static final Logger LOGGER = LogManager.getLogger(); + private static final BiFunction constructor; + private static final Function jarGetter; + + static { + BiFunction constructorFunc = (path, jar) -> null; + Function jarGetterFunc = (pathOrJar) -> null; + try { + IModuleLayerManager layerManager = Launcher.INSTANCE.findLayerManager().orElseThrow(); + + Field layersField = layerManager.getClass().getDeclaredField("layers"); + layersField.setAccessible(true); + @SuppressWarnings("unchecked") + Map> layers = + (Map>) layersField.get(layerManager); + + Class pathOrJarClass = + layers.values().stream() + .flatMap(Collection::stream) + .findAny() + .orElseThrow() // should always at least contain our own jar + .getClass(); + + if (SecureJar.class.isAssignableFrom(pathOrJarClass)) { + constructorFunc = (path, jar) -> jar; + jarGetterFunc = (pathOrJar) -> (SecureJar) pathOrJar; + } else { + Constructor constructor = pathOrJarClass.getDeclaredConstructors()[0]; + constructor.setAccessible(true); + constructorFunc = (path, jar) -> { + try { + return constructor.newInstance(path, jar); + } catch (Throwable t) { + LOGGER.error("Failed to construct PathOrJar:", t); + return null; + } + }; + + Field jarField = pathOrJarClass.getDeclaredField("jar"); + jarField.setAccessible(true); + jarGetterFunc = wrapper -> { + try { + return (SecureJar) jarField.get(wrapper); + } catch (Throwable t) { + LOGGER.error("Failed to get jar from PathOrJar:", t); + return null; + } + }; + } + } catch (Throwable t) { + LOGGER.error("Failed to initialize:", t); + } + constructor = constructorFunc; + jarGetter = jarGetterFunc; + } + + public static @Nullable Object from(SecureJar jar) { + return constructor.apply(null, jar); + } + + public static @Nullable SecureJar getJar(Object pathOrJar) { + return jarGetter.apply(pathOrJar); + } +} diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/SortedJarOrPathList.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/SortedJarOrPathList.java index d26205ac..41e2f310 100644 --- a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/SortedJarOrPathList.java +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/SortedJarOrPathList.java @@ -2,37 +2,19 @@ import cpw.mods.jarhandling.JarMetadata; import cpw.mods.jarhandling.SecureJar; -import cpw.mods.modlauncher.api.NamedPath; import gg.essential.loader.stage2.modlauncher.CompatibilityLayer; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collection; import java.util.Comparator; -import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; -import java.util.function.BiFunction; -import java.util.function.Function; +import java.util.function.Consumer; import java.util.jar.Manifest; -/** - * List which keeps its JarOrPath elements sorted by their module version, latest first. - * - * See [EssentialTransformationService.configureLayerToBeSortedByVersion]. - */ -public class SortedJarOrPathList extends ArrayList { - private static final Logger LOGGER = LogManager.getLogger(); +public class SortedJarOrPathList implements Consumer> { private static final ArtifactVersion FALLBACK_VERSION = new DefaultArtifactVersion("1"); - private static Function jarGetter; - private final Map, Function> metadataGetters = new HashMap<>(); - private BiFunction pathOrJarConstructor; private final Map versionCache = new IdentityHashMap<>(); @@ -41,23 +23,22 @@ public class SortedJarOrPathList extends ArrayList { private final CompatibilityLayer compatibilityLayer; - // This does not actually have anything to do with the original functionality of this class but we needed an entry - // point for replacing one jar (KFF with old Kotlin) with another jar (same KFF but with newer Kotlin merged into - // it) and this class is perfect for that. - private final Function> substitute; - - public SortedJarOrPathList(CompatibilityLayer compatibilityLayer, Function> substitute) { + public SortedJarOrPathList(CompatibilityLayer compatibilityLayer) { this.compatibilityLayer = compatibilityLayer; - this.substitute = substitute; + } + + @Override + public void accept(List pathOrJarList) { + pathOrJarList.sort(COMPARATOR); } private ArtifactVersion getVersion(Object pathOrJar) { - SecureJar jar = getJar(pathOrJar); + SecureJar jar = PathOrJarAccessor.getJar(pathOrJar); if (jar == null) return FALLBACK_VERSION; String version = null; - JarMetadata metadata = getMetadata(jar); + JarMetadata metadata = JarMetadataAccessor.getMetadata(jar); if (metadata != null) { version = metadata.version(); } @@ -97,109 +78,4 @@ private ArtifactVersion getVersion(Object pathOrJar) { } return new DefaultArtifactVersion(version); } - - public static SecureJar getJar(Object pathOrJar) { - if (pathOrJar instanceof SecureJar secureJar) { - return secureJar; - } - - if (jarGetter == null) { - try { - Field jarField = pathOrJar.getClass().getDeclaredField("jar"); - jarField.setAccessible(true); - jarGetter = wrapper -> { - try { - return (SecureJar) jarField.get(wrapper); - } catch (Throwable t) { - LOGGER.error("Failed to get jar from PathOrJar:", t); - return null; - } - }; - } catch (Throwable t) { - LOGGER.error("Failed to get jar from PathOrJar:", t); - jarGetter = __ -> null; - } - } - return jarGetter.apply(pathOrJar); - } - - private JarMetadata getMetadata(SecureJar jar) { - Class implClass = jar.getClass(); - Function metadataGetter = metadataGetters.get(implClass); - if (metadataGetter == null) { - try { - String implName = implClass.getName(); - switch (implName) { - case "org.sinytra.connector.service.DummyVirtualJar": - // Used for the dummy fabric-loader mod as well as for code generated by ManninghamMills - metadataGetter = __ -> null; - break; - default: // probably the main ModLauncher-internal impl - metadataGetter = UnsafeHacks.makeGetter(implClass.getDeclaredField("metadata")); - } - } catch (Throwable t) { - LOGGER.error("Failed to get metadata from " + implClass + ":", t); - metadataGetter = __ -> null; - } - metadataGetters.put(implClass, metadataGetter); - } - return metadataGetter.apply(jar); - } - - @Override - public boolean add(Object o) { - //noinspection RedundantCollectionOperation - return addAll(List.of(o)); - } - - @Override - public boolean addAll(Collection c) { - boolean changed = super.addAll(c.stream().flatMap(it -> substitute(it).stream()).toList()); - sort(COMPARATOR); - return changed; - } - - private List substitute(Object orgPathOrJar) { - SecureJar orgJar = getJar(orgPathOrJar); - if (orgJar == null) { - return List.of(orgPathOrJar); - } - - List newJars = substitute.apply(orgJar); - if (newJars == null) { - return List.of(orgPathOrJar); - } - - if (orgPathOrJar instanceof SecureJar) { - return newJars; - } - - if (pathOrJarConstructor == null) { - try { - Constructor constructor = orgPathOrJar.getClass().getDeclaredConstructors()[0]; - constructor.setAccessible(true); - pathOrJarConstructor = (path, jar) -> { - try { - return constructor.newInstance(path, jar); - } catch (Throwable t) { - LOGGER.error("Failed to construct PathOrJar:", t); - return null; - } - }; - } catch (Throwable t) { - LOGGER.error("Failed to construct PathOrJar:", t); - pathOrJarConstructor = (path, jar) -> null; - } - } - List newPathOrJars = new ArrayList<>(newJars.size()); - for (SecureJar newJar : newJars) { - Object newPathOrJar = pathOrJarConstructor.apply(null, newJar); - if (newPathOrJar == null) { - return List.of(orgPathOrJar); - } - newPathOrJars.add(newPathOrJar); - } - - return newPathOrJars; - } }