Skip to content

Commit

Permalink
feat: allow overriding JarSate classloader (to enable cli) (#2427)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg authored Feb 20, 2025
2 parents a410e9f + 06c6ca8 commit d88a76e
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 34 additions & 4 deletions lib/src/main/java/com/diffplug/spotless/JarState.java
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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.
* <br/>
* 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.
* <br/>
* 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);
}
}
107 changes: 107 additions & 0 deletions lib/src/test/java/com/diffplug/spotless/JarStateTest.java
Original file line number Diff line number Diff line change
@@ -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();
}
}

}

0 comments on commit d88a76e

Please sign in to comment.