diff --git a/CHANGES.md b/CHANGES.md index b170b7086d..6e706fc8e4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,7 +10,9 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added * Support for`clang-format` on maven-plugin ([#2406](https://github.com/diffplug/spotless/pull/2406)) +* Allow overriding classLoader for all `JarState`s to enable spotless-cli ([#2427](https://github.com/diffplug/spotless/pull/2427)) ## [3.0.2] - 2025-01-14 ### Fixed diff --git a/lib/src/main/java/com/diffplug/spotless/JarState.java b/lib/src/main/java/com/diffplug/spotless/JarState.java index 8680932b9e..76dee4f438 100644 --- a/lib/src/main/java/com/diffplug/spotless/JarState.java +++ b/lib/src/main/java/com/diffplug/spotless/JarState.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,11 @@ import java.util.Set; import java.util.stream.Collectors; +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Grabs a jar and its dependencies from maven, * and makes it easy to access the collection in @@ -37,6 +42,21 @@ * catch changes in a SNAPSHOT version. */ public final class JarState implements Serializable { + + private static final Logger logger = LoggerFactory.getLogger(JarState.class); + + // Let the classloader be overridden for tools using different approaches to classloading + @Nullable + private static ClassLoader forcedClassLoader = null; + + /** Overrides the classloader used by all JarStates. */ + public static void setForcedClassLoader(@Nullable ClassLoader forcedClassLoader) { + if (!Objects.equals(JarState.forcedClassLoader, forcedClassLoader)) { + logger.info("Overriding the forced classloader for JarState from {} to {}", JarState.forcedClassLoader, forcedClassLoader); + } + JarState.forcedClassLoader = forcedClassLoader; + } + /** A lazily evaluated JarState, which becomes a set of files when serialized. */ public static class Promised implements Serializable { private static final long serialVersionUID = 1L; @@ -125,26 +145,36 @@ URL[] jarUrls() { } /** - * Returns a classloader containing the only jars in this JarState. + * Returns either a forcedClassloader ({@code JarState.setForcedClassLoader()}) or a classloader containing the only jars in this JarState. * Look-up of classes in the {@code org.slf4j} package * are not taken from the JarState, but instead redirected to the class loader of this class to enable * passthrough logging. *
* The lifetime of the underlying cacheloader is controlled by {@link SpotlessCache}. + * + * @see com.diffplug.spotless.JarState#setForcedClassLoader(ClassLoader) */ public ClassLoader getClassLoader() { + if (forcedClassLoader != null) { + return forcedClassLoader; + } return SpotlessCache.instance().classloader(this); } /** - * Returns a classloader containing the only jars in this JarState. + * Returns either a forcedClassloader ({@code JarState.setForcedClassLoader}) or a classloader containing the only jars in this JarState. * Look-up of classes in the {@code org.slf4j} package * are not taken from the JarState, but instead redirected to the class loader of this class to enable * passthrough logging. *
- * The lifetime of the underlying cacheloader is controlled by {@link SpotlessCache}. + * The lifetime of the underlying cacheloader is controlled by {@link SpotlessCache} + * + * @see com.diffplug.spotless.JarState#setForcedClassLoader(ClassLoader) */ public ClassLoader getClassLoader(Serializable key) { + if (forcedClassLoader != null) { + return forcedClassLoader; + } return SpotlessCache.instance().classloader(key, this); } } diff --git a/lib/src/test/java/com/diffplug/spotless/JarStateTest.java b/lib/src/test/java/com/diffplug/spotless/JarStateTest.java new file mode 100644 index 0000000000..f44aa4a0b3 --- /dev/null +++ b/lib/src/test/java/com/diffplug/spotless/JarStateTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2025 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.util.stream.Collectors; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class JarStateTest { + + @TempDir + java.nio.file.Path tempDir; + + File a; + + File b; + + Provisioner provisioner = (withTransitives, deps) -> deps.stream().map(name -> name.equals("a") ? a : b).collect(Collectors.toSet()); + + @BeforeEach + void setUp() throws IOException { + a = Files.createTempFile(tempDir, "a", ".class").toFile(); + Files.writeString(a.toPath(), "a"); + b = Files.createTempFile(tempDir, "b", ".class").toFile(); + Files.writeString(b.toPath(), "b"); + } + + @AfterEach + void tearDown() { + JarState.setForcedClassLoader(null); + } + + @Test + void itCreatesClassloaderWhenForcedClassLoaderNotSet() throws IOException { + JarState state1 = JarState.from(a.getName(), provisioner); + JarState state2 = JarState.from(b.getName(), provisioner); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(state1.getClassLoader()).isNotNull(); + softly.assertThat(state2.getClassLoader()).isNotNull(); + }); + } + + @Test + void itReturnsForcedClassloaderIfSetNoMatterIfSetBeforeOrAfterCreation() throws IOException { + JarState stateA = JarState.from(a.getName(), provisioner); + ClassLoader forcedClassLoader = new URLClassLoader(new java.net.URL[0]); + JarState.setForcedClassLoader(forcedClassLoader); + JarState stateB = JarState.from(b.getName(), provisioner); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(stateA.getClassLoader()).isSameAs(forcedClassLoader); + softly.assertThat(stateB.getClassLoader()).isSameAs(forcedClassLoader); + }); + } + + @Test + void itReturnsForcedClassloaderEvenWhenRountripSerialized() throws IOException, ClassNotFoundException { + JarState stateA = JarState.from(a.getName(), provisioner); + ClassLoader forcedClassLoader = new URLClassLoader(new java.net.URL[0]); + JarState.setForcedClassLoader(forcedClassLoader); + JarState stateB = JarState.from(b.getName(), provisioner); + + JarState stateARoundtripSerialized = roundtripSerialize(stateA); + JarState stateBRoundtripSerialized = roundtripSerialize(stateB); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(stateARoundtripSerialized.getClassLoader()).isSameAs(forcedClassLoader); + softly.assertThat(stateBRoundtripSerialized.getClassLoader()).isSameAs(forcedClassLoader); + }); + } + + private JarState roundtripSerialize(JarState state) throws IOException, ClassNotFoundException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (ObjectOutputStream oOut = new ObjectOutputStream(outputStream)) { + oOut.writeObject(state); + } + try (ObjectInputStream oIn = new ObjectInputStream(new java.io.ByteArrayInputStream(outputStream.toByteArray()))) { + return (JarState) oIn.readObject(); + } + } + +}