Skip to content

Commit

Permalink
Added support for Maven POM sorting/formatting (#946)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg authored Sep 30, 2021
2 parents a9e8357 + 617abf5 commit 5951f3d
Show file tree
Hide file tree
Showing 18 changed files with 631 additions and 26 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
* Added support for custom JSR223 formatters ([#945](https://github.com/diffplug/spotless/pull/945))
* Added support for formating and sorting Maven POMs ([#946](https://github.com/diffplug/spotless/pull/946))

## [2.17.0] - 2021-09-27
### Added
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ lib('generic.EndWithNewlineStep') +'{{yes}} | {{yes}}
lib('generic.IndentStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('generic.Jsr223Step') +'{{no}} | {{yes}} | {{no}} | {{no}} |',
lib('generic.LicenseHeaderStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
lib('generic.NativeCmdStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |',
lib('generic.NativeCmdStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |',
lib('generic.ReplaceRegexStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('generic.ReplaceStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('generic.TrimTrailingWhitespaceStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
Expand All @@ -66,6 +66,7 @@ lib('kotlin.DiktatStep') +'{{yes}} | {{yes}}
lib('markdown.FreshMarkStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
lib('npm.PrettierFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('npm.TsFmtFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('pom.SortPomStepStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |',
lib('python.BlackStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
lib('scala.ScalaFmtStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
lib('sql.DBeaverSQLFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
Expand All @@ -86,7 +87,7 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}}
| [`generic.IndentStep`](lib/src/main/java/com/diffplug/spotless/generic/IndentStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`generic.Jsr223Step`](lib/src/main/java/com/diffplug/spotless/generic/Jsr223Step.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: |
| [`generic.LicenseHeaderStep`](lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
| [`generic.NativeCmdStep`](lib/src/main/java/com/diffplug/spotless/generic/NativeCmdStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: |
| [`generic.NativeCmdStep`](lib/src/main/java/com/diffplug/spotless/generic/NativeCmdStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: |
| [`generic.ReplaceRegexStep`](lib/src/main/java/com/diffplug/spotless/generic/ReplaceRegexStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`generic.ReplaceStep`](lib/src/main/java/com/diffplug/spotless/generic/ReplaceStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`generic.TrimTrailingWhitespaceStep`](lib/src/main/java/com/diffplug/spotless/generic/TrimTrailingWhitespaceStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
Expand All @@ -104,6 +105,7 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}}
| [`markdown.FreshMarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
| [`npm.PrettierFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`npm.TsFmtFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`pom.SortPomStepStep`](lib/src/main/java/com/diffplug/spotless/pom/SortPomStepStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: |
| [`python.BlackStep`](lib/src/main/java/com/diffplug/spotless/python/BlackStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
| [`scala.ScalaFmtStep`](lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
| [`sql.DBeaverSQLFormatterStep`](lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
Expand Down
20 changes: 20 additions & 0 deletions lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,35 @@ version = rootProject.spotlessChangelog.versionNext
apply from: rootProject.file('gradle/java-setup.gradle')
apply from: rootProject.file('gradle/java-publish.gradle')

def NEEDS_GLUE = [
'sortPom'
]
for (glue in NEEDS_GLUE) {
sourceSets.register(glue) {
compileClasspath += sourceSets.main.output
runtimeClasspath += sourceSets.main.output
java {}
}
}

dependencies {
// zero runtime reqs is a hard requirements for spotless-lib
// if you need a dep, put it in lib-extra
testImplementation "org.junit.jupiter:junit-jupiter:${VER_JUNIT}"
testImplementation "org.assertj:assertj-core:${VER_ASSERTJ}"
testImplementation "com.diffplug.durian:durian-testlib:${VER_DURIAN}"

// used for pom sorting
sortPomCompileOnly 'com.github.ekryd.sortpom:sortpom-sorter:3.0.0'
}

// we'll hold the core lib to a high standard
spotbugs { reportLevel = 'low' } // low|medium|high (low = sensitive to even minor mistakes)

test { useJUnitPlatform() }

jar {
for (glue in NEEDS_GLUE) {
from sourceSets.getByName(glue).output.classesDirs
}
}
83 changes: 60 additions & 23 deletions lib/src/main/java/com/diffplug/spotless/FeatureClassLoader.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016 DiffPlug
* Copyright 2016-2021 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,11 +15,13 @@
*/
package com.diffplug.spotless;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.nio.ByteBuffer;
import java.security.ProtectionDomain;
import java.util.Objects;

import javax.annotation.Nullable;
Expand All @@ -29,37 +31,31 @@
* path of URLs.<br/>
* Features shall be independent from build tools. Hence the class loader of the
* underlying build tool is e.g. skipped during the the search for classes.<br/>
* Only {@link #BUILD_TOOLS_PACKAGES } are explicitly looked up from the class loader of
* the build tool and the provided URLs are ignored. This allows the feature to use
* distinct functionality of the build tool.
*
* For `com.diffplug.spotless.glue.`, classes are redefined from within the lib jar
* but linked against the `Url[]`. This allows us to ship classfiles which function as glue
* code but delay linking/definition to runtime after the user has specified which version
* of the formatter they want.
*
* For `"org.slf4j.` and (`com.diffplug.spotless.` but not `com.diffplug.spotless.extra.`)
* the classes are loaded from the buildToolClassLoader.
*/
class FeatureClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}

/**
* The following packages must be provided by the build tool or the corresponding Spotless plugin:
* <ul>
* <li>org.slf4j - SLF4J API must be provided. If no SLF4J binding is provided, log messages are dropped.</li>
* </ul>
*/
static final List<String> BUILD_TOOLS_PACKAGES = Collections.unmodifiableList(Arrays.asList("org.slf4j."));
// NOTE: if this changes, you need to also update the `JarState.getClassLoader` methods.

private final ClassLoader buildToolClassLoader;

/**
* Constructs a new FeatureClassLoader for the given URLs, based on an {@code URLClassLoader},
* using the system class loader as parent. For {@link #BUILD_TOOLS_PACKAGES }, the build
* tool class loader is used.
* using the system class loader as parent.
*
* @param urls the URLs from which to load classes and resources
* @param buildToolClassLoader The build tool class loader
* @exception SecurityException If a security manager exists and prevents the creation of a class loader.
* @exception NullPointerException if {@code urls} is {@code null}.
*/

FeatureClassLoader(URL[] urls, ClassLoader buildToolClassLoader) {
super(urls, getParentClassLoader());
Objects.requireNonNull(buildToolClassLoader);
Expand All @@ -68,12 +64,53 @@ class FeatureClassLoader extends URLClassLoader {

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
for (String buildToolPackage : BUILD_TOOLS_PACKAGES) {
if (name.startsWith(buildToolPackage)) {
return buildToolClassLoader.loadClass(name);
if (name.startsWith("com.diffplug.spotless.glue.")) {
String path = name.replace('.', '/') + ".class";
URL url = findResource(path);
if (url == null) {
throw new ClassNotFoundException(name);
}
try {
return defineClass(name, urlToByteBuffer(url), (ProtectionDomain) null);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else if (useBuildToolClassLoader(name)) {
return buildToolClassLoader.loadClass(name);
} else {
return super.findClass(name);
}
}

private static boolean useBuildToolClassLoader(String name) {
if (name.startsWith("org.slf4j.")) {
return true;
} else if (!name.startsWith("com.diffplug.spotless.extra") && name.startsWith("com.diffplug.spotless.")) {
return true;
} else {
return false;
}
}

@Override
public URL findResource(String name) {
URL resource = super.findResource(name);
if (resource != null) {
return resource;
}
return buildToolClassLoader.getResource(name);
}

private static ByteBuffer urlToByteBuffer(URL url) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[1024];
InputStream inputStream = url.openStream();
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
return super.findClass(name);
buffer.flush();
return ByteBuffer.wrap(buffer.toByteArray());
}

/**
Expand Down
55 changes: 55 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/pom/SortPomCfg.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2021 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.pom;

import java.io.Serializable;

// Class and members must be public, otherwise we get failed to access class com.diffplug.spotless.pom.SortPomInternalState from class com.diffplug.spotless.pom.SortPomFormatterFunc (com.diffplug.spotless.pom.SortPomInternalState is in unnamed module of loader org.codehaus.plexus.classworlds.realm.ClassRealm @682bd3c4; com.diffplug.spotless.pom.SortPomFormatterFunc is in unnamed module of loader com.diffplug.spotless.pom.DelegatingClassLoader @573284a5)
public class SortPomCfg implements Serializable {
private static final long serialVersionUID = 1L;

public String encoding = "UTF-8";

public String lineSeparator = System.getProperty("line.separator");

public boolean expandEmptyElements = true;

public boolean spaceBeforeCloseEmptyElement = false;

public boolean keepBlankLines = true;

public int nrOfIndentSpace = 2;

public boolean indentBlankLines = false;

public boolean indentSchemaLocation = false;

public String predefinedSortOrder = "recommended_2008_06";

public String sortOrderFile = null;

public String sortDependencies = null;

public String sortDependencyExclusions = null;

public String sortPlugins = null;

public boolean sortProperties = false;

public boolean sortModules = false;

public boolean sortExecutions = false;
}
54 changes: 54 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/pom/SortPomStep.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2021 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.pom;

import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

import com.diffplug.spotless.FormatterFunc;
import com.diffplug.spotless.FormatterStep;
import com.diffplug.spotless.JarState;
import com.diffplug.spotless.Provisioner;

public class SortPomStep {
public static final String NAME = "sortPom";

private SortPomStep() {}

private SortPomCfg cfg;

public static FormatterStep create(SortPomCfg cfg, Provisioner provisioner) {
return FormatterStep.createLazy(NAME, () -> new State(cfg, provisioner), State::createFormat);
}

static class State implements Serializable {
SortPomCfg cfg;
JarState jarState;

public State(SortPomCfg cfg, Provisioner provisioner) throws IOException {
this.cfg = cfg;
this.jarState = JarState.from("com.github.ekryd.sortpom:sortpom-sorter:3.0.0", provisioner);
}

FormatterFunc createFormat() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<?> formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.pom.SortPomFormatterFunc");
Constructor<?> constructor = formatterFunc.getConstructor(SortPomCfg.class);
return (FormatterFunc) constructor.newInstance(cfg);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2021 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.glue.pom;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.logging.Logger;

import org.apache.commons.io.IOUtils;

import com.diffplug.spotless.FormatterFunc;
import com.diffplug.spotless.pom.SortPomCfg;

import sortpom.SortPomImpl;
import sortpom.logger.SortPomLogger;
import sortpom.parameter.PluginParameters;

public class SortPomFormatterFunc implements FormatterFunc {
private static final Logger logger = Logger.getLogger(SortPomFormatterFunc.class.getName());
private final SortPomCfg cfg;

public SortPomFormatterFunc(SortPomCfg cfg) {
this.cfg = cfg;
}

@Override
public String apply(String input) throws Exception {
// SortPom expects a file to sort, so we write the inpout into a temporary file
File pom = File.createTempFile("pom", ".xml");
pom.deleteOnExit();
IOUtils.write(input, new FileOutputStream(pom), cfg.encoding);
SortPomImpl sortPom = new SortPomImpl();
sortPom.setup(new MySortPomLogger(), PluginParameters.builder()
.setPomFile(pom)
.setFileOutput(false, null, null, false)
.setEncoding(cfg.encoding)
.setFormatting(cfg.lineSeparator, cfg.expandEmptyElements, cfg.spaceBeforeCloseEmptyElement, cfg.keepBlankLines)
.setIndent(cfg.nrOfIndentSpace, cfg.indentBlankLines, cfg.indentSchemaLocation)
.setSortOrder(cfg.sortOrderFile, cfg.predefinedSortOrder)
.setSortEntities(cfg.sortDependencies, cfg.sortDependencyExclusions, cfg.sortPlugins, cfg.sortProperties, cfg.sortModules, cfg.sortExecutions)
.setTriggers(false)
.build());
sortPom.sortPom();
return IOUtils.toString(new FileInputStream(pom), cfg.encoding);
}

private static class MySortPomLogger implements SortPomLogger {
@Override
public void warn(String content) {
logger.warning(content);
}

@Override
public void info(String content) {
logger.info(content);
}

@Override
public void error(String content) {
logger.severe(content);
}
}
}
Loading

0 comments on commit 5951f3d

Please sign in to comment.