Skip to content

feat: supply eclipse formatter settings as XML content #2361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (

## [Unreleased]
### Changed
* Allow setting Eclipse XML config from a string, not only from files ([#2361](https://github.com/diffplug/spotless/pull/2361))
* Use palantir-java-format 2.57.0 on Java 21. ([#2447](https://github.com/diffplug/spotless/pull/2447))
* Re-try `npm install` with `--prefer-online` after `ERESOLVE` error. ([#2448](https://github.com/diffplug/spotless/pull/2448))

Original file line number Diff line number Diff line change
@@ -56,6 +56,7 @@ public abstract class EquoBasedStepBuilder {
private String formatterVersion;
private Iterable<File> settingsFiles = new ArrayList<>();
private List<String> settingProperties = new ArrayList<>();
private List<String> settingXml = new ArrayList<>();
private Map<String, String> p2Mirrors = Map.of();
private File cacheDirectory;

@@ -86,6 +87,10 @@ public void setPropertyPreferences(List<String> propertyPreferences) {
this.settingProperties = propertyPreferences;
}

public void setXmlPreferences(List<String> settingXml) {
this.settingXml = settingXml;
}

public void setP2Mirrors(Map<String, String> p2Mirrors) {
this.p2Mirrors = Map.copyOf(p2Mirrors);
}
@@ -119,7 +124,7 @@ protected void addPlatformRepo(P2Model model, String version) {

/** Returns the FormatterStep (whose state will be calculated lazily). */
public FormatterStep build() {
var roundtrippableState = new EquoStep(formatterVersion, settingProperties, FileSignature.promise(settingsFiles), JarState.promise(() -> {
var roundtrippableState = new EquoStep(formatterVersion, settingProperties, settingXml, FileSignature.promise(settingsFiles), JarState.promise(() -> {
P2QueryResult query;
try {
if (null != cacheDirectory) {
@@ -174,23 +179,26 @@ static class EquoStep implements Serializable {
private final JarState.Promised jarPromise;
private final ImmutableMap<String, String> stepProperties;
private List<String> settingProperties;
private List<String> settingXml;

EquoStep(
String semanticVersion,
List<String> settingProperties,
List<String> settingXml,
FileSignature.Promised settingsPromise,
JarState.Promised jarPromise,
ImmutableMap<String, String> stepProperties) {

this.semanticVersion = semanticVersion;
this.settingProperties = Optional.ofNullable(settingProperties).orElse(new ArrayList<>());
this.settingXml = Optional.ofNullable(settingXml).orElse(new ArrayList<>());
this.settingsPromise = settingsPromise;
this.jarPromise = jarPromise;
this.stepProperties = stepProperties;
}

private State state() {
return new State(semanticVersion, jarPromise.get(), settingProperties, settingsPromise.get(), stepProperties);
return new State(semanticVersion, jarPromise.get(), settingProperties, settingXml, settingsPromise.get(), stepProperties);
}
}

@@ -205,11 +213,13 @@ public static class State implements Serializable {
final FileSignature settingsFiles;
final ImmutableMap<String, String> stepProperties;
private List<String> settingProperties;
private List<String> settingXml;

public State(String semanticVersion, JarState jarState, List<String> settingProperties, FileSignature settingsFiles, ImmutableMap<String, String> stepProperties) {
public State(String semanticVersion, JarState jarState, List<String> settingProperties, List<String> settingXml, FileSignature settingsFiles, ImmutableMap<String, String> stepProperties) {
this.semanticVersion = semanticVersion;
this.jarState = jarState;
this.settingProperties = Optional.ofNullable(settingProperties).orElse(new ArrayList<>());
this.settingXml = Optional.ofNullable(settingXml).orElse(new ArrayList<>());
this.settingsFiles = settingsFiles;
this.stepProperties = stepProperties;
}
@@ -225,7 +235,8 @@ public String getSemanticVersion() {
public Properties getPreferences() {
FormatterProperties fromFiles = FormatterProperties.from(settingsFiles.files());
FormatterProperties fromPropertiesContent = FormatterProperties.fromPropertiesContent(settingProperties);
return FormatterProperties.merge(fromFiles.getProperties(), fromPropertiesContent.getProperties()).getProperties();
FormatterProperties fromXmlContent = FormatterProperties.fromXmlContent(settingXml);
return FormatterProperties.merge(fromFiles.getProperties(), fromPropertiesContent.getProperties(), fromXmlContent.getProperties()).getProperties();
}

public ImmutableMap<String, String> getStepProperties() {
67 changes: 54 additions & 13 deletions lib/src/main/java/com/diffplug/spotless/FormatterProperties.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.
@@ -20,6 +20,7 @@
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@@ -28,6 +29,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@@ -90,6 +92,25 @@ public static FormatterProperties fromPropertiesContent(Iterable<String> content
return properties;
}

public static FormatterProperties fromXmlContent(final Iterable<String> content) throws IllegalArgumentException {
final List<String> nonNullElements = toNullHostileList(content);
final FormatterProperties properties = new FormatterProperties();
nonNullElements.forEach(contentElement -> {
try {
final Properties newSettings = FileParser.XML.executeXmlContent(contentElement);
properties.properties.putAll(newSettings);
} catch (IOException | IllegalArgumentException exception) {
String message = String.format("Failed to add preferences from XML:%n%s%n", contentElement);
final String detailedMessage = exception.getMessage();
if (null != detailedMessage) {
message += String.format(" %s", detailedMessage);
}
throw new IllegalArgumentException(message, exception);
}
});
return properties;
}

public static FormatterProperties merge(Properties... properties) {
FormatterProperties merged = new FormatterProperties();
List.of(properties).stream().forEach((source) -> merged.properties.putAll(source));
@@ -139,20 +160,40 @@ protected Properties execute(final File file) throws IOException, IllegalArgumen
}
return properties;
}

@Override
protected Properties executeXmlContent(String content) throws IOException, IllegalArgumentException {
throw new RuntimeException("Not implemented");
}
},

XML("xml") {
@Override
protected Properties execute(final File file) throws IOException, IllegalArgumentException {
Node rootNode = getRootNode(file);
return executeWithSupplier(() -> {
try {
return new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new RuntimeException("File not found: " + file, e);
}
});
}

@Override
protected Properties executeXmlContent(String content) throws IOException, IllegalArgumentException {
return executeWithSupplier(() -> new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
}

private Properties executeWithSupplier(Supplier<InputStream> isSupplier) throws IOException, IllegalArgumentException {
Node rootNode = getRootNode(isSupplier.get());
String nodeName = rootNode.getNodeName();
if (null == nodeName) {
throw new IllegalArgumentException("XML document does not contain a root node.");
}
return XmlParser.parse(file, rootNode);
return XmlParser.parse(isSupplier.get(), rootNode);
}

private Node getRootNode(final File file) throws IOException, IllegalArgumentException {
private Node getRootNode(final InputStream is) throws IOException, IllegalArgumentException {
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
/*
@@ -166,7 +207,7 @@ private Node getRootNode(final File file) throws IOException, IllegalArgumentExc
*/
dbf.setFeature(LOAD_EXTERNAL_DTD_PROP, false);
DocumentBuilder db = dbf.newDocumentBuilder();
return db.parse(file).getDocumentElement();
return db.parse(is).getDocumentElement();
} catch (SAXException | ParserConfigurationException e) {
throw new IllegalArgumentException("File has no valid XML syntax.", e);
}
@@ -186,6 +227,8 @@ private Node getRootNode(final File file) throws IOException, IllegalArgumentExc

protected abstract Properties execute(File file) throws IOException, IllegalArgumentException;

protected abstract Properties executeXmlContent(String content) throws IOException, IllegalArgumentException;

public static Properties parse(final File file) throws IOException, IllegalArgumentException {
String fileNameExtension = getFileNameExtension(file);
for (FileParser parser : FileParser.values()) {
@@ -211,19 +254,17 @@ private static String getFileNameExtension(File file) {
private enum XmlParser {
PROPERTIES("properties") {
@Override
protected Properties execute(final File xmlFile, final Node rootNode)
protected Properties execute(final InputStream xmlFile, final Node rootNode)
throws IOException, IllegalArgumentException {
final Properties properties = new Properties();
try (InputStream xmlInput = new FileInputStream(xmlFile)) {
properties.loadFromXML(xmlInput);
}
properties.loadFromXML(xmlFile);
return properties;
}
},

PROFILES("profiles") {
@Override
protected Properties execute(File file, Node rootNode) throws IOException, IllegalArgumentException {
protected Properties execute(InputStream file, Node rootNode) throws IOException, IllegalArgumentException {
final Properties properties = new Properties();
Node firstProfile = getSingleProfile(rootNode);
for (Object settingObj : getChildren(firstProfile, "setting")) {
@@ -285,14 +326,14 @@ public String toString() {
return this.rootNodeName;
}

protected abstract Properties execute(File file, Node rootNode) throws IOException, IllegalArgumentException;
protected abstract Properties execute(InputStream is, Node rootNode) throws IOException, IllegalArgumentException;

public static Properties parse(final File file, final Node rootNode)
public static Properties parse(final InputStream is, final Node rootNode)
throws IOException, IllegalArgumentException {
String rootNodeName = rootNode.getNodeName();
for (XmlParser parser : XmlParser.values()) {
if (parser.rootNodeName.equals(rootNodeName)) {
return parser.execute(file, rootNode);
return parser.execute(is, rootNode);
}
}
String msg = String.format("The XML root node '%1$s' is not part of the supported root nodes [%2$s].",
18 changes: 15 additions & 3 deletions plugin-gradle/README.md
Original file line number Diff line number Diff line change
@@ -262,10 +262,14 @@ spotless {
eclipse()
// optional: you can specify a specific version and/or config file
eclipse('4.26').configFile('eclipse-prefs.xml')
// Or supply the configuration as a string
// Or supply the configuration properties as a string
eclipse('4.26').configProperties("""
...
""")
// Or supply the configuration XML as a string
eclipse('4.26').configXml("""
...
""")
// if the access to the p2 repositories is restricted, mirrors can be
// specified using a URI prefix map as follows:
eclipse().withP2Mirrors(['https://download.eclipse.org/eclipse/updates/4.29/':'https://some.internal.mirror/4-29-updates-p2/'])
@@ -422,10 +426,14 @@ spotless {
greclipse()
// optional: you can specify a specific version or config file(s), version matches the Eclipse Platform
greclipse('4.26').configFile('spotless.eclipseformat.xml', 'org.codehaus.groovy.eclipse.ui.prefs')
// Or supply the configuration as a string
// Or supply the configuration properties as a string
greclipse('4.26').configProperties("""
...
""")
// Or supply the configuration XML as a string
greclipse('4.26').configXml("""
...
""")
```

Groovy-Eclipse formatting errors/warnings lead per default to a build failure. This behavior can be changed by adding the property/key value `ignoreFormatterProblems=true` to a configuration file. In this scenario, files causing problems, will not be modified by this formatter step.
@@ -580,10 +588,14 @@ spotles {
cpp {
// version and configFile are both optional
eclipseCdt('4.13.0').configFile('eclipse-cdt.xml')
// Or supply the configuration as a string
// Or supply the configuration properties as a string
eclipseCdt('4.13.0').configProperties("""
...
""")
// Or supply the configuration XML as a string
eclipseCdt('4.13.0').configXml("""
...
""")
}
}
```
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 DiffPlug
* Copyright 2023-2025 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -80,6 +80,13 @@ public GrEclipseConfig configProperties(String... configs) {
return this;
}

public GrEclipseConfig configXml(String... configs) {
requireElementsNonNull(configs);
builder.setXmlPreferences(List.of(configs));
extension.replaceStep(builder.build());
return this;
}

public GrEclipseConfig withP2Mirrors(Map<String, String> mirrors) {
builder.setP2Mirrors(mirrors);
extension.replaceStep(builder.build());
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.
@@ -69,6 +69,13 @@ public EclipseConfig configProperties(String... configs) {
return this;
}

public EclipseConfig configXml(String... configs) {
requireElementsNonNull(configs);
builder.setXmlPreferences(List.of(configs));
replaceStep(builder.build());
return this;
}

public EclipseConfig withP2Mirrors(Map<String, String> mirrors) {
builder.setP2Mirrors(mirrors);
replaceStep(builder.build());
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.
@@ -312,6 +312,13 @@ public EclipseConfig configProperties(String... configs) {
return this;
}

public EclipseConfig configXml(String... configs) {
requireElementsNonNull(configs);
builder.setXmlPreferences(List.of(configs));
replaceStep(builder.build());
return this;
}

public EclipseConfig sortMembersDoNotSortFields(boolean doNotSortFields) {
builder.sortMembersDoNotSortFields(doNotSortFields);
replaceStep(builder.build());
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.
@@ -21,7 +21,7 @@

class JavaEclipseTest extends GradleIntegrationHarness {
@Test
void settingsWithContentWithoutFile() throws IOException {
void settingsWithProprtiesContent() throws IOException {
setFile("build.gradle").toLines(
"plugins {",
" id 'com.diffplug.spotless'",
@@ -37,4 +37,27 @@ void settingsWithContentWithoutFile() throws IOException {

gradleRunner().withArguments("spotlessApply").build();
}

@Test
void settingsWithXmlContent() throws IOException {
setFile("build.gradle").toLines(
"plugins {",
" id 'com.diffplug.spotless'",
" id 'java'",
"}",
"repositories { mavenCentral() }",
"",
"spotless {",
" java { eclipse().configXml(\"\"\"",
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>",
"<profiles version=\"12\">",
" <profile kind=\"CodeFormatterProfile\" name=\"Spotless\" version=\"12\">",
" <setting id=\"valid_line_oriented.prefs.string\" value=\"string\" />",
" </profile>",
"</profiles>",
"\"\"\") }",
"}");

gradleRunner().withArguments("spotlessApply").build();
}
}
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.
@@ -16,6 +16,7 @@
package com.diffplug.spotless;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.io.File;
import java.io.IOException;
@@ -52,8 +53,12 @@ private List<String> validPropertiesResources() {
return List.of(VALID_SETTINGS_RESOURCES).stream().filter(it -> !it.endsWith(".xml")).collect(Collectors.toList());
}

private List<String> invalidPropertiesResources() {
return List.of(INVALID_SETTINGS_RESOURCES).stream().filter(it -> !it.endsWith(".xml")).collect(Collectors.toList());
private List<String> validXmlResources() {
return List.of(VALID_SETTINGS_RESOURCES).stream().filter(it -> it.endsWith(".xml")).collect(Collectors.toList());
}

private List<String> invalidXmlResources() {
return List.of(INVALID_SETTINGS_RESOURCES).stream().filter(it -> it.endsWith(".xml")).collect(Collectors.toList());
}

private static final String[] VALID_VALUES = {
@@ -86,6 +91,18 @@ void differentPropertyFileTypes_content_properties() throws IOException {
}
}

@Test
void differentPropertyFileTypes_content_xml() throws IOException {
for (String settingsResource : validXmlResources()) {
File settingsFile = createTestFile(settingsResource);
String content = Files.readString(settingsFile.toPath());
FormatterProperties preferences = FormatterProperties.fromXmlContent(List.of(content));
assertFor(preferences)
.containsSpecificValuesOf(settingsFile)
.containsCommonValueOf(settingsFile);
}
}

@Test
void multiplePropertyFiles() throws IOException {
LinkedList<File> settingsFiles = new LinkedList<>();
@@ -116,6 +133,22 @@ void multiplePropertyFiles_content_properties() throws IOException {
.containsCommonValueOf(settingsFiles.getLast());
}

@Test
void multiplePropertyFiles_content_xml() throws IOException {
LinkedList<File> settingsFiles = new LinkedList<>();
LinkedList<String> content = new LinkedList<>();
for (String settingsResource : validXmlResources()) {
File settingsFile = createTestFile(settingsResource);
content.add(Files.readString(settingsFile.toPath()));
settingsFiles.add(settingsFile);
}
FormatterProperties preferences = FormatterProperties.fromXmlContent(content);
/* Settings are loaded / overridden in the sequence they are configured. */
assertFor(preferences)
.containsSpecificValuesOf(settingsFiles)
.containsCommonValueOf(settingsFiles.getLast());
}

@Test
void invalidPropertyFiles() throws IOException {
for (String settingsResource : INVALID_SETTINGS_RESOURCES) {
@@ -136,22 +169,10 @@ void invalidPropertyFiles() throws IOException {
}

@Test
void invalidPropertyFiles_content_properties() throws IOException {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this test, the list from invalidPropertiesResources() was empty because there are no such invalid property files.

for (String settingsResource : invalidPropertiesResources()) {
File settingsFile = createTestFile(settingsResource);
String content = Files.readString(settingsFile.toPath());
boolean exceptionCaught = false;
try {
FormatterProperties.fromPropertiesContent(List.of(content));
} catch (IllegalArgumentException ex) {
exceptionCaught = true;
assertThat(ex.getMessage())
.as("IllegalArgumentException does not contain absolute path of file '%s'", settingsFile.getName())
.contains(settingsFile.getAbsolutePath());
}
assertThat(exceptionCaught)
.as("No IllegalArgumentException thrown when parsing '%s'", settingsFile.getName())
.isTrue();
void invalidPropertyFiles_content_xml() throws IOException {
for (String settingsResource : invalidXmlResources()) {
IllegalArgumentException actual = assertThrows(IllegalArgumentException.class, () -> FormatterProperties.fromXmlContent(List.of(ResourceHarness.getTestResource(settingsResource))));
assertThat(actual.getMessage()).startsWith("Failed to add preferences from XML:");
}
}