Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
Expand Up @@ -13,6 +13,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
### Added
- Add support for Java formatting via [`prince-of-space`](https://github.com/agustafson/prince-of-space). ([#2991](https://github.com/diffplug/spotless/pull/2991))
### Changes
- Replace RDF formatter library `de.atextor:turtle-formatter` (discontinued) with `cool.rdf:cool-rdf-formatter` (its new coordinates); the RDF/turtle formatter now requires Java 25+. ([#2995](https://github.com/diffplug/spotless/pull/2995))
- Bump default `greclipse` version to latest `4.39` -> `4.40`. ([#2989](https://github.com/diffplug/spotless/pull/2989))
- Bump default `tabletest-formatter` version `1.1.1` -> `1.1.2`.

Expand Down
1 change: 0 additions & 1 deletion gradle/special-tests.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,3 @@ special.forEach { tag ->
}
}
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 DiffPlug
* Copyright 2024-2026 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 @@ -86,6 +86,9 @@ public Builder verify() {
}

public RdfFormatterConfig build() {
if (config.turtleFormatterVersion == null) {
config.turtleFormatterVersion = RdfFormatterStep.getDefaultFormatterVersion();
}
return config;
}
}
Expand All @@ -98,7 +101,7 @@ public boolean equals(Object o) {
if (!(o instanceof RdfFormatterConfig)) {
return false;
}
RdfFormatterConfig that = (RdfFormatterConfig) o;
var that = (RdfFormatterConfig) o;
return isFailOnWarning() == that.isFailOnWarning()
&& Objects.equals(turtleFormatterVersion, that.turtleFormatterVersion);
}
Expand Down
20 changes: 15 additions & 5 deletions lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterStep.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 DiffPlug
* Copyright 2024-2026 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 @@ -24,14 +24,18 @@

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

public class RdfFormatterStep implements Serializable {
public static final String LATEST_TURTLE_FORMATTER_VERSION = "1.2.13";
public static final String LATEST_TURTLE_FORMATTER_VERSION = "2.0.1";
@Serial
private static final long serialVersionUID = 1L;

private static final String TURTLE_FORMATTER_COORDINATES = "de.atextor:turtle-formatter";
private static final Jvm.Support<String> JVM_SUPPORT = Jvm.<String> support("cool-rdf-formatter")
.add(25, "2.0.1");

private static final String TURTLE_FORMATTER_COORDINATES = "cool.rdf:cool-rdf-formatter";

private final JarState.Promised jarState;
private final Map<String, String> turtleFormatterStyle;
Expand All @@ -51,14 +55,20 @@ public static State state(RdfFormatterStep step) {

public static RdfFormatterFunc formatterFunc(State state)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
return new RdfFormatterFunc(state);
var formatterFunc = new RdfFormatterFunc(state);
return (RdfFormatterFunc) JVM_SUPPORT.suggestLaterVersionOnError(state.config.getTurtleFormatterVersion(), formatterFunc);
}

public RdfFormatterStep(JarState.Promised jarState, RdfFormatterConfig config,
Map<String, String> turtleFormatterStyle) {
this.jarState = jarState;
this.turtleFormatterStyle = turtleFormatterStyle;
this.config = config;
JVM_SUPPORT.assertFormatterSupported(this.config.getTurtleFormatterVersion());
}

static String getDefaultFormatterVersion() {
return JVM_SUPPORT.getMinimumRequiredFormatterVersion();
}

public static class State implements Serializable {
Expand Down Expand Up @@ -98,7 +108,7 @@ public boolean equals(Object o) {
if (!(o instanceof State)) {
return false;
}
State state = (State) o;
var state = (State) o;
return Objects.equals(getConfig(), state.getConfig()) && Objects.equals(
getTurtleFormatterStyle(), state.getTurtleFormatterStyle())
&& Objects.equals(
Expand Down
41 changes: 18 additions & 23 deletions lib/src/main/java/com/diffplug/spotless/rdf/ReflectionHelper.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 DiffPlug
* Copyright 2024-2026 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 @@ -18,7 +18,6 @@
import java.io.File;
import java.io.StringWriter;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
Expand Down Expand Up @@ -64,7 +63,8 @@ class ReflectionHelper {
private final Class<?> TurtleFormatFormattingStyleClass;
private final Class<?> TurtleFormatFormattingStyleBuilderClass;
private final Class<?> TurtleFormatFormatterClass;
private final Class<?> TurtleFormatKnownPrefix;
private final Class<?> CoolRdfPrefixesClass;
private final Class<?> CoolRdfPrefixClass;

private final Method graphStream;
private final Method graphFindTriple;
Expand Down Expand Up @@ -96,12 +96,11 @@ public ReflectionHelper(RdfFormatterStep.State state)
this.JenaModelFactoryClass = classLoader.loadClass("org.apache.jena.rdf.model.ModelFactory");
this.JenaLangClass = classLoader.loadClass("org.apache.jena.riot.Lang");
this.JenaRDFFormatClass = classLoader.loadClass("org.apache.jena.riot.RDFFormat");
this.TurtleFormatFormatterClass = classLoader.loadClass("de.atextor.turtle.formatter.TurtleFormatter");
this.TurtleFormatFormattingStyleClass = classLoader.loadClass("de.atextor.turtle.formatter.FormattingStyle");
Class<?>[] innerClasses = TurtleFormatFormattingStyleClass.getDeclaredClasses();
this.TurtleFormatFormattingStyleBuilderClass = Arrays.stream(innerClasses)
.filter(c -> "FormattingStyleBuilder".equals(c.getSimpleName())).findFirst().orElseThrow();
this.TurtleFormatKnownPrefix = Arrays.stream(innerClasses).filter(c -> "KnownPrefix".equals(c.getSimpleName())).findFirst().orElseThrow();
this.TurtleFormatFormatterClass = classLoader.loadClass("cool.rdf.formatter.TurtleFormatter");
this.TurtleFormatFormattingStyleClass = classLoader.loadClass("cool.rdf.formatter.FormattingStyle");
this.TurtleFormatFormattingStyleBuilderClass = TurtleFormatFormattingStyleClass.getMethod("builder").getReturnType();
this.CoolRdfPrefixesClass = classLoader.loadClass("cool.rdf.core.Prefixes");
this.CoolRdfPrefixClass = classLoader.loadClass("cool.rdf.core.model.RdfPrefix");
this.getSubject = JenaStatementClass.getMethod("getSubject");
this.getPredicate = JenaStatementClass.getMethod("getPredicate");
this.getObject = JenaStatementClass.getMethod("getObject");
Expand Down Expand Up @@ -391,7 +390,7 @@ private Object instantiate(Type type, String stringRepresentation)
.invoke(this.jenaModelInstance, namespace, localname);
}
}
if (type.equals(TurtleFormatKnownPrefix)) {
if (type.equals(CoolRdfPrefixClass)) {
return getKnownPrefix(stringRepresentation);
}
throw new IllegalArgumentException("Cannot instantiate class %s from string representation %s".formatted(type, stringRepresentation));
Expand All @@ -404,7 +403,7 @@ private String tryToMakeUri(String stringRepresentation)
//could be a known prefix
String prefix = stringRepresentation.substring(0, colonIndex);
Object knownPrefix = getKnownPrefix(prefix);
String base = this.TurtleFormatKnownPrefix.getMethod("iri").invoke(knownPrefix).toString();
String base = this.CoolRdfPrefixClass.getMethod("uri").invoke(knownPrefix).toString();
return base + stringRepresentation.substring(colonIndex + 1);
}
// try to parse a URI - throws an IllegalArgumentException if it is not a URI
Expand All @@ -414,20 +413,16 @@ private String tryToMakeUri(String stringRepresentation)

private Object getKnownPrefix(String stringRepresentation)
throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Field[] fields = TurtleFormatFormattingStyleClass.getDeclaredFields();
List<String> options = new ArrayList<>();
for (Field field : fields) {
if (field.getType().equals(TurtleFormatKnownPrefix)) {
Object knownPrefix = field.get(TurtleFormatFormattingStyleClass);
String prefix = (String) TurtleFormatKnownPrefix.getMethod("prefix").invoke(knownPrefix);
options.add(prefix);
if (stringRepresentation.equals(prefix)) {
return knownPrefix;
}
for (Object knownPrefix : CoolRdfPrefixesClass.getEnumConstants()) {
String prefix = (String) CoolRdfPrefixClass.getMethod("prefix").invoke(knownPrefix);
options.add(prefix);
if (stringRepresentation.equals(prefix)) {
return knownPrefix;
}
}
throw new IllegalArgumentException("Unable to find FormattingStyle.KnownPrefix for prefix '%s'. Options are: %s".formatted(stringRepresentation, options.stream().collect(
Collectors.joining(",\n\t", "\n\t", "\n"))));
throw new IllegalArgumentException("Unable to find a known Cool RDF prefix for '%s'. Options are: %s".formatted(stringRepresentation, options.stream().collect(
Collectors.joining(", "))));
}

private static String[] split(String parameterValueAsString) {
Expand All @@ -439,7 +434,7 @@ private static String[] split(String parameterValueAsString) {

private Method getBuilderMethod(String optionName) {
Method[] allMethods = TurtleFormatFormattingStyleBuilderClass.getDeclaredMethods();
List<Method> methods = Arrays.stream(allMethods).filter(m -> m.getName().equals(optionName))
List<Method> methods = Arrays.stream(allMethods).filter(m -> m.getName().equals(optionName) && m.getParameterCount() == 1)
.collect(
Collectors.toList());
if (methods.isEmpty()) {
Expand Down
1 change: 1 addition & 0 deletions plugin-maven/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
### Added
- Add support for Java formatting via [`prince-of-space`](https://github.com/agustafson/prince-of-space) with the new `<princeOfSpace>` step. ([#2991](https://github.com/diffplug/spotless/pull/2991))
### Changes
- Replace RDF formatter library `de.atextor:turtle-formatter` (discontinued) with `cool.rdf:cool-rdf-formatter` (its new coordinates); the RDF/turtle formatter now requires Java 25+. ([#2995](https://github.com/diffplug/spotless/pull/2995))
- Bump default `greclipse` version to latest `4.39` -> `4.40`. ([#2989](https://github.com/diffplug/spotless/pull/2989))
- Bump default `tabletest-formatter` version `1.1.1` -> `1.1.2`.

Expand Down
19 changes: 11 additions & 8 deletions plugin-maven/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1337,17 +1337,19 @@ List of generic configuration `parameters (type/default)`
the build fails for any of them. You can ignore warnings using this parameter. They will still be logged in the plugin's
output.
* `verify (boolean/true)`: If `true`, the content before and after formatting is parsed to an RDF model and compared for isomorphicity.
* `turtleFormatterVersion (string|RdfFormatterStep.LATEST_TURTLE_FORMATTER_VERSION)`: the version of turtle-formatter to use (see below).
* `turtleFormatterVersion (string|RdfFormatterStep.LATEST_TURTLE_FORMATTER_VERSION)`: the version of Cool RDF Formatter to use (see below).

### Supported RDF formats: only TTL (at the moment)

Formatting TTL is done using [turtle-formatter](https://github.com/atextor/turtle-formatter),
which is highly configurable (have a look at the [Style Documentation](https://github.com/atextor/turtle-formatter?tab=readme-ov-file#customizing-the-style))
Formatting TTL is done using [Cool RDF Formatter](https://github.com/cool-rdf/cool-rdf/tree/main/cool-rdf-formatter),
which is highly configurable (have a look at the [Style Documentation](https://github.com/cool-rdf/cool-rdf/tree/main/cool-rdf-formatter))
and will handle blank nodes the way you'd hope.

The style options can be configured via spotless. Wherever the style wants a URI (for example, for the `predicateOrder`, you can
use the abbreviated form if it is a `FormattingStyle.KnownPrefix` (currently `rdf`, `rdfs`, `xsd`, `owl`, `dcterms`)
Error messages will give you hints. To configure the TTL formatting style, pass the configuration parameters under `<turtle>`
The style options can be configured via spotless. Wherever the style wants a URI (for example, for the `predicateOrder`, you can
use the abbreviated form if it is a known `RdfPrefix` from Cool RDF's `Prefixes` enum.
Error messages will give you hints. To configure the TTL formatting style, pass the configuration parameters under `<turtle>`.
For example, Cool RDF Formatter versions which support `preserveBlankNodeLabelsAndOrdering` (default true) can set it with
`<preserveBlankNodeLabelsAndOrdering>false</preserveBlankNodeLabelsAndOrdering>`.Set it to `false`, to use Cool RDF's default behavior (stable blank node ordering).

### Examples
Minimal:
Expand All @@ -1371,10 +1373,11 @@ Configuring some generic and TTL options:
<format>
<failOnWarning>false</failOnWarning>
<verify>false</verify>
<turtleFormatterVersion>1.2.13</turtleFormatterVersion>
<turtleFormatterVersion>2.0.0</turtleFormatterVersion>
<turtle>
<alignPrefixes>RIGHT</alignPrefixes>
<enableDoubleFormatting>true</enableDoubleFormatting>
<preserveBlankNodeLabelsAndOrdering>false</preserveBlankNodeLabelsAndOrdering>
</turtle>
</format>
</rdf>
Expand All @@ -1383,7 +1386,7 @@ Configuring some generic and TTL options:
### Libraries and versions

RDF parsing is done via [Apache Jena](https://jena.apache.org/) in the version that
[turtle-formatter](https://github.com/atextor/turtle-formatter) depends on (not necessarily the latest).
[Cool RDF Formatter](https://github.com/cool-rdf/cool-rdf/tree/main/cool-rdf-formatter) depends on (not necessarily the latest).

## Protobuf

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.com/ns#> .

ex:aThing ex:has [
ex:value "a" ;
] ;
ex:has [
ex:value "z" ;
] .


Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.com/ns#> .

ex:aThing ex:has [
ex:value "a" ;
] ;
ex:has [
ex:value "z" ;
] .


Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@prefix ex: <http://example.com/ns#> .

ex:aThing ex:has [
ex:value "z" ;
] ;
ex:has [
ex:value "a" ;
] .
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@prefix ex: <http://example.com/ns#> .

_:second ex:value "z" .

ex:aThing ex:has _:first ;
ex:has _:second .

_:first ex:value "a" .
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024-2025 DiffPlug
* Copyright 2024-2026 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,6 +15,9 @@
*/
package com.diffplug.spotless.rdf;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
Expand All @@ -24,7 +27,10 @@
import java.util.stream.Collectors;

import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnJre;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.params.provider.Arguments;
import org.opentest4j.AssertionFailedError;

Expand All @@ -33,6 +39,7 @@
import com.diffplug.spotless.StepHarness;
import com.diffplug.spotless.TestProvisioner;

@EnabledOnJre(JRE.JAVA_25)
public class RdfFormatterTest extends ResourceHarness {

private static FormatterStep forTurtleFormatterVersion(String version) throws ClassNotFoundException {
Expand All @@ -49,17 +56,58 @@ private static FormatterStep forTurtleFormatterVersionAndStyle(String version, M
public RdfFormatterTest() {}

@Test
void testTurtleFormatter_1_2_12_DefaultStyle() throws IOException, ClassNotFoundException {
void testCoolRdfFormatter_2_0_0_DefaultStyle() throws IOException, ClassNotFoundException {
String inputDir = "/rdf/ttl/input/";
String expectedOutputDir = "/rdf/ttl/expected/v1.2.12-default/";
testBeforeAfterFolders(inputDir, expectedOutputDir, StepHarness.forStep(forTurtleFormatterVersion("1.2.12")));
String expectedOutputDir = "/rdf/ttl/expected/v2.0.0-default/";
testBeforeAfterFolders(inputDir, expectedOutputDir, StepHarness.forStep(forTurtleFormatterVersion("2.0.0")));
}

@Test
void testTurtleFormatter_1_2_12_style01() throws IOException, ClassNotFoundException {
void testCoolRdfFormatter_2_0_0_style01() throws IOException, ClassNotFoundException {
String inputDir = "/rdf/ttl/input/";
String expectedOutputDir = "/rdf/ttl/expected/v1.2.12-style01/";
testBeforeAfterFolders(inputDir, expectedOutputDir, StepHarness.forStep(forTurtleFormatterVersionAndStyle("1.2.12", style01())));
String expectedOutputDir = "/rdf/ttl/expected/v2.0.0-style01/";
testBeforeAfterFolders(inputDir, expectedOutputDir, StepHarness.forStep(forTurtleFormatterVersionAndStyle("2.0.0", style01())));
}

@RepeatedTest(3)
void blankNodeOrderingIsStableInCoolRdfFormatter_2_0_1() throws IOException, ClassNotFoundException {
String inputDir = "/rdf/ttl/v2.0.1-stable-blank-node-order/input/";
String expectedOutputDir = "/rdf/ttl/v2.0.1-stable-blank-node-order/expected/";
Map<String, String> style = Map.of("preserveBlankNodeLabelsAndOrdering", "false");
testBeforeAfterFolders(inputDir, expectedOutputDir, StepHarness.forStep(forTurtleFormatterVersionAndStyle("2.0.1", style)));
}

@Test
void blankNodeOrderingIsNotStableInCoolRdfFormatter_2_0_0() throws Exception {
FormatterStep step = forTurtleFormatterVersion("2.0.0");
File file = new File("blank-node-order.ttl");

String alphaThenBeta = """
@prefix ex: <http://example.com/> .

ex:root
ex:child [
ex:id "alpha" ;
ex:value "1"
], [
ex:id "beta" ;
ex:value "2"
] .
""";
String betaThenAlpha = """
@prefix ex: <http://example.com/> .

ex:root
ex:child [
ex:id "beta" ;
ex:value "2"
], [
ex:id "alpha" ;
ex:value "1"
] .
""";

assertThat(step.format(alphaThenBeta, file)).isNotEqualTo(step.format(betaThenAlpha, file));
}

private static @NotNull Map<String, String> defaultStyle() {
Expand Down
Loading