From 2e892e8811d63521e8480e2870a45678dac5d238 Mon Sep 17 00:00:00 2001 From: dan-s1 Date: Mon, 15 Jun 2026 18:57:02 +0000 Subject: [PATCH 1/3] NIFI-16021 Improved JoltTransformJSON error message when specified JOLT spec is invalid and ensured leading single and multi-line Java comments are supported when the Jolt spec is specified as text. Also added tests to ensure ValidateJson causes Json parsing exception when any Java comments is specified before or inside a schema. --- .../jolt/AbstractJoltTransform.java | 2 +- .../jolt/TestJoltTransformJSON.java | 54 ++++++-- .../chainrSpecWithSingleLineComment.json | 2 +- .../processors/standard/TestValidateJson.java | 115 ++++++++++++++++++ 4 files changed, 164 insertions(+), 9 deletions(-) diff --git a/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/main/java/org/apache/nifi/processors/jolt/AbstractJoltTransform.java b/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/main/java/org/apache/nifi/processors/jolt/AbstractJoltTransform.java index dfc8126a9b86..7b084ff7fdee 100644 --- a/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/main/java/org/apache/nifi/processors/jolt/AbstractJoltTransform.java +++ b/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/main/java/org/apache/nifi/processors/jolt/AbstractJoltTransform.java @@ -256,7 +256,7 @@ String readTransform(final PropertyValue propertyValue) { try (final BufferedReader reader = new BufferedReader(new InputStreamReader(resourceReference.read(), StandardCharsets.UTF_8))) { return reader.lines().collect(Collectors.joining(System.lineSeparator())); } catch (final IOException e) { - throw new UncheckedIOException("Read JOLT Transform failed", e); + throw new UncheckedIOException("Read JOLT Transform failed, reason: " + e.getMessage(), e); } } diff --git a/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/test/java/org/apache/nifi/processors/jolt/TestJoltTransformJSON.java b/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/test/java/org/apache/nifi/processors/jolt/TestJoltTransformJSON.java index 8efe2e9db920..8b3fc58ea2bd 100644 --- a/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/test/java/org/apache/nifi/processors/jolt/TestJoltTransformJSON.java +++ b/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/test/java/org/apache/nifi/processors/jolt/TestJoltTransformJSON.java @@ -211,10 +211,9 @@ void testCustomTransformationWithInvalidClassName() throws IOException { } @ParameterizedTest() - @MethodSource("getChainrArguments") - void testTransformInputWithChainr(Path specPath) throws IOException { - final String spec = Files.readString(specPath); - runner.setProperty(JoltTransformJSON.JOLT_SPEC, spec); + @MethodSource("getChainrArgumentContents") + void testTransformInputWithChainrContents(String chainrSpecContents) throws IOException { + runner.setProperty(JoltTransformJSON.JOLT_SPEC, chainrSpecContents); runner.enqueue(JSON_INPUT); runner.run(); @@ -615,9 +614,50 @@ private static Stream unicodeEscaping() { ); } - private static Stream getChainrArguments() { + private static Stream getChainrArgumentContents() throws IOException { + final String chainrSpecWithLeadingMultLineJavaComment = """ + /* + Multiline Java comment + */ + [ + { + "operation": "shift", + "spec": { + "rating": { + "primary": { + "value": "Rating", + "max": "RatingRange" + }, + "*": { + "max": "SecondaryRatings.&1.Range", + "value": "SecondaryRatings.&1.Value", + "$": "SecondaryRatings.&1.Id" + } + } + } + }, + { + "operation": "default", + "spec": { + "Range": 5, + "SecondaryRatings": { + "*": { + "Range": 5 + } + } + } + } + ] + """; + + final Path chainrSpecWithoutCommentsPath = Paths.get(CHAINR_SPEC_PATH); + final String chainrSpecWithoutComments = Files.readString(chainrSpecWithoutCommentsPath); + final Path chainrSpecWithLeadingSingleLineCommentPath = Paths.get("src/test/resources/specs/chainrSpecWithSingleLineComment.json"); + final String chainrSpecWithLeadingSingleLineComment = Files.readString(chainrSpecWithLeadingSingleLineCommentPath); + return Stream.of( - Arguments.argumentSet("has no single line comments", Paths.get(CHAINR_SPEC_PATH)), - Arguments.argumentSet("has a single line comment", Paths.get("src/test/resources/specs/chainrSpecWithSingleLineComment.json"))); + Arguments.argumentSet("No Java comments", chainrSpecWithoutComments), + Arguments.argumentSet("Leading single line Java comment", chainrSpecWithLeadingSingleLineComment), + Arguments.argumentSet("Leading multi-line Java comment", chainrSpecWithLeadingMultLineJavaComment)); } } diff --git a/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/test/resources/specs/chainrSpecWithSingleLineComment.json b/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/test/resources/specs/chainrSpecWithSingleLineComment.json index 3b8d0d0b6b18..29a93cd147d8 100644 --- a/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/test/resources/specs/chainrSpecWithSingleLineComment.json +++ b/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/test/resources/specs/chainrSpecWithSingleLineComment.json @@ -1,5 +1,5 @@ +// Single line comment [ - // Single line comment { "operation": "shift", "spec": { diff --git a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestValidateJson.java b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestValidateJson.java index 9ee0450c7c3d..e5d1cbc7764e 100644 --- a/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestValidateJson.java +++ b/nifi-extension-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestValidateJson.java @@ -16,6 +16,7 @@ */ package org.apache.nifi.processors.standard; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.nifi.controller.AbstractControllerService; import org.apache.nifi.json.schema.JsonSchema; import org.apache.nifi.json.schema.SchemaVersion; @@ -252,6 +253,120 @@ void testMultilineJsonWhereSecondLineInvalid(ValidateJson.InputFormat inputForma runner.clearTransferState(); } + @ParameterizedTest + @MethodSource("schemasWithLeadingComments") + void testSchemaContentWithLeadingComments(String schema) { + runner.setProperty(ValidateJson.SCHEMA_CONTENT, schema); + runner.setProperty(JsonSchemaRegistryComponent.SCHEMA_VERSION, SCHEMA_VERSION); + runner.enqueue(getFileContent("simple-example-with-comments.json")); + runner.assertValid(); + + final AssertionFailedError assertionFailedError = assertThrows(AssertionFailedError.class, () -> runner.run()); + final String stackTrace = ExceptionUtils.getStackTrace(assertionFailedError); + assertTrue(stackTrace.contains("JsonParseException") && !stackTrace.contains("FileNotFoundException")); + } + + @ParameterizedTest + @MethodSource("schemasWithEmbeddedComments") + void testSchemaContentWithEmbeddedComments(String schema) { + runner.setProperty(ValidateJson.SCHEMA_CONTENT, schema); + runner.setProperty(JsonSchemaRegistryComponent.SCHEMA_VERSION, SCHEMA_VERSION); + runner.enqueue(getFileContent("simple-example-with-comments.json")); + runner.assertValid(); + + final AssertionFailedError assertionFailedError = assertThrows(AssertionFailedError.class, () -> runner.run()); + final String stackTrace = ExceptionUtils.getStackTrace(assertionFailedError); + assertTrue(stackTrace.contains("JsonParseException")); + } + + @ParameterizedTest + @MethodSource("schemasWithTrailingComments") + void testSchemaContentWithTrailingCommentsIgnored(String schema) { + runner.setProperty(ValidateJson.SCHEMA_CONTENT, schema); + runner.setProperty(JsonSchemaRegistryComponent.SCHEMA_VERSION, SCHEMA_VERSION); + runner.enqueue(getFileContent("simple-example-with-comments.json")); + runner.assertValid(); + + runner.run(); + + runner.assertAllFlowFilesTransferred(ValidateJson.REL_VALID); + } + + private static Stream schemasWithLeadingComments() { + final String schemasWithSingleLineComment = """ + // Single line Java comment + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true + } + """; + final String schemaWithMultiLineComment = """ + /* + Multi-line Java comment + */ + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true + } + """; + return Stream.of( + Arguments.argumentSet("Leading Java single line comment", schemasWithSingleLineComment), + Arguments.argumentSet("Leading Java multi-line comment", schemaWithMultiLineComment) + ); + } + + private static Stream schemasWithEmbeddedComments() { + final String schemasWithSingleLineComment = """ + { + // Single line Java comment + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true + } + """; + final String schemaWithMultiLineComment = """ + { + /* + Multi-line Java comment + */ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true + } + """; + return Stream.of( + Arguments.argumentSet("Embedded Java single line comment", schemasWithSingleLineComment), + Arguments.argumentSet("Embedded Java multi-line comment", schemaWithMultiLineComment) + ); + } + + private static Stream schemasWithTrailingComments() { + final String schemasWithSingleLineComment = """ + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true + } + // Single line Java comment + """; + final String schemaWithMultiLineComment = """ + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true + } + /* + Multi-line Java comment + */ + """; + return Stream.of( + Arguments.argumentSet("Trailing Java single line comment", schemasWithSingleLineComment), + Arguments.argumentSet("Trailing Java multi-line comment", schemaWithMultiLineComment) + ); + } + private void assertValidationErrors(Relationship relationship, boolean expected) { final Map attributes = runner.getFlowFilesForRelationship(relationship).getFirst().getAttributes(); From b887ca2e7ee273a74e9360c8e0a01bfb34d5163e Mon Sep 17 00:00:00 2001 From: dan-s1 Date: Mon, 13 Jul 2026 21:33:03 +0000 Subject: [PATCH 2/3] NIFI-16021 Reverted change on UncheckedIOException message and specified the cause message in customValidate. --- .../processors/jolt/AbstractJoltTransform.java | 5 +++-- .../processors/jolt/TestJoltTransformJSON.java | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/main/java/org/apache/nifi/processors/jolt/AbstractJoltTransform.java b/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/main/java/org/apache/nifi/processors/jolt/AbstractJoltTransform.java index 7b084ff7fdee..0ae141cfa580 100644 --- a/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/main/java/org/apache/nifi/processors/jolt/AbstractJoltTransform.java +++ b/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/main/java/org/apache/nifi/processors/jolt/AbstractJoltTransform.java @@ -190,7 +190,8 @@ protected Collection customValidate(ValidationContext validati } } } catch (final Exception e) { - String message = String.format("Specification not valid for the selected transformation: %s", e.getMessage()); + String message = String.format("Specification not valid for the selected transformation: %s", + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage())); results.add(new ValidationResult.Builder() .valid(false) .subject(JOLT_SPEC.getDisplayName()) @@ -256,7 +257,7 @@ String readTransform(final PropertyValue propertyValue) { try (final BufferedReader reader = new BufferedReader(new InputStreamReader(resourceReference.read(), StandardCharsets.UTF_8))) { return reader.lines().collect(Collectors.joining(System.lineSeparator())); } catch (final IOException e) { - throw new UncheckedIOException("Read JOLT Transform failed, reason: " + e.getMessage(), e); + throw new UncheckedIOException("Read JOLT Transform failed", e); } } diff --git a/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/test/java/org/apache/nifi/processors/jolt/TestJoltTransformJSON.java b/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/test/java/org/apache/nifi/processors/jolt/TestJoltTransformJSON.java index 8b3fc58ea2bd..f5e2ebc0ee60 100644 --- a/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/test/java/org/apache/nifi/processors/jolt/TestJoltTransformJSON.java +++ b/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/test/java/org/apache/nifi/processors/jolt/TestJoltTransformJSON.java @@ -18,6 +18,7 @@ import io.joltcommunity.jolt.Diffy; import io.joltcommunity.jolt.JsonUtils; +import org.apache.nifi.components.ValidationResult; import org.apache.nifi.flowfile.attributes.CoreAttributes; import org.apache.nifi.jolt.util.JoltTransformStrategy; import org.apache.nifi.processor.Processor; @@ -40,6 +41,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -109,6 +111,19 @@ void testInvalidJOLTSpec() { runner.assertValid(); } + @Test + void testNonExistingJOLTSpec() throws IOException { + final Path testResourcesDir = Paths.get("src/test/resources"); + final Path nonexistingSpec = testResourcesDir.resolve("nonExistingSpec.json"); + runner.setProperty(JoltTransformJSON.JOLT_SPEC, nonexistingSpec.toAbsolutePath().toString()); + runner.enqueue(JSON_INPUT); + runner.assertNotValid(); + + final Collection validationResults = runner.validate(); + final String explanation = validationResults.iterator().next().getExplanation(); + assertTrue(explanation.contains("file")); + } + @Test void testIncorrectJOLTSpec() { runner.setProperty(JoltTransformJSON.JOLT_SPEC, chainrSpecContents); From 8620f3de79933430ae9fc2ff5c47866ff94ff8f3 Mon Sep 17 00:00:00 2001 From: dan-s1 Date: Mon, 13 Jul 2026 22:36:44 +0000 Subject: [PATCH 3/3] NIFI-16021 Implemented review suggestion --- .../nifi/processors/jolt/AbstractJoltTransform.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/main/java/org/apache/nifi/processors/jolt/AbstractJoltTransform.java b/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/main/java/org/apache/nifi/processors/jolt/AbstractJoltTransform.java index 0ae141cfa580..3466ffa7fc6f 100644 --- a/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/main/java/org/apache/nifi/processors/jolt/AbstractJoltTransform.java +++ b/nifi-extension-bundles/nifi-jolt-bundle/nifi-jolt-processors/src/main/java/org/apache/nifi/processors/jolt/AbstractJoltTransform.java @@ -190,8 +190,15 @@ protected Collection customValidate(ValidationContext validati } } } catch (final Exception e) { - String message = String.format("Specification not valid for the selected transformation: %s", - (e.getCause() != null ? e.getCause().getMessage() : e.getMessage())); + final String reason; + final Throwable cause = e.getCause(); + if (cause == null) { + reason = e.getMessage(); + } else { + reason = "%s [%s]".formatted(cause.getMessage(), e.getMessage()); + } + + final String message = "Specification not valid... %s".formatted(reason); results.add(new ValidationResult.Builder() .valid(false) .subject(JOLT_SPEC.getDisplayName())