diff --git a/lib/build.gradle b/lib/build.gradle index 115d27195..0cf07f953 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -14,7 +14,8 @@ def NEEDS_GLUE = [ 'ktlint', 'flexmark', 'diktat', - 'scalafmt' + 'scalafmt', + 'jackson' ] for (glue in NEEDS_GLUE) { sourceSets.register(glue) { @@ -55,6 +56,9 @@ dependencies { palantirJavaFormatCompileOnly 'com.palantir.javaformat:palantir-java-format:1.1.0' // this version needs to stay compilable against Java 8 for CI Job testNpm + // used jackson-based formatters + jacksonCompileOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.4' + String VER_KTFMT = '0.42' ktfmtCompileOnly "com.facebook:ktfmt:$VER_KTFMT" String VER_KTLINT_GOOGLE_JAVA_FORMAT = '1.7' // for JDK 8 compatibility diff --git a/lib/src/jackson/java/com/diffplug/spotless/glue/yaml/YamlJacksonFormatterFunc.java b/lib/src/jackson/java/com/diffplug/spotless/glue/yaml/YamlJacksonFormatterFunc.java new file mode 100644 index 000000000..604ad048c --- /dev/null +++ b/lib/src/jackson/java/com/diffplug/spotless/glue/yaml/YamlJacksonFormatterFunc.java @@ -0,0 +1,99 @@ +/* + * Copyright 2021-2023 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.yaml; + +import java.io.IOException; +import java.util.List; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import com.diffplug.spotless.FormatterFunc; + +public class YamlJacksonFormatterFunc implements FormatterFunc { + private List enabledFeatures; + private List disabledFeatures; + + // private static final Logger logger = LoggerFactory.getLogger(YamlJacksonFormatterFunc.class); + + public YamlJacksonFormatterFunc(List enabledFeatures, List disabledFeatures) { + this.enabledFeatures = enabledFeatures; + this.disabledFeatures = disabledFeatures; + } + + @Override + public String apply(String input) throws Exception { + ObjectMapper objectMapper = makeObjectMapper(); + + return format(objectMapper, input); + } + + protected ObjectMapper makeObjectMapper() { + YAMLFactory yamlFactory = new YAMLFactory(); + ObjectMapper objectMapper = new ObjectMapper(yamlFactory); + + // Configure the ObjectMapper + // https://github.com/FasterXML/jackson-databind#commonly-used-features + for (String rawFeature : enabledFeatures) { + // https://stackoverflow.com/questions/3735927/java-instantiating-an-enum-using-reflection + SerializationFeature feature = SerializationFeature.valueOf(rawFeature); + + objectMapper.enable(feature); + } + + for (String rawFeature : disabledFeatures) { + // https://stackoverflow.com/questions/3735927/java-instantiating-an-enum-using-reflection + SerializationFeature feature = SerializationFeature.valueOf(rawFeature); + + objectMapper.disable(feature); + } + return objectMapper; + } + + protected String format(ObjectMapper objectMapper, String input) throws IllegalArgumentException, IOException { + // We may consider adding manually an initial '---' prefix to help management of multiple documents + // if (!input.trim().startsWith("---")) { + // input = "---" + "\n" + input; + // } + + try { + // https://stackoverflow.com/questions/25222327/deserialize-pojos-from-multiple-yaml-documents-in-a-single-file-in-jackson + // https://github.com/FasterXML/jackson-dataformats-text/issues/66#issuecomment-375328648 + // 2023-01: For now, we get 'Cannot deserialize value of type `com.fasterxml.jackson.databind.node.ObjectNode` from Array value' + // JsonParser yamlParser = objectMapper.getFactory().createParser(input); + // List docs = objectMapper.readValues(yamlParser, ObjectNode.class).readAll(); + // return objectMapper.writeValueAsString(docs); + + // 2023-01: This returns JSON instead of YAML + // This will transit with a JsonNode + // A JsonNode may keep the comments from the input node + // JsonNode jsonNode = objectMapper.readTree(input); + //Not 'toPrettyString' as one could require no INDENT_OUTPUT + // return jsonNode.toPrettyString(); + ObjectNode objectNode = objectMapper.readValue(input, ObjectNode.class); + return objectMapper.writeValueAsString(objectNode); + } catch (JsonProcessingException e) { + throw new AssertionError("Unable to format YAML. input='" + input + "'", e); + } + } + + // Spotbugs + private static class ObjectNodeTypeReference extends TypeReference {} +} diff --git a/lib/src/main/java/com/diffplug/spotless/yaml/YamlJacksonStep.java b/lib/src/main/java/com/diffplug/spotless/yaml/YamlJacksonStep.java index b29c6ad26..c0c660694 100644 --- a/lib/src/main/java/com/diffplug/spotless/yaml/YamlJacksonStep.java +++ b/lib/src/main/java/com/diffplug/spotless/yaml/YamlJacksonStep.java @@ -31,9 +31,10 @@ /** * Simple YAML formatter which reformats the file according to Jackson YAMLFactory. */ +// https://stackoverflow.com/questions/14515994/convert-json-string-to-pretty-print-json-output-using-jackson public final class YamlJacksonStep { - private static final String MAVEN_COORDINATE = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:"; - private static final String DEFAULT_VERSION = "2.13.4"; + static final String MAVEN_COORDINATE = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:"; + static final String DEFAULT_VERSION = "2.13.4"; public static String defaultVersion() { return DEFAULT_VERSION; @@ -80,8 +81,8 @@ FormatterFunc toFormatter() { Method enableFeature; Method disableFeature; - Method stringToObject; - Method objectToString; + Method stringToNode; + Method nodeToString; try { ClassLoader classLoader = jarState.getClassLoader(); jsonFactoryClass = classLoader.loadClass("com.fasterxml.jackson.core.JsonFactory"); @@ -97,8 +98,18 @@ FormatterFunc toFormatter() { disableFeature = objectMapperClass.getMethod("disable", serializationFeatureClass); } - stringToObject = objectMapperClass.getMethod("readValue", String.class, Class.class); - objectToString = objectMapperClass.getMethod("writeValueAsString", Object.class); + // https://stackoverflow.com/questions/25222327/deserialize-pojos-from-multiple-yaml-documents-in-a-single-file-in-jackson + // List docs = mapper + // .readValues(yamlParser, new TypeReference {}) + // .readAll(); + + Class jsonNodeClass = classLoader.loadClass("com.fasterxml.jackson.databind.JsonNode"); + + // This will transit with a JsonNode + // A JsonNode may keep the comments from the input node + stringToNode = objectMapperClass.getMethod("readTree", String.class); + // Not 'toPrettyString' as one could require no INDENT_OUTPUT + nodeToString = jsonNodeClass.getMethod("toPrettyString"); } catch (ClassNotFoundException | NoSuchMethodException e) { throw new IllegalStateException("There was a problem preparing org.json dependencies", e); } @@ -125,15 +136,15 @@ FormatterFunc toFormatter() { disableFeature.invoke(objectMapper, indentOutput); } - return format(objectMapper, stringToObject, objectToString, s); + return format(objectMapper, stringToNode, nodeToString, s); }; } - private String format(Object objectMapper, Method stringToObject, Method objectToString, String s) + private String format(Object objectMapper, Method stringToNode, Method nodeToString, String s) throws IllegalAccessException, IllegalArgumentException { try { - Object parsed = stringToObject.invoke(objectMapper, s, Object.class); - return (String) objectToString.invoke(objectMapper, parsed); + Object node = stringToNode.invoke(objectMapper, s); + return (String) nodeToString.invoke(node); } catch (InvocationTargetException ex) { throw new AssertionError("Unable to format YAML", ex.getCause()); } diff --git a/lib/src/main/java/com/diffplug/spotless/yaml/YamlJacksonV2Step.java b/lib/src/main/java/com/diffplug/spotless/yaml/YamlJacksonV2Step.java new file mode 100644 index 000000000..f9b1d6b7d --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/yaml/YamlJacksonV2Step.java @@ -0,0 +1,77 @@ +/* + * Copyright 2021-2023 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.yaml; + +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Provisioner; + +public class YamlJacksonV2Step { + private YamlJacksonV2Step() {} + + public static String defaultVersion() { + return YamlJacksonStep.DEFAULT_VERSION; + } + + public static FormatterStep create(List enabledFeatures, + List disabledFeatures, + String jacksonVersion, + Provisioner provisioner) { + Objects.requireNonNull(provisioner, "provisioner cannot be null"); + return FormatterStep.createLazy("yaml", + () -> new State(enabledFeatures, disabledFeatures, jacksonVersion, provisioner), + State::toFormatter); + } + + public static FormatterStep create(Provisioner provisioner) { + return create(Arrays.asList("INDENT_OUTPUT"), Arrays.asList(), defaultVersion(), provisioner); + } + + private static final class State implements Serializable { + private static final long serialVersionUID = 1L; + + private final List enabledFeatures; + private final List disabledFeatures; + + private final JarState jarState; + + private State(List enabledFeatures, + List disabledFeatures, + String jacksonVersion, + Provisioner provisioner) throws IOException { + this.enabledFeatures = enabledFeatures; + this.disabledFeatures = disabledFeatures; + + this.jarState = JarState.from(YamlJacksonStep.MAVEN_COORDINATE + jacksonVersion, provisioner); + } + + FormatterFunc toFormatter() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, + InstantiationException, IllegalAccessException { + Class formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.yaml.YamlJacksonFormatterFunc"); + Constructor constructor = formatterFunc.getConstructor(List.class, List.class); + return (FormatterFunc) constructor.newInstance(enabledFeatures, disabledFeatures); + } + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Jackson.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Jackson.java index 2bc7617a3..28fdf5809 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Jackson.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Jackson.java @@ -24,6 +24,7 @@ import com.diffplug.spotless.maven.FormatterStepConfig; import com.diffplug.spotless.maven.FormatterStepFactory; import com.diffplug.spotless.yaml.YamlJacksonStep; +import com.diffplug.spotless.yaml.YamlJacksonV2Step; public class Jackson implements FormatterStepFactory { @@ -40,7 +41,7 @@ public class Jackson implements FormatterStepFactory { public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { List enabledFeaturesAsList = Arrays.asList(enabledFeatures); List disabledFeaturesAsList = Arrays.asList(disabledFeatures); - return YamlJacksonStep + return YamlJacksonV2Step .create(enabledFeaturesAsList, disabledFeaturesAsList, version, stepConfig.getProvisioner()); } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/json/JsonTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/json/JsonTest.java index 45607807b..e492109fa 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/json/JsonTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/json/JsonTest.java @@ -16,17 +16,21 @@ package com.diffplug.spotless.maven.json; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.diffplug.spotless.maven.MavenIntegrationHarness; public class JsonTest extends MavenIntegrationHarness { + private static final Logger LOGGER = LoggerFactory.getLogger(JsonTest.class); + @Test public void testFormatJson_WithSimple_defaultConfig_sortByKeys() throws Exception { writePomWithJsonSteps(""); setFile("json_test.json").toResource("json/sortByKeysBefore.json"); - mavenRunner().withArguments("spotless:apply").runNoError().error(); - assertFile("json_test.json").sameAsResource("json/sortByKeysAfterDisabled.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("json_test.json").sameAsResource("json/sortByKeysAfterDisabled_Simple.json"); } @Test @@ -34,7 +38,7 @@ public void testFormatJson_WithSimple_defaultConfig_nestedObject() throws Except writePomWithJsonSteps(""); setFile("json_test.json").toResource("json/nestedObjectBefore.json"); - mavenRunner().withArguments("spotless:apply").runNoError().error(); + mavenRunner().withArguments("spotless:apply").runNoError(); assertFile("json_test.json").sameAsResource("json/nestedObjectAfter.json"); } @@ -43,7 +47,7 @@ public void testFormatJson_WithGson_defaultConfig_sortByKeys() throws Exception writePomWithJsonSteps(""); setFile("json_test.json").toResource("json/sortByKeysBefore.json"); - mavenRunner().withArguments("spotless:apply").runNoError().error(); + mavenRunner().withArguments("spotless:apply").runNoError(); assertFile("json_test.json").sameAsResource("json/sortByKeysAfterDisabled.json"); } @@ -52,8 +56,20 @@ public void testFormatJson_WithGson_sortByKeys() throws Exception { writePomWithJsonSteps("true"); setFile("json_test.json").toResource("json/sortByKeysBefore.json"); - mavenRunner().withArguments("spotless:apply").runNoError().error(); + + String output = mavenRunner().withArguments("spotless:apply").runNoError().output(); + LOGGER.error(output); + System.err.println(output); assertFile("json_test.json").sameAsResource("json/sortByKeysAfter.json"); } + @Test + public void testFormatJson_WithGson_defaultConfig_nestedObject() throws Exception { + writePomWithJsonSteps(""); + + setFile("json_test.json").toResource("json/nestedObjectBefore.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("json_test.json").sameAsResource("json/nestedObjectAfter.json"); + } + } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/yaml/YamlTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/yaml/YamlTest.java index 4614b42f2..b5a417c53 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/yaml/YamlTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/yaml/YamlTest.java @@ -15,17 +15,27 @@ */ package com.diffplug.spotless.maven.yaml; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.diffplug.spotless.maven.MavenIntegrationHarness; +import com.diffplug.spotless.maven.MavenRunner.Result; public class YamlTest extends MavenIntegrationHarness { + private static final Logger LOGGER = LoggerFactory.getLogger(YamlTest.class); + @Test public void testFormatYaml_WithJackson_defaultConfig_separatorComments() throws Exception { writePomWithYamlSteps(""); setFile("yaml_test.yaml").toResource("yaml/separator_comments.yaml"); - mavenRunner().withArguments("spotless:apply").runNoError().error(); + Result runNoError = mavenRunner().withArguments("spotless:apply").runNoError(); + LOGGER.error("result: {}", runNoError); + assertThat(runNoError.exitValue()).as("Run without error %s", runNoError).isEqualTo(0); + LOGGER.error("GOGO"); assertFile("yaml_test.yaml").sameAsResource("yaml/separator_comments.clean.yaml"); } @@ -34,7 +44,7 @@ public void testFormatYaml_WithJackson_defaultConfig_arrayBrackets() throws Exce writePomWithYamlSteps(""); setFile("yaml_test.yaml").toResource("yaml/array_with_bracket.yaml"); - mavenRunner().withArguments("spotless:apply").runNoError().error(); + mavenRunner().withArguments("spotless:apply").runNoError(); assertFile("yaml_test.yaml").sameAsResource("yaml/array_with_bracket.clean.yaml"); } @@ -43,7 +53,7 @@ public void testFormatYaml_WithJackson_defaultConfig_multipleDocuments() throws writePomWithYamlSteps(""); setFile("yaml_test.yaml").toResource("yaml/multiple_documents.yaml"); - mavenRunner().withArguments("spotless:apply").runNoError().error(); - assertFile("yaml_test.yaml").sameAsResource("yaml/multiple_documents.clean.yaml"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("yaml_test.yaml").sameAsResource("yaml/multiple_documents.clean.jackson.yaml"); } } diff --git a/testlib/src/main/resources/json/sortByKeysAfterDisabled_Simple.json b/testlib/src/main/resources/json/sortByKeysAfterDisabled_Simple.json new file mode 100644 index 000000000..d2d3612fb --- /dev/null +++ b/testlib/src/main/resources/json/sortByKeysAfterDisabled_Simple.json @@ -0,0 +1,19 @@ +{ + "A": 1, + "a": 3, + "c": 4, + "x": 5, + "X": 2, + "z": { + "A": 1, + "a": 3, + "c": 4, + "x": 5, + "X": 2 + }, + "_arraysNotSorted": [ + 3, + 2, + 1 + ] +} diff --git a/testlib/src/main/resources/yaml/multiple_documents.clean.jackson.yaml b/testlib/src/main/resources/yaml/multiple_documents.clean.jackson.yaml new file mode 100644 index 000000000..aa731919b --- /dev/null +++ b/testlib/src/main/resources/yaml/multiple_documents.clean.jackson.yaml @@ -0,0 +1,2 @@ +--- +document: "this is document 1"