Skip to content
Draft
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (

## [Unreleased]

## Changes
- Replace RDF formatter library `de.atextor:turtle-formatter` (discontinued) with `cool.rdf:cool-rdf-formatter` (its new coordinates).

## [4.8.0] - 2026-06-29
### Added
- Add support for custom string format for license header copyright year via `yearStringFormat()`. ([#2965](https://github.com/diffplug/spotless/pull/2965))
Expand Down
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 @@ -27,11 +27,11 @@
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.0";
@Serial
private static final long serialVersionUID = 1L;

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

private final JarState.Promised jarState;
private final Map<String, String> turtleFormatterStyle;
Expand Down
35 changes: 16 additions & 19 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,13 @@ 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");
this.TurtleFormatFormatterClass = classLoader.loadClass("cool.rdf.formatter.TurtleFormatter");
this.TurtleFormatFormattingStyleClass = classLoader.loadClass("cool.rdf.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.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 +392,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 +405,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 +415,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 Down
21 changes: 12 additions & 9 deletions plugin-maven/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ output = [
output = prefixDelimiterReplace(input, 'https://{{org}}.github.io/{{name}}/javadoc/spotless-plugin-maven/', '/', versionLast)
-->

Spotless is a general-purpose formatting plugin used by [6,000 projects on GitHub (Jan 2023)](https://github.com/search?l=Maven+POM&q=spotless&type=Code). It is completely à la carte, but also includes powerful "batteries-included" if you opt-in. Plugin requires a version of Maven higher or equal to 3.1.0.
Spotless is a general-purpose formatting plugin used by [6,000 projects on GitHub (Jan 2023)](https://github.com/search?l=Maven+POM&q=spotless&type=Code). It is completely la carte, but also includes powerful "batteries-included" if you opt-in. Plugin requires a version of Maven higher or equal to 3.1.0.

To people who use your build, it looks like this:

Expand Down Expand Up @@ -1320,17 +1320,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 @@ -1354,10 +1356,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 @@ -1366,7 +1369,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
@@ -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 Down Expand Up @@ -49,17 +52,50 @@ 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())));
}

@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