From 5519a6d9a50a05eb51256d964e7904ac2a1477c3 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 18:49:45 +0100 Subject: [PATCH 01/29] use JDK 17 in CI/CD --- .github/workflows/build.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03a79ab..c76377f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,10 +31,10 @@ jobs: timezoneMacos: "Europe/Berlin" timezoneWindows: "W. Europe Standard Time" - - name: Set up JDK 21 + - name: Set up JDK 17 uses: actions/setup-java@v5.2.0 with: - java-version: '21' + java-version: '17' distribution: 'temurin' cache: 'gradle' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c59197..ab5e93c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,10 +30,10 @@ jobs: echo "gradle.properties criado:" cat gradle.properties - - name: Set up JDK 21 + - name: Set up JDK 17 uses: actions/setup-java@v5.2.0 with: - java-version: '21' + java-version: '17' distribution: 'temurin' cache: 'gradle' From 78de567beb3c033193c701dc6ab6df46d832f87e Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 18:50:12 +0100 Subject: [PATCH 02/29] remove leading space in .gitattributes --- .gitattributes => .gitattributes | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .gitattributes => .gitattributes (100%) diff --git a/ .gitattributes b/.gitattributes similarity index 100% rename from .gitattributes rename to .gitattributes From fe951bfb49a93d2d73a4cc8456248494f9804301 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 18:51:13 +0100 Subject: [PATCH 03/29] remove .idea folder --- .idea/.gitignore | 8 -------- .idea/.name | 1 - .idea/aws.xml | 11 ----------- .idea/dictionaries/project.xml | 7 ------- .idea/gradle.xml | 17 ----------------- .idea/misc.xml | 10 ---------- .idea/sonarlint.xml | 11 ----------- .idea/vcs.xml | 6 ------ 8 files changed, 71 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/.name delete mode 100644 .idea/aws.xml delete mode 100644 .idea/dictionaries/project.xml delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/sonarlint.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 5c147d9..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -JToon \ No newline at end of file diff --git a/.idea/aws.xml b/.idea/aws.xml deleted file mode 100644 index b63b642..0000000 --- a/.idea/aws.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml deleted file mode 100644 index 9923ee7..0000000 --- a/.idea/dictionaries/project.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - toon - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 2a65317..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 59f0034..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/sonarlint.xml b/.idea/sonarlint.xml deleted file mode 100644 index c78dfb6..0000000 --- a/.idea/sonarlint.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 281a6999a9e3b7d27730671a19d810c91d5d036c Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 18:57:21 +0100 Subject: [PATCH 04/29] install spotbugs --- build.gradle | 37 +++++++++++++++++++++++++++++++++++++ spotbugs-exclude.xml | 27 +++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 spotbugs-exclude.xml diff --git a/build.gradle b/build.gradle index 03323a5..09f227f 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id 'maven-publish' id 'signing' id 'jacoco' + id 'com.github.spotbugs' version '6.0.15' } group = 'dev.toonformat' @@ -35,9 +36,44 @@ jacoco { reportsDirectory = layout.buildDirectory.dir('customJacocoReportDir') } +spotbugs { + toolVersion = '4.8.6' + excludeFilter = file('spotbugs-exclude.xml') + effort = "max" + reportLevel = "low" + reportsDir = layout.buildDirectory.dir('spotbugs') +} + +tasks.spotbugsMain { + reports { + html { + required = true + outputLocation = file("${spotbugs.reportsDir.get()}/spotbugs-main.html") + } + xml { + required = true + outputLocation = file("${spotbugs.reportsDir.get()}/spotbugs-main.xml") + } + } +} + +tasks.spotbugsTest { + reports { + html { + required = true + outputLocation = file("${spotbugs.reportsDir.get()}/spotbugs-test.html") + } + xml { + required = true + outputLocation = file("${spotbugs.reportsDir.get()}/spotbugs-test.xml") + } + } +} + dependencies { implementation 'tools.jackson.core:jackson-databind:3.0.4' implementation 'tools.jackson.module:jackson-module-afterburner:3.0.4' + compileOnly 'com.github.spotbugs:spotbugs-annotations:4.8.6' testImplementation platform('org.junit:junit-bom:6.0.2') testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' @@ -79,6 +115,7 @@ jacocoTestCoverageVerification { } } check.dependsOn jacocoTestReport +check.dependsOn spotbugsMain tasks.register('generateJavadoc', Javadoc) { description = 'Generates Javadoc HTML documentation in the docs/javadoc folder' diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml new file mode 100644 index 0000000..a8c7876 --- /dev/null +++ b/spotbugs-exclude.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + From 35cddf63e61d277c3ca16019a0a5870822a6b7c3 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 19:33:14 +0100 Subject: [PATCH 05/29] adding pmd --- build.gradle | 26 +++++++++ pmd-rules-test.xml | 133 +++++++++++++++++++++++++++++++++++++++++++++ pmd-rules.xml | 100 ++++++++++++++++++++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 pmd-rules-test.xml create mode 100644 pmd-rules.xml diff --git a/build.gradle b/build.gradle index 09f227f..f74416d 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ plugins { id 'signing' id 'jacoco' id 'com.github.spotbugs' version '6.0.15' + id 'pmd' } group = 'dev.toonformat' @@ -70,6 +71,30 @@ tasks.spotbugsTest { } } +pmd { + toolVersion = '7.0.0' + ruleSetFiles = files('pmd-rules.xml') + ruleSets = [] // Disable default rulesets, use custom file only + consoleOutput = true + ignoreFailures = true +} + +tasks.pmdMain { + reports { + html.required = true + xml.required = true + } +} + +tasks.pmdTest { + ruleSetFiles = files('pmd-rules-test.xml') + ignoreFailures = true + reports { + html.required = true + xml.required = true + } +} + dependencies { implementation 'tools.jackson.core:jackson-databind:3.0.4' implementation 'tools.jackson.module:jackson-module-afterburner:3.0.4' @@ -116,6 +141,7 @@ jacocoTestCoverageVerification { } check.dependsOn jacocoTestReport check.dependsOn spotbugsMain +check.dependsOn pmdMain tasks.register('generateJavadoc', Javadoc) { description = 'Generates Javadoc HTML documentation in the docs/javadoc folder' diff --git a/pmd-rules-test.xml b/pmd-rules-test.xml new file mode 100644 index 0000000..19628b4 --- /dev/null +++ b/pmd-rules-test.xml @@ -0,0 +1,133 @@ + + + + + PMD ruleset for JToon project + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pmd-rules.xml b/pmd-rules.xml new file mode 100644 index 0000000..4c41fed --- /dev/null +++ b/pmd-rules.xml @@ -0,0 +1,100 @@ + + + + + PMD ruleset for JToon project + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 71f8a6c6c893fbb89aacb695f01e26bc203548cf Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 19:33:38 +0100 Subject: [PATCH 06/29] running PMD and fix the warnnings --- checkstyle.xml | 84 +++++++++++++++++++ spotbugs-exclude.xml | 28 +++++++ .../jtoon/decoder/DecodeContext.java | 2 +- .../jtoon/decoder/DecodeHelper.java | 2 +- .../toonformat/jtoon/decoder/KeyDecoder.java | 27 +++--- .../jtoon/decoder/ListItemDecoder.java | 2 +- .../jtoon/decoder/ObjectDecoder.java | 9 +- .../jtoon/decoder/ValueDecoder.java | 2 +- .../jtoon/encoder/ArrayEncoder.java | 2 +- .../dev/toonformat/jtoon/encoder/Flatten.java | 4 +- .../jtoon/encoder/HeaderFormatter.java | 5 +- .../jtoon/encoder/ObjectEncoder.java | 42 +++++----- .../jtoon/normalizer/JsonNormalizer.java | 3 +- .../jtoon/util/ObjectMapperSingleton.java | 8 +- .../jtoon/util/StringValidator.java | 12 +-- 15 files changed, 172 insertions(+), 60 deletions(-) create mode 100644 checkstyle.xml diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..a691216 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index a8c7876..78214ec 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -24,4 +24,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java b/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java index f319efe..dbb0b70 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java @@ -23,7 +23,7 @@ public class DecodeContext { /** * Current line being decoded. */ - protected int currentLine = 0; + protected int currentLine; /** * Default constructor diff --git a/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java b/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java index f0db743..2a75b90 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java @@ -84,7 +84,7 @@ private static int computeLeadingSpaces(String line, DecodeContext context) { * @return true or false depending on if the line is blank or not */ static boolean isBlankLine(String line) { - return line.trim().isEmpty(); + return line.isBlank(); } /** diff --git a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java index 8d90be5..20151f6 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java @@ -56,22 +56,23 @@ static void processKeyedArrayLine(Map result, String content, St */ static void expandPathIntoMap(Map current, String dottedKey, Object value, DecodeContext context) { String[] segments = dottedKey.split("\\."); + Map currentMap = current; // Navigate/create nested structure for (int i = 0; i < segments.length - 1; i++) { String segment = segments[i]; - Object existing = current.get(segment); + Object existing = currentMap.get(segment); if (existing == null) { // Create a new nested object Map nested = new LinkedHashMap<>(); - current.put(segment, nested); - current = nested; + currentMap.put(segment, nested); + currentMap = nested; } else if (existing instanceof Map) { // Use existing nested object @SuppressWarnings("unchecked") Map existingMap = (Map) existing; - current = existingMap; + currentMap = existingMap; } else { // Conflict: existing is not a Map if (context.options.strict()) { @@ -81,22 +82,24 @@ static void expandPathIntoMap(Map current, String dottedKey, Obj } // LWW: overwrite with new nested object Map nested = new LinkedHashMap<>(); - current.put(segment, nested); - current = nested; + currentMap.put(segment, nested); + currentMap = nested; } } // Set the final value String finalSegment = segments[segments.length - 1]; - Object existing = current.get(finalSegment); + Object existing = currentMap.get(finalSegment); DecodeHelper.checkFinalValueConflict(finalSegment, existing, value, context); // LWW: last write wins (always overwrite in non-strict, or if types match in // strict) - current.put(finalSegment, value); + currentMap.put(finalSegment, value); } + + /** * Processes a key-value line (e.g., "key: value"). * @@ -191,7 +194,7 @@ private static Object parseKeyValue(String value, int depth, DecodeContext conte } else { // If the value is empty, create an empty object; otherwise parse as primitive Object parsedValue; - if (value.trim().isEmpty()) { + if (value.isBlank()) { parsedValue = new LinkedHashMap<>(); } else { parsedValue = PrimitiveDecoder.parse(value); @@ -202,7 +205,7 @@ private static Object parseKeyValue(String value, int depth, DecodeContext conte } else { // If the value is empty, create an empty object; otherwise parse as primitive Object parsedValue; - if (value.trim().isEmpty()) { + if (value.isBlank()) { parsedValue = new LinkedHashMap<>(); } else { parsedValue = PrimitiveDecoder.parse(value); @@ -267,7 +270,7 @@ static Object parseKeyedArrayValue(Matcher keyedArray, String content, int depth String key = StringEscaper.unescape(originalKey); String arrayHeader = content.substring(keyedArray.group(1).length()); - var arrayValue = ArrayDecoder.parseArray(arrayHeader, depth, context); + List arrayValue = ArrayDecoder.parseArray(arrayHeader, depth, context); Map obj = new LinkedHashMap<>(); // Handle path expansion for array keys @@ -308,7 +311,7 @@ static boolean parseKeyedArrayField(String fieldContent, Map ite // For nested arrays in list items, default to comma delimiter if not specified Delimiter nestedArrayDelimiter = ArrayDecoder.extractDelimiterFromHeader(arrayHeader, context); - var arrayValue = ArrayDecoder.parseArrayWithDelimiter(arrayHeader, depth + 2, nestedArrayDelimiter, context); + List arrayValue = ArrayDecoder.parseArrayWithDelimiter(arrayHeader, depth + 2, nestedArrayDelimiter, context); // Handle path expansion for array keys if (shouldExpandKey(originalKey, context)) { diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java index 2d5c118..1433081 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java @@ -122,7 +122,7 @@ public static Object parseListItem(String content, int depth, DecodeContext cont Object parsedValue; // If no next line exists, handle a simple case if (context.currentLine >= context.lines.length) { - parsedValue = value.trim().isEmpty() ? new LinkedHashMap<>() : PrimitiveDecoder.parse(value); + parsedValue = value.isBlank() ? new LinkedHashMap<>() : PrimitiveDecoder.parse(value); } else { // List item is at depth + 1, so pass depth + 1 to parseObjectItemValue parsedValue = ObjectDecoder.parseObjectItemValue(value, depth + 1, context); diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java index c1f23a1..430dbb0 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java @@ -3,6 +3,7 @@ import dev.toonformat.jtoon.util.StringEscaper; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.regex.Matcher; @@ -124,7 +125,7 @@ private static void processRootKeyedArrayLine(Map objectMap, Str String key = StringEscaper.unescape(originalKey); String arrayHeader = content.substring(originalKey.length()); - var arrayValue = ArrayDecoder.parseArray(arrayHeader, depth, context); + List arrayValue = ArrayDecoder.parseArray(arrayHeader, depth, context); // Handle path expansion for array keys if (KeyDecoder.shouldExpandKey(originalKeyTrimmed, context)) { @@ -174,7 +175,7 @@ static Object parseFieldValue(String fieldValue, int fieldDepth, DecodeContext c return parseNestedObject(fieldDepth, context); } else { // If the value is empty, create an empty object; otherwise parse as primitive - if (fieldValue.trim().isEmpty()) { + if (fieldValue.isBlank()) { context.currentLine++; return new LinkedHashMap<>(); } else { @@ -184,7 +185,7 @@ static Object parseFieldValue(String fieldValue, int fieldDepth, DecodeContext c } } else { // If the value is empty, create an empty object; otherwise parse as primitive - if (fieldValue.trim().isEmpty()) { + if (fieldValue.isBlank()) { context.currentLine++; return new LinkedHashMap<>(); } else { @@ -205,7 +206,7 @@ static Object parseFieldValue(String fieldValue, int fieldDepth, DecodeContext c * @return the parsed value (Map, List, or primitive) */ static Object parseObjectItemValue(String value, int depth, DecodeContext context) { - boolean isEmpty = value.trim().isEmpty(); + boolean isEmpty = value.isBlank(); // Find the next non-blank line and its depth Integer nextDepth = DecodeHelper.findNextNonBlankLineDepth(context); diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java index 129b3c6..a3cc59c 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java @@ -48,7 +48,7 @@ private ValueDecoder() { * invalid */ public static Object decode(String toon, DecodeOptions options) { - if (toon == null || toon.trim().isEmpty()) { + if (toon == null || toon.isBlank()) { return new LinkedHashMap<>(); } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java index 1e3404f..3a2abe9 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java @@ -60,7 +60,7 @@ public static void encodeArray(String key, ArrayNode value, LineWriter writer, i // Array of objects if (isArrayOfObjects(value)) { - var header = TabularArrayEncoder.detectTabularHeader(value); + List header = TabularArrayEncoder.detectTabularHeader(value); if (!header.isEmpty()) { TabularArrayEncoder.encodeArrayOfObjectsAsTabular(key, value, header, writer, depth, options); } else { diff --git a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java index fc8bc7a..82e7b4a 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java @@ -17,12 +17,12 @@ */ public final class Flatten { + private static final Pattern SAFE_IDENTIFIER = Pattern.compile("(?i)^[A-Z_]\\w*$"); + private Flatten() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); } - private static final Pattern SAFE_IDENTIFIER = Pattern.compile("(?i)^[A-Z_]\\w*$"); - /** * Represents the result of a key-folding operation. * diff --git a/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java b/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java index 164e3d6..68a1635 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java @@ -96,7 +96,7 @@ private static void appendArrayLength( } private static void appendDelimiterIfNotDefault(StringBuilder header, String delimiter) { - if (!delimiter.equals(COMMA)) { + if (!COMMA.equals(delimiter)) { header.append(delimiter); } } @@ -110,8 +110,7 @@ private static void appendFieldsIfPresent( } header.append(OPEN_BRACE); - String quotedFields = formatFields(fields, delimiter); - header.append(quotedFields); + header.append(formatFields(fields, delimiter)); header.append(CLOSE_BRACE); } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java index fabf737..7b4a30f 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java @@ -76,15 +76,15 @@ public static void encodeObject(ObjectNode value, LineWriter writer, int depth, * @param blockedKeys contains only keys that have undergone a successful flattening */ public static void encodeKeyValuePair(String key, - JsonNode value, - LineWriter writer, - int depth, - EncodeOptions options, - Set siblings, - Set rootLiteralKeys, - String pathPrefix, - Integer flattenDepth, - Set blockedKeys + JsonNode value, + LineWriter writer, + int depth, + EncodeOptions options, + Set siblings, + Set rootLiteralKeys, + String pathPrefix, + Integer flattenDepth, + Set blockedKeys ) { if (key == null) { return; @@ -93,33 +93,34 @@ public static void encodeKeyValuePair(String key, String currentPath = pathPrefix != null ? pathPrefix + DOT + key : key; int effectiveFlattenDepth = flattenDepth != null && flattenDepth > 0 ? flattenDepth : options.flattenDepth(); int remainingDepth = effectiveFlattenDepth - depth; + EncodeOptions currentOptions = options; // Attempt key folding when enabled - if (KeyFolding.SAFE.equals(options.flatten()) + if (KeyFolding.SAFE.equals(currentOptions.flatten()) && !siblings.isEmpty() && remainingDepth > 0 && blockedKeys != null && !blockedKeys.contains(key)) { Flatten.FoldResult foldResult = Flatten.tryFoldKeyChain(key, value, siblings, rootLiteralKeys, pathPrefix, remainingDepth); if (foldResult != null) { - options = flatten(key, foldResult, writer, depth, options, rootLiteralKeys, pathPrefix, blockedKeys, remainingDepth); - if (options == null) { + currentOptions = flatten(key, foldResult, writer, depth, currentOptions, rootLiteralKeys, pathPrefix, blockedKeys, remainingDepth); + if (currentOptions == null) { return; } } } if (value.isValueNode()) { - writer.push(depth, encodedKey + COLON + SPACE + PrimitiveEncoder.encodePrimitive(value, options.delimiter().toString())); + writer.push(depth, encodedKey + COLON + SPACE + PrimitiveEncoder.encodePrimitive(value, currentOptions.delimiter().toString())); } if (value.isArray()) { - ArrayEncoder.encodeArray(key, (ArrayNode) value, writer, depth, options); + ArrayEncoder.encodeArray(key, (ArrayNode) value, writer, depth, currentOptions); } if (value.isObject()) { ObjectNode objValue = (ObjectNode) value; writer.push(depth, encodedKey + COLON); if (!objValue.isEmpty()) { - encodeObject(objValue, writer, depth + 1, options, rootLiteralKeys, currentPath, effectiveFlattenDepth, blockedKeys); + encodeObject(objValue, writer, depth + 1, currentOptions, rootLiteralKeys, currentPath, effectiveFlattenDepth, blockedKeys); } } } @@ -141,6 +142,7 @@ public static void encodeKeyValuePair(String key, private static EncodeOptions flatten(String key, Flatten.FoldResult foldResult, LineWriter writer, int depth, EncodeOptions options, Set rootLiteralKeys, String pathPrefix, Set blockedKeys, int remainingDepth) { String foldedKey = foldResult.foldedKey(); + EncodeOptions currentOptions = options; // prevent second folding pass blockedKeys.add(key); @@ -151,13 +153,13 @@ private static EncodeOptions flatten(String key, Flatten.FoldResult foldResult, // Case 1: Fully folded to a leaf value if (remainder == null) { - handleFullyFoldedLeaf(foldResult, writer, depth, options, encodedFoldedKey); + handleFullyFoldedLeaf(foldResult, writer, depth, currentOptions, encodedFoldedKey); return null; } // Case 2: Partially folded with a tail object if (remainder.isObject()) { - writer.push(depth, indentedLine(depth, encodedFoldedKey + COLON, options.indent())); + writer.push(depth, indentedLine(depth, encodedFoldedKey + COLON, currentOptions.indent())); String foldedPath = pathPrefix != null ? String.join(DOT, pathPrefix, foldedKey) : foldedKey; int newRemainingDepth = remainingDepth - foldResult.segmentCount(); @@ -166,14 +168,14 @@ private static EncodeOptions flatten(String key, Flatten.FoldResult foldResult, // Pass "-1" if remainingDepth is exhausted and set the encoding in the option to false. // to encode normally without flattening newRemainingDepth = -1; - options = new EncodeOptions(options.indent(), options.delimiter(), options.lengthMarker(), KeyFolding.OFF, options.flattenDepth()); + currentOptions = new EncodeOptions(currentOptions.indent(), currentOptions.delimiter(), currentOptions.lengthMarker(), KeyFolding.OFF, currentOptions.flattenDepth()); } - encodeObject((ObjectNode) remainder, writer, depth + 1, options, rootLiteralKeys, foldedPath, newRemainingDepth, blockedKeys); + encodeObject((ObjectNode) remainder, writer, depth + 1, currentOptions, rootLiteralKeys, foldedPath, newRemainingDepth, blockedKeys); return null; } - return options; + return currentOptions; } private static void handleFullyFoldedLeaf(Flatten.FoldResult foldResult, LineWriter writer, int depth, EncodeOptions options, String encodedFoldedKey) { diff --git a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java index 52581f5..1efaabd 100644 --- a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java +++ b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java @@ -1,7 +1,6 @@ package dev.toonformat.jtoon.normalizer; import dev.toonformat.jtoon.util.ObjectMapperSingleton; -import tools.jackson.core.JacksonException; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.node.ArrayNode; @@ -73,7 +72,7 @@ private JsonNormalizer() { * @throws IllegalArgumentException if the input is blank or not valid JSON */ public static JsonNode parse(String json) { - if (json == null || json.trim().isEmpty()) { + if (json == null || json.isBlank()) { throw new IllegalArgumentException("Invalid JSON"); } try { diff --git a/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java b/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java index 8d24086..7f2c3f3 100644 --- a/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java +++ b/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java @@ -15,7 +15,7 @@ public final class ObjectMapperSingleton { /** * Holds the singleton ObjectMapper. */ - private static volatile ObjectMapper INSTANCE; + private static volatile ObjectMapper instance; private ObjectMapperSingleton() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); @@ -27,12 +27,12 @@ private ObjectMapperSingleton() { * @return ObjectMapper */ public static ObjectMapper getInstance() { - ObjectMapper result = INSTANCE; + ObjectMapper result = instance; if (result == null) { synchronized (ObjectMapperSingleton.class) { - result = INSTANCE; + result = instance; if (result == null) { - INSTANCE = result = JsonMapper.builder() + instance = result = JsonMapper.builder() .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.ALWAYS)) .addModule(new AfterburnerModule()) // Speeds up Jackson by 20–40% in most real-world cases .defaultTimeZone(TimeZone.getTimeZone("UTC")) // set a default timezone for dates diff --git a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java index 9ecee67..a4c2251 100644 --- a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java +++ b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java @@ -64,11 +64,7 @@ public static boolean isSafeUnquoted(String value, String delimiter) { return false; } - if (containsDelimiter(value, delimiter)) { - return false; - } - - return !startsWithListMarker(value); + return !containsDelimiter(value, delimiter) && !startsWithListMarker(value); } /** @@ -90,9 +86,9 @@ private static boolean isPaddedWithWhitespace(String value) { } private static boolean looksLikeKeyword(String value) { - return value.equals(TRUE_LITERAL) - || value.equals(FALSE_LITERAL) - || value.equals(NULL_LITERAL); + return TRUE_LITERAL.equals(value) + || FALSE_LITERAL.equals(value) + || NULL_LITERAL.equals(value); } private static boolean looksLikeNumber(String value) { From feeec03b9b9749c4a62b21ff6265d9859ecf855e Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 19:41:26 +0100 Subject: [PATCH 07/29] install checkstyle --- build.gradle | 20 ++++++++++++++++++++ checkstyle.xml | 13 ++++--------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index f74416d..2c6a93b 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,7 @@ plugins { id 'jacoco' id 'com.github.spotbugs' version '6.0.15' id 'pmd' + id 'checkstyle' } group = 'dev.toonformat' @@ -95,6 +96,24 @@ tasks.pmdTest { } } +checkstyle { + toolVersion = '10.12.5' + configFile = file('checkstyle.xml') + showViolations = true + ignoreFailures = true +} + +tasks.checkstyleMain { + reports { + xml.required = true + html.required = true + } +} + +tasks.checkstyleTest { + enabled = false +} + dependencies { implementation 'tools.jackson.core:jackson-databind:3.0.4' implementation 'tools.jackson.module:jackson-module-afterburner:3.0.4' @@ -142,6 +161,7 @@ jacocoTestCoverageVerification { check.dependsOn jacocoTestReport check.dependsOn spotbugsMain check.dependsOn pmdMain +check.dependsOn checkstyleMain tasks.register('generateJavadoc', Javadoc) { description = 'Generates Javadoc HTML documentation in the docs/javadoc folder' diff --git a/checkstyle.xml b/checkstyle.xml index a691216..3785d24 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -22,7 +22,6 @@ - @@ -37,8 +36,8 @@ - - + + @@ -62,14 +61,10 @@ - - - - - + @@ -81,4 +76,4 @@ - \ No newline at end of file + From a8a266a9399431da4a6af6492eccb953f5c52ab1 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 19:47:15 +0100 Subject: [PATCH 08/29] run checkstyle and fix warnings --- .../toonformat/jtoon/decoder/KeyDecoder.java | 82 ++++++++++--------- .../jtoon/decoder/ListItemDecoder.java | 42 +++++----- .../jtoon/decoder/TabularArrayDecoder.java | 17 ++-- .../jtoon/decoder/ValueDecoder.java | 24 +++--- .../jtoon/encoder/ArrayEncoder.java | 49 ++++++----- .../dev/toonformat/jtoon/encoder/Flatten.java | 8 +- .../jtoon/encoder/ListItemEncoder.java | 34 ++++---- .../jtoon/encoder/ObjectEncoder.java | 69 +++++++++------- 8 files changed, 169 insertions(+), 156 deletions(-) diff --git a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java index 20151f6..8ce8f22 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java @@ -3,12 +3,10 @@ import dev.toonformat.jtoon.Delimiter; import dev.toonformat.jtoon.PathExpansion; import dev.toonformat.jtoon.util.StringEscaper; - import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; - import static dev.toonformat.jtoon.util.Constants.DOT; import static dev.toonformat.jtoon.util.Headers.KEYED_ARRAY_PATTERN; @@ -32,9 +30,9 @@ private KeyDecoder() { */ static void processKeyedArrayLine(Map result, String content, String originalKey, int parentDepth, DecodeContext context) { - String key = StringEscaper.unescape(originalKey); - String arrayHeader = content.substring(originalKey.length()); - List arrayValue = ArrayDecoder.parseArray(arrayHeader, parentDepth + 1, context); + final String key = StringEscaper.unescape(originalKey); + final String arrayHeader = content.substring(originalKey.length()); + final List arrayValue = ArrayDecoder.parseArray(arrayHeader, parentDepth + 1, context); // Handle path expansion for array keys if (shouldExpandKey(originalKey, context)) { @@ -54,24 +52,25 @@ static void processKeyedArrayLine(Map result, String content, St * @param value value * @param context decode an object to deal with lines, delimiter and options */ - static void expandPathIntoMap(Map current, String dottedKey, Object value, DecodeContext context) { - String[] segments = dottedKey.split("\\."); + static void expandPathIntoMap(Map current, String dottedKey, Object value, + DecodeContext context) { + final String[] segments = dottedKey.split("\\."); Map currentMap = current; // Navigate/create nested structure for (int i = 0; i < segments.length - 1; i++) { - String segment = segments[i]; - Object existing = currentMap.get(segment); + final String segment = segments[i]; + final Object existing = currentMap.get(segment); if (existing == null) { // Create a new nested object - Map nested = new LinkedHashMap<>(); + final Map nested = new LinkedHashMap<>(); currentMap.put(segment, nested); currentMap = nested; } else if (existing instanceof Map) { // Use existing nested object @SuppressWarnings("unchecked") - Map existingMap = (Map) existing; + final Map existingMap = (Map) existing; currentMap = existingMap; } else { // Conflict: existing is not a Map @@ -81,15 +80,15 @@ static void expandPathIntoMap(Map current, String dottedKey, Obj segment, existing.getClass().getSimpleName())); } // LWW: overwrite with new nested object - Map nested = new LinkedHashMap<>(); + final Map nested = new LinkedHashMap<>(); currentMap.put(segment, nested); currentMap = nested; } } // Set the final value - String finalSegment = segments[segments.length - 1]; - Object existing = currentMap.get(finalSegment); + final String finalSegment = segments[segments.length - 1]; + final Object existing = currentMap.get(finalSegment); DecodeHelper.checkFinalValueConflict(finalSegment, existing, value, context); @@ -109,11 +108,11 @@ static void expandPathIntoMap(Map current, String dottedKey, Obj * @param context decode an object to deal with lines, delimiter and options */ static void processKeyValueLine(Map result, String content, int depth, DecodeContext context) { - int colonIdx = DecodeHelper.findUnquotedColon(content); + final int colonIdx = DecodeHelper.findUnquotedColon(content); if (colonIdx > 0) { - String key = content.substring(0, colonIdx).trim(); - String value = content.substring(colonIdx + 1).trim(); + final String key = content.substring(0, colonIdx).trim(); + final String value = content.substring(colonIdx + 1).trim(); parseKeyValuePairIntoMap(result, key, value, depth, context); } else { // No colon found in key-value context - this is an error @@ -136,9 +135,9 @@ static void processKeyValueLine(Map result, String content, int */ static void parseKeyValuePairIntoMap(Map map, String key, String value, int depth, DecodeContext context) { - String unescapedKey = StringEscaper.unescape(key); + final String unescapedKey = StringEscaper.unescape(key); - Object parsedValue = parseKeyValue(value, depth, context); + final Object parsedValue = parseKeyValue(value, depth, context); putKeyValueIntoMap(map, key, unescapedKey, parsedValue, context); } @@ -186,14 +185,14 @@ static boolean shouldExpandKey(String key, DecodeContext context) { private static Object parseKeyValue(String value, int depth, DecodeContext context) { // Check if the next line is nested (deeper indentation) if (context.currentLine + 1 < context.lines.length) { - int nextDepth = DecodeHelper.getDepth(context.lines[context.currentLine + 1], context); + final int nextDepth = DecodeHelper.getDepth(context.lines[context.currentLine + 1], context); if (nextDepth > depth) { context.currentLine++; // parseNestedObject manages the currentLine, so we don't increment here return ObjectDecoder.parseNestedObject(depth, context); } else { // If the value is empty, create an empty object; otherwise parse as primitive - Object parsedValue; + final Object parsedValue; if (value.isBlank()) { parsedValue = new LinkedHashMap<>(); } else { @@ -204,7 +203,7 @@ private static Object parseKeyValue(String value, int depth, DecodeContext conte } } else { // If the value is empty, create an empty object; otherwise parse as primitive - Object parsedValue; + final Object parsedValue; if (value.isBlank()) { parsedValue = new LinkedHashMap<>(); } else { @@ -247,7 +246,7 @@ private static void putKeyValueIntoMap(Map map, String originalK */ static Object parseKeyValuePair(String key, String value, int depth, boolean parseRootFields, DecodeContext context) { - Map obj = new LinkedHashMap<>(); + final Map obj = new LinkedHashMap<>(); parseKeyValuePairIntoMap(obj, key, value, depth, context); if (parseRootFields) { @@ -266,12 +265,12 @@ static Object parseKeyValuePair(String key, String value, int depth, boolean par * @return parsed keyed array value */ static Object parseKeyedArrayValue(Matcher keyedArray, String content, int depth, DecodeContext context) { - String originalKey = keyedArray.group(1).trim(); - String key = StringEscaper.unescape(originalKey); - String arrayHeader = content.substring(keyedArray.group(1).length()); + final String originalKey = keyedArray.group(1).trim(); + final String key = StringEscaper.unescape(originalKey); + final String arrayHeader = content.substring(keyedArray.group(1).length()); - List arrayValue = ArrayDecoder.parseArray(arrayHeader, depth, context); - Map obj = new LinkedHashMap<>(); + final List arrayValue = ArrayDecoder.parseArray(arrayHeader, depth, context); + final Map obj = new LinkedHashMap<>(); // Handle path expansion for array keys if (shouldExpandKey(originalKey, context)) { @@ -299,19 +298,21 @@ static Object parseKeyedArrayValue(Matcher keyedArray, String content, int depth * @param context decode an object to deal with lines, delimiter and options * @return true if the field was processed as a keyed array, false otherwise */ - static boolean parseKeyedArrayField(String fieldContent, Map item, int depth, DecodeContext context) { - Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(fieldContent); + static boolean parseKeyedArrayField(String fieldContent, Map item, int depth, + DecodeContext context) { + final Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(fieldContent); if (!keyedArray.matches()) { return false; } - String originalKey = keyedArray.group(1).trim(); - String key = StringEscaper.unescape(originalKey); - String arrayHeader = fieldContent.substring(keyedArray.group(1).length()); + final String originalKey = keyedArray.group(1).trim(); + final String key = StringEscaper.unescape(originalKey); + final String arrayHeader = fieldContent.substring(keyedArray.group(1).length()); // For nested arrays in list items, default to comma delimiter if not specified - Delimiter nestedArrayDelimiter = ArrayDecoder.extractDelimiterFromHeader(arrayHeader, context); - List arrayValue = ArrayDecoder.parseArrayWithDelimiter(arrayHeader, depth + 2, nestedArrayDelimiter, context); + final Delimiter nestedArrayDelimiter = ArrayDecoder.extractDelimiterFromHeader(arrayHeader, context); + final List arrayValue = ArrayDecoder.parseArrayWithDelimiter(arrayHeader, depth + 2, + nestedArrayDelimiter, context); // Handle path expansion for array keys if (shouldExpandKey(originalKey, context)) { @@ -333,16 +334,17 @@ static boolean parseKeyedArrayField(String fieldContent, Map ite * @param context decode an object to deal with lines, delimiter and options * @return true if the field was processed as a key-value pair, false otherwise */ - static boolean parseKeyValueField(String fieldContent, Map item, int depth, DecodeContext context) { - int colonIdx = DecodeHelper.findUnquotedColon(fieldContent); + static boolean parseKeyValueField(String fieldContent, Map item, int depth, + DecodeContext context) { + final int colonIdx = DecodeHelper.findUnquotedColon(fieldContent); if (colonIdx <= 0) { return false; } - String fieldKey = StringEscaper.unescape(fieldContent.substring(0, colonIdx).trim()); - String fieldValue = fieldContent.substring(colonIdx + 1).trim(); + final String fieldKey = StringEscaper.unescape(fieldContent.substring(0, colonIdx).trim()); + final String fieldValue = fieldContent.substring(colonIdx + 1).trim(); - Object parsedValue = ObjectDecoder.parseFieldValue(fieldValue, depth + 2, context); + final Object parsedValue = ObjectDecoder.parseFieldValue(fieldValue, depth + 2, context); // Handle path expansion if (shouldExpandKey(fieldKey, context)) { diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java index 1433081..45ac4e4 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java @@ -17,7 +17,9 @@ */ public final class ListItemDecoder { - private ListItemDecoder() {throw new UnsupportedOperationException("Utility class cannot be instantiated");} + private ListItemDecoder() { + throw new UnsupportedOperationException("Utility class cannot be instantiated"); + } /** * Processes a single list array item if it matches the expected depth. @@ -31,7 +33,7 @@ public final class ListItemDecoder { public static void processListArrayItem(String line, int lineDepth, int depth, List result, DecodeContext context) { if (lineDepth == depth + 1) { - String content = line.substring((depth + 1) * context.options.indent()); + final String content = line.substring((depth + 1) * context.options.indent()); if (content.startsWith(LIST_ITEM_MARKER)) { result.add(parseListItem(content, depth, context)); @@ -54,7 +56,7 @@ public static void processListArrayItem(String line, int lineDepth, int depth, */ public static Object parseListItem(String content, int depth, DecodeContext context) { // Handle empty item: just "-" or "- " - String itemContent; + final String itemContent; if (content.length() > 2) { itemContent = content.substring(2).trim(); } else { @@ -70,7 +72,7 @@ public static Object parseListItem(String content, int depth, DecodeContext cont // Check for standalone array (e.g., "[2]: 1,2") if (itemContent.startsWith(OPEN_BRACKET)) { // For nested arrays in list items, default to comma delimiter if not specified - Delimiter nestedArrayDelimiter = ArrayDecoder.extractDelimiterFromHeader(itemContent, context); + final Delimiter nestedArrayDelimiter = ArrayDecoder.extractDelimiterFromHeader(itemContent, context); // parseArrayWithDelimiter handles currentLine increment internally // For inline arrays, it increments. For multi-line arrays, parseListArray // handles it. @@ -81,17 +83,19 @@ public static Object parseListItem(String content, int depth, DecodeContext cont } // Check for keyed array pattern (e.g., "tags[3]: a,b,c" or "data[2]{id}: ...") - Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(itemContent); + final Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(itemContent); if (keyedArray.matches()) { - String originalKey = keyedArray.group(1).trim(); - String key = StringEscaper.unescape(originalKey); - String arrayHeader = itemContent.substring(keyedArray.group(1).length()); + final String originalKey = keyedArray.group(1).trim(); + final String key = StringEscaper.unescape(originalKey); + final String arrayHeader = itemContent.substring(keyedArray.group(1).length()); // For nested arrays in list items, default to comma delimiter if not specified - Delimiter nestedArrayDelimiter = ArrayDecoder.extractDelimiterFromHeader(arrayHeader, context); - List arrayValue = ArrayDecoder.parseArrayWithDelimiter(arrayHeader, depth + 2, nestedArrayDelimiter, context); + final Delimiter nestedArrayDelimiter = ArrayDecoder.extractDelimiterFromHeader(arrayHeader, context); + final List arrayValue = ArrayDecoder.parseArrayWithDelimiter( + arrayHeader, depth + 2, nestedArrayDelimiter, context + ); - Map item = new LinkedHashMap<>(); + final Map item = new LinkedHashMap<>(); item.put(key, arrayValue); // parseArrayWithDelimiter manages currentLine correctly: @@ -104,7 +108,7 @@ public static Object parseListItem(String content, int depth, DecodeContext cont return item; } - int colonIdx = DecodeHelper.findUnquotedColon(itemContent); + final int colonIdx = DecodeHelper.findUnquotedColon(itemContent); // Simple scalar: - value if (colonIdx <= 0) { @@ -113,13 +117,13 @@ public static Object parseListItem(String content, int depth, DecodeContext cont } // Object item: - key: value - String key = StringEscaper.unescape(itemContent.substring(0, colonIdx).trim()); - String value = itemContent.substring(colonIdx + 1).trim(); + final String key = StringEscaper.unescape(itemContent.substring(0, colonIdx).trim()); + final String value = itemContent.substring(colonIdx + 1).trim(); context.currentLine++; - Map item = new LinkedHashMap<>(); - Object parsedValue; + final Map item = new LinkedHashMap<>(); + final Object parsedValue; // If no next line exists, handle a simple case if (context.currentLine >= context.lines.length) { parsedValue = value.isBlank() ? new LinkedHashMap<>() : PrimitiveDecoder.parse(value); @@ -142,15 +146,15 @@ public static Object parseListItem(String content, int depth, DecodeContext cont */ private static void parseListItemFields(Map item, int depth, DecodeContext context) { while (context.currentLine < context.lines.length) { - String line = context.lines[context.currentLine]; - int lineDepth = DecodeHelper.getDepth(line, context); + final String line = context.lines[context.currentLine]; + final int lineDepth = DecodeHelper.getDepth(line, context); if (lineDepth < depth + 2) { return; } if (lineDepth == depth + 2) { - String fieldContent = line.substring((depth + 2) * context.options.indent()); + final String fieldContent = line.substring((depth + 2) * context.options.indent()); // Try to parse as a keyed array first, then as a key-value pair boolean wasParsed = KeyDecoder.parseKeyedArrayField(fieldContent, item, depth, context); diff --git a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java index 60a3dc3..7a0827d 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java @@ -34,22 +34,23 @@ private TabularArrayDecoder() { * @param context decode an object to deal with lines, delimiter and options * @return tabular array converted to JSON format */ - public static List parseTabularArray(String header, int depth, Delimiter arrayDelimiter, DecodeContext context) { - Matcher matcher = TABULAR_HEADER_PATTERN.matcher(header); + public static List parseTabularArray(String header, int depth, Delimiter arrayDelimiter, + DecodeContext context) { + final Matcher matcher = TABULAR_HEADER_PATTERN.matcher(header); if (!matcher.find()) { return Collections.emptyList(); } - String keysStr = matcher.group(4); - List keys = parseTabularKeys(keysStr, arrayDelimiter, context); + final String keysStr = matcher.group(4); + final List keys = parseTabularKeys(keysStr, arrayDelimiter, context); - List result = new ArrayList<>(); + final List result = new ArrayList<>(); context.currentLine++; // Determine the expected row depth dynamically from the first non-blank line int expectedRowDepth = depth + 1; if (context.currentLine < context.lines.length) { - int nextNonBlankLine = DecodeHelper.findNextNonBlankLine(context.currentLine, context); + final int nextNonBlankLine = DecodeHelper.findNextNonBlankLine(context.currentLine, context); if (nextNonBlankLine < context.lines.length) { expectedRowDepth = DecodeHelper.getDepth(context.lines[nextNonBlankLine], context); } @@ -80,8 +81,8 @@ private static List parseTabularKeys(String keysStr, Delimiter arrayDeli validateKeysDelimiter(keysStr, arrayDelimiter); } - List result = new ArrayList<>(); - List rawValues = ArrayDecoder.parseDelimitedValues(keysStr, arrayDelimiter); + final List result = new ArrayList<>(); + final List rawValues = ArrayDecoder.parseDelimitedValues(keysStr, arrayDelimiter); for (String key : rawValues) { result.add(StringEscaper.unescape(key)); } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java index a3cc59c..2e30d60 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java @@ -3,10 +3,8 @@ import dev.toonformat.jtoon.DecodeOptions; import dev.toonformat.jtoon.util.ObjectMapperSingleton; import tools.jackson.databind.ObjectMapper; - import java.util.LinkedHashMap; import java.util.regex.Matcher; - import static dev.toonformat.jtoon.util.Constants.NULL_LITERAL; import static dev.toonformat.jtoon.util.Constants.OPEN_BRACKET; import static dev.toonformat.jtoon.util.Headers.KEYED_ARRAY_PATTERN; @@ -53,14 +51,16 @@ public static Object decode(String toon, DecodeOptions options) { } // Special case: if input is exactly "null", return null - String trimmed = toon.trim(); + final String trimmed = toon.trim(); if (NULL_LITERAL.equals(trimmed)) { return null; } // Don't trim leading whitespace - we need it for indentation validation // Only trim trailing whitespace to avoid issues with empty lines at the end - String processed = Character.isWhitespace(toon.charAt(toon.length() - 1)) ? toon.stripTrailing() : toon; + final String processed = Character.isWhitespace(toon.charAt(toon.length() - 1)) + ? toon.stripTrailing() + : toon; //set an own decode context final DecodeContext context = new DecodeContext(); @@ -68,9 +68,9 @@ public static Object decode(String toon, DecodeOptions options) { context.options = options; context.delimiter = options.delimiter(); - int lineIndex = context.currentLine; - String line = context.lines[lineIndex]; - int depth = DecodeHelper.getDepth(line, context); + final int lineIndex = context.currentLine; + final String line = context.lines[lineIndex]; + final int depth = DecodeHelper.getDepth(line, context); if (depth > 0) { if (context.options.strict()) { @@ -85,15 +85,15 @@ public static Object decode(String toon, DecodeOptions options) { } // Handle keyed arrays: items[2]{id,name}: - Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(line); + final Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(line); if (keyedArray.matches()) { return KeyDecoder.parseKeyedArrayValue(keyedArray, line, depth, context); } // Handle key-value pairs: name: Ada - int colonIdx = DecodeHelper.findUnquotedColon(line); + final int colonIdx = DecodeHelper.findUnquotedColon(line); if (colonIdx > 0) { - String key = line.substring(0, colonIdx).trim(); - String value = line.substring(colonIdx + 1).trim(); + final String key = line.substring(0, colonIdx).trim(); + final String value = line.substring(colonIdx + 1).trim(); return KeyDecoder.parseKeyValuePair(key, value, depth, depth == 0, context); } @@ -118,7 +118,7 @@ public static Object decode(String toon, DecodeOptions options) { */ public static String decodeToJson(String toon, DecodeOptions options) { try { - Object decoded = decode(toon, options); + final Object decoded = decode(toon, options); return MAPPER.writeValueAsString(decoded); } catch (Exception e) { throw new IllegalArgumentException("Failed to convert decoded value to JSON: " + e.getMessage(), e); diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java index 3a2abe9..0adf509 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java @@ -4,11 +4,9 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; - import java.util.ArrayList; import java.util.List; import java.util.stream.StreamSupport; - import static dev.toonformat.jtoon.util.Constants.LIST_ITEM_PREFIX; import static dev.toonformat.jtoon.util.Constants.SPACE; @@ -34,7 +32,7 @@ private ArrayEncoder() { */ public static void encodeArray(String key, ArrayNode value, LineWriter writer, int depth, EncodeOptions options) { if (value.isEmpty()) { - String header = PrimitiveEncoder.formatHeader(0, key, null, options.delimiter().toString(), + final String header = PrimitiveEncoder.formatHeader(0, key, null, options.delimiter().toString(), options.lengthMarker()); writer.push(depth, header); return; @@ -48,7 +46,7 @@ public static void encodeArray(String key, ArrayNode value, LineWriter writer, i // Array of arrays (all primitives) if (isArrayOfArrays(value)) { - boolean allPrimitiveArrays = StreamSupport.stream(value.spliterator(), false) + final boolean allPrimitiveArrays = StreamSupport.stream(value.spliterator(), false) .filter(JsonNode::isArray) .allMatch(ArrayEncoder::isArrayOfPrimitives); @@ -60,7 +58,7 @@ public static void encodeArray(String key, ArrayNode value, LineWriter writer, i // Array of objects if (isArrayOfObjects(value)) { - List header = TabularArrayEncoder.detectTabularHeader(value); + final List header = TabularArrayEncoder.detectTabularHeader(value); if (!header.isEmpty()) { TabularArrayEncoder.encodeArrayOfObjectsAsTabular(key, value, header, writer, depth, options); } else { @@ -128,11 +126,12 @@ public static boolean isArrayOfObjects(JsonNode array) { } /** - * Encodes a primitive array inline: key[N]: v1,v2,v3 + * Encodes a primitive array inline: key[N]: v1,v2,v3. */ private static void encodeInlinePrimitiveArray(String prefix, ArrayNode values, LineWriter writer, int depth, EncodeOptions options) { - String formatted = formatInlineArray(values, options.delimiter().toString(), prefix, options.lengthMarker()); + final String formatted = formatInlineArray(values, options.delimiter().toString(), prefix, + options.lengthMarker()); writer.push(depth, formatted); } @@ -146,11 +145,11 @@ private static void encodeInlinePrimitiveArray(String prefix, ArrayNode values, * @return the formatted inline array string */ public static String formatInlineArray(ArrayNode values, String delimiter, String prefix, boolean lengthMarker) { - List valueList = new ArrayList<>(); + final List valueList = new ArrayList<>(); values.forEach(valueList::add); - String header = PrimitiveEncoder.formatHeader(values.size(), prefix, null, delimiter, lengthMarker); - String joinedValue = PrimitiveEncoder.joinEncodedValues(valueList, delimiter); + final String header = PrimitiveEncoder.formatHeader(values.size(), prefix, null, delimiter, lengthMarker); + final String joinedValue = PrimitiveEncoder.joinEncodedValues(valueList, delimiter); // Only add space if there are values if (values.isEmpty()) { @@ -164,14 +163,14 @@ public static String formatInlineArray(ArrayNode values, String delimiter, Strin */ private static void encodeArrayOfArraysAsListItems(String prefix, ArrayNode values, LineWriter writer, int depth, EncodeOptions options) { - String header = PrimitiveEncoder.formatHeader(values.size(), prefix, null, options.delimiter().toString(), - options.lengthMarker()); + final String header = PrimitiveEncoder.formatHeader(values.size(), prefix, null, + options.delimiter().toString(), options.lengthMarker()); writer.push(depth, header); for (JsonNode arr : values) { if (arr.isArray() && isArrayOfPrimitives(arr)) { - String inline = formatInlineArray((ArrayNode) arr, options.delimiter().toString(), null, - options.lengthMarker()); + final String inline = formatInlineArray((ArrayNode) arr, options.delimiter().toString(), null, + options.lengthMarker()); writer.push(depth + 1, LIST_ITEM_PREFIX + inline); } } @@ -185,8 +184,8 @@ private static void encodeMixedArrayAsListItems(String prefix, LineWriter writer, int depth, EncodeOptions options) { - String header = PrimitiveEncoder.formatHeader(items.size(), prefix, null, options.delimiter().toString(), - options.lengthMarker()); + final String header = PrimitiveEncoder.formatHeader(items.size(), prefix, null, + options.delimiter().toString(), options.lengthMarker()); writer.push(depth, header); for (JsonNode item : items) { @@ -197,23 +196,23 @@ private static void encodeMixedArrayAsListItems(String prefix, } else if (item.isArray()) { // Direct array as list item if (isArrayOfPrimitives(item)) { - String inline = formatInlineArray((ArrayNode) item, options.delimiter().toString(), null, - options.lengthMarker()); + final String inline = formatInlineArray((ArrayNode) item, options.delimiter().toString(), null, + options.lengthMarker()); writer.push(depth + 1, LIST_ITEM_PREFIX + inline); } if (isArrayOfObjects(item)) { - ArrayNode arrayItems = (ArrayNode) item; - String nestedHeader = PrimitiveEncoder.formatHeader(arrayItems.size(), null, null, - options.delimiter().toString(), options.lengthMarker()); + final ArrayNode arrayItems = (ArrayNode) item; + final String nestedHeader = PrimitiveEncoder.formatHeader(arrayItems.size(), null, null, + options.delimiter().toString(), + options.lengthMarker()); writer.push(depth + 1, LIST_ITEM_PREFIX + nestedHeader); - arrayItems.elements() - .forEach(e -> ListItemEncoder.encodeObjectAsListItem((ObjectNode) e, writer, depth + 2, options)); + arrayItems.elements().forEach(e -> ListItemEncoder.encodeObjectAsListItem((ObjectNode) e, writer, + depth + 2, options)); } } else if (item.isObject()) { // Object as list item - delegate to ListItemEncoder - ListItemEncoder.encodeObjectAsListItem((ObjectNode) item, writer, - depth + 1, options); + ListItemEncoder.encodeObjectAsListItem((ObjectNode) item, writer, depth + 1, options); } } } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java index 82e7b4a..b6aa66f 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java @@ -2,14 +2,12 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ObjectNode; - import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; - import static dev.toonformat.jtoon.util.Constants.DOT; /** @@ -38,7 +36,7 @@ public record FoldResult(String foldedKey, } /** - * Represents the result of the Collect segments of the single-key chain + * Represents the result of the Collect segments of the single-key chain. * * @param segments collected single-key object * @param tail the tail node (if any) @@ -77,7 +75,7 @@ public static FoldResult tryFoldKeyChain(String key, } // start chain from absolute key - String absKey = (pathPrefix == null) ? key : String.join(DOT, pathPrefix, key); + final String absKey = (pathPrefix == null) ? key : String.join(DOT, pathPrefix, key); // Collect segments of the single-key chain final ChainResult chain = collectSingleKeyChain(absKey, value, remainingDepth); @@ -99,7 +97,7 @@ public static FoldResult tryFoldKeyChain(String key, } // Build folded key - String foldedKey = String.join(DOT, chain.segments); + final String foldedKey = String.join(DOT, chain.segments); // Detect collisions with sibling keys if (siblings.contains(foldedKey)) { diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ListItemEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ListItemEncoder.java index 2424f05..26e6fe5 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ListItemEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ListItemEncoder.java @@ -4,12 +4,10 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; - import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; - import static dev.toonformat.jtoon.util.Constants.LIST_ITEM_MARKER; import static dev.toonformat.jtoon.util.Constants.COLON; import static dev.toonformat.jtoon.util.Constants.SPACE; @@ -38,7 +36,7 @@ private ListItemEncoder() { * @param options Encoding options */ public static void encodeObjectAsListItem(ObjectNode obj, LineWriter writer, int depth, EncodeOptions options) { - List keys = new ArrayList<>(obj.propertyNames()); + final List keys = new ArrayList<>(obj.propertyNames()); if (keys.isEmpty()) { writer.push(depth, LIST_ITEM_MARKER); @@ -46,14 +44,15 @@ public static void encodeObjectAsListItem(ObjectNode obj, LineWriter writer, int } // First key-value on the same line as "- " - String firstKey = keys.get(0); - JsonNode firstValue = obj.get(firstKey); + final String firstKey = keys.get(0); + final JsonNode firstValue = obj.get(firstKey); encodeFirstKeyValue(firstKey, firstValue, writer, depth, options); // Remaining keys on indented lines for (int i = 1; i < keys.size(); i++) { - String key = keys.get(i); - ObjectEncoder.encodeKeyValuePair(key, obj.get(key), writer, depth + 1, options, new HashSet<>(keys), Set.of(), null, null, new HashSet<>()); + final String key = keys.get(i); + ObjectEncoder.encodeKeyValuePair(key, obj.get(key), writer, depth + 1, options, new HashSet<>(keys), + Set.of(), null, null, new HashSet<>()); } } @@ -63,7 +62,7 @@ public static void encodeObjectAsListItem(ObjectNode obj, LineWriter writer, int */ private static void encodeFirstKeyValue(String key, JsonNode value, LineWriter writer, int depth, EncodeOptions options) { - String encodedKey = PrimitiveEncoder.encodeKey(key); + final String encodedKey = PrimitiveEncoder.encodeKey(key); if (value.isValueNode()) { encodeFirstValueAsPrimitive(encodedKey, value, writer, depth, options); @@ -93,17 +92,18 @@ private static void encodeFirstValueAsArray(String key, String encodedKey, Array private static void encodeFirstArrayAsPrimitives(String key, ArrayNode arrayValue, LineWriter writer, int depth, EncodeOptions options) { - String formatted = ArrayEncoder.formatInlineArray(arrayValue, options.delimiter().toString(), key, - options.lengthMarker()); + final String formatted = ArrayEncoder.formatInlineArray(arrayValue, options.delimiter().toString(), key, + options.lengthMarker()); writer.push(depth, LIST_ITEM_PREFIX + formatted); } private static void encodeFirstArrayAsObjects(String key, String encodedKey, ArrayNode arrayValue, LineWriter writer, int depth, EncodeOptions options) { - List header = TabularArrayEncoder.detectTabularHeader(arrayValue); + final List header = TabularArrayEncoder.detectTabularHeader(arrayValue); if (!header.isEmpty()) { - String headerStr = PrimitiveEncoder.formatHeader(arrayValue.size(), key, header, - options.delimiter().toString(), options.lengthMarker()); + final String headerStr = PrimitiveEncoder.formatHeader(arrayValue.size(), key, header, + options.delimiter().toString(), + options.lengthMarker()); writer.push(depth, LIST_ITEM_PREFIX + headerStr); // Write just the rows, header was already written above TabularArrayEncoder.writeTabularRows(arrayValue, header, writer, depth + 2, options); @@ -118,8 +118,8 @@ private static void encodeFirstArrayAsObjects(String key, String encodedKey, Arr } } - private static void encodeFirstArrayAsComplex(String encodedKey, ArrayNode arrayValue, LineWriter writer, int depth, - EncodeOptions options) { + private static void encodeFirstArrayAsComplex(String encodedKey, ArrayNode arrayValue, LineWriter writer, + int depth, EncodeOptions options) { writer.push(depth, LIST_ITEM_PREFIX + encodedKey + OPEN_BRACKET + arrayValue.size() + CLOSE_BRACKET + COLON); for (JsonNode item : arrayValue) { @@ -127,8 +127,8 @@ private static void encodeFirstArrayAsComplex(String encodedKey, ArrayNode array writer.push(depth + 2, LIST_ITEM_PREFIX + PrimitiveEncoder.encodePrimitive(item, options.delimiter().toString())); } else if (item.isArray() && ArrayEncoder.isArrayOfPrimitives(item)) { - String inline = ArrayEncoder.formatInlineArray((ArrayNode) item, options.delimiter().toString(), null, - options.lengthMarker()); + final String inline = ArrayEncoder.formatInlineArray((ArrayNode) item, options.delimiter().toString(), + null, options.lengthMarker()); writer.push(depth + 2, LIST_ITEM_PREFIX + inline); } else if (item.isObject()) { encodeObjectAsListItem((ObjectNode) item, writer, depth + 2, options); diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java index 7b4a30f..fea2885 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java @@ -5,13 +5,11 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; - import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; - import static dev.toonformat.jtoon.util.Constants.DOT; import static dev.toonformat.jtoon.util.Constants.COLON; import static dev.toonformat.jtoon.util.Constants.SPACE; @@ -38,8 +36,10 @@ private ObjectEncoder() { * @param remainingDepth optional override for the remaining depth * @param blockedKeys contains only keys that have undergone a successful flattening */ - public static void encodeObject(ObjectNode value, LineWriter writer, int depth, EncodeOptions options, Set rootLiteralKeys, String pathPrefix, Integer remainingDepth, Set blockedKeys) { - List> fields = value.properties().stream().toList(); + public static void encodeObject(ObjectNode value, LineWriter writer, int depth, EncodeOptions options, + Set rootLiteralKeys, String pathPrefix, Integer remainingDepth, + Set blockedKeys) { + final List> fields = value.properties().stream().toList(); // At root level (depth 0), collect all literal dotted keys for collision checking if (depth == 0 && rootLiteralKeys != null) { @@ -49,15 +49,16 @@ public static void encodeObject(ObjectNode value, LineWriter writer, int depth, .map(Map.Entry::getKey) .forEach(rootLiteralKeys::add); } - int effectiveFlattenDepth = remainingDepth != null ? remainingDepth : options.flattenDepth(); + final int effectiveFlattenDepth = remainingDepth != null ? remainingDepth : options.flattenDepth(); //the siblings collision do not need the absolute path - Set siblings = fields.stream() + final Set siblings = fields.stream() .map(Map.Entry::getKey) .collect(Collectors.toCollection(LinkedHashSet::new)); for (Map.Entry entry : fields) { - encodeKeyValuePair(entry.getKey(), entry.getValue(), writer, depth, options, siblings, rootLiteralKeys, pathPrefix, effectiveFlattenDepth, blockedKeys); + encodeKeyValuePair(entry.getKey(), entry.getValue(), writer, depth, options, siblings, rootLiteralKeys, + pathPrefix, effectiveFlattenDepth, blockedKeys); } } @@ -89,10 +90,10 @@ public static void encodeKeyValuePair(String key, if (key == null) { return; } - String encodedKey = PrimitiveEncoder.encodeKey(key); - String currentPath = pathPrefix != null ? pathPrefix + DOT + key : key; - int effectiveFlattenDepth = flattenDepth != null && flattenDepth > 0 ? flattenDepth : options.flattenDepth(); - int remainingDepth = effectiveFlattenDepth - depth; + final String encodedKey = PrimitiveEncoder.encodeKey(key); + final String currentPath = pathPrefix != null ? pathPrefix + DOT + key : key; + final int effectiveFlattenDepth = flattenDepth != null && flattenDepth > 0 ? flattenDepth : options.flattenDepth(); + final int remainingDepth = effectiveFlattenDepth - depth; EncodeOptions currentOptions = options; // Attempt key folding when enabled @@ -101,9 +102,11 @@ public static void encodeKeyValuePair(String key, && remainingDepth > 0 && blockedKeys != null && !blockedKeys.contains(key)) { - Flatten.FoldResult foldResult = Flatten.tryFoldKeyChain(key, value, siblings, rootLiteralKeys, pathPrefix, remainingDepth); + final Flatten.FoldResult foldResult = Flatten.tryFoldKeyChain(key, value, siblings, rootLiteralKeys, + pathPrefix, remainingDepth); if (foldResult != null) { - currentOptions = flatten(key, foldResult, writer, depth, currentOptions, rootLiteralKeys, pathPrefix, blockedKeys, remainingDepth); + currentOptions = flatten(key, foldResult, writer, depth, currentOptions, rootLiteralKeys, pathPrefix, + blockedKeys, remainingDepth); if (currentOptions == null) { return; } @@ -111,16 +114,18 @@ public static void encodeKeyValuePair(String key, } if (value.isValueNode()) { - writer.push(depth, encodedKey + COLON + SPACE + PrimitiveEncoder.encodePrimitive(value, currentOptions.delimiter().toString())); + writer.push(depth, encodedKey + COLON + SPACE + + PrimitiveEncoder.encodePrimitive(value, currentOptions.delimiter().toString())); } if (value.isArray()) { ArrayEncoder.encodeArray(key, (ArrayNode) value, writer, depth, currentOptions); } if (value.isObject()) { - ObjectNode objValue = (ObjectNode) value; + final ObjectNode objValue = (ObjectNode) value; writer.push(depth, encodedKey + COLON); if (!objValue.isEmpty()) { - encodeObject(objValue, writer, depth + 1, currentOptions, rootLiteralKeys, currentPath, effectiveFlattenDepth, blockedKeys); + encodeObject(objValue, writer, depth + 1, currentOptions, rootLiteralKeys, currentPath, + effectiveFlattenDepth, blockedKeys); } } } @@ -139,17 +144,18 @@ public static void encodeKeyValuePair(String key, * @param remainingDepth the depth that remind to the limit * @return EncodeOptions changes for Case 2 */ - private static EncodeOptions flatten(String key, Flatten.FoldResult foldResult, LineWriter writer, int depth, EncodeOptions options, Set rootLiteralKeys, String pathPrefix, Set blockedKeys, - int remainingDepth) { - String foldedKey = foldResult.foldedKey(); + private static EncodeOptions flatten(String key, Flatten.FoldResult foldResult, LineWriter writer, int depth, + EncodeOptions options, Set rootLiteralKeys, String pathPrefix, + Set blockedKeys, int remainingDepth) { + final String foldedKey = foldResult.foldedKey(); EncodeOptions currentOptions = options; // prevent second folding pass blockedKeys.add(key); blockedKeys.add(foldedKey); - String encodedFoldedKey = PrimitiveEncoder.encodeKey(foldedKey); - JsonNode remainder = foldResult.remainder(); + final String encodedFoldedKey = PrimitiveEncoder.encodeKey(foldedKey); + final JsonNode remainder = foldResult.remainder(); // Case 1: Fully folded to a leaf value if (remainder == null) { @@ -161,32 +167,36 @@ private static EncodeOptions flatten(String key, Flatten.FoldResult foldResult, if (remainder.isObject()) { writer.push(depth, indentedLine(depth, encodedFoldedKey + COLON, currentOptions.indent())); - String foldedPath = pathPrefix != null ? String.join(DOT, pathPrefix, foldedKey) : foldedKey; + final String foldedPath = pathPrefix != null ? String.join(DOT, pathPrefix, foldedKey) : foldedKey; int newRemainingDepth = remainingDepth - foldResult.segmentCount(); if (newRemainingDepth <= 0) { // Pass "-1" if remainingDepth is exhausted and set the encoding in the option to false. // to encode normally without flattening newRemainingDepth = -1; - currentOptions = new EncodeOptions(currentOptions.indent(), currentOptions.delimiter(), currentOptions.lengthMarker(), KeyFolding.OFF, currentOptions.flattenDepth()); + currentOptions = new EncodeOptions(currentOptions.indent(), currentOptions.delimiter(), + currentOptions.lengthMarker(), KeyFolding.OFF, + currentOptions.flattenDepth()); } - encodeObject((ObjectNode) remainder, writer, depth + 1, currentOptions, rootLiteralKeys, foldedPath, newRemainingDepth, blockedKeys); + encodeObject((ObjectNode) remainder, writer, depth + 1, currentOptions, rootLiteralKeys, foldedPath, + newRemainingDepth, blockedKeys); return null; } return currentOptions; } - private static void handleFullyFoldedLeaf(Flatten.FoldResult foldResult, LineWriter writer, int depth, EncodeOptions options, String encodedFoldedKey) { - JsonNode leaf = foldResult.leafValue(); + private static void handleFullyFoldedLeaf(Flatten.FoldResult foldResult, LineWriter writer, int depth, + EncodeOptions options, String encodedFoldedKey) { + final JsonNode leaf = foldResult.leafValue(); // Primitive if (leaf.isValueNode()) { writer.push(depth, indentedLine(depth, - encodedFoldedKey + COLON + SPACE + - PrimitiveEncoder.encodePrimitive(leaf, options.delimiter().toString()), + encodedFoldedKey + COLON + SPACE + + PrimitiveEncoder.encodePrimitive(leaf, options.delimiter().toString()), options.indent())); return; } @@ -201,8 +211,7 @@ private static void handleFullyFoldedLeaf(Flatten.FoldResult foldResult, LineWri if (leaf.isObject()) { writer.push(depth, indentedLine(depth, encodedFoldedKey + COLON, options.indent())); if (!leaf.isEmpty()) { - encodeObject((ObjectNode) leaf, writer, depth + 1, options, - null, null, null, null); + encodeObject((ObjectNode) leaf, writer, depth + 1, options, null, null, null, null); } } } From ce9fd32e757492cdc22c1096bd60a99a4a0b1711 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 20:03:44 +0100 Subject: [PATCH 09/29] cleanup warnings --- .../dev/toonformat/jtoon/DecodeOptions.java | 2 +- .../java/dev/toonformat/jtoon/Delimiter.java | 10 +-- .../dev/toonformat/jtoon/EncodeOptions.java | 12 ++-- src/main/java/dev/toonformat/jtoon/JToon.java | 4 +- .../java/dev/toonformat/jtoon/KeyFolding.java | 2 +- .../jtoon/decoder/ArrayDecoder.java | 69 ++++++++++--------- .../jtoon/decoder/DecodeContext.java | 4 +- .../jtoon/decoder/DecodeHelper.java | 14 ++-- .../toonformat/jtoon/decoder/KeyDecoder.java | 2 +- .../jtoon/decoder/ListItemDecoder.java | 2 - .../jtoon/decoder/ObjectDecoder.java | 45 ++++++------ .../jtoon/decoder/PrimitiveDecoder.java | 5 +- .../jtoon/decoder/TabularArrayDecoder.java | 42 +++++------ .../dev/toonformat/jtoon/encoder/Flatten.java | 10 +-- .../jtoon/encoder/HeaderFormatter.java | 5 +- .../toonformat/jtoon/encoder/LineWriter.java | 3 +- .../jtoon/encoder/ObjectEncoder.java | 4 +- .../jtoon/encoder/PrimitiveEncoder.java | 8 +-- .../jtoon/encoder/TabularArrayEncoder.java | 23 +++---- .../jtoon/encoder/ValueEncoder.java | 5 +- .../jtoon/normalizer/JsonNormalizer.java | 11 ++- .../dev/toonformat/jtoon/util/Headers.java | 8 +-- .../jtoon/util/ObjectMapperSingleton.java | 1 - .../toonformat/jtoon/util/StringEscaper.java | 4 +- .../jtoon/util/StringValidator.java | 5 +- 25 files changed, 146 insertions(+), 154 deletions(-) diff --git a/src/main/java/dev/toonformat/jtoon/DecodeOptions.java b/src/main/java/dev/toonformat/jtoon/DecodeOptions.java index 8163247..6f085a0 100644 --- a/src/main/java/dev/toonformat/jtoon/DecodeOptions.java +++ b/src/main/java/dev/toonformat/jtoon/DecodeOptions.java @@ -18,7 +18,7 @@ public record DecodeOptions( boolean strict, PathExpansion expandPaths) { /** - * Default decoding options: 2 spaces indent, comma delimiter, strict validation, path expansion off + * Default decoding options: 2 spaces indent, comma delimiter, strict validation, path expansion off. */ public static final DecodeOptions DEFAULT = new DecodeOptions(2, Delimiter.COMMA, true, PathExpansion.OFF); diff --git a/src/main/java/dev/toonformat/jtoon/Delimiter.java b/src/main/java/dev/toonformat/jtoon/Delimiter.java index 4e5ebe0..e5bb5ae 100644 --- a/src/main/java/dev/toonformat/jtoon/Delimiter.java +++ b/src/main/java/dev/toonformat/jtoon/Delimiter.java @@ -5,24 +5,24 @@ */ public enum Delimiter { /** - * Comma delimiter (,) - default option + * Comma delimiter (,) - default option. */ COMMA(","), /** - * Tab delimiter (\t) + * Tab delimiter (\t). */ TAB("\t"), /** - * Pipe delimiter (|) + * Pipe delimiter (|). */ PIPE("|"); private final String value; - Delimiter(String value) { - this.value = value; + Delimiter(String delimiterValue) { + this.value = delimiterValue; } /** diff --git a/src/main/java/dev/toonformat/jtoon/EncodeOptions.java b/src/main/java/dev/toonformat/jtoon/EncodeOptions.java index 43ae51d..1e92a2d 100644 --- a/src/main/java/dev/toonformat/jtoon/EncodeOptions.java +++ b/src/main/java/dev/toonformat/jtoon/EncodeOptions.java @@ -21,9 +21,10 @@ public record EncodeOptions( KeyFolding flatten, int flattenDepth) { /** - * Default encoding options: 2 spaces indent, comma delimiter, no length marker + * Default encoding options: 2 spaces indent, comma delimiter, no length marker. */ - public static final EncodeOptions DEFAULT = new EncodeOptions(2, Delimiter.COMMA, false, KeyFolding.OFF, Integer.MAX_VALUE); + public static final EncodeOptions DEFAULT = new EncodeOptions( + 2, Delimiter.COMMA, false, KeyFolding.OFF, Integer.MAX_VALUE); /** * Creates EncodeOptions with default values. @@ -73,12 +74,13 @@ public static EncodeOptions withLengthMarker(boolean lengthMarker) { * @return a new EncodeOptions instance with the flatten setting */ public static EncodeOptions withFlatten(boolean flatten) { - return new EncodeOptions(2, Delimiter.COMMA, false, flatten ? KeyFolding.SAFE : KeyFolding.OFF, Integer.MAX_VALUE); + return new EncodeOptions(2, Delimiter.COMMA, false, + flatten ? KeyFolding.SAFE : KeyFolding.OFF, Integer.MAX_VALUE); } /** - * Creates EncodeOptions with custom flatten flag and the depth of to flatten the nested objects, using default indent and - * delimiter. + * Creates EncodeOptions with custom flatten flag and the depth of to flatten + * the nested objects, using default indent and delimiter. * * @param flattenDepth optional maximum depth to flatten nested objects. * @return a new EncodeOptions instance with the flatten setting and the depth of to flatten the nested objects. diff --git a/src/main/java/dev/toonformat/jtoon/JToon.java b/src/main/java/dev/toonformat/jtoon/JToon.java index 518de7f..09bc59b 100644 --- a/src/main/java/dev/toonformat/jtoon/JToon.java +++ b/src/main/java/dev/toonformat/jtoon/JToon.java @@ -43,7 +43,7 @@ public static String encode(Object input) { * @return The JToon-formatted string */ public static String encode(Object input, EncodeOptions options) { - JsonNode normalizedValue = JsonNormalizer.normalize(input); + final JsonNode normalizedValue = JsonNormalizer.normalize(input); return ValueEncoder.encodeValue(normalizedValue, options); } @@ -79,7 +79,7 @@ public static String encodeJson(String json) { * @throws IllegalArgumentException if the input is not valid JSON */ public static String encodeJson(String json, EncodeOptions options) { - JsonNode parsed = JsonNormalizer.parse(json); + final JsonNode parsed = JsonNormalizer.parse(json); return ValueEncoder.encodeValue(parsed, options); } diff --git a/src/main/java/dev/toonformat/jtoon/KeyFolding.java b/src/main/java/dev/toonformat/jtoon/KeyFolding.java index 3189cea..23d8f45 100644 --- a/src/main/java/dev/toonformat/jtoon/KeyFolding.java +++ b/src/main/java/dev/toonformat/jtoon/KeyFolding.java @@ -12,7 +12,7 @@ public enum KeyFolding { SAFE, /** - * Off mode: default + * Off mode: default. */ OFF } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java index 8d992df..1b4193e 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java @@ -1,12 +1,10 @@ package dev.toonformat.jtoon.decoder; import dev.toonformat.jtoon.Delimiter; - import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; - import static dev.toonformat.jtoon.util.Constants.BACKSLASH; import static dev.toonformat.jtoon.util.Constants.COLON; import static dev.toonformat.jtoon.util.Constants.DOUBLE_QUOTE; @@ -19,6 +17,8 @@ */ public final class ArrayDecoder { + private static final int DELIMITER_GROUP_INDEX = 3; + private ArrayDecoder() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); } @@ -33,7 +33,7 @@ private ArrayDecoder() { * @return parsed array with delimiter */ static List parseArray(String header, int depth, DecodeContext context) { - Delimiter arrayDelimiter = extractDelimiterFromHeader(header, context); + final Delimiter arrayDelimiter = extractDelimiterFromHeader(header, context); return parseArrayWithDelimiter(header, depth, arrayDelimiter, context); } @@ -47,9 +47,9 @@ static List parseArray(String header, int depth, DecodeContext context) * @return extracted delimiter from header */ static Delimiter extractDelimiterFromHeader(String header, DecodeContext context) { - Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header); + final Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header); if (matcher.find()) { - String delimiter = matcher.group(3); + final String delimiter = matcher.group(DELIMITER_GROUP_INDEX); if (delimiter != null) { if (Delimiter.TAB.toString().equals(delimiter)) { return Delimiter.TAB; @@ -74,23 +74,24 @@ static Delimiter extractDelimiterFromHeader(String header, DecodeContext context * @param context decode an object to deal with lines, delimiter and options * @return parsed array */ - static List parseArrayWithDelimiter(String header, int depth, Delimiter arrayDelimiter, DecodeContext context) { - Matcher tabularMatcher = TABULAR_HEADER_PATTERN.matcher(header); - Matcher arrayMatcher = ARRAY_HEADER_PATTERN.matcher(header); + static List parseArrayWithDelimiter(String header, int depth, Delimiter arrayDelimiter, + DecodeContext context) { + final Matcher tabularMatcher = TABULAR_HEADER_PATTERN.matcher(header); + final Matcher arrayMatcher = ARRAY_HEADER_PATTERN.matcher(header); if (tabularMatcher.find()) { return TabularArrayDecoder.parseTabularArray(header, depth, arrayDelimiter, context); } if (arrayMatcher.find()) { - int headerEndIdx = arrayMatcher.end(); - String afterHeader = header.substring(headerEndIdx).trim(); + final int headerEndIdx = arrayMatcher.end(); + final String afterHeader = header.substring(headerEndIdx).trim(); if (afterHeader.startsWith(COLON)) { - String inlineContent = afterHeader.substring(1).trim(); + final String inlineContent = afterHeader.substring(1).trim(); if (!inlineContent.isEmpty()) { - List result = parseArrayValues(inlineContent, arrayDelimiter); + final List result = parseArrayValues(inlineContent, arrayDelimiter); validateArrayLength(header, result.size()); context.currentLine++; return result; @@ -99,9 +100,9 @@ static List parseArrayWithDelimiter(String header, int depth, Delimiter context.currentLine++; if (context.currentLine < context.lines.length) { - String nextLine = context.lines[context.currentLine]; - int nextDepth = DecodeHelper.getDepth(nextLine, context); - String nextContent = nextLine.substring(nextDepth * context.options.indent()); + final String nextLine = context.lines[context.currentLine]; + final int nextDepth = DecodeHelper.getDepth(nextLine, context); + final String nextContent = nextLine.substring(nextDepth * context.options.indent()); if (nextDepth <= depth) { // The next line is not a child of this array, @@ -115,12 +116,12 @@ static List parseArrayWithDelimiter(String header, int depth, Delimiter return parseListArray(depth, header, context); } else { context.currentLine++; - List result = parseArrayValues(nextContent, arrayDelimiter); + final List result = parseArrayValues(nextContent, arrayDelimiter); validateArrayLength(header, result.size()); return result; } } - List empty = new ArrayList<>(); + final List empty = new ArrayList<>(); validateArrayLength(header, 0); return empty; } @@ -138,7 +139,7 @@ static List parseArrayWithDelimiter(String header, int depth, Delimiter * @param actualLength actual length */ static void validateArrayLength(String header, int actualLength) { - Integer declaredLength = extractLengthFromHeader(header); + final Integer declaredLength = extractLengthFromHeader(header); if (declaredLength != null && declaredLength != actualLength) { throw new IllegalArgumentException( String.format("Array length mismatch: declared %d, found %d", declaredLength, actualLength)); @@ -153,7 +154,7 @@ static void validateArrayLength(String header, int actualLength) { * @return extracted length from header */ private static Integer extractLengthFromHeader(String header) { - Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header); + final Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header); if (matcher.find()) { return Integer.parseInt(matcher.group(2)); } @@ -168,9 +169,9 @@ private static Integer extractLengthFromHeader(String header) { * @return parsed array values */ static List parseArrayValues(String values, Delimiter arrayDelimiter) { - List result = new ArrayList<>(); - List rawValues = parseDelimitedValues(values, arrayDelimiter); - for (String value : rawValues) { + final List result = new ArrayList<>(); + final List rawValues = parseDelimitedValues(values, arrayDelimiter); + for (final String value : rawValues) { result.add(PrimitiveDecoder.parse(value)); } return result; @@ -185,15 +186,15 @@ static List parseArrayValues(String values, Delimiter arrayDelimiter) { * @return parsed delimited values */ static List parseDelimitedValues(String input, Delimiter arrayDelimiter) { - List result = new ArrayList<>(); - StringBuilder stringBuilder = new StringBuilder(); + final List result = new ArrayList<>(); + final StringBuilder stringBuilder = new StringBuilder(); boolean inQuotes = false; boolean escaped = false; - char delimiterChar = arrayDelimiter.toString().charAt(0); + final char delimiterChar = arrayDelimiter.toString().charAt(0); int i = 0; while (i < input.length()) { - char currentChar = input.charAt(i); + final char currentChar = input.charAt(i); if (escaped) { stringBuilder.append(currentChar); @@ -209,9 +210,9 @@ static List parseDelimitedValues(String input, Delimiter arrayDelimiter) i++; } else if (currentChar == delimiterChar && !inQuotes) { // Found delimiter - add stringBuilder value (trimmed) and reset - String value = stringBuilder.toString().trim(); + final String value = stringBuilder.toString().trim(); result.add(value); - stringBuilder = new StringBuilder(); + stringBuilder.setLength(0); // Skip whitespace after delimiter do { i++; @@ -235,19 +236,19 @@ static List parseDelimitedValues(String input, Delimiter arrayDelimiter) * Example: items[2]:\n - item1\n - item2 */ private static List parseListArray(int depth, String header, DecodeContext context) { - List result = new ArrayList<>(); + final List result = new ArrayList<>(); context.currentLine++; boolean shouldContinue = true; while (shouldContinue && context.currentLine < context.lines.length) { - String line = context.lines[context.currentLine]; + final String line = context.lines[context.currentLine]; if (DecodeHelper.isBlankLine(line)) { if (handleBlankLineInListArray(depth, context)) { shouldContinue = false; } } else { - int lineDepth = DecodeHelper.getDepth(line, context); + final int lineDepth = DecodeHelper.getDepth(line, context); if (shouldTerminateListArray(lineDepth, depth, line, context)) { shouldContinue = false; } else { @@ -271,13 +272,13 @@ private static List parseListArray(int depth, String header, DecodeConte * @return true if an array should terminate, false if a line should be skipped */ private static boolean handleBlankLineInListArray(int depth, DecodeContext context) { - int nextNonBlankLine = DecodeHelper.findNextNonBlankLine(context.currentLine + 1, context); + final int nextNonBlankLine = DecodeHelper.findNextNonBlankLine(context.currentLine + 1, context); if (nextNonBlankLine >= context.lines.length) { return true; // EOF - terminate array } - int nextDepth = DecodeHelper.getDepth(context.lines[nextNonBlankLine], context); + final int nextDepth = DecodeHelper.getDepth(context.lines[nextNonBlankLine], context); if (nextDepth <= depth) { return true; // Blank line is outside array - terminate } @@ -305,7 +306,7 @@ private static boolean shouldTerminateListArray(int lineDepth, int depth, String } // Also terminate if line is at expected depth but doesn't start with "-" if (lineDepth == depth + 1) { - String content = line.substring((depth + 1) * context.options.indent()); + final String content = line.substring((depth + 1) * context.options.indent()); return !content.startsWith("-"); // Not an array item - terminate } return false; diff --git a/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java b/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java index dbb0b70..2447915 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/DecodeContext.java @@ -4,7 +4,7 @@ import dev.toonformat.jtoon.Delimiter; /** - * Deals with the main attributes used to decode TOON to JSON format + * Deals with the main attributes used to decode TOON to JSON format. */ public class DecodeContext { @@ -26,7 +26,7 @@ public class DecodeContext { protected int currentLine; /** - * Default constructor + * Default constructor. */ public DecodeContext() { } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java b/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java index 2a75b90..f15a59d 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java @@ -1,10 +1,8 @@ package dev.toonformat.jtoon.decoder; import dev.toonformat.jtoon.Delimiter; - import java.util.List; import java.util.Map; - import static dev.toonformat.jtoon.util.Constants.BACKSLASH; import static dev.toonformat.jtoon.util.Constants.DOUBLE_QUOTE; import static dev.toonformat.jtoon.util.Constants.SPACE; @@ -45,13 +43,13 @@ public static int getDepth(String line, DecodeContext context) { * @return amount of leading spaces */ private static int computeLeadingSpaces(String line, DecodeContext context) { - int indentSize = context.options.indent(); + final int indentSize = context.options.indent(); int leadingSpaces = 0; int i = 0; - int lengthOfLine = line.length(); + final int lengthOfLine = line.length(); while (i < lengthOfLine) { - char c = line.charAt(i); + final char c = line.charAt(i); if (c == SPACE.charAt(0)) { leadingSpaces++; } else if (c == Delimiter.TAB.getValue()) { @@ -99,7 +97,7 @@ static int findUnquotedColon(String content) { boolean escaped = false; for (int i = 0; i < content.length(); i++) { - char c = content.charAt(i); + final char c = content.charAt(i); if (escaped) { escaped = false; @@ -169,7 +167,7 @@ static void checkPathExpansionConflict(Map map, String key, Obje return; } - Object existing = map.get(key); + final Object existing = map.get(key); checkFinalValueConflict(key, existing, value, context); } @@ -204,7 +202,7 @@ static void validateNoMultiplePrimitivesAtRoot(DecodeContext context) { lineIndex++; } if (lineIndex < context.lines.length) { - int nextDepth = getDepth(context.lines[lineIndex], context); + final int nextDepth = getDepth(context.lines[lineIndex], context); if (nextDepth == 0) { throw new IllegalArgumentException( "Multiple primitives at root depth in strict mode at line " + (lineIndex + 1)); diff --git a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java index 8ce8f22..4dde16c 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java @@ -165,7 +165,7 @@ static boolean shouldExpandKey(String key, DecodeContext context) { // Valid identifier: starts with a letter or underscore, followed by letters, // digits, underscores // Each segment must match this pattern - String[] segments = key.split("\\."); + final String[] segments = key.split("\\."); for (String segment : segments) { if (!segment.matches("^[a-zA-Z_]\\w*$")) { return false; diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java index 45ac4e4..58fadf7 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java @@ -2,12 +2,10 @@ import dev.toonformat.jtoon.Delimiter; import dev.toonformat.jtoon.util.StringEscaper; - import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; - import static dev.toonformat.jtoon.util.Constants.LIST_ITEM_MARKER; import static dev.toonformat.jtoon.util.Constants.OPEN_BRACKET; import static dev.toonformat.jtoon.util.Headers.KEYED_ARRAY_PATTERN; diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java index 430dbb0..e147d44 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java @@ -1,12 +1,10 @@ package dev.toonformat.jtoon.decoder; import dev.toonformat.jtoon.util.StringEscaper; - import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; - import static dev.toonformat.jtoon.util.Headers.KEYED_ARRAY_PATTERN; /** @@ -26,10 +24,10 @@ private ObjectDecoder() { * @return parsed nested object */ static Map parseNestedObject(int parentDepth, DecodeContext context) { - Map result = new LinkedHashMap<>(); + final Map result = new LinkedHashMap<>(); while (context.currentLine < context.lines.length) { - String line = context.lines[context.currentLine]; + final String line = context.lines[context.currentLine]; // Skip blank lines if (DecodeHelper.isBlankLine(line)) { @@ -37,7 +35,7 @@ static Map parseNestedObject(int parentDepth, DecodeContext cont continue; } - int depth = DecodeHelper.getDepth(line, context); + final int depth = DecodeHelper.getDepth(line, context); if (depth <= parentDepth) { return result; @@ -58,9 +56,10 @@ static Map parseNestedObject(int parentDepth, DecodeContext cont * Returns true if the line was processed, false if it was a blank line that was * skipped. */ - private static void processDirectChildLine(Map result, String line, int parentDepth, int depth, DecodeContext context) { - String content = line.substring((parentDepth + 1) * context.options.indent()); - Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(content); + private static void processDirectChildLine(Map result, String line, int parentDepth, int depth, + DecodeContext context) { + final String content = line.substring((parentDepth + 1) * context.options.indent()); + final Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(content); if (keyedArray.find()) { KeyDecoder.processKeyedArrayLine(result, content, keyedArray.group(1), parentDepth, context); @@ -78,8 +77,8 @@ private static void processDirectChildLine(Map result, String li */ static void parseRootObjectFields(Map obj, int depth, DecodeContext context) { while (context.currentLine < context.lines.length) { - String line = context.lines[context.currentLine]; - int lineDepth = DecodeHelper.getDepth(line, context); + final String line = context.lines[context.currentLine]; + final int lineDepth = DecodeHelper.getDepth(line, context); if (lineDepth != depth) { return; @@ -91,16 +90,16 @@ static void parseRootObjectFields(Map obj, int depth, DecodeCont continue; } - String content = line.substring(depth * context.options.indent()); + final String content = line.substring(depth * context.options.indent()); - Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(content); + final Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(content); if (keyedArray.matches()) { processRootKeyedArrayLine(obj, content, keyedArray.group(1), depth, context); } else { - int colonIdx = DecodeHelper.findUnquotedColon(content); + final int colonIdx = DecodeHelper.findUnquotedColon(content); if (colonIdx > 0) { - String key = content.substring(0, colonIdx).trim(); - String value = content.substring(colonIdx + 1).trim(); + final String key = content.substring(0, colonIdx).trim(); + final String value = content.substring(colonIdx + 1).trim(); KeyDecoder.parseKeyValuePairIntoMap(obj, key, value, depth, context); } else { @@ -121,11 +120,11 @@ static void parseRootObjectFields(Map obj, int depth, DecodeCont */ private static void processRootKeyedArrayLine(Map objectMap, String content, String originalKey, int depth, DecodeContext context) { - String originalKeyTrimmed = originalKey.trim(); - String key = StringEscaper.unescape(originalKey); - String arrayHeader = content.substring(originalKey.length()); + final String originalKeyTrimmed = originalKey.trim(); + final String key = StringEscaper.unescape(originalKey); + final String arrayHeader = content.substring(originalKey.length()); - List arrayValue = ArrayDecoder.parseArray(arrayHeader, depth, context); + final List arrayValue = ArrayDecoder.parseArray(arrayHeader, depth, context); // Handle path expansion for array keys if (KeyDecoder.shouldExpandKey(originalKeyTrimmed, context)) { @@ -146,7 +145,7 @@ private static void processRootKeyedArrayLine(Map objectMap, Str * @return the parsed scalar value */ static Object parseBareScalarValue(String content, int depth, DecodeContext context) { - Object result = PrimitiveDecoder.parse(content); + final Object result = PrimitiveDecoder.parse(content); context.currentLine++; // In strict mode, check if there are more primitives at the root level @@ -168,7 +167,7 @@ static Object parseBareScalarValue(String content, int depth, DecodeContext cont static Object parseFieldValue(String fieldValue, int fieldDepth, DecodeContext context) { // Check if the next line is nested if (context.currentLine + 1 < context.lines.length) { - int nextDepth = DecodeHelper.getDepth(context.lines[context.currentLine + 1], context); + final int nextDepth = DecodeHelper.getDepth(context.lines[context.currentLine + 1], context); if (nextDepth > fieldDepth) { context.currentLine++; // parseNestedObject manages the currentLine, so we don't increment here @@ -206,10 +205,10 @@ static Object parseFieldValue(String fieldValue, int fieldDepth, DecodeContext c * @return the parsed value (Map, List, or primitive) */ static Object parseObjectItemValue(String value, int depth, DecodeContext context) { - boolean isEmpty = value.isBlank(); + final boolean isEmpty = value.isBlank(); // Find the next non-blank line and its depth - Integer nextDepth = DecodeHelper.findNextNonBlankLineDepth(context); + final Integer nextDepth = DecodeHelper.findNextNonBlankLineDepth(context); if (nextDepth == null) { // No non-blank line found - create an empty object return new LinkedHashMap<>(); diff --git a/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java index e88aa55..5ffb466 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java @@ -1,7 +1,6 @@ package dev.toonformat.jtoon.decoder; import dev.toonformat.jtoon.util.StringEscaper; - import static dev.toonformat.jtoon.util.Constants.DOT; import static dev.toonformat.jtoon.util.Constants.NULL_LITERAL; import static dev.toonformat.jtoon.util.Constants.TRUE_LITERAL; @@ -75,7 +74,7 @@ static Object parse(String value) { } // Check for leading zeros (treat as string, except for "0", "-0", "0.0", etc.) - String trimmed = value.trim(); + final String trimmed = value.trim(); if (trimmed.length() > 1 && trimmed.matches("^-?0+[0-7].*")) { return value; } @@ -84,7 +83,7 @@ static Object parse(String value) { try { // Check if it contains exponent notation or decimal point if (value.contains(DOT) || value.contains("e") || value.contains("E")) { - double parsed = Double.parseDouble(value); + final double parsed = Double.parseDouble(value); // Handle negative zero - Java doesn't distinguish, but spec says it should be 0 if (parsed == 0.0) { return 0L; diff --git a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java index 7a0827d..87b1406 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java @@ -2,14 +2,12 @@ import dev.toonformat.jtoon.Delimiter; import dev.toonformat.jtoon.util.StringEscaper; - import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; - import static dev.toonformat.jtoon.util.Constants.BACKSLASH; import static dev.toonformat.jtoon.util.Constants.DOUBLE_QUOTE; import static dev.toonformat.jtoon.util.Headers.TABULAR_HEADER_PATTERN; @@ -83,7 +81,7 @@ private static List parseTabularKeys(String keysStr, Delimiter arrayDeli final List result = new ArrayList<>(); final List rawValues = ArrayDecoder.parseDelimitedValues(keysStr, arrayDelimiter); - for (String key : rawValues) { + for (final String key : rawValues) { result.add(StringEscaper.unescape(key)); } return result; @@ -96,12 +94,12 @@ private static List parseTabularKeys(String keysStr, Delimiter arrayDeli * @param expectedDelimiter the expected delimiter used in the array */ private static void validateKeysDelimiter(String keysStr, Delimiter expectedDelimiter) { - char expectedChar = expectedDelimiter.toString().charAt(0); + final char expectedChar = expectedDelimiter.toString().charAt(0); boolean inQuotes = false; boolean escaped = false; for (int i = 0; i < keysStr.length(); i++) { - char c = keysStr.charAt(i); + final char c = keysStr.charAt(i); if (escaped) { escaped = false; } else if (c == BACKSLASH) { @@ -148,13 +146,13 @@ private static void checkDelimiterMismatch(char expectedChar, char actualChar) { */ private static boolean processTabularArrayLine(int expectedRowDepth, List keys, Delimiter arrayDelimiter, List result, DecodeContext context) { - String line = context.lines[context.currentLine]; + final String line = context.lines[context.currentLine]; if (DecodeHelper.isBlankLine(line)) { return !handleBlankLineInTabularArray(expectedRowDepth, context); } - int lineDepth = DecodeHelper.getDepth(line, context); + final int lineDepth = DecodeHelper.getDepth(line, context); if (shouldTerminateTabularArray(line, lineDepth, expectedRowDepth, context)) { return false; } @@ -173,12 +171,12 @@ private static boolean processTabularArrayLine(int expectedRowDepth, List 0) { return true; // Key-value pair at the same depth-terminate an array } @@ -220,8 +219,8 @@ private static boolean shouldTerminateTabularArray(String line, int lineDepth, i // Check for a key-value pair at the expected row depth if (lineDepth == expectedRowDepth) { - String rowContent = line.substring(expectedRowDepth * context.options.indent()); - int colonIdx = DecodeHelper.findUnquotedColon(rowContent); + final String rowContent = line.substring(expectedRowDepth * context.options.indent()); + final int colonIdx = DecodeHelper.findUnquotedColon(rowContent); return colonIdx > 0; // Key-value pair at the same depth as rows - terminate an array } @@ -243,8 +242,8 @@ private static boolean shouldTerminateTabularArray(String line, int lineDepth, i private static boolean processTabularRow(String line, int lineDepth, int expectedRowDepth, List keys, Delimiter arrayDelimiter, List result, DecodeContext context) { if (lineDepth == expectedRowDepth) { - String rowContent = line.substring(expectedRowDepth * context.options.indent()); - Map row = parseTabularRow(rowContent, keys, arrayDelimiter, context); + final String rowContent = line.substring(expectedRowDepth * context.options.indent()); + final Map row = parseTabularRow(rowContent, keys, arrayDelimiter, context); result.add(row); return true; } else if (lineDepth > expectedRowDepth) { @@ -265,9 +264,10 @@ private static boolean processTabularRow(String line, int lineDepth, int expecte * @param context decode an object to deal with lines, delimiter and options * @return a Map containing the parsed row values */ - private static Map parseTabularRow(String rowContent, List keys, Delimiter arrayDelimiter, DecodeContext context) { - Map row = new LinkedHashMap<>(); - List values = ArrayDecoder.parseArrayValues(rowContent, arrayDelimiter); + private static Map parseTabularRow(String rowContent, List keys, + Delimiter arrayDelimiter, DecodeContext context) { + final Map row = new LinkedHashMap<>(); + final List values = ArrayDecoder.parseArrayValues(rowContent, arrayDelimiter); // Validate value count matches key count if (context.options.strict() && values.size() != keys.size()) { diff --git a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java index b6aa66f..60d6e25 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java @@ -105,7 +105,7 @@ public static FoldResult tryFoldKeyChain(String key, } // Compute absolute dotted path - String absolutePath = + final String absolutePath = (pathPrefix != null && !pathPrefix.isEmpty()) ? String.join(DOT, pathPrefix, foldedKey) : foldedKey; @@ -125,7 +125,7 @@ public static FoldResult tryFoldKeyChain(String key, /** * Traverses nested single-key {@link ObjectNode} values, collecting the - * sequence of keys until one of the following occurs: + * sequence of keys until one of the following occurs. * - A non-object value is encountered * - An object with zero or more than one key is encountered * - An empty object is encountered (treated as a leaf) @@ -138,7 +138,7 @@ public static FoldResult tryFoldKeyChain(String key, */ private static ChainResult collectSingleKeyChain(String startKey, JsonNode startValue, int maxDepth) { // normalize absolute key to its local segment - String localStartKey = startKey.contains(DOT) + final String localStartKey = startKey.contains(DOT) ? startKey.substring(startKey.lastIndexOf(DOT.charAt(0)) + 1) : startKey; @@ -151,14 +151,14 @@ private static ChainResult collectSingleKeyChain(String startKey, JsonNode start while (currentValue.isObject() && depthCounter < maxDepth) { final ObjectNode obj = (ObjectNode) currentValue; - Iterator> it = obj.properties().iterator(); + final Iterator> it = obj.properties().iterator(); // empty object leaf if (!it.hasNext()) { return new ChainResult(segments, null, currentValue); } - Map.Entry entry = it.next(); + final Map.Entry entry = it.next(); // >1 field, this is a tail object if (it.hasNext()) { diff --git a/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java b/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java index 68a1635..dfb83b1 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java @@ -1,7 +1,6 @@ package dev.toonformat.jtoon.encoder; import java.util.List; - import static dev.toonformat.jtoon.util.Constants.COLON; import static dev.toonformat.jtoon.util.Constants.OPEN_BRACKET; import static dev.toonformat.jtoon.util.Constants.COMMA; @@ -43,7 +42,7 @@ public record HeaderConfig( * @return Formatted header string */ public static String format(HeaderConfig config) { - StringBuilder header = new StringBuilder(); + final StringBuilder header = new StringBuilder(); appendKeyIfPresent(header, config.key()); appendArrayLength(header, config.length(), config.delimiter(), config.lengthMarker()); @@ -69,7 +68,7 @@ public static String format( List fields, String delimiter, boolean lengthMarker) { - HeaderConfig config = new HeaderConfig(length, key, fields, delimiter, lengthMarker); + final HeaderConfig config = new HeaderConfig(length, key, fields, delimiter, lengthMarker); return format(config); } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java b/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java index 59850b5..64dc296 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.List; - import static dev.toonformat.jtoon.util.Constants.SPACE; /** @@ -28,7 +27,7 @@ public LineWriter(int indentSize) { * @param content Line content to add */ public void push(int depth, String content) { - String indent = indentationString.repeat(depth); + final String indent = indentationString.repeat(depth); lines.add(indent + content); } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java index fea2885..63df414 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java @@ -92,7 +92,9 @@ public static void encodeKeyValuePair(String key, } final String encodedKey = PrimitiveEncoder.encodeKey(key); final String currentPath = pathPrefix != null ? pathPrefix + DOT + key : key; - final int effectiveFlattenDepth = flattenDepth != null && flattenDepth > 0 ? flattenDepth : options.flattenDepth(); + final int effectiveFlattenDepth = flattenDepth != null && flattenDepth > 0 + ? flattenDepth + : options.flattenDepth(); final int remainingDepth = effectiveFlattenDepth - depth; EncodeOptions currentOptions = options; diff --git a/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java index 8675bd2..78aab20 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java @@ -3,11 +3,9 @@ import dev.toonformat.jtoon.util.StringEscaper; import dev.toonformat.jtoon.util.StringValidator; import tools.jackson.databind.JsonNode; - import java.math.BigDecimal; import java.util.List; import java.util.Objects; - import static dev.toonformat.jtoon.util.Constants.NULL_LITERAL; import static dev.toonformat.jtoon.util.Constants.DOUBLE_QUOTE; @@ -47,9 +45,9 @@ private static String encodeNumber(JsonNode value) { return value.asString(); } - double doubleValue = value.asDouble(); - BigDecimal decimal = BigDecimal.valueOf(doubleValue); - String plainString = decimal.toPlainString(); + final double doubleValue = value.asDouble(); + final BigDecimal decimal = BigDecimal.valueOf(doubleValue); + final String plainString = decimal.toPlainString(); return stripTrailingZeros(plainString); } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java index 0a9552f..8cd24a8 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java @@ -4,7 +4,6 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -31,13 +30,13 @@ public static List detectTabularHeader(ArrayNode rows) { return Collections.emptyList(); } - JsonNode firstRow = rows.get(0); + final JsonNode firstRow = rows.get(0); if (!firstRow.isObject()) { return Collections.emptyList(); } - ObjectNode firstObj = (ObjectNode) firstRow; - List firstKeys = new ArrayList<>(firstObj.propertyNames()); + final ObjectNode firstObj = (ObjectNode) firstRow; + final List firstKeys = new ArrayList<>(firstObj.propertyNames()); if (firstKeys.isEmpty()) { return Collections.emptyList(); @@ -59,8 +58,8 @@ private static boolean isTabularArray(ArrayNode rows, List header) { return false; } - ObjectNode obj = (ObjectNode) row; - List keys = new ArrayList<>(obj.propertyNames()); + final ObjectNode obj = (ObjectNode) row; + final List keys = new ArrayList<>(obj.propertyNames()); // All objects must have the same keys (but order can differ) if (keys.size() != header.size()) { @@ -92,9 +91,9 @@ private static boolean isTabularArray(ArrayNode rows, List header) { * @param options Encoding options */ public static void encodeArrayOfObjectsAsTabular(String prefix, ArrayNode rows, List header, - LineWriter writer, int depth, EncodeOptions options) { - String headerStr = PrimitiveEncoder.formatHeader(rows.size(), prefix, header, options.delimiter().toString(), - options.lengthMarker()); + LineWriter writer, int depth, EncodeOptions options) { + final String headerStr = PrimitiveEncoder.formatHeader(rows.size(), prefix, header, + options.delimiter().toString(), options.lengthMarker()); writer.push(depth, headerStr); writeTabularRows(rows, header, writer, depth + 1, options); @@ -117,12 +116,12 @@ public static void writeTabularRows(ArrayNode rows, List header, LineWri if (!row.isObject()) { continue; } - ObjectNode obj = (ObjectNode) row; - List values = new ArrayList<>(); + final ObjectNode obj = (ObjectNode) row; + final List values = new ArrayList<>(); for (String key : header) { values.add(obj.get(key)); } - String joinedValue = PrimitiveEncoder.joinEncodedValues(values, options.delimiter().toString()); + final String joinedValue = PrimitiveEncoder.joinEncodedValues(values, options.delimiter().toString()); writer.push(depth, joinedValue); } } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ValueEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ValueEncoder.java index 697c89f..b3960a4 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ValueEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ValueEncoder.java @@ -4,7 +4,6 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; - import java.util.HashSet; import java.util.Set; @@ -32,12 +31,12 @@ public static String encodeValue(JsonNode value, EncodeOptions options) { } // Complex values need a LineWriter for indentation - LineWriter writer = new LineWriter(options.indent()); + final LineWriter writer = new LineWriter(options.indent()); if (value.isArray()) { ArrayEncoder.encodeArray(null, (ArrayNode) value, writer, 0, options); } else if (value.isObject()) { - Set jsonNodes = new HashSet<>(value.propertyNames()); + final Set jsonNodes = new HashSet<>(value.propertyNames()); ObjectEncoder.encodeObject((ObjectNode) value, writer, 0, options, jsonNodes, null, null, new HashSet<>()); } diff --git a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java index 1efaabd..d10b443 100644 --- a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java +++ b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java @@ -14,7 +14,6 @@ import tools.jackson.databind.node.ObjectNode; import tools.jackson.databind.node.ShortNode; import tools.jackson.databind.node.StringNode; - import java.math.BigDecimal; import java.math.BigInteger; import java.time.Instant; @@ -174,7 +173,7 @@ private static Optional tryConvertToLong(Double value) { if (value > Long.MAX_VALUE || value < Long.MIN_VALUE) { return Optional.empty(); } - long longVal = value.longValue(); + final long longVal = value.longValue(); return Optional.of(LongNode.valueOf(longVal)); } @@ -196,7 +195,7 @@ private static JsonNode tryNormalizeBigNumber(Object value) { * Normalizes BigInteger, converting to long if within range. */ private static JsonNode normalizeBigInteger(BigInteger value) { - boolean fitsInLong = value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) <= 0 + final boolean fitsInLong = value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) <= 0 && value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) >= 0; return fitsInLong ? LongNode.valueOf(value.longValue()) @@ -260,7 +259,7 @@ private static JsonNode tryNormalizeCollection(Object value) { * Normalizes a Collection to an ArrayNode. */ private static ArrayNode normalizeCollection(Collection collection) { - ArrayNode arrayNode = MAPPER.createArrayNode(); + final ArrayNode arrayNode = MAPPER.createArrayNode(); collection.forEach(item -> arrayNode.add(normalize(item))); return arrayNode; } @@ -269,7 +268,7 @@ private static ArrayNode normalizeCollection(Collection collection) { * Normalizes a Map to an ObjectNode. */ private static ObjectNode normalizeMap(Map map) { - ObjectNode objectNode = MAPPER.createObjectNode(); + final ObjectNode objectNode = MAPPER.createObjectNode(); map.forEach((key, value) -> objectNode.set(String.valueOf(key), normalize(value))); return objectNode; } @@ -317,7 +316,7 @@ private static JsonNode normalizeArray(Object array) { * Builds an ArrayNode using a functional approach. */ private static ArrayNode buildArrayNode(int length, IntFunction mapper) { - ArrayNode arrayNode = MAPPER.createArrayNode(); + final ArrayNode arrayNode = MAPPER.createArrayNode(); for (int i = 0; i < length; i++) { arrayNode.add(mapper.apply(i)); } diff --git a/src/main/java/dev/toonformat/jtoon/util/Headers.java b/src/main/java/dev/toonformat/jtoon/util/Headers.java index dadf18f..db5b4cb 100644 --- a/src/main/java/dev/toonformat/jtoon/util/Headers.java +++ b/src/main/java/dev/toonformat/jtoon/util/Headers.java @@ -3,25 +3,25 @@ import java.util.regex.Pattern; /** - * Patterns in form of regex that must be followed in order to decode arrays, tabular, keyed arrays + * Patterns in form of regex that must be followed in order to decode arrays, tabular, keyed arrays. */ public class Headers { /** - * Matches standalone array headers: [3], [#2], [3\t], [2|] + * Matches standalone array headers: [3], [#2], [3\t], [2|]. * Group 1: optional # marker, Group 2: digits, Group 3: optional delimiter */ public static final Pattern ARRAY_HEADER_PATTERN = Pattern.compile("^\\[(#?)(\\d+)([\\t|])?]"); /** - * Matches tabular array headers with field names: [2]{id,name,role}: + * Matches tabular array headers with field names: [2]{id,name,role}:. * Group 1: optional # marker, Group 2: digits, Group 3: optional delimiter, * Group 4: field spec */ public static final Pattern TABULAR_HEADER_PATTERN = Pattern.compile("^\\[(#?)(\\d+)([\\t|])?]\\{(.+)}:"); /** - * Matches keyed array headers: items[2]{id,name}: or tags[3]: or data[4]{id}: + * Matches keyed array headers: items[2]{id,name}: or tags[3]: or data[4]{id}:. * Captures: group(1)=key, group(2)=#marker, group(3)=delimiter, * group(4)=optional field spec */ diff --git a/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java b/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java index 7f2c3f3..d7c39bc 100644 --- a/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java +++ b/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java @@ -5,7 +5,6 @@ import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import tools.jackson.module.afterburner.AfterburnerModule; - import java.util.TimeZone; /** diff --git a/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java b/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java index de49057..795bdc7 100644 --- a/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java +++ b/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java @@ -44,7 +44,7 @@ public static void validateString(String value) { // Check for invalid escape sequences in quoted strings if (value.startsWith("\"") && value.endsWith("\"")) { - String unquoted = value.substring(1, value.length() - 1); + final String unquoted = value.substring(1, value.length() - 1); boolean escaped = false; for (char c : unquoted.toCharArray()) { @@ -90,7 +90,7 @@ public static String unescape(String value) { unquoted = value.substring(1, value.length() - 1); } - StringBuilder result = new StringBuilder(); + final StringBuilder result = new StringBuilder(); boolean escaped = false; for (char c : unquoted.toCharArray()) { diff --git a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java index a4c2251..d441665 100644 --- a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java +++ b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java @@ -1,7 +1,6 @@ package dev.toonformat.jtoon.util; import java.util.regex.Pattern; - import static dev.toonformat.jtoon.util.Constants.*; /** @@ -92,7 +91,9 @@ private static boolean looksLikeKeyword(String value) { } private static boolean looksLikeNumber(String value) { - return OCTAL_PATTERN.matcher(value).matches() || LEADING_ZERO_PATTERN.matcher(value).matches() || NUMERIC_PATTERN.matcher(value).matches(); + return OCTAL_PATTERN.matcher(value).matches() + || LEADING_ZERO_PATTERN.matcher(value).matches() + || NUMERIC_PATTERN.matcher(value).matches(); } private static boolean containsColon(String value) { From 536b8c2e001e0735ceb203141e5f2f26fd0210e7 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 20:15:23 +0100 Subject: [PATCH 10/29] adding pitest --- build.gradle | 11 +++++++++++ settings.gradle | 14 ++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/build.gradle b/build.gradle index 2c6a93b..fc613fd 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ plugins { id 'com.github.spotbugs' version '6.0.15' id 'pmd' id 'checkstyle' + id 'info.solidsoft.pitest' version '1.19.0-rc.3' } group = 'dev.toonformat' @@ -38,6 +39,15 @@ jacoco { reportsDirectory = layout.buildDirectory.dir('customJacocoReportDir') } +pitest { + pitestVersion = '1.17.4' + targetClasses = ['dev.toonformat.jtoon.*'] + targetTests = ['dev.toonformat.jtoon.*'] + outputFormats = ['XML', 'HTML'] + mutationThreshold = 70 + coverageThreshold = 70 +} + spotbugs { toolVersion = '4.8.6' excludeFilter = file('spotbugs-exclude.xml') @@ -122,6 +132,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation 'org.awaitility:awaitility:4.2.1' + testImplementation 'org.pitest:pitest-junit5-plugin:1.2.3' } test { diff --git a/settings.gradle b/settings.gradle index 7e825da..4dc8f38 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,15 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == 'info.solidsoft.pitest') { + useModule("info.solidsoft.gradle.pitest:gradle-pitest-plugin:${requested.version}") + } + } + } +} + rootProject.name = 'JToon' \ No newline at end of file From f177c3cf1fe5f28f48651ce6cf876ae9997a5e59 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 20:18:39 +0100 Subject: [PATCH 11/29] update versions numbers --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index fc613fd..dc5773a 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ pitest { } spotbugs { - toolVersion = '4.8.6' + toolVersion = '4.9.8' excludeFilter = file('spotbugs-exclude.xml') effort = "max" reportLevel = "low" @@ -127,11 +127,11 @@ tasks.checkstyleTest { dependencies { implementation 'tools.jackson.core:jackson-databind:3.0.4' implementation 'tools.jackson.module:jackson-module-afterburner:3.0.4' - compileOnly 'com.github.spotbugs:spotbugs-annotations:4.8.6' + compileOnly 'com.github.spotbugs:spotbugs-annotations:4.9.8' testImplementation platform('org.junit:junit-bom:6.0.2') testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - testImplementation 'org.awaitility:awaitility:4.2.1' + testImplementation 'org.awaitility:awaitility:4.3.0' testImplementation 'org.pitest:pitest-junit5-plugin:1.2.3' } From 4994a4dd5420e96e2c8f73b5eee94aaf65c87d60 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 20:46:44 +0100 Subject: [PATCH 12/29] adding JHM --- build.gradle | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/build.gradle b/build.gradle index dc5773a..64b2a07 100644 --- a/build.gradle +++ b/build.gradle @@ -133,6 +133,9 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation 'org.awaitility:awaitility:4.3.0' testImplementation 'org.pitest:pitest-junit5-plugin:1.2.3' + testImplementation 'org.openjdk.jmh:jmh-core:1.37' + testImplementation 'org.openjdk.jmh:jmh-generator-annprocess:1.37' + testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' } test { @@ -194,3 +197,14 @@ tasks.register('specsValidation', Test) { include '**/ConformanceTest.class' } +tasks.register('jmh', JavaExec) { + group = 'verification' + description = 'Run JMH benchmarks' + classpath = configurations.testRuntimeClasspath + sourceSets.test.runtimeClasspath + mainClass = 'dev.toonformat.jtoon.JToonBenchmark' + workingDir = projectDir + doFirst { + file('build/jmh-results').mkdirs() + } +} + From 5caf07becd9eceeaa3f3a067680e92c889e8d214 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 20:47:01 +0100 Subject: [PATCH 13/29] adding test --- .../dev/toonformat/jtoon/JToonBenchmark.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/test/java/dev/toonformat/jtoon/JToonBenchmark.java diff --git a/src/test/java/dev/toonformat/jtoon/JToonBenchmark.java b/src/test/java/dev/toonformat/jtoon/JToonBenchmark.java new file mode 100644 index 0000000..b430061 --- /dev/null +++ b/src/test/java/dev/toonformat/jtoon/JToonBenchmark.java @@ -0,0 +1,88 @@ +package dev.toonformat.jtoon; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@State(Scope.Thread) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 3, time = 1) +@Measurement(iterations = 5, time = 2) +@Fork(2) +public class JToonBenchmark { + + @Param({"10", "100", "1000"}) + private int size; + + private Map testObject; + private String toonString; + private String jsonString; + + @Setup + public void setup() { + testObject = new HashMap<>(); + for (int i = 0; i < size; i++) { + Map nested = new HashMap<>(); + nested.put("id", i); + nested.put("name", "item_" + i); + nested.put("value", Math.random() * 1000); + nested.put("active", i % 2 == 0); + testObject.put("key_" + i, nested); + } + toonString = JToon.encode(testObject); + jsonString = "{\"name\":\"test\",\"value\":42,\"items\":[" + + String.join(",", java.util.Collections.nCopies(10, "{\"id\":1,\"name\":\"test\"}")) + + "],\"nested\":{\"a\":1,\"b\":2,\"c\":3}}"; + } + + @Benchmark + public String encodeObject() { + return JToon.encode(testObject); + } + + @Benchmark + public String encodeJson() { + return JToon.encodeJson(jsonString); + } + + @Benchmark + public Object decodeToon() { + return JToon.decode(toonString); + } + + @Benchmark + public String decodeToonToJson() { + return JToon.decodeToJson(toonString); + } + + @Benchmark + public Object decodeJson() { + return JToon.decode(toonString); + } + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include(JToonBenchmark.class.getSimpleName()) + .result("build/jmh-results/results.json") + .build(); + + new Runner(opt).run(); + } +} From f67531f51619c2146a398a36fe440c139154eedc Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 20:59:08 +0100 Subject: [PATCH 14/29] update pmd rules and spotbugs rules --- pmd-rules-test.xml | 12 ------------ spotbugs-exclude.xml | 5 +++++ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/pmd-rules-test.xml b/pmd-rules-test.xml index 19628b4..380b587 100644 --- a/pmd-rules-test.xml +++ b/pmd-rules-test.xml @@ -10,11 +10,7 @@ - - - - @@ -30,8 +26,6 @@ - - @@ -50,7 +44,6 @@ - @@ -61,8 +54,6 @@ - - @@ -70,7 +61,6 @@ - @@ -88,12 +78,10 @@ - - diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 78214ec..ba794cc 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -6,6 +6,11 @@ + + + + + From 9c8be1771e001e6c8b39d971d3299d7a2a61e207 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 21:04:14 +0100 Subject: [PATCH 15/29] fix javaDoc --- src/main/java/dev/toonformat/jtoon/Delimiter.java | 5 +++++ src/main/java/dev/toonformat/jtoon/util/Headers.java | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/main/java/dev/toonformat/jtoon/Delimiter.java b/src/main/java/dev/toonformat/jtoon/Delimiter.java index e5bb5ae..072b759 100644 --- a/src/main/java/dev/toonformat/jtoon/Delimiter.java +++ b/src/main/java/dev/toonformat/jtoon/Delimiter.java @@ -34,6 +34,11 @@ public String toString() { return value; } + + /** + * Returns the character representation of this delimiter. + * @return the character value of this delimiter + */ public char getValue() { return value.charAt(0); } diff --git a/src/main/java/dev/toonformat/jtoon/util/Headers.java b/src/main/java/dev/toonformat/jtoon/util/Headers.java index db5b4cb..7783d0d 100644 --- a/src/main/java/dev/toonformat/jtoon/util/Headers.java +++ b/src/main/java/dev/toonformat/jtoon/util/Headers.java @@ -7,6 +7,10 @@ */ public class Headers { + private Headers() { + throw new UnsupportedOperationException("Utility class cannot be instantiated"); + } + /** * Matches standalone array headers: [3], [#2], [3\t], [2|]. * Group 1: optional # marker, Group 2: digits, Group 3: optional delimiter From 0012704c9181e17dc2ea4a70fe55d2414e423519 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 21:20:53 +0100 Subject: [PATCH 16/29] clean up rules --- build.gradle | 2 ++ pmd-rules-test.xml | 6 ++++++ src/main/java/dev/toonformat/jtoon/util/Headers.java | 10 +++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 64b2a07..ed82cd5 100644 --- a/build.gradle +++ b/build.gradle @@ -80,6 +80,7 @@ tasks.spotbugsTest { outputLocation = file("${spotbugs.reportsDir.get()}/spotbugs-test.xml") } } + ignoreFailures = true } pmd { @@ -122,6 +123,7 @@ tasks.checkstyleMain { tasks.checkstyleTest { enabled = false + ignoreFailures = true } dependencies { diff --git a/pmd-rules-test.xml b/pmd-rules-test.xml index 380b587..362a0c8 100644 --- a/pmd-rules-test.xml +++ b/pmd-rules-test.xml @@ -16,6 +16,7 @@ + @@ -47,6 +48,7 @@ + @@ -105,6 +107,10 @@ + + + + diff --git a/src/main/java/dev/toonformat/jtoon/util/Headers.java b/src/main/java/dev/toonformat/jtoon/util/Headers.java index 7783d0d..b2b6d16 100644 --- a/src/main/java/dev/toonformat/jtoon/util/Headers.java +++ b/src/main/java/dev/toonformat/jtoon/util/Headers.java @@ -5,11 +5,7 @@ /** * Patterns in form of regex that must be followed in order to decode arrays, tabular, keyed arrays. */ -public class Headers { - - private Headers() { - throw new UnsupportedOperationException("Utility class cannot be instantiated"); - } +public final class Headers { /** * Matches standalone array headers: [3], [#2], [3\t], [2|]. @@ -31,4 +27,8 @@ private Headers() { */ public static final Pattern KEYED_ARRAY_PATTERN = Pattern.compile("^(.+?)\\[(#?)\\d+([\\t|])?](\\{[^}]+})?:.*$"); + private Headers() { + throw new UnsupportedOperationException("Utility class cannot be instantiated"); + } + } From ebfdf2a274422319f355e7b11345313409bc56a8 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 21:28:54 +0100 Subject: [PATCH 17/29] remove warnings for pmd test --- pmd-rules-test.xml | 1 - src/test/java/dev/toonformat/jtoon/TestPojos.java | 2 +- .../dev/toonformat/jtoon/conformance/ConformanceTest.java | 2 ++ .../java/dev/toonformat/jtoon/encoder/ArrayEncoderTest.java | 4 ++-- .../dev/toonformat/jtoon/encoder/ListItemEncoderTest.java | 4 ++-- .../java/dev/toonformat/jtoon/encoder/ObjectEncoderTest.java | 2 +- .../dev/toonformat/jtoon/encoder/TabularArrayEncoderTest.java | 4 ++-- .../java/dev/toonformat/jtoon/encoder/ValueEncoderTest.java | 2 +- .../dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java | 2 +- 9 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pmd-rules-test.xml b/pmd-rules-test.xml index 362a0c8..1f138b2 100644 --- a/pmd-rules-test.xml +++ b/pmd-rules-test.xml @@ -110,7 +110,6 @@ - diff --git a/src/test/java/dev/toonformat/jtoon/TestPojos.java b/src/test/java/dev/toonformat/jtoon/TestPojos.java index ac76d32..bc9ac3d 100644 --- a/src/test/java/dev/toonformat/jtoon/TestPojos.java +++ b/src/test/java/dev/toonformat/jtoon/TestPojos.java @@ -191,7 +191,7 @@ public CustomHotelInfoLlmRerankDTOSerializer(Class t) { } @Override - public void serialize(HotelInfoLlmRerankDTO value, JsonGenerator jsonGenerator, SerializationContext provider) throws JacksonException { + public void serialize(HotelInfoLlmRerankDTO value, JsonGenerator jsonGenerator, SerializationContext provider) { jsonGenerator.writeString(value.hotelId); } } diff --git a/src/test/java/dev/toonformat/jtoon/conformance/ConformanceTest.java b/src/test/java/dev/toonformat/jtoon/conformance/ConformanceTest.java index 6155904..d3c90c9 100644 --- a/src/test/java/dev/toonformat/jtoon/conformance/ConformanceTest.java +++ b/src/test/java/dev/toonformat/jtoon/conformance/ConformanceTest.java @@ -48,6 +48,7 @@ private Stream loadTestFixtures(File directory) { .map(this::parseFixture); } + @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") private EncodeTestFile parseFixture(File file) { try { EncodeTestFixture fixture = mapper.readValue(file, EncodeTestFixture.class); @@ -131,6 +132,7 @@ private Stream loadTestFixtures(File directory) { .map(this::parseFixture); } + @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") private DecodeTestFile parseFixture(File file) { try { var fixture = mapper.readValue(file, DecodeTestFixture.class); diff --git a/src/test/java/dev/toonformat/jtoon/encoder/ArrayEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/ArrayEncoderTest.java index 67bc058..ac3d84c 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/ArrayEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/ArrayEncoderTest.java @@ -19,8 +19,8 @@ class ArrayEncoderTest { - private final ObjectMapper MAPPER = new ObjectMapper(); - private final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; @Test void isArrayOfPrimitivesTestWithObjectNode() { diff --git a/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java index 00f7586..891dbd3 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java @@ -16,8 +16,8 @@ import static org.junit.jupiter.api.Assertions.*; class ListItemEncoderTest { - private final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; - private final EncodeOptions options = EncodeOptions.DEFAULT; + private static final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; + private static final EncodeOptions options = EncodeOptions.DEFAULT; @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") diff --git a/src/test/java/dev/toonformat/jtoon/encoder/ObjectEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/ObjectEncoderTest.java index f19634f..af91bcf 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/ObjectEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/ObjectEncoderTest.java @@ -33,7 +33,7 @@ class ObjectEncoderTest { private static final ObjectMapper MAPPER = new ObjectMapper(); - private final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; + private static final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; @Test void givenSimpleObject_whenEncoding_thenOutputsCorrectLines() { diff --git a/src/test/java/dev/toonformat/jtoon/encoder/TabularArrayEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/TabularArrayEncoderTest.java index 8b9d5e2..4d1f0f6 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/TabularArrayEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/TabularArrayEncoderTest.java @@ -15,8 +15,8 @@ class TabularArrayEncoderTest { - private final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; - private final EncodeOptions options = EncodeOptions.DEFAULT; + private static final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; + private static final EncodeOptions options = EncodeOptions.DEFAULT; @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") diff --git a/src/test/java/dev/toonformat/jtoon/encoder/ValueEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/ValueEncoderTest.java index e9495f8..6a96e3c 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/ValueEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/ValueEncoderTest.java @@ -14,7 +14,7 @@ class ValueEncoderTest { - private final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; + private static final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; @Test @DisplayName("throws unsupported Operation Exception for calling the constructor") diff --git a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java index 376cd25..0c5bcfa 100644 --- a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java +++ b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java @@ -1876,7 +1876,7 @@ void NormalizeArray_thenNullNode() throws Exception { class NormalizePojo { class ExplodingPojo { public String getValue() { - throw new RuntimeException("Boom"); + throw new IllegalStateException("Boom"); } } From e802e921d2ccc38f768c0ad1dc3b140dbfdfebc9 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 12 Feb 2026 22:04:35 +0100 Subject: [PATCH 18/29] adding final to public methods calls --- build.gradle | 4 +- checkstyle.xml | 6 +- .../dev/toonformat/jtoon/DecodeOptions.java | 6 +- .../java/dev/toonformat/jtoon/Delimiter.java | 2 +- .../dev/toonformat/jtoon/EncodeOptions.java | 10 +-- src/main/java/dev/toonformat/jtoon/JToon.java | 16 ++-- .../jtoon/decoder/ArrayDecoder.java | 22 +++--- .../jtoon/decoder/DecodeHelper.java | 18 ++--- .../toonformat/jtoon/decoder/KeyDecoder.java | 36 ++++----- .../jtoon/decoder/ListItemDecoder.java | 8 +- .../jtoon/decoder/ObjectDecoder.java | 18 ++--- .../jtoon/decoder/PrimitiveDecoder.java | 2 +- .../jtoon/decoder/TabularArrayDecoder.java | 28 +++---- .../jtoon/decoder/ValueDecoder.java | 4 +- .../jtoon/encoder/ArrayEncoder.java | 28 +++---- .../dev/toonformat/jtoon/encoder/Flatten.java | 14 ++-- .../jtoon/encoder/HeaderFormatter.java | 32 ++++---- .../toonformat/jtoon/encoder/LineWriter.java | 50 ++++++++++--- .../jtoon/encoder/ListItemEncoder.java | 30 ++++---- .../jtoon/encoder/ObjectEncoder.java | 38 +++++----- .../jtoon/encoder/PrimitiveEncoder.java | 73 ++++++++++++------- .../jtoon/encoder/TabularArrayEncoder.java | 12 +-- .../jtoon/encoder/ValueEncoder.java | 2 +- .../jtoon/normalizer/JsonNormalizer.java | 38 +++++----- .../toonformat/jtoon/util/StringEscaper.java | 10 +-- .../jtoon/util/StringValidator.java | 24 +++--- 26 files changed, 291 insertions(+), 240 deletions(-) diff --git a/build.gradle b/build.gradle index ed82cd5..65b2fe1 100644 --- a/build.gradle +++ b/build.gradle @@ -51,8 +51,8 @@ pitest { spotbugs { toolVersion = '4.9.8' excludeFilter = file('spotbugs-exclude.xml') - effort = "max" - reportLevel = "low" + effort = com.github.spotbugs.snom.Effort.MAX + reportLevel = com.github.spotbugs.snom.Confidence.values()[0] reportsDir = layout.buildDirectory.dir('spotbugs') } diff --git a/checkstyle.xml b/checkstyle.xml index 3785d24..b8f2e90 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -58,7 +58,11 @@ - + + + + + diff --git a/src/main/java/dev/toonformat/jtoon/DecodeOptions.java b/src/main/java/dev/toonformat/jtoon/DecodeOptions.java index 6f085a0..452676e 100644 --- a/src/main/java/dev/toonformat/jtoon/DecodeOptions.java +++ b/src/main/java/dev/toonformat/jtoon/DecodeOptions.java @@ -35,7 +35,7 @@ public DecodeOptions() { * @param indent number of spaces per indentation level * @return a new DecodeOptions instance with the specified indent */ - public static DecodeOptions withIndent(int indent) { + public static DecodeOptions withIndent(final int indent) { return new DecodeOptions(indent, Delimiter.COMMA, true, PathExpansion.OFF); } @@ -45,7 +45,7 @@ public static DecodeOptions withIndent(int indent) { * @param delimiter the delimiter to use for tabular arrays and inline primitive arrays * @return a new DecodeOptions instance with the specified delimiter */ - public static DecodeOptions withDelimiter(Delimiter delimiter) { + public static DecodeOptions withDelimiter(final Delimiter delimiter) { return new DecodeOptions(2, delimiter, true, PathExpansion.OFF); } @@ -55,7 +55,7 @@ public static DecodeOptions withDelimiter(Delimiter delimiter) { * @param strict whether to enable strict validation mode * @return a new DecodeOptions instance with the specified strict mode */ - public static DecodeOptions withStrict(boolean strict) { + public static DecodeOptions withStrict(final boolean strict) { return new DecodeOptions(2, Delimiter.COMMA, strict, PathExpansion.OFF); } } diff --git a/src/main/java/dev/toonformat/jtoon/Delimiter.java b/src/main/java/dev/toonformat/jtoon/Delimiter.java index 072b759..6825120 100644 --- a/src/main/java/dev/toonformat/jtoon/Delimiter.java +++ b/src/main/java/dev/toonformat/jtoon/Delimiter.java @@ -21,7 +21,7 @@ public enum Delimiter { private final String value; - Delimiter(String delimiterValue) { + Delimiter(final String delimiterValue) { this.value = delimiterValue; } diff --git a/src/main/java/dev/toonformat/jtoon/EncodeOptions.java b/src/main/java/dev/toonformat/jtoon/EncodeOptions.java index 1e92a2d..c522826 100644 --- a/src/main/java/dev/toonformat/jtoon/EncodeOptions.java +++ b/src/main/java/dev/toonformat/jtoon/EncodeOptions.java @@ -40,7 +40,7 @@ public EncodeOptions() { * @param indent number of spaces per indentation level * @return a new EncodeOptions instance with the specified indent */ - public static EncodeOptions withIndent(int indent) { + public static EncodeOptions withIndent(final int indent) { return new EncodeOptions(indent, Delimiter.COMMA, false, KeyFolding.OFF, Integer.MAX_VALUE); } @@ -51,7 +51,7 @@ public static EncodeOptions withIndent(int indent) { * @param delimiter the delimiter to use for tabular arrays and inline primitive arrays * @return a new EncodeOptions instance with the specified delimiter */ - public static EncodeOptions withDelimiter(Delimiter delimiter) { + public static EncodeOptions withDelimiter(final Delimiter delimiter) { return new EncodeOptions(2, delimiter, false, KeyFolding.OFF, Integer.MAX_VALUE); } @@ -62,7 +62,7 @@ public static EncodeOptions withDelimiter(Delimiter delimiter) { * @param lengthMarker whether to include the # marker before array lengths * @return a new EncodeOptions instance with the specified length marker setting */ - public static EncodeOptions withLengthMarker(boolean lengthMarker) { + public static EncodeOptions withLengthMarker(final boolean lengthMarker) { return new EncodeOptions(2, Delimiter.COMMA, lengthMarker, KeyFolding.OFF, Integer.MAX_VALUE); } @@ -73,7 +73,7 @@ public static EncodeOptions withLengthMarker(boolean lengthMarker) { * @param flatten optional flag to flatten nested objects to a single level. * @return a new EncodeOptions instance with the flatten setting */ - public static EncodeOptions withFlatten(boolean flatten) { + public static EncodeOptions withFlatten(final boolean flatten) { return new EncodeOptions(2, Delimiter.COMMA, false, flatten ? KeyFolding.SAFE : KeyFolding.OFF, Integer.MAX_VALUE); } @@ -85,7 +85,7 @@ public static EncodeOptions withFlatten(boolean flatten) { * @param flattenDepth optional maximum depth to flatten nested objects. * @return a new EncodeOptions instance with the flatten setting and the depth of to flatten the nested objects. */ - public static EncodeOptions withFlattenDepth(int flattenDepth) { + public static EncodeOptions withFlattenDepth(final int flattenDepth) { return new EncodeOptions(2, Delimiter.COMMA, false, KeyFolding.SAFE, flattenDepth); } } diff --git a/src/main/java/dev/toonformat/jtoon/JToon.java b/src/main/java/dev/toonformat/jtoon/JToon.java index 09bc59b..250f730 100644 --- a/src/main/java/dev/toonformat/jtoon/JToon.java +++ b/src/main/java/dev/toonformat/jtoon/JToon.java @@ -26,7 +26,7 @@ private JToon() { * @param input The object to encode (can be null) * @return The JToon-formatted string */ - public static String encode(Object input) { + public static String encode(final Object input) { return encode(input, EncodeOptions.DEFAULT); } @@ -42,7 +42,7 @@ public static String encode(Object input) { * @param options Encoding options (indent, delimiter, length marker) * @return The JToon-formatted string */ - public static String encode(Object input, EncodeOptions options) { + public static String encode(final Object input, final EncodeOptions options) { final JsonNode normalizedValue = JsonNormalizer.normalize(input); return ValueEncoder.encodeValue(normalizedValue, options); } @@ -60,7 +60,7 @@ public static String encode(Object input, EncodeOptions options) { * @return The TOON-formatted string * @throws IllegalArgumentException if the input is not valid JSON */ - public static String encodeJson(String json) { + public static String encodeJson(final String json) { return encodeJson(json, EncodeOptions.DEFAULT); } @@ -78,7 +78,7 @@ public static String encodeJson(String json) { * @return The TOON-formatted string * @throws IllegalArgumentException if the input is not valid JSON */ - public static String encodeJson(String json, EncodeOptions options) { + public static String encodeJson(final String json, final EncodeOptions options) { final JsonNode parsed = JsonNormalizer.parse(json); return ValueEncoder.encodeValue(parsed, options); } @@ -96,7 +96,7 @@ public static String encodeJson(String json, EncodeOptions options) { * @throws IllegalArgumentException if strict mode is enabled and input is * invalid */ - public static Object decode(String toon) { + public static Object decode(final String toon) { return decode(toon, DecodeOptions.DEFAULT); } @@ -114,7 +114,7 @@ public static Object decode(String toon) { * @throws IllegalArgumentException if strict mode is enabled and input is * invalid */ - public static Object decode(String toon, DecodeOptions options) { + public static Object decode(final String toon, final DecodeOptions options) { return ValueDecoder.decode(toon, options); } @@ -132,7 +132,7 @@ public static Object decode(String toon, DecodeOptions options) { * @throws IllegalArgumentException if strict mode is enabled and input is * invalid */ - public static String decodeToJson(String toon) { + public static String decodeToJson(final String toon) { return decodeToJson(toon, DecodeOptions.DEFAULT); } @@ -151,7 +151,7 @@ public static String decodeToJson(String toon) { * @throws IllegalArgumentException if strict mode is enabled and input is * invalid */ - public static String decodeToJson(String toon, DecodeOptions options) { + public static String decodeToJson(final String toon, final DecodeOptions options) { return ValueDecoder.decodeToJson(toon, options); } } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java index 1b4193e..f49ad01 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java @@ -32,7 +32,7 @@ private ArrayDecoder() { * @param context decode an object to deal with lines, delimiter and options * @return parsed array with delimiter */ - static List parseArray(String header, int depth, DecodeContext context) { + static List parseArray(final String header, final int depth, final DecodeContext context) { final Delimiter arrayDelimiter = extractDelimiterFromHeader(header, context); return parseArrayWithDelimiter(header, depth, arrayDelimiter, context); @@ -46,7 +46,7 @@ static List parseArray(String header, int depth, DecodeContext context) * @param context decode an object to deal with lines, delimiter and options * @return extracted delimiter from header */ - static Delimiter extractDelimiterFromHeader(String header, DecodeContext context) { + static Delimiter extractDelimiterFromHeader(final String header, final DecodeContext context) { final Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header); if (matcher.find()) { final String delimiter = matcher.group(DELIMITER_GROUP_INDEX); @@ -74,8 +74,8 @@ static Delimiter extractDelimiterFromHeader(String header, DecodeContext context * @param context decode an object to deal with lines, delimiter and options * @return parsed array */ - static List parseArrayWithDelimiter(String header, int depth, Delimiter arrayDelimiter, - DecodeContext context) { + static List parseArrayWithDelimiter(final String header, final int depth, final Delimiter arrayDelimiter, + final DecodeContext context) { final Matcher tabularMatcher = TABULAR_HEADER_PATTERN.matcher(header); final Matcher arrayMatcher = ARRAY_HEADER_PATTERN.matcher(header); @@ -138,7 +138,7 @@ static List parseArrayWithDelimiter(String header, int depth, Delimiter * @param header header * @param actualLength actual length */ - static void validateArrayLength(String header, int actualLength) { + static void validateArrayLength(final String header, final int actualLength) { final Integer declaredLength = extractLengthFromHeader(header); if (declaredLength != null && declaredLength != actualLength) { throw new IllegalArgumentException( @@ -153,7 +153,7 @@ static void validateArrayLength(String header, int actualLength) { * @param header header string for length check * @return extracted length from header */ - private static Integer extractLengthFromHeader(String header) { + private static Integer extractLengthFromHeader(final String header) { final Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header); if (matcher.find()) { return Integer.parseInt(matcher.group(2)); @@ -168,7 +168,7 @@ private static Integer extractLengthFromHeader(String header) { * @param arrayDelimiter array delimiter * @return parsed array values */ - static List parseArrayValues(String values, Delimiter arrayDelimiter) { + static List parseArrayValues(final String values, final Delimiter arrayDelimiter) { final List result = new ArrayList<>(); final List rawValues = parseDelimitedValues(values, arrayDelimiter); for (final String value : rawValues) { @@ -185,7 +185,7 @@ static List parseArrayValues(String values, Delimiter arrayDelimiter) { * @param arrayDelimiter array delimiter * @return parsed delimited values */ - static List parseDelimitedValues(String input, Delimiter arrayDelimiter) { + static List parseDelimitedValues(final String input, final Delimiter arrayDelimiter) { final List result = new ArrayList<>(); final StringBuilder stringBuilder = new StringBuilder(); boolean inQuotes = false; @@ -235,7 +235,7 @@ static List parseDelimitedValues(String input, Delimiter arrayDelimiter) * Parses list an array format where items are prefixed with "- ". * Example: items[2]:\n - item1\n - item2 */ - private static List parseListArray(int depth, String header, DecodeContext context) { + private static List parseListArray(final int depth, final String header, final DecodeContext context) { final List result = new ArrayList<>(); context.currentLine++; @@ -271,7 +271,7 @@ private static List parseListArray(int depth, String header, DecodeConte * @param context decode an object to deal with lines, delimiter and options * @return true if an array should terminate, false if a line should be skipped */ - private static boolean handleBlankLineInListArray(int depth, DecodeContext context) { + private static boolean handleBlankLineInListArray(final int depth, final DecodeContext context) { final int nextNonBlankLine = DecodeHelper.findNextNonBlankLine(context.currentLine + 1, context); if (nextNonBlankLine >= context.lines.length) { @@ -300,7 +300,7 @@ private static boolean handleBlankLineInListArray(int depth, DecodeContext conte * @param context decode an object to deal with lines, delimiter and options * @return true if an array should terminate, false otherwise. */ - private static boolean shouldTerminateListArray(int lineDepth, int depth, String line, DecodeContext context) { + private static boolean shouldTerminateListArray(final int lineDepth, final int depth, final String line, final DecodeContext context) { if (lineDepth < depth + 1) { return true; // Line depth is less than expected - terminate } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java b/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java index f15a59d..5903994 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java @@ -26,7 +26,7 @@ private DecodeHelper() { * @param context decode an object to deal with lines, delimiter, and options * @return the depth of a line */ - public static int getDepth(String line, DecodeContext context) { + public static int getDepth(final String line, final DecodeContext context) { // Blank lines (including lines with only spaces) have depth 0 if (isBlankLine(line)) { return 0; @@ -42,7 +42,7 @@ public static int getDepth(String line, DecodeContext context) { * @param context decode object in order to deal with lines, delimiter and options * @return amount of leading spaces */ - private static int computeLeadingSpaces(String line, DecodeContext context) { + private static int computeLeadingSpaces(final String line, final DecodeContext context) { final int indentSize = context.options.indent(); int leadingSpaces = 0; @@ -81,7 +81,7 @@ private static int computeLeadingSpaces(String line, DecodeContext context) { * @param line the line string to parse * @return true or false depending on if the line is blank or not */ - static boolean isBlankLine(String line) { + static boolean isBlankLine(final String line) { return line.isBlank(); } @@ -92,7 +92,7 @@ static boolean isBlankLine(String line) { * @param content the content string to parse * @return the unquoted colon */ - static int findUnquotedColon(String content) { + static int findUnquotedColon(final String content) { boolean inQuotes = false; boolean escaped = false; @@ -120,7 +120,7 @@ static int findUnquotedColon(String content) { * @param context decode an object to deal with lines, delimiter, and options * @return index aiming for the next non-blank line */ - static int findNextNonBlankLine(int startIndex, DecodeContext context) { + static int findNextNonBlankLine(final int startIndex, final DecodeContext context) { int index = startIndex; while (index < context.lines.length && isBlankLine(context.lines[index])) { index++; @@ -137,7 +137,7 @@ static int findNextNonBlankLine(int startIndex, DecodeContext context) { * @param context decode an object to deal with lines, delimiter, and options * @throws IllegalArgumentException in case there's a expansion conflict */ - static void checkFinalValueConflict(String finalSegment, Object existing, Object value, DecodeContext context) { + static void checkFinalValueConflict(final String finalSegment, final Object existing, final Object value, final DecodeContext context) { if (existing != null && context.options.strict()) { // Check for conflicts in strict mode if (existing instanceof Map && !(value instanceof Map)) { @@ -162,7 +162,7 @@ static void checkFinalValueConflict(String finalSegment, Object existing, Object * @param value present value in a map * @param context decode an object to deal with lines, delimiter, and options */ - static void checkPathExpansionConflict(Map map, String key, Object value, DecodeContext context) { + static void checkPathExpansionConflict(final Map map, final String key, final Object value, final DecodeContext context) { if (!context.options.strict()) { return; } @@ -177,7 +177,7 @@ static void checkPathExpansionConflict(Map map, String key, Obje * @param context decode an object to deal with lines, delimiter, and options * @return the depth of the next non-blank line, or null if none exists */ - static Integer findNextNonBlankLineDepth(DecodeContext context) { + static Integer findNextNonBlankLineDepth(final DecodeContext context) { int nextLineIdx = context.currentLine; while (nextLineIdx < context.lines.length && isBlankLine(context.lines[nextLineIdx])) { nextLineIdx++; @@ -196,7 +196,7 @@ static Integer findNextNonBlankLineDepth(DecodeContext context) { * @param context decode an object to deal with lines, delimiter, and options * @throws IllegalArgumentException in case the next depth is equal to 0 */ - static void validateNoMultiplePrimitivesAtRoot(DecodeContext context) { + static void validateNoMultiplePrimitivesAtRoot(final DecodeContext context) { int lineIndex = context.currentLine; while (lineIndex < context.lines.length && isBlankLine(context.lines[lineIndex])) { lineIndex++; diff --git a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java index 4dde16c..d4b4e08 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java @@ -28,8 +28,8 @@ private KeyDecoder() { * @param parentDepth parent depth of keyed array line * @param context decode an object to deal with lines, delimiter and options */ - static void processKeyedArrayLine(Map result, String content, String originalKey, - int parentDepth, DecodeContext context) { + static void processKeyedArrayLine(final Map result, final String content, final String originalKey, + final int parentDepth, final DecodeContext context) { final String key = StringEscaper.unescape(originalKey); final String arrayHeader = content.substring(originalKey.length()); final List arrayValue = ArrayDecoder.parseArray(arrayHeader, parentDepth + 1, context); @@ -52,8 +52,8 @@ static void processKeyedArrayLine(Map result, String content, St * @param value value * @param context decode an object to deal with lines, delimiter and options */ - static void expandPathIntoMap(Map current, String dottedKey, Object value, - DecodeContext context) { + static void expandPathIntoMap(final Map current, final String dottedKey, final Object value, + final DecodeContext context) { final String[] segments = dottedKey.split("\\."); Map currentMap = current; @@ -107,7 +107,7 @@ static void expandPathIntoMap(Map current, String dottedKey, Obj * @param depth the depth of the value line * @param context decode an object to deal with lines, delimiter and options */ - static void processKeyValueLine(Map result, String content, int depth, DecodeContext context) { + static void processKeyValueLine(final Map result, final String content, final int depth, final DecodeContext context) { final int colonIdx = DecodeHelper.findUnquotedColon(content); if (colonIdx > 0) { @@ -133,8 +133,8 @@ static void processKeyValueLine(Map result, String content, int * @param depth the depth of the value pair * @param context decode an object to deal with lines, delimiter and options */ - static void parseKeyValuePairIntoMap(Map map, String key, String value, - int depth, DecodeContext context) { + static void parseKeyValuePairIntoMap(final Map map, final String key, final String value, + final int depth, final DecodeContext context) { final String unescapedKey = StringEscaper.unescape(key); final Object parsedValue = parseKeyValue(value, depth, context); @@ -150,7 +150,7 @@ static void parseKeyValuePairIntoMap(Map map, String key, String * @param context decode an object to deal with lines, delimiter and options * @return true if a key should be expanded or false if not */ - static boolean shouldExpandKey(String key, DecodeContext context) { + static boolean shouldExpandKey(final String key, final DecodeContext context) { if (context.options.expandPaths() != PathExpansion.SAFE) { return false; } @@ -182,7 +182,7 @@ static boolean shouldExpandKey(String key, DecodeContext context) { * @param depth the depth at which the key-value pair is located * @return the parsed value (Map, List, or primitive) */ - private static Object parseKeyValue(String value, int depth, DecodeContext context) { + private static Object parseKeyValue(final String value, final int depth, final DecodeContext context) { // Check if the next line is nested (deeper indentation) if (context.currentLine + 1 < context.lines.length) { final int nextDepth = DecodeHelper.getDepth(context.lines[context.currentLine + 1], context); @@ -223,8 +223,8 @@ private static Object parseKeyValue(String value, int depth, DecodeContext conte * @param unescapedKey the unescaped key * @param value the value to put */ - private static void putKeyValueIntoMap(Map map, String originalKey, String unescapedKey, - Object value, DecodeContext context) { + private static void putKeyValueIntoMap(final Map map, final String originalKey, final String unescapedKey, + final Object value, final DecodeContext context) { // Handle path expansion if (shouldExpandKey(originalKey, context)) { expandPathIntoMap(map, unescapedKey, value, context); @@ -244,8 +244,8 @@ private static void putKeyValueIntoMap(Map map, String originalK * @param context decode an object to deal with lines, delimiter, and options * @return parsed a key-value pair */ - static Object parseKeyValuePair(String key, String value, int depth, boolean parseRootFields, - DecodeContext context) { + static Object parseKeyValuePair(final String key, final String value, final int depth, final boolean parseRootFields, + final DecodeContext context) { final Map obj = new LinkedHashMap<>(); parseKeyValuePairIntoMap(obj, key, value, depth, context); @@ -264,7 +264,7 @@ static Object parseKeyValuePair(String key, String value, int depth, boolean par * @param context decode an object to deal with lines, delimiter, and options * @return parsed keyed array value */ - static Object parseKeyedArrayValue(Matcher keyedArray, String content, int depth, DecodeContext context) { + static Object parseKeyedArrayValue(final Matcher keyedArray, final String content, final int depth, final DecodeContext context) { final String originalKey = keyedArray.group(1).trim(); final String key = StringEscaper.unescape(originalKey); final String arrayHeader = content.substring(keyedArray.group(1).length()); @@ -298,8 +298,8 @@ static Object parseKeyedArrayValue(Matcher keyedArray, String content, int depth * @param context decode an object to deal with lines, delimiter and options * @return true if the field was processed as a keyed array, false otherwise */ - static boolean parseKeyedArrayField(String fieldContent, Map item, int depth, - DecodeContext context) { + static boolean parseKeyedArrayField(final String fieldContent, final Map item, final int depth, + final DecodeContext context) { final Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(fieldContent); if (!keyedArray.matches()) { return false; @@ -334,8 +334,8 @@ static boolean parseKeyedArrayField(String fieldContent, Map ite * @param context decode an object to deal with lines, delimiter and options * @return true if the field was processed as a key-value pair, false otherwise */ - static boolean parseKeyValueField(String fieldContent, Map item, int depth, - DecodeContext context) { + static boolean parseKeyValueField(final String fieldContent, final Map item, final int depth, + final DecodeContext context) { final int colonIdx = DecodeHelper.findUnquotedColon(fieldContent); if (colonIdx <= 0) { return false; diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java index 58fadf7..3e3d2f4 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java @@ -28,8 +28,8 @@ private ListItemDecoder() { * @param result the stored result of each list item parse * @param context decode an object to deal with lines, delimiter and options */ - public static void processListArrayItem(String line, int lineDepth, int depth, - List result, DecodeContext context) { + public static void processListArrayItem(final String line, final int lineDepth, final int depth, + final List result, final DecodeContext context) { if (lineDepth == depth + 1) { final String content = line.substring((depth + 1) * context.options.indent()); @@ -52,7 +52,7 @@ public static void processListArrayItem(String line, int lineDepth, int depth, * @param context decode an object to deal with lines, delimiter and options * @return parsed item (scalar value or object) */ - public static Object parseListItem(String content, int depth, DecodeContext context) { + public static Object parseListItem(final String content, final int depth, final DecodeContext context) { // Handle empty item: just "-" or "- " final String itemContent; if (content.length() > 2) { @@ -142,7 +142,7 @@ public static Object parseListItem(String content, int depth, DecodeContext cont * @param depth the depth of the item * @param context decode an object to deal with lines, delimiter and options * */ - private static void parseListItemFields(Map item, int depth, DecodeContext context) { + private static void parseListItemFields(final Map item, final int depth, final DecodeContext context) { while (context.currentLine < context.lines.length) { final String line = context.lines[context.currentLine]; final int lineDepth = DecodeHelper.getDepth(line, context); diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java index e147d44..e9577ff 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java @@ -23,7 +23,7 @@ private ObjectDecoder() { * @param context decode an object to deal with lines, delimiter and options * @return parsed nested object */ - static Map parseNestedObject(int parentDepth, DecodeContext context) { + static Map parseNestedObject(final int parentDepth, final DecodeContext context) { final Map result = new LinkedHashMap<>(); while (context.currentLine < context.lines.length) { @@ -56,8 +56,8 @@ static Map parseNestedObject(int parentDepth, DecodeContext cont * Returns true if the line was processed, false if it was a blank line that was * skipped. */ - private static void processDirectChildLine(Map result, String line, int parentDepth, int depth, - DecodeContext context) { + private static void processDirectChildLine(final Map result, final String line, final int parentDepth, final int depth, + final DecodeContext context) { final String content = line.substring((parentDepth + 1) * context.options.indent()); final Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(content); @@ -75,7 +75,7 @@ private static void processDirectChildLine(Map result, String li * @param depth the depth of the object field * @param context decode an object to deal with lines, delimiter and options */ - static void parseRootObjectFields(Map obj, int depth, DecodeContext context) { + static void parseRootObjectFields(final Map obj, final int depth, final DecodeContext context) { while (context.currentLine < context.lines.length) { final String line = context.lines[context.currentLine]; final int lineDepth = DecodeHelper.getDepth(line, context); @@ -118,8 +118,8 @@ static void parseRootObjectFields(Map obj, int depth, DecodeCont * @param depth the depth of the object field * @param context decode an object to deal with lines, delimiter and options */ - private static void processRootKeyedArrayLine(Map objectMap, String content, String originalKey, - int depth, DecodeContext context) { + private static void processRootKeyedArrayLine(final Map objectMap, final String content, final String originalKey, + final int depth, final DecodeContext context) { final String originalKeyTrimmed = originalKey.trim(); final String key = StringEscaper.unescape(originalKey); final String arrayHeader = content.substring(originalKey.length()); @@ -144,7 +144,7 @@ private static void processRootKeyedArrayLine(Map objectMap, Str * @param context decode an object to deal with lines, delimiter and options * @return the parsed scalar value */ - static Object parseBareScalarValue(String content, int depth, DecodeContext context) { + static Object parseBareScalarValue(final String content, final int depth, final DecodeContext context) { final Object result = PrimitiveDecoder.parse(content); context.currentLine++; @@ -164,7 +164,7 @@ static Object parseBareScalarValue(String content, int depth, DecodeContext cont * @param context decode an object to deal with lines, delimiter and options * @return the parsed value (Map, List, or primitive) */ - static Object parseFieldValue(String fieldValue, int fieldDepth, DecodeContext context) { + static Object parseFieldValue(final String fieldValue, final int fieldDepth, final DecodeContext context) { // Check if the next line is nested if (context.currentLine + 1 < context.lines.length) { final int nextDepth = DecodeHelper.getDepth(context.lines[context.currentLine + 1], context); @@ -204,7 +204,7 @@ static Object parseFieldValue(String fieldValue, int fieldDepth, DecodeContext c * @param context decode an object to deal with lines, delimiter and options * @return the parsed value (Map, List, or primitive) */ - static Object parseObjectItemValue(String value, int depth, DecodeContext context) { + static Object parseObjectItemValue(final String value, final int depth, final DecodeContext context) { final boolean isEmpty = value.isBlank(); // Find the next non-blank line and its depth diff --git a/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java index 5ffb466..3915c00 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java @@ -45,7 +45,7 @@ private PrimitiveDecoder() { * @return The parsed value as {@code Boolean}, {@code Long}, {@code Double}, * {@code String}, or {@code null} */ - static Object parse(String value) { + static Object parse(final String value) { if (value == null || value.isEmpty()) { return ""; } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java index 87b1406..ed6e23e 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java @@ -32,8 +32,8 @@ private TabularArrayDecoder() { * @param context decode an object to deal with lines, delimiter and options * @return tabular array converted to JSON format */ - public static List parseTabularArray(String header, int depth, Delimiter arrayDelimiter, - DecodeContext context) { + public static List parseTabularArray(final String header, final int depth, final Delimiter arrayDelimiter, + final DecodeContext context) { final Matcher matcher = TABULAR_HEADER_PATTERN.matcher(header); if (!matcher.find()) { return Collections.emptyList(); @@ -73,7 +73,7 @@ public static List parseTabularArray(String header, int depth, Delimiter * @param context decode an object to deal with lines, delimiter and options * @return list of keys */ - private static List parseTabularKeys(String keysStr, Delimiter arrayDelimiter, DecodeContext context) { + private static List parseTabularKeys(final String keysStr, final Delimiter arrayDelimiter, final DecodeContext context) { // Validate delimiter mismatch between bracket and brace fields if (context.options.strict()) { validateKeysDelimiter(keysStr, arrayDelimiter); @@ -93,7 +93,7 @@ private static List parseTabularKeys(String keysStr, Delimiter arrayDeli * @param keysStr the string representation of keys * @param expectedDelimiter the expected delimiter used in the array */ - private static void validateKeysDelimiter(String keysStr, Delimiter expectedDelimiter) { + private static void validateKeysDelimiter(final String keysStr, final Delimiter expectedDelimiter) { final char expectedChar = expectedDelimiter.toString().charAt(0); boolean inQuotes = false; boolean escaped = false; @@ -118,7 +118,7 @@ private static void validateKeysDelimiter(String keysStr, Delimiter expectedDeli * @param expectedChar the expected delimiter character * @param actualChar the actual delimiter character */ - private static void checkDelimiterMismatch(char expectedChar, char actualChar) { + private static void checkDelimiterMismatch(final char expectedChar, final char actualChar) { if (expectedChar == Delimiter.TAB.getValue() && actualChar == Delimiter.COMMA.getValue()) { throw new IllegalArgumentException( "Delimiter mismatch: bracket declares tab, brace fields use comma"); @@ -144,8 +144,8 @@ private static void checkDelimiterMismatch(char expectedChar, char actualChar) { * @param context decode an object to deal with lines, delimiter and options * @return true if parsing should continue, false if an array should terminate */ - private static boolean processTabularArrayLine(int expectedRowDepth, List keys, Delimiter arrayDelimiter, - List result, DecodeContext context) { + private static boolean processTabularArrayLine(final int expectedRowDepth, final List keys, final Delimiter arrayDelimiter, + final List result, final DecodeContext context) { final String line = context.lines[context.currentLine]; if (DecodeHelper.isBlankLine(line)) { @@ -170,7 +170,7 @@ private static boolean processTabularArrayLine(int expectedRowDepth, List keys, - Delimiter arrayDelimiter, List result, DecodeContext context) { + private static boolean processTabularRow(final String line, final int lineDepth, final int expectedRowDepth, final List keys, + final Delimiter arrayDelimiter, final List result, final DecodeContext context) { if (lineDepth == expectedRowDepth) { final String rowContent = line.substring(expectedRowDepth * context.options.indent()); final Map row = parseTabularRow(rowContent, keys, arrayDelimiter, context); @@ -264,8 +264,8 @@ private static boolean processTabularRow(String line, int lineDepth, int expecte * @param context decode an object to deal with lines, delimiter and options * @return a Map containing the parsed row values */ - private static Map parseTabularRow(String rowContent, List keys, - Delimiter arrayDelimiter, DecodeContext context) { + private static Map parseTabularRow(final String rowContent, final List keys, + final Delimiter arrayDelimiter, final DecodeContext context) { final Map row = new LinkedHashMap<>(); final List values = ArrayDecoder.parseArrayValues(rowContent, arrayDelimiter); diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java index 2e30d60..b147060 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java @@ -45,7 +45,7 @@ private ValueDecoder() { * @throws IllegalArgumentException if strict mode is enabled and input is * invalid */ - public static Object decode(String toon, DecodeOptions options) { + public static Object decode(final String toon, final DecodeOptions options) { if (toon == null || toon.isBlank()) { return new LinkedHashMap<>(); } @@ -116,7 +116,7 @@ public static Object decode(String toon, DecodeOptions options) { * @throws IllegalArgumentException if strict mode is enabled and input is * invalid */ - public static String decodeToJson(String toon, DecodeOptions options) { + public static String decodeToJson(final String toon, final DecodeOptions options) { try { final Object decoded = decode(toon, options); return MAPPER.writeValueAsString(decoded); diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java index 0adf509..09ceda1 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java @@ -30,7 +30,7 @@ private ArrayEncoder() { * @param depth Indentation depth * @param options Encoding options */ - public static void encodeArray(String key, ArrayNode value, LineWriter writer, int depth, EncodeOptions options) { + public static void encodeArray(final String key, final ArrayNode value, final LineWriter writer, final int depth, final EncodeOptions options) { if (value.isEmpty()) { final String header = PrimitiveEncoder.formatHeader(0, key, null, options.delimiter().toString(), options.lengthMarker()); @@ -77,7 +77,7 @@ public static void encodeArray(String key, ArrayNode value, LineWriter writer, i * @param array for testing that all items are primitives * @return true if all items in the array are primitive values, false otherwise */ - public static boolean isArrayOfPrimitives(JsonNode array) { + public static boolean isArrayOfPrimitives(final JsonNode array) { if (!array.isArray()) { return false; } @@ -95,7 +95,7 @@ public static boolean isArrayOfPrimitives(JsonNode array) { * @param array the array to check * @return true if all items in the array are arrays, false otherwise */ - public static boolean isArrayOfArrays(JsonNode array) { + public static boolean isArrayOfArrays(final JsonNode array) { if (!array.isArray()) { return false; } @@ -113,7 +113,7 @@ public static boolean isArrayOfArrays(JsonNode array) { * @param array the array to check * @return true if all items in the array are objects, false otherwise */ - public static boolean isArrayOfObjects(JsonNode array) { + public static boolean isArrayOfObjects(final JsonNode array) { if (!array.isArray()) { return false; } @@ -128,8 +128,8 @@ public static boolean isArrayOfObjects(JsonNode array) { /** * Encodes a primitive array inline: key[N]: v1,v2,v3. */ - private static void encodeInlinePrimitiveArray(String prefix, ArrayNode values, LineWriter writer, int depth, - EncodeOptions options) { + private static void encodeInlinePrimitiveArray(final String prefix, final ArrayNode values, final LineWriter writer, final int depth, + final EncodeOptions options) { final String formatted = formatInlineArray(values, options.delimiter().toString(), prefix, options.lengthMarker()); writer.push(depth, formatted); @@ -144,7 +144,7 @@ private static void encodeInlinePrimitiveArray(String prefix, ArrayNode values, * @param lengthMarker whether to include the # marker before the length * @return the formatted inline array string */ - public static String formatInlineArray(ArrayNode values, String delimiter, String prefix, boolean lengthMarker) { + public static String formatInlineArray(final ArrayNode values, final String delimiter, final String prefix, final boolean lengthMarker) { final List valueList = new ArrayList<>(); values.forEach(valueList::add); @@ -161,8 +161,8 @@ public static String formatInlineArray(ArrayNode values, String delimiter, Strin /** * Encodes an array of primitive arrays as list items. */ - private static void encodeArrayOfArraysAsListItems(String prefix, ArrayNode values, LineWriter writer, int depth, - EncodeOptions options) { + private static void encodeArrayOfArraysAsListItems(final String prefix, final ArrayNode values, final LineWriter writer, final int depth, + final EncodeOptions options) { final String header = PrimitiveEncoder.formatHeader(values.size(), prefix, null, options.delimiter().toString(), options.lengthMarker()); writer.push(depth, header); @@ -179,11 +179,11 @@ private static void encodeArrayOfArraysAsListItems(String prefix, ArrayNode valu /** * Encodes a mixed array (non-uniform) as list items. */ - private static void encodeMixedArrayAsListItems(String prefix, - ArrayNode items, - LineWriter writer, - int depth, - EncodeOptions options) { + private static void encodeMixedArrayAsListItems(final String prefix, + final ArrayNode items, + final LineWriter writer, + final int depth, + final EncodeOptions options) { final String header = PrimitiveEncoder.formatHeader(items.size(), prefix, null, options.delimiter().toString(), options.lengthMarker()); writer.push(depth, header); diff --git a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java index 60d6e25..b9e97be 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java @@ -63,12 +63,12 @@ private record ChainResult(List segments, JsonNode tail, JsonNode leafVa * @param remainingDepth the remaining depth of the object * @return a {@link FoldResult}, or null if folding is not possible */ - public static FoldResult tryFoldKeyChain(String key, - JsonNode value, - Set siblings, - Set rootLiteralKeys, - String pathPrefix, - Integer remainingDepth) { + public static FoldResult tryFoldKeyChain(final String key, + final JsonNode value, + final Set siblings, + final Set rootLiteralKeys, + final String pathPrefix, + final Integer remainingDepth) { // Must be an object to begin folding if (!value.isObject() || remainingDepth <= 1) { return null; @@ -136,7 +136,7 @@ public static FoldResult tryFoldKeyChain(String key, * @param maxDepth maximum number of allowed segments * @return a {@link ChainResult} containing segments, tail, and leafValue */ - private static ChainResult collectSingleKeyChain(String startKey, JsonNode startValue, int maxDepth) { + private static ChainResult collectSingleKeyChain(final String startKey, final JsonNode startValue, final int maxDepth) { // normalize absolute key to its local segment final String localStartKey = startKey.contains(DOT) ? startKey.substring(startKey.lastIndexOf(DOT.charAt(0)) + 1) diff --git a/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java b/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java index dfb83b1..450c5f6 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java @@ -41,7 +41,7 @@ public record HeaderConfig( * @param config Header configuration * @return Formatted header string */ - public static String format(HeaderConfig config) { + public static String format(final HeaderConfig config) { final StringBuilder header = new StringBuilder(); appendKeyIfPresent(header, config.key()); @@ -63,26 +63,26 @@ public static String format(HeaderConfig config) { * @return formatted header string */ public static String format( - int length, - String key, - List fields, - String delimiter, - boolean lengthMarker) { + final int length, + final String key, + final List fields, + final String delimiter, + final boolean lengthMarker) { final HeaderConfig config = new HeaderConfig(length, key, fields, delimiter, lengthMarker); return format(config); } - private static void appendKeyIfPresent(StringBuilder header, String key) { + private static void appendKeyIfPresent(final StringBuilder header, final String key) { if (key != null) { header.append(PrimitiveEncoder.encodeKey(key)); } } private static void appendArrayLength( - StringBuilder header, - int length, - String delimiter, - boolean lengthMarker) { + final StringBuilder header, + final int length, + final String delimiter, + final boolean lengthMarker) { header.append(OPEN_BRACKET); if (lengthMarker) { @@ -94,16 +94,16 @@ private static void appendArrayLength( header.append(CLOSE_BRACKET); } - private static void appendDelimiterIfNotDefault(StringBuilder header, String delimiter) { + private static void appendDelimiterIfNotDefault(final StringBuilder header, final String delimiter) { if (!COMMA.equals(delimiter)) { header.append(delimiter); } } private static void appendFieldsIfPresent( - StringBuilder header, - List fields, - String delimiter) { + final StringBuilder header, + final List fields, + final String delimiter) { if (fields == null || fields.isEmpty()) { return; } @@ -113,7 +113,7 @@ private static void appendFieldsIfPresent( header.append(CLOSE_BRACE); } - private static String formatFields(List fields, String delimiter) { + private static String formatFields(final List fields, final String delimiter) { return fields.stream() .map(PrimitiveEncoder::encodeKey) .reduce((a, b) -> a + delimiter + b) diff --git a/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java b/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java index 64dc296..8ab1631 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java @@ -1,43 +1,69 @@ package dev.toonformat.jtoon.encoder; -import java.util.ArrayList; -import java.util.List; import static dev.toonformat.jtoon.util.Constants.SPACE; /** * Line writer that accumulates indented lines for building the final output. + * Uses StringBuilder for efficient string building. */ +@SuppressWarnings("PMD.AvoidStringBufferField") public final class LineWriter { - private final List lines = new ArrayList<>(); + private final StringBuilder stringBuilder; private final String indentationString; + private final String[] indentCache; + private boolean firstLine = true; + + private static final int MAX_INDENT_CACHE = 16; + private static final int INITIAL_BUFFER_SIZE = 1024; /** * Creates a LineWriter with the specified indentation size. - * + * * @param indentSize Number of spaces per indentation level */ - public LineWriter(int indentSize) { + public LineWriter(final int indentSize) { + this.stringBuilder = new StringBuilder(INITIAL_BUFFER_SIZE); this.indentationString = SPACE.repeat(indentSize); + this.indentCache = new String[MAX_INDENT_CACHE]; + + if (indentSize > 0) { + final StringBuilder indent = new StringBuilder(); + for (int i = 0; i < MAX_INDENT_CACHE; i++) { + indentCache[i] = indent.toString(); + indent.append(indentationString); + } + } } /** * Adds a line with the specified depth and content. - * + * * @param depth Indentation depth (0 = no indentation) * @param content Line content to add */ - public void push(int depth, String content) { - final String indent = indentationString.repeat(depth); - lines.add(indent + content); + public void push(final int depth, final String content) { + if (!firstLine) { + stringBuilder.append('\n'); + } + firstLine = false; + + if (depth > 0) { + if (depth < indentCache.length) { + stringBuilder.append(indentCache[depth]); + } else { + stringBuilder.append(String.valueOf(indentationString).repeat(depth)); + } + } + stringBuilder.append(content); } /** - * Joins all accumulated lines with newlines. - * + * Returns the complete output string. + * * @return The complete output string */ @Override public String toString() { - return String.join("\n", lines); + return stringBuilder.toString(); } } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ListItemEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ListItemEncoder.java index 26e6fe5..bba13ec 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ListItemEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ListItemEncoder.java @@ -35,7 +35,7 @@ private ListItemEncoder() { * @param depth Indentation depth * @param options Encoding options */ - public static void encodeObjectAsListItem(ObjectNode obj, LineWriter writer, int depth, EncodeOptions options) { + public static void encodeObjectAsListItem(final ObjectNode obj, final LineWriter writer, final int depth, final EncodeOptions options) { final List keys = new ArrayList<>(obj.propertyNames()); if (keys.isEmpty()) { @@ -60,8 +60,8 @@ public static void encodeObjectAsListItem(ObjectNode obj, LineWriter writer, int * Encodes the first key-value pair of a list item. * Handles special formatting for arrays and objects. */ - private static void encodeFirstKeyValue(String key, JsonNode value, LineWriter writer, int depth, - EncodeOptions options) { + private static void encodeFirstKeyValue(final String key, final JsonNode value, final LineWriter writer, final int depth, + final EncodeOptions options) { final String encodedKey = PrimitiveEncoder.encodeKey(key); if (value.isValueNode()) { @@ -73,14 +73,14 @@ private static void encodeFirstKeyValue(String key, JsonNode value, LineWriter w } } - private static void encodeFirstValueAsPrimitive(String encodedKey, JsonNode value, LineWriter writer, int depth, - EncodeOptions options) { + private static void encodeFirstValueAsPrimitive(final String encodedKey, final JsonNode value, final LineWriter writer, final int depth, + final EncodeOptions options) { writer.push(depth, LIST_ITEM_PREFIX + encodedKey + COLON + SPACE + PrimitiveEncoder.encodePrimitive(value, options.delimiter().toString())); } - private static void encodeFirstValueAsArray(String key, String encodedKey, ArrayNode arrayValue, LineWriter writer, - int depth, EncodeOptions options) { + private static void encodeFirstValueAsArray(final String key, final String encodedKey, final ArrayNode arrayValue, final LineWriter writer, + final int depth, final EncodeOptions options) { if (ArrayEncoder.isArrayOfPrimitives(arrayValue)) { encodeFirstArrayAsPrimitives(key, arrayValue, writer, depth, options); } else if (ArrayEncoder.isArrayOfObjects(arrayValue)) { @@ -90,15 +90,15 @@ private static void encodeFirstValueAsArray(String key, String encodedKey, Array } } - private static void encodeFirstArrayAsPrimitives(String key, ArrayNode arrayValue, LineWriter writer, int depth, - EncodeOptions options) { + private static void encodeFirstArrayAsPrimitives(final String key, final ArrayNode arrayValue, final LineWriter writer, final int depth, + final EncodeOptions options) { final String formatted = ArrayEncoder.formatInlineArray(arrayValue, options.delimiter().toString(), key, options.lengthMarker()); writer.push(depth, LIST_ITEM_PREFIX + formatted); } - private static void encodeFirstArrayAsObjects(String key, String encodedKey, ArrayNode arrayValue, - LineWriter writer, int depth, EncodeOptions options) { + private static void encodeFirstArrayAsObjects(final String key, final String encodedKey, final ArrayNode arrayValue, + final LineWriter writer, final int depth, final EncodeOptions options) { final List header = TabularArrayEncoder.detectTabularHeader(arrayValue); if (!header.isEmpty()) { final String headerStr = PrimitiveEncoder.formatHeader(arrayValue.size(), key, header, @@ -118,8 +118,8 @@ private static void encodeFirstArrayAsObjects(String key, String encodedKey, Arr } } - private static void encodeFirstArrayAsComplex(String encodedKey, ArrayNode arrayValue, LineWriter writer, - int depth, EncodeOptions options) { + private static void encodeFirstArrayAsComplex(final String encodedKey, final ArrayNode arrayValue, final LineWriter writer, + final int depth, final EncodeOptions options) { writer.push(depth, LIST_ITEM_PREFIX + encodedKey + OPEN_BRACKET + arrayValue.size() + CLOSE_BRACKET + COLON); for (JsonNode item : arrayValue) { @@ -136,8 +136,8 @@ private static void encodeFirstArrayAsComplex(String encodedKey, ArrayNode array } } - private static void encodeFirstValueAsObject(String encodedKey, ObjectNode nestedObj, LineWriter writer, int depth, - EncodeOptions options) { + private static void encodeFirstValueAsObject(final String encodedKey, final ObjectNode nestedObj, final LineWriter writer, final int depth, + final EncodeOptions options) { writer.push(depth, LIST_ITEM_PREFIX + encodedKey + COLON); if (!nestedObj.isEmpty()) { ObjectEncoder.encodeObject(nestedObj, writer, depth + 2, options, Set.of(), null, null, new HashSet<>()); diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java index 63df414..ce8af8f 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java @@ -36,9 +36,9 @@ private ObjectEncoder() { * @param remainingDepth optional override for the remaining depth * @param blockedKeys contains only keys that have undergone a successful flattening */ - public static void encodeObject(ObjectNode value, LineWriter writer, int depth, EncodeOptions options, - Set rootLiteralKeys, String pathPrefix, Integer remainingDepth, - Set blockedKeys) { + public static void encodeObject(final ObjectNode value, final LineWriter writer, final int depth, final EncodeOptions options, + final Set rootLiteralKeys, final String pathPrefix, final Integer remainingDepth, + final Set blockedKeys) { final List> fields = value.properties().stream().toList(); // At root level (depth 0), collect all literal dotted keys for collision checking @@ -76,16 +76,16 @@ public static void encodeObject(ObjectNode value, LineWriter writer, int depth, * @param flattenDepth optional override for depth limit * @param blockedKeys contains only keys that have undergone a successful flattening */ - public static void encodeKeyValuePair(String key, - JsonNode value, - LineWriter writer, - int depth, - EncodeOptions options, - Set siblings, - Set rootLiteralKeys, - String pathPrefix, - Integer flattenDepth, - Set blockedKeys + public static void encodeKeyValuePair(final String key, + final JsonNode value, + final LineWriter writer, + final int depth, + final EncodeOptions options, + final Set siblings, + final Set rootLiteralKeys, + final String pathPrefix, + final Integer flattenDepth, + final Set blockedKeys ) { if (key == null) { return; @@ -146,9 +146,9 @@ public static void encodeKeyValuePair(String key, * @param remainingDepth the depth that remind to the limit * @return EncodeOptions changes for Case 2 */ - private static EncodeOptions flatten(String key, Flatten.FoldResult foldResult, LineWriter writer, int depth, - EncodeOptions options, Set rootLiteralKeys, String pathPrefix, - Set blockedKeys, int remainingDepth) { + private static EncodeOptions flatten(final String key, final Flatten.FoldResult foldResult, final LineWriter writer, final int depth, + final EncodeOptions options, final Set rootLiteralKeys, final String pathPrefix, + final Set blockedKeys, final int remainingDepth) { final String foldedKey = foldResult.foldedKey(); EncodeOptions currentOptions = options; @@ -189,8 +189,8 @@ private static EncodeOptions flatten(String key, Flatten.FoldResult foldResult, return currentOptions; } - private static void handleFullyFoldedLeaf(Flatten.FoldResult foldResult, LineWriter writer, int depth, - EncodeOptions options, String encodedFoldedKey) { + private static void handleFullyFoldedLeaf(final Flatten.FoldResult foldResult, final LineWriter writer, final int depth, + final EncodeOptions options, final String encodedFoldedKey) { final JsonNode leaf = foldResult.leafValue(); // Primitive @@ -218,7 +218,7 @@ private static void handleFullyFoldedLeaf(Flatten.FoldResult foldResult, LineWri } } - private static String indentedLine(int depth, String content, int indentSize) { + private static String indentedLine(final int depth, final String content, final int indentSize) { return "%s%s".formatted(" ".repeat(indentSize * depth), content); } } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java index 78aab20..bf322d2 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java @@ -5,7 +5,6 @@ import tools.jackson.databind.JsonNode; import java.math.BigDecimal; import java.util.List; -import java.util.Objects; import static dev.toonformat.jtoon.util.Constants.NULL_LITERAL; import static dev.toonformat.jtoon.util.Constants.DOUBLE_QUOTE; @@ -16,17 +15,20 @@ */ public final class PrimitiveEncoder { + private static final int INITIAL_BUFFER_SIZE = 128; + private PrimitiveEncoder() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); } /** * Encodes a primitive JsonNode value. - * @param value the primitive value to encode + * + * @param value the primitive value to encode * @param delimiter the delimiter to use (for string validation) * @return the encoded string representation */ - public static String encodePrimitive(JsonNode value, String delimiter) { + public static String encodePrimitive(final JsonNode value, final String delimiter) { return switch (value.getNodeType()) { case BOOLEAN -> String.valueOf(value.asBoolean()); case NUMBER -> encodeNumber(value); @@ -40,7 +42,7 @@ public static String encodePrimitive(JsonNode value, String delimiter) { * Ensures LLM-safe output by converting all numbers to plain decimal * representation. */ - private static String encodeNumber(JsonNode value) { + private static String encodeNumber(final JsonNode value) { if (value.isIntegralNumber()) { return value.asString(); } @@ -57,28 +59,33 @@ private static String encodeNumber(JsonNode value) { * decimal point. * Examples: "1.500" -> "1.5", "1.0" -> "1", "0.000001" -> "0.000001" */ - private static String stripTrailingZeros(String value) { - if (!value.contains(".")) { + private static String stripTrailingZeros(final String value) { + final int dotIndex = value.indexOf('.'); + if (dotIndex < 0) { return value; } - String stripped = value.replaceAll("0+$", ""); + int lastNonZero = value.length() - 1; + while (lastNonZero > dotIndex && value.charAt(lastNonZero) == '0') { + lastNonZero--; + } - if (stripped.endsWith(".")) { - stripped = stripped.substring(0, stripped.length() - 1); + if (lastNonZero == dotIndex) { + return value.substring(0, dotIndex); } - return stripped; + return value.substring(0, lastNonZero + 1); } /** * Encodes a string literal, quoting if necessary. * Delegates validation to StringValidator and escaping to StringEscaper. - * @param value the string value to encode + * + * @param value the string value to encode * @param delimiter the delimiter to use (for validation) * @return the encoded string, quoted if necessary */ - public static String encodeStringLiteral(String value, String delimiter) { + public static String encodeStringLiteral(final String value, final String delimiter) { if (StringValidator.isSafeUnquoted(value, delimiter)) { return value; } @@ -89,10 +96,11 @@ public static String encodeStringLiteral(String value, String delimiter) { /** * Encodes an object key, quoting if necessary. * Delegates validation to StringValidator and escaping to StringEscaper. + * * @param key the key to encode * @return the encoded key, quoted if necessary */ - public static String encodeKey(String key) { + public static String encodeKey(final String key) { if (StringValidator.isValidUnquotedKey(key)) { return key; } @@ -102,22 +110,35 @@ public static String encodeKey(String key) { /** * Joins encoded primitive values with the specified delimiter. - * @param values the list of primitive values to join + * + * @param values the list of primitive values to join * @param delimiter the delimiter to use between values * @return the joined string of encoded values */ - public static String joinEncodedValues(List values, String delimiter) { - return values.stream() - .filter(Objects::nonNull) - .map(v -> encodePrimitive(v, delimiter)) - .reduce((a, b) -> a + delimiter + b) - .orElse(""); + public static String joinEncodedValues(final List values, final String delimiter) { + if (values == null || values.isEmpty()) { + return ""; + } + + final StringBuilder stringBuilder = new StringBuilder(INITIAL_BUFFER_SIZE); + boolean first = true; + for (final JsonNode node : values) { + if (node == null) { + continue; + } + if (!first) { + stringBuilder.append(delimiter); + } + first = false; + stringBuilder.append(encodePrimitive(node, delimiter)); + } + return stringBuilder.toString(); } /** * Formats a header for arrays and tables. * Delegates to HeaderFormatter for implementation. - * + * * @param length Array length * @param key Optional key prefix * @param fields Optional field names for tabular format @@ -126,11 +147,11 @@ public static String joinEncodedValues(List values, String delimiter) * @return Formatted header string */ public static String formatHeader( - int length, - String key, - List fields, - String delimiter, - boolean lengthMarker) { + final int length, + final String key, + final List fields, + final String delimiter, + final boolean lengthMarker) { return HeaderFormatter.format(length, key, fields, delimiter, lengthMarker); } } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java index 8cd24a8..ed07bfc 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java @@ -25,7 +25,7 @@ private TabularArrayEncoder() { * @param rows The array to analyze * @return List of field names for tabular header, or empty list if not tabular */ - public static List detectTabularHeader(ArrayNode rows) { + public static List detectTabularHeader(final ArrayNode rows) { if (rows.isEmpty()) { return Collections.emptyList(); } @@ -52,7 +52,7 @@ public static List detectTabularHeader(ArrayNode rows) { /** * Checks if all rows in the array have the same keys with primitive values. */ - private static boolean isTabularArray(ArrayNode rows, List header) { + private static boolean isTabularArray(final ArrayNode rows, final List header) { for (JsonNode row : rows) { if (!row.isObject()) { return false; @@ -90,8 +90,8 @@ private static boolean isTabularArray(ArrayNode rows, List header) { * @param depth Indentation depth * @param options Encoding options */ - public static void encodeArrayOfObjectsAsTabular(String prefix, ArrayNode rows, List header, - LineWriter writer, int depth, EncodeOptions options) { + public static void encodeArrayOfObjectsAsTabular(final String prefix, final ArrayNode rows, final List header, + final LineWriter writer, final int depth, final EncodeOptions options) { final String headerStr = PrimitiveEncoder.formatHeader(rows.size(), prefix, header, options.delimiter().toString(), options.lengthMarker()); writer.push(depth, headerStr); @@ -109,8 +109,8 @@ public static void encodeArrayOfObjectsAsTabular(String prefix, ArrayNode rows, * @param depth Indentation depth * @param options Encoding options */ - public static void writeTabularRows(ArrayNode rows, List header, LineWriter writer, int depth, - EncodeOptions options) { + public static void writeTabularRows(final ArrayNode rows, final List header, final LineWriter writer, final int depth, + final EncodeOptions options) { for (JsonNode row : rows) { //skip non-object rows if (!row.isObject()) { diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ValueEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ValueEncoder.java index b3960a4..8c00180 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ValueEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ValueEncoder.java @@ -24,7 +24,7 @@ private ValueEncoder() { * @param options Encoding options (indent, delimiter, length marker) * @return The TOON-formatted string */ - public static String encodeValue(JsonNode value, EncodeOptions options) { + public static String encodeValue(final JsonNode value, final EncodeOptions options) { // Handle primitive values directly if (value.isValueNode()) { return PrimitiveEncoder.encodePrimitive(value, options.delimiter().toString()); diff --git a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java index d10b443..ddc68e6 100644 --- a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java +++ b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java @@ -70,7 +70,7 @@ private JsonNormalizer() { * @return Parsed JsonNode * @throws IllegalArgumentException if the input is blank or not valid JSON */ - public static JsonNode parse(String json) { + public static JsonNode parse(final String json) { if (json == null || json.isBlank()) { throw new IllegalArgumentException("Invalid JSON"); } @@ -87,7 +87,7 @@ public static JsonNode parse(String json) { * @param value The value to normalize * @return The normalized JsonNode */ - public static JsonNode normalize(Object value) { + public static JsonNode normalize(final Object value) { if (value == null) { return NullNode.getInstance(); } else if (value instanceof JsonNode jsonNode) { @@ -106,7 +106,7 @@ public static JsonNode normalize(Object value) { /** * Attempts normalization using chain of responsibility pattern. */ - private static JsonNode normalizeWithStrategy(Object value) { + private static JsonNode normalizeWithStrategy(final Object value) { return NORMALIZERS.stream() .map(normalizer -> normalizer.apply(value)) .filter(Objects::nonNull) @@ -118,7 +118,7 @@ private static JsonNode normalizeWithStrategy(Object value) { * Attempts to normalize primitive types and their wrappers. * Returns null if the value is not a primitive type. */ - private static JsonNode tryNormalizePrimitive(Object value) { + private static JsonNode tryNormalizePrimitive(final Object value) { if (value instanceof String stringValue) { return StringNode.valueOf(stringValue); } else if (value instanceof Boolean boolValue) { @@ -143,7 +143,7 @@ private static JsonNode tryNormalizePrimitive(Object value) { /** * Normalizes Double values handling special cases. */ - private static JsonNode normalizeDouble(Double value) { + private static JsonNode normalizeDouble(final Double value) { if (!Double.isFinite(value)) { return NullNode.getInstance(); } @@ -157,7 +157,7 @@ private static JsonNode normalizeDouble(Double value) { /** * Normalizes Float values handling special cases. */ - private static JsonNode normalizeFloat(Float value) { + private static JsonNode normalizeFloat(final Float value) { return Float.isFinite(value) ? FloatNode.valueOf(value) : NullNode.getInstance(); @@ -166,7 +166,7 @@ private static JsonNode normalizeFloat(Float value) { /** * Attempts to convert a double to a long if it's a whole number. */ - private static Optional tryConvertToLong(Double value) { + private static Optional tryConvertToLong(final Double value) { if (value != Math.floor(value)) { return Optional.empty(); } @@ -181,7 +181,7 @@ private static Optional tryConvertToLong(Double value) { * Attempts to normalize BigInteger and BigDecimal. * Returns null if the value is not a big number type. */ - private static JsonNode tryNormalizeBigNumber(Object value) { + private static JsonNode tryNormalizeBigNumber(final Object value) { if (value instanceof BigInteger bigInteger) { return normalizeBigInteger(bigInteger); } else if (value instanceof BigDecimal bigDecimal) { @@ -194,7 +194,7 @@ private static JsonNode tryNormalizeBigNumber(Object value) { /** * Normalizes BigInteger, converting to long if within range. */ - private static JsonNode normalizeBigInteger(BigInteger value) { + private static JsonNode normalizeBigInteger(final BigInteger value) { final boolean fitsInLong = value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) <= 0 && value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) >= 0; return fitsInLong @@ -206,7 +206,7 @@ private static JsonNode normalizeBigInteger(BigInteger value) { * Attempts to normalize temporal types (date/time) to ISO strings. * Returns null if the value is not a temporal type. */ - private static JsonNode tryNormalizeTemporal(Object value) { + private static JsonNode tryNormalizeTemporal(final Object value) { if (value instanceof LocalDateTime ldt) { return formatTemporal(ldt, DateTimeFormatter.ISO_LOCAL_DATE_TIME); } else if (value instanceof LocalDate ld) { @@ -237,7 +237,7 @@ private static JsonNode tryNormalizeTemporal(Object value) { /** * Helper method to format temporal values consistently. */ - private static JsonNode formatTemporal(T temporal, DateTimeFormatter formatter) { + private static JsonNode formatTemporal(final T temporal, final DateTimeFormatter formatter) { return StringNode.valueOf(formatter.format((java.time.temporal.TemporalAccessor) temporal)); } @@ -245,7 +245,7 @@ private static JsonNode formatTemporal(T temporal, DateTimeFormatter formatt * Attempts to normalize collections (Collection and Map). * Returns null if the value is not a collection type. */ - private static JsonNode tryNormalizeCollection(Object value) { + private static JsonNode tryNormalizeCollection(final Object value) { if (value instanceof Collection) { return normalizeCollection((Collection) value); } else if (value instanceof Map) { @@ -258,7 +258,7 @@ private static JsonNode tryNormalizeCollection(Object value) { /** * Normalizes a Collection to an ArrayNode. */ - private static ArrayNode normalizeCollection(Collection collection) { + private static ArrayNode normalizeCollection(final Collection collection) { final ArrayNode arrayNode = MAPPER.createArrayNode(); collection.forEach(item -> arrayNode.add(normalize(item))); return arrayNode; @@ -267,7 +267,7 @@ private static ArrayNode normalizeCollection(Collection collection) { /** * Normalizes a Map to an ObjectNode. */ - private static ObjectNode normalizeMap(Map map) { + private static ObjectNode normalizeMap(final Map map) { final ObjectNode objectNode = MAPPER.createObjectNode(); map.forEach((key, value) -> objectNode.set(String.valueOf(key), normalize(value))); return objectNode; @@ -277,7 +277,7 @@ private static ObjectNode normalizeMap(Map map) { * Attempts to normalize POJOs using Jackson's default conversion. * Returns null for non-serializable objects. */ - private static JsonNode tryNormalizePojo(Object value) { + private static JsonNode tryNormalizePojo(final Object value) { try { return MAPPER.valueToTree(value); } catch (Exception e) { @@ -288,7 +288,7 @@ private static JsonNode tryNormalizePojo(Object value) { /** * Normalizes arrays to ArrayNode. */ - private static JsonNode normalizeArray(Object array) { + private static JsonNode normalizeArray(final Object array) { if (array instanceof int[] intArr) { return buildArrayNode(intArr.length, i -> IntNode.valueOf(intArr[i])); } else if (array instanceof long[] longArr) { @@ -315,7 +315,7 @@ private static JsonNode normalizeArray(Object array) { /** * Builds an ArrayNode using a functional approach. */ - private static ArrayNode buildArrayNode(int length, IntFunction mapper) { + private static ArrayNode buildArrayNode(final int length, final IntFunction mapper) { final ArrayNode arrayNode = MAPPER.createArrayNode(); for (int i = 0; i < length; i++) { arrayNode.add(mapper.apply(i)); @@ -326,7 +326,7 @@ private static ArrayNode buildArrayNode(int length, IntFunction mapper /** * Normalizes a single double element from an array. */ - private static JsonNode normalizeDoubleElement(double value) { + private static JsonNode normalizeDoubleElement(final double value) { return Double.isFinite(value) ? DoubleNode.valueOf(value) : NullNode.getInstance(); @@ -335,7 +335,7 @@ private static JsonNode normalizeDoubleElement(double value) { /** * Normalizes a single float element from an array. */ - private static JsonNode normalizeFloatElement(float value) { + private static JsonNode normalizeFloatElement(final float value) { return Float.isFinite(value) ? FloatNode.valueOf(value) : NullNode.getInstance(); diff --git a/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java b/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java index 795bdc7..5f5fce2 100644 --- a/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java +++ b/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java @@ -17,7 +17,7 @@ private StringEscaper() { * @param value The string to escape * @return The escaped string */ - public static String escape(String value) { + public static String escape(final String value) { return value .replace("\\", "\\\\") .replace("\"", "\\\"") @@ -32,7 +32,7 @@ public static String escape(String value) { * @param value The string to validate * @throws IllegalArgumentException if the string has invalid escape sequences or is unterminated */ - public static void validateString(String value) { + public static void validateString(final String value) { if (value == null || value.isEmpty()) { return; } @@ -69,7 +69,7 @@ public static void validateString(String value) { /** * Checks if a character is a valid escape sequence. */ - private static boolean isValidEscapeChar(char c) { + private static boolean isValidEscapeChar(final char c) { return c == 'n' || c == 'r' || c == 't' || c == '"' || c == '\\'; } @@ -80,7 +80,7 @@ private static boolean isValidEscapeChar(char c) { * @param value The string to unescape (may be quoted) * @return The unescaped string with quotes removed */ - public static String unescape(String value) { + public static String unescape(final String value) { if (value == null || value.length() < 2) { return value; } @@ -113,7 +113,7 @@ public static String unescape(String value) { * @param c The character following a backslash * @return The unescaped character */ - private static char unescapeChar(char c) { + private static char unescapeChar(final char c) { return switch (c) { case 'n' -> '\n'; case 'r' -> '\r'; diff --git a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java index d441665..42d0581 100644 --- a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java +++ b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java @@ -30,7 +30,7 @@ private StringValidator() { * @param delimiter the delimiter being used (for validation) * @return true if the string can be safely written without quotes, false otherwise */ - public static boolean isSafeUnquoted(String value, String delimiter) { + public static boolean isSafeUnquoted(final String value, final String delimiter) { if (isNullOrEmpty(value)) { return false; } @@ -72,52 +72,52 @@ public static boolean isSafeUnquoted(String value, String delimiter) { * @param key the key to validate * @return true if the key can be used without quotes, false otherwise */ - public static boolean isValidUnquotedKey(String key) { + public static boolean isValidUnquotedKey(final String key) { return UNQUOTED_KEY_PATTERN.matcher(key).matches(); } - private static boolean isNullOrEmpty(String value) { + private static boolean isNullOrEmpty(final String value) { return value == null || value.isEmpty(); } - private static boolean isPaddedWithWhitespace(String value) { + private static boolean isPaddedWithWhitespace(final String value) { return !value.equals(value.trim()); } - private static boolean looksLikeKeyword(String value) { + private static boolean looksLikeKeyword(final String value) { return TRUE_LITERAL.equals(value) || FALSE_LITERAL.equals(value) || NULL_LITERAL.equals(value); } - private static boolean looksLikeNumber(String value) { + private static boolean looksLikeNumber(final String value) { return OCTAL_PATTERN.matcher(value).matches() || LEADING_ZERO_PATTERN.matcher(value).matches() || NUMERIC_PATTERN.matcher(value).matches(); } - private static boolean containsColon(String value) { + private static boolean containsColon(final String value) { return value.contains(COLON); } - static boolean containsQuotesOrBackslash(String value) { + static boolean containsQuotesOrBackslash(final String value) { return value.indexOf(DOUBLE_QUOTE) >= 0 || value.indexOf(BACKSLASH) >= 0; } - private static boolean containsStructuralCharacters(String value) { + private static boolean containsStructuralCharacters(final String value) { return STRUCTURAL_CHARS.matcher(value).find(); } - private static boolean containsControlCharacters(String value) { + private static boolean containsControlCharacters(final String value) { return CONTROL_CHARS.matcher(value).find(); } - private static boolean containsDelimiter(String value, String delimiter) { + private static boolean containsDelimiter(final String value, final String delimiter) { return value.contains(delimiter); } - private static boolean startsWithListMarker(String value) { + private static boolean startsWithListMarker(final String value) { return value.startsWith(LIST_ITEM_MARKER); } } From c5e78cde39c08f8798bbccc2305a197c0b1b66fd Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Fri, 13 Feb 2026 19:52:10 +0100 Subject: [PATCH 19/29] cleanup run with spotbugs --- .gitignore | 1 + build.gradle | 4 +- spotbugs-exclude.xml | 72 +++++++++++++++++++ .../jtoon/decoder/ArrayDecoder.java | 6 +- .../jtoon/decoder/DecodeHelper.java | 14 ++-- .../toonformat/jtoon/decoder/KeyDecoder.java | 25 ++++--- .../jtoon/decoder/ListItemDecoder.java | 5 +- .../jtoon/decoder/ObjectDecoder.java | 6 +- .../jtoon/decoder/PrimitiveDecoder.java | 8 +-- .../jtoon/decoder/TabularArrayDecoder.java | 7 +- .../jtoon/encoder/ArrayEncoder.java | 15 ++-- .../dev/toonformat/jtoon/encoder/Flatten.java | 2 +- .../jtoon/encoder/HeaderFormatter.java | 7 +- .../jtoon/encoder/ObjectEncoder.java | 6 +- .../jtoon/encoder/PrimitiveEncoder.java | 5 +- .../jtoon/encoder/TabularArrayEncoder.java | 30 +++++--- .../jtoon/normalizer/JsonNormalizer.java | 4 +- .../jtoon/util/StringValidator.java | 8 ++- 18 files changed, 164 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index 93067e6..3512a58 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ bin/ ### Mac OS ### .DS_Store +/.idea/ diff --git a/build.gradle b/build.gradle index 65b2fe1..ed82cd5 100644 --- a/build.gradle +++ b/build.gradle @@ -51,8 +51,8 @@ pitest { spotbugs { toolVersion = '4.9.8' excludeFilter = file('spotbugs-exclude.xml') - effort = com.github.spotbugs.snom.Effort.MAX - reportLevel = com.github.spotbugs.snom.Confidence.values()[0] + effort = "max" + reportLevel = "low" reportsDir = layout.buildDirectory.dir('spotbugs') } diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index ba794cc..b9311e6 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -57,4 +57,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java index f49ad01..10b12a1 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java @@ -151,12 +151,12 @@ static void validateArrayLength(final String header, final int actualLength) { * Returns the number specified in [n] or null if not found. * * @param header header string for length check - * @return extracted length from header + * @return extracted length from header, or null if not found */ private static Integer extractLengthFromHeader(final String header) { final Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header); if (matcher.find()) { - return Integer.parseInt(matcher.group(2)); + return Integer.valueOf(matcher.group(2)); } return null; } @@ -169,8 +169,8 @@ private static Integer extractLengthFromHeader(final String header) { * @return parsed array values */ static List parseArrayValues(final String values, final Delimiter arrayDelimiter) { - final List result = new ArrayList<>(); final List rawValues = parseDelimitedValues(values, arrayDelimiter); + final List result = new ArrayList<>(rawValues.size()); for (final String value : rawValues) { result.add(PrimitiveDecoder.parse(value)); } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java b/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java index 5903994..cfc294a 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/DecodeHelper.java @@ -65,7 +65,7 @@ private static int computeLeadingSpaces(final String line, final DecodeContext c i++; } - if (context.options.strict() && leadingSpaces > 0 && indentSize > 0 && leadingSpaces % indentSize != 0) { + if (indentSize > 0 && leadingSpaces > 0 && leadingSpaces % indentSize != 0 && context.options.strict()) { throw new IllegalArgumentException( String.format("Non-multiple indentation: %d leadingSpaces with indent=%d at line %d", leadingSpaces, indentSize, context.currentLine + 1)); @@ -99,14 +99,14 @@ static int findUnquotedColon(final String content) { for (int i = 0; i < content.length(); i++) { final char c = content.charAt(i); - if (escaped) { + if (c == COLON.charAt(0) && !inQuotes) { + return i; + } else if (escaped) { escaped = false; } else if (c == BACKSLASH) { escaped = true; } else if (c == DOUBLE_QUOTE) { inQuotes = !inQuotes; - } else if (c == COLON.charAt(0) && !inQuotes) { - return i; } } @@ -137,7 +137,8 @@ static int findNextNonBlankLine(final int startIndex, final DecodeContext contex * @param context decode an object to deal with lines, delimiter, and options * @throws IllegalArgumentException in case there's a expansion conflict */ - static void checkFinalValueConflict(final String finalSegment, final Object existing, final Object value, final DecodeContext context) { + static void checkFinalValueConflict(final String finalSegment, final Object existing, + final Object value, final DecodeContext context) { if (existing != null && context.options.strict()) { // Check for conflicts in strict mode if (existing instanceof Map && !(value instanceof Map)) { @@ -162,7 +163,8 @@ static void checkFinalValueConflict(final String finalSegment, final Object exis * @param value present value in a map * @param context decode an object to deal with lines, delimiter, and options */ - static void checkPathExpansionConflict(final Map map, final String key, final Object value, final DecodeContext context) { + static void checkPathExpansionConflict(final Map map, final String key, + final Object value, final DecodeContext context) { if (!context.options.strict()) { return; } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java index d4b4e08..7dd4938 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Map; import java.util.regex.Matcher; +import java.util.regex.MatchResult; import static dev.toonformat.jtoon.util.Constants.DOT; import static dev.toonformat.jtoon.util.Headers.KEYED_ARRAY_PATTERN; @@ -107,7 +108,8 @@ static void expandPathIntoMap(final Map current, final String do * @param depth the depth of the value line * @param context decode an object to deal with lines, delimiter and options */ - static void processKeyValueLine(final Map result, final String content, final int depth, final DecodeContext context) { + static void processKeyValueLine(final Map result, final String content, + final int depth, final DecodeContext context) { final int colonIdx = DecodeHelper.findUnquotedColon(content); if (colonIdx > 0) { @@ -223,8 +225,8 @@ private static Object parseKeyValue(final String value, final int depth, final D * @param unescapedKey the unescaped key * @param value the value to put */ - private static void putKeyValueIntoMap(final Map map, final String originalKey, final String unescapedKey, - final Object value, final DecodeContext context) { + private static void putKeyValueIntoMap(final Map map, final String originalKey, + final String unescapedKey, final Object value, final DecodeContext context) { // Handle path expansion if (shouldExpandKey(originalKey, context)) { expandPathIntoMap(map, unescapedKey, value, context); @@ -244,8 +246,8 @@ private static void putKeyValueIntoMap(final Map map, final Stri * @param context decode an object to deal with lines, delimiter, and options * @return parsed a key-value pair */ - static Object parseKeyValuePair(final String key, final String value, final int depth, final boolean parseRootFields, - final DecodeContext context) { + static Object parseKeyValuePair(final String key, final String value, final int depth, + final boolean parseRootFields, final DecodeContext context) { final Map obj = new LinkedHashMap<>(); parseKeyValuePairIntoMap(obj, key, value, depth, context); @@ -264,10 +266,12 @@ static Object parseKeyValuePair(final String key, final String value, final int * @param context decode an object to deal with lines, delimiter, and options * @return parsed keyed array value */ - static Object parseKeyedArrayValue(final Matcher keyedArray, final String content, final int depth, final DecodeContext context) { - final String originalKey = keyedArray.group(1).trim(); + static Object parseKeyedArrayValue(final MatchResult keyedArray, final String content, + final int depth, final DecodeContext context) { + final String group1 = keyedArray.group(1); + final String originalKey = group1.trim(); final String key = StringEscaper.unescape(originalKey); - final String arrayHeader = content.substring(keyedArray.group(1).length()); + final String arrayHeader = content.substring(group1.length()); final List arrayValue = ArrayDecoder.parseArray(arrayHeader, depth, context); final Map obj = new LinkedHashMap<>(); @@ -305,9 +309,10 @@ static boolean parseKeyedArrayField(final String fieldContent, final Map result, final DecodeContext context) { + final Collection result, final DecodeContext context) { if (lineDepth == depth + 1) { final String content = line.substring((depth + 1) * context.options.indent()); @@ -52,7 +53,7 @@ public static void processListArrayItem(final String line, final int lineDepth, * @param context decode an object to deal with lines, delimiter and options * @return parsed item (scalar value or object) */ - public static Object parseListItem(final String content, final int depth, final DecodeContext context) { + static Object parseListItem(final String content, final int depth, final DecodeContext context) { // Handle empty item: just "-" or "- " final String itemContent; if (content.length() > 2) { diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java index e9577ff..5190428 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java @@ -56,8 +56,8 @@ static Map parseNestedObject(final int parentDepth, final Decode * Returns true if the line was processed, false if it was a blank line that was * skipped. */ - private static void processDirectChildLine(final Map result, final String line, final int parentDepth, final int depth, - final DecodeContext context) { + private static void processDirectChildLine(final Map result, final String line, + final int parentDepth, final int depth, final DecodeContext context) { final String content = line.substring((parentDepth + 1) * context.options.indent()); final Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(content); @@ -149,7 +149,7 @@ static Object parseBareScalarValue(final String content, final int depth, final context.currentLine++; // In strict mode, check if there are more primitives at the root level - if (context.options.strict() && depth == 0) { + if (depth == 0 && context.options.strict()) { DecodeHelper.validateNoMultiplePrimitivesAtRoot(context); } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java index 3915c00..d7a2c3a 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/PrimitiveDecoder.java @@ -82,17 +82,17 @@ static Object parse(final String value) { // Try parsing as number try { // Check if it contains exponent notation or decimal point - if (value.contains(DOT) || value.contains("e") || value.contains("E")) { + if (value.contains("e") || value.contains("E") || value.contains(DOT)) { final double parsed = Double.parseDouble(value); // Handle negative zero - Java doesn't distinguish, but spec says it should be 0 if (parsed == 0.0) { return 0L; } // Check if the result is a whole number - if so, return as Long - if (parsed == Math.floor(parsed) - && !Double.isInfinite(parsed) + if (!Double.isInfinite(parsed) && parsed >= Long.MIN_VALUE - && parsed <= Long.MAX_VALUE) { + && parsed <= Long.MAX_VALUE + && parsed == Math.floor(parsed)) { return (long) parsed; } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java index ed6e23e..ee36873 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java @@ -3,6 +3,7 @@ import dev.toonformat.jtoon.Delimiter; import dev.toonformat.jtoon.util.StringEscaper; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -79,8 +80,8 @@ private static List parseTabularKeys(final String keysStr, final Delimit validateKeysDelimiter(keysStr, arrayDelimiter); } - final List result = new ArrayList<>(); final List rawValues = ArrayDecoder.parseDelimitedValues(keysStr, arrayDelimiter); + final List result = new ArrayList<>(rawValues.size()); for (final String key : rawValues) { result.add(StringEscaper.unescape(key)); } @@ -121,11 +122,11 @@ private static void validateKeysDelimiter(final String keysStr, final Delimiter private static void checkDelimiterMismatch(final char expectedChar, final char actualChar) { if (expectedChar == Delimiter.TAB.getValue() && actualChar == Delimiter.COMMA.getValue()) { throw new IllegalArgumentException( - "Delimiter mismatch: bracket declares tab, brace fields use comma"); + "Delimiter mismatch: bracket declares tab (expected='" + expectedChar + "', actual='" + actualChar + "')"); } if (expectedChar == Delimiter.PIPE.getValue() && actualChar == Delimiter.COMMA.getValue()) { throw new IllegalArgumentException( - "Delimiter mismatch: bracket declares pipe, brace fields use comma"); + "Delimiter mismatch: bracket declares pipe (expected='" + expectedChar + "', actual='" + actualChar + "')"); } if (expectedChar == Delimiter.COMMA.getValue() && (actualChar == Delimiter.TAB.getValue() || actualChar == Delimiter.PIPE.getValue())) { diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java index 09ceda1..fa1972c 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java @@ -95,7 +95,7 @@ public static boolean isArrayOfPrimitives(final JsonNode array) { * @param array the array to check * @return true if all items in the array are arrays, false otherwise */ - public static boolean isArrayOfArrays(final JsonNode array) { + static boolean isArrayOfArrays(final JsonNode array) { if (!array.isArray()) { return false; } @@ -128,10 +128,10 @@ public static boolean isArrayOfObjects(final JsonNode array) { /** * Encodes a primitive array inline: key[N]: v1,v2,v3. */ - private static void encodeInlinePrimitiveArray(final String prefix, final ArrayNode values, final LineWriter writer, final int depth, - final EncodeOptions options) { + private static void encodeInlinePrimitiveArray(final String prefix, final ArrayNode values, + final LineWriter writer, final int depth, final EncodeOptions options) { final String formatted = formatInlineArray(values, options.delimiter().toString(), prefix, - options.lengthMarker()); + options.lengthMarker()); writer.push(depth, formatted); } @@ -144,7 +144,8 @@ private static void encodeInlinePrimitiveArray(final String prefix, final ArrayN * @param lengthMarker whether to include the # marker before the length * @return the formatted inline array string */ - public static String formatInlineArray(final ArrayNode values, final String delimiter, final String prefix, final boolean lengthMarker) { + public static String formatInlineArray(final ArrayNode values, final String delimiter, + final String prefix, final boolean lengthMarker) { final List valueList = new ArrayList<>(); values.forEach(valueList::add); @@ -161,8 +162,8 @@ public static String formatInlineArray(final ArrayNode values, final String deli /** * Encodes an array of primitive arrays as list items. */ - private static void encodeArrayOfArraysAsListItems(final String prefix, final ArrayNode values, final LineWriter writer, final int depth, - final EncodeOptions options) { + private static void encodeArrayOfArraysAsListItems(final String prefix, final ArrayNode values, + final LineWriter writer, final int depth, final EncodeOptions options) { final String header = PrimitiveEncoder.formatHeader(values.size(), prefix, null, options.delimiter().toString(), options.lengthMarker()); writer.push(depth, header); diff --git a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java index b9e97be..faab4f0 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java @@ -149,7 +149,7 @@ private static ChainResult collectSingleKeyChain(final String startKey, final Js // track depth of folding int depthCounter = 1; - while (currentValue.isObject() && depthCounter < maxDepth) { + while (depthCounter < maxDepth && currentValue.isObject()) { final ObjectNode obj = (ObjectNode) currentValue; final Iterator> it = obj.properties().iterator(); diff --git a/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java b/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java index 450c5f6..2baec22 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java @@ -1,5 +1,6 @@ package dev.toonformat.jtoon.encoder; +import java.util.Collection; import java.util.List; import static dev.toonformat.jtoon.util.Constants.COLON; import static dev.toonformat.jtoon.util.Constants.OPEN_BRACKET; @@ -41,7 +42,7 @@ public record HeaderConfig( * @param config Header configuration * @return Formatted header string */ - public static String format(final HeaderConfig config) { + static String format(final HeaderConfig config) { final StringBuilder header = new StringBuilder(); appendKeyIfPresent(header, config.key()); @@ -102,7 +103,7 @@ private static void appendDelimiterIfNotDefault(final StringBuilder header, fina private static void appendFieldsIfPresent( final StringBuilder header, - final List fields, + final Collection fields, final String delimiter) { if (fields == null || fields.isEmpty()) { return; @@ -113,7 +114,7 @@ private static void appendFieldsIfPresent( header.append(CLOSE_BRACE); } - private static String formatFields(final List fields, final String delimiter) { + private static String formatFields(final Collection fields, final String delimiter) { return fields.stream() .map(PrimitiveEncoder::encodeKey) .reduce((a, b) -> a + delimiter + b) diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java index ce8af8f..b2a3417 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java @@ -99,11 +99,11 @@ public static void encodeKeyValuePair(final String key, EncodeOptions currentOptions = options; // Attempt key folding when enabled - if (KeyFolding.SAFE.equals(currentOptions.flatten()) + if (remainingDepth > 0 && !siblings.isEmpty() - && remainingDepth > 0 && blockedKeys != null - && !blockedKeys.contains(key)) { + && !blockedKeys.contains(key) + && KeyFolding.SAFE.equals(currentOptions.flatten())) { final Flatten.FoldResult foldResult = Flatten.tryFoldKeyChain(key, value, siblings, rootLiteralKeys, pathPrefix, remainingDepth); if (foldResult != null) { diff --git a/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java index bf322d2..e3f50cf 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/PrimitiveEncoder.java @@ -4,6 +4,7 @@ import dev.toonformat.jtoon.util.StringValidator; import tools.jackson.databind.JsonNode; import java.math.BigDecimal; +import java.util.Collection; import java.util.List; import static dev.toonformat.jtoon.util.Constants.NULL_LITERAL; import static dev.toonformat.jtoon.util.Constants.DOUBLE_QUOTE; @@ -85,7 +86,7 @@ private static String stripTrailingZeros(final String value) { * @param delimiter the delimiter to use (for validation) * @return the encoded string, quoted if necessary */ - public static String encodeStringLiteral(final String value, final String delimiter) { + static String encodeStringLiteral(final String value, final String delimiter) { if (StringValidator.isSafeUnquoted(value, delimiter)) { return value; } @@ -115,7 +116,7 @@ public static String encodeKey(final String key) { * @param delimiter the delimiter to use between values * @return the joined string of encoded values */ - public static String joinEncodedValues(final List values, final String delimiter) { + public static String joinEncodedValues(final Collection values, final String delimiter) { if (values == null || values.isEmpty()) { return ""; } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java index ed07bfc..e4ce900 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java @@ -52,7 +52,12 @@ public static List detectTabularHeader(final ArrayNode rows) { /** * Checks if all rows in the array have the same keys with primitive values. */ - private static boolean isTabularArray(final ArrayNode rows, final List header) { + private static boolean isTabularArray(final Iterable rows, final Iterable header) { + final List headerList = new ArrayList<>(); + for (String h : header) { + headerList.add(h); + } + for (JsonNode row : rows) { if (!row.isObject()) { return false; @@ -62,12 +67,12 @@ private static boolean isTabularArray(final ArrayNode rows, final List h final List keys = new ArrayList<>(obj.propertyNames()); // All objects must have the same keys (but order can differ) - if (keys.size() != header.size()) { + if (keys.size() != headerList.size()) { return false; } // Check that all header keys exist in the row and all values are primitives - for (String key : header) { + for (String key : headerList) { if (!obj.has(key)) { return false; } @@ -90,8 +95,9 @@ private static boolean isTabularArray(final ArrayNode rows, final List h * @param depth Indentation depth * @param options Encoding options */ - public static void encodeArrayOfObjectsAsTabular(final String prefix, final ArrayNode rows, final List header, - final LineWriter writer, final int depth, final EncodeOptions options) { + public static void encodeArrayOfObjectsAsTabular(final String prefix, final ArrayNode rows, + final List header, final LineWriter writer, final int depth, + final EncodeOptions options) { final String headerStr = PrimitiveEncoder.formatHeader(rows.size(), prefix, header, options.delimiter().toString(), options.lengthMarker()); writer.push(depth, headerStr); @@ -109,16 +115,22 @@ public static void encodeArrayOfObjectsAsTabular(final String prefix, final Arra * @param depth Indentation depth * @param options Encoding options */ - public static void writeTabularRows(final ArrayNode rows, final List header, final LineWriter writer, final int depth, - final EncodeOptions options) { + public static void writeTabularRows(final Iterable rows, final Iterable header, + final LineWriter writer, final int depth, final EncodeOptions options) { + final List headerList = new ArrayList<>(); + for (String h : header) { + headerList.add(h); + } + final int headerSize = headerList.size(); + for (JsonNode row : rows) { //skip non-object rows if (!row.isObject()) { continue; } final ObjectNode obj = (ObjectNode) row; - final List values = new ArrayList<>(); - for (String key : header) { + final List values = new ArrayList<>(headerSize); + for (String key : headerList) { values.add(obj.get(key)); } final String joinedValue = PrimitiveEncoder.joinEncodedValues(values, options.delimiter().toString()); diff --git a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java index ddc68e6..3de03c6 100644 --- a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java +++ b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java @@ -111,7 +111,7 @@ private static JsonNode normalizeWithStrategy(final Object value) { .map(normalizer -> normalizer.apply(value)) .filter(Objects::nonNull) .findFirst() - .orElse(NullNode.getInstance()); + .orElseGet(NullNode::getInstance); } /** @@ -151,7 +151,7 @@ private static JsonNode normalizeDouble(final Double value) { return IntNode.valueOf(0); } return tryConvertToLong(value) - .orElse(DoubleNode.valueOf(value)); + .orElseGet(() -> DoubleNode.valueOf(value)); } /** diff --git a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java index 42d0581..65d6b3b 100644 --- a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java +++ b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java @@ -1,7 +1,13 @@ package dev.toonformat.jtoon.util; import java.util.regex.Pattern; -import static dev.toonformat.jtoon.util.Constants.*; +import static dev.toonformat.jtoon.util.Constants.BACKSLASH; +import static dev.toonformat.jtoon.util.Constants.COLON; +import static dev.toonformat.jtoon.util.Constants.DOUBLE_QUOTE; +import static dev.toonformat.jtoon.util.Constants.FALSE_LITERAL; +import static dev.toonformat.jtoon.util.Constants.LIST_ITEM_MARKER; +import static dev.toonformat.jtoon.util.Constants.NULL_LITERAL; +import static dev.toonformat.jtoon.util.Constants.TRUE_LITERAL; /** * Validates strings for safe unquoted usage in TOON format. From 5ffca6fe46a6693e3922cd7fbfbf800e90161ad4 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Fri, 13 Feb 2026 19:57:10 +0100 Subject: [PATCH 20/29] cleanup run with spotbugs part 2 --- .../jtoon/decoder/ArrayDecoder.java | 3 +- .../jtoon/decoder/ListItemDecoder.java | 5 ++-- .../jtoon/decoder/ObjectDecoder.java | 5 ++-- .../jtoon/decoder/TabularArrayDecoder.java | 30 ++++++++++--------- .../jtoon/encoder/ArrayEncoder.java | 3 +- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java index 10b12a1..4c4accf 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ArrayDecoder.java @@ -300,7 +300,8 @@ private static boolean handleBlankLineInListArray(final int depth, final DecodeC * @param context decode an object to deal with lines, delimiter and options * @return true if an array should terminate, false otherwise. */ - private static boolean shouldTerminateListArray(final int lineDepth, final int depth, final String line, final DecodeContext context) { + private static boolean shouldTerminateListArray(final int lineDepth, final int depth, + final String line, final DecodeContext context) { if (lineDepth < depth + 1) { return true; // Line depth is less than expected - terminate } diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java index 2405876..42b3806 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java @@ -141,9 +141,10 @@ static Object parseListItem(final String content, final int depth, final DecodeC * * @param item the item to parse * @param depth the depth of the item - * @param context decode an object to deal with lines, delimiter and options * + * @param context decode an object to deal with lines, delimiter and options */ - private static void parseListItemFields(final Map item, final int depth, final DecodeContext context) { + private static void parseListItemFields(final Map item, + final int depth, final DecodeContext context) { while (context.currentLine < context.lines.length) { final String line = context.lines[context.currentLine]; final int lineDepth = DecodeHelper.getDepth(line, context); diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java index 5190428..6ff16b2 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java @@ -118,8 +118,9 @@ static void parseRootObjectFields(final Map obj, final int depth * @param depth the depth of the object field * @param context decode an object to deal with lines, delimiter and options */ - private static void processRootKeyedArrayLine(final Map objectMap, final String content, final String originalKey, - final int depth, final DecodeContext context) { + private static void processRootKeyedArrayLine(final Map objectMap, + final String content, final String originalKey, final int depth, + final DecodeContext context) { final String originalKeyTrimmed = originalKey.trim(); final String key = StringEscaper.unescape(originalKey); final String arrayHeader = content.substring(originalKey.length()); diff --git a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java index ee36873..c75a321 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/TabularArrayDecoder.java @@ -3,7 +3,6 @@ import dev.toonformat.jtoon.Delimiter; import dev.toonformat.jtoon.util.StringEscaper; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -74,7 +73,8 @@ public static List parseTabularArray(final String header, final int dept * @param context decode an object to deal with lines, delimiter and options * @return list of keys */ - private static List parseTabularKeys(final String keysStr, final Delimiter arrayDelimiter, final DecodeContext context) { + private static List parseTabularKeys(final String keysStr, final Delimiter arrayDelimiter, + final DecodeContext context) { // Validate delimiter mismatch between bracket and brace fields if (context.options.strict()) { validateKeysDelimiter(keysStr, arrayDelimiter); @@ -121,15 +121,15 @@ private static void validateKeysDelimiter(final String keysStr, final Delimiter */ private static void checkDelimiterMismatch(final char expectedChar, final char actualChar) { if (expectedChar == Delimiter.TAB.getValue() && actualChar == Delimiter.COMMA.getValue()) { - throw new IllegalArgumentException( - "Delimiter mismatch: bracket declares tab (expected='" + expectedChar + "', actual='" + actualChar + "')"); + throw new IllegalArgumentException("Delimiter mismatch: bracket declares tab (expected='" + + expectedChar + "', actual='" + actualChar + "')"); } if (expectedChar == Delimiter.PIPE.getValue() && actualChar == Delimiter.COMMA.getValue()) { - throw new IllegalArgumentException( - "Delimiter mismatch: bracket declares pipe (expected='" + expectedChar + "', actual='" + actualChar + "')"); + throw new IllegalArgumentException("Delimiter mismatch: bracket declares pipe (expected='" + + expectedChar + "', actual='" + actualChar + "')"); } - if (expectedChar == Delimiter.COMMA.getValue() && - (actualChar == Delimiter.TAB.getValue() || actualChar == Delimiter.PIPE.getValue())) { + if (expectedChar == Delimiter.COMMA.getValue() + && (actualChar == Delimiter.TAB.getValue() || actualChar == Delimiter.PIPE.getValue())) { throw new IllegalArgumentException( "Delimiter mismatch: bracket declares comma, brace fields use different delimiter"); } @@ -145,8 +145,9 @@ private static void checkDelimiterMismatch(final char expectedChar, final char a * @param context decode an object to deal with lines, delimiter and options * @return true if parsing should continue, false if an array should terminate */ - private static boolean processTabularArrayLine(final int expectedRowDepth, final List keys, final Delimiter arrayDelimiter, - final List result, final DecodeContext context) { + private static boolean processTabularArrayLine(final int expectedRowDepth, final List keys, + final Delimiter arrayDelimiter, final List result, + final DecodeContext context) { final String line = context.lines[context.currentLine]; if (DecodeHelper.isBlankLine(line)) { @@ -202,8 +203,8 @@ private static boolean handleBlankLineInTabularArray(final int expectedRowDepth, * @param context decode an object to deal with lines, delimiter and options * @return true if an array should terminate, false otherwise. */ - private static boolean shouldTerminateTabularArray(final String line, final int lineDepth, final int expectedRowDepth, - final DecodeContext context) { + private static boolean shouldTerminateTabularArray(final String line, final int lineDepth, + final int expectedRowDepth, final DecodeContext context) { // Header depth is one level above the expected row depth final int headerDepth = expectedRowDepth - 1; @@ -240,8 +241,9 @@ private static boolean shouldTerminateTabularArray(final String line, final int * @param context decode an object to deal with lines, delimiter and options * @return true if a line was processed and the currentLine should be incremented, false otherwise. */ - private static boolean processTabularRow(final String line, final int lineDepth, final int expectedRowDepth, final List keys, - final Delimiter arrayDelimiter, final List result, final DecodeContext context) { + private static boolean processTabularRow(final String line, final int lineDepth, + final int expectedRowDepth, final List keys, final Delimiter arrayDelimiter, + final List result, final DecodeContext context) { if (lineDepth == expectedRowDepth) { final String rowContent = line.substring(expectedRowDepth * context.options.indent()); final Map row = parseTabularRow(rowContent, keys, arrayDelimiter, context); diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java index fa1972c..d562d46 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java @@ -30,7 +30,8 @@ private ArrayEncoder() { * @param depth Indentation depth * @param options Encoding options */ - public static void encodeArray(final String key, final ArrayNode value, final LineWriter writer, final int depth, final EncodeOptions options) { + public static void encodeArray(final String key, final ArrayNode value, + final LineWriter writer, final int depth, final EncodeOptions options) { if (value.isEmpty()) { final String header = PrimitiveEncoder.formatHeader(0, key, null, options.delimiter().toString(), options.lengthMarker()); From be6e71f893d481304484dfb6c4c3bc477b07becb Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Fri, 13 Feb 2026 20:02:09 +0100 Subject: [PATCH 21/29] adding exclude rule for pmd-test --- pmd-rules-test.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pmd-rules-test.xml b/pmd-rules-test.xml index 1f138b2..2811908 100644 --- a/pmd-rules-test.xml +++ b/pmd-rules-test.xml @@ -17,6 +17,7 @@ + From c6087bbb4ecd3397c614fdca387d013e3522c0f5 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Fri, 13 Feb 2026 20:11:44 +0100 Subject: [PATCH 22/29] no warnings for checkstyle --- .../dev/toonformat/jtoon/encoder/Flatten.java | 4 +- .../jtoon/encoder/ListItemEncoder.java | 54 ++++++++++++++----- .../jtoon/encoder/ObjectEncoder.java | 30 ++++++++--- 3 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java index faab4f0..3126c0c 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java @@ -136,7 +136,9 @@ public static FoldResult tryFoldKeyChain(final String key, * @param maxDepth maximum number of allowed segments * @return a {@link ChainResult} containing segments, tail, and leafValue */ - private static ChainResult collectSingleKeyChain(final String startKey, final JsonNode startValue, final int maxDepth) { + private static ChainResult collectSingleKeyChain(final String startKey, + final JsonNode startValue, + final int maxDepth) { // normalize absolute key to its local segment final String localStartKey = startKey.contains(DOT) ? startKey.substring(startKey.lastIndexOf(DOT.charAt(0)) + 1) diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ListItemEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ListItemEncoder.java index bba13ec..9a1bf75 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ListItemEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ListItemEncoder.java @@ -35,7 +35,10 @@ private ListItemEncoder() { * @param depth Indentation depth * @param options Encoding options */ - public static void encodeObjectAsListItem(final ObjectNode obj, final LineWriter writer, final int depth, final EncodeOptions options) { + public static void encodeObjectAsListItem(final ObjectNode obj, + final LineWriter writer, + final int depth, + final EncodeOptions options) { final List keys = new ArrayList<>(obj.propertyNames()); if (keys.isEmpty()) { @@ -60,8 +63,11 @@ public static void encodeObjectAsListItem(final ObjectNode obj, final LineWriter * Encodes the first key-value pair of a list item. * Handles special formatting for arrays and objects. */ - private static void encodeFirstKeyValue(final String key, final JsonNode value, final LineWriter writer, final int depth, - final EncodeOptions options) { + private static void encodeFirstKeyValue(final String key, + final JsonNode value, + final LineWriter writer, + final int depth, + final EncodeOptions options) { final String encodedKey = PrimitiveEncoder.encodeKey(key); if (value.isValueNode()) { @@ -73,14 +79,21 @@ private static void encodeFirstKeyValue(final String key, final JsonNode value, } } - private static void encodeFirstValueAsPrimitive(final String encodedKey, final JsonNode value, final LineWriter writer, final int depth, - final EncodeOptions options) { + private static void encodeFirstValueAsPrimitive(final String encodedKey, + final JsonNode value, + final LineWriter writer, + final int depth, + final EncodeOptions options) { writer.push(depth, LIST_ITEM_PREFIX + encodedKey + COLON + SPACE + PrimitiveEncoder.encodePrimitive(value, options.delimiter().toString())); } - private static void encodeFirstValueAsArray(final String key, final String encodedKey, final ArrayNode arrayValue, final LineWriter writer, - final int depth, final EncodeOptions options) { + private static void encodeFirstValueAsArray(final String key, + final String encodedKey, + final ArrayNode arrayValue, + final LineWriter writer, + final int depth, + final EncodeOptions options) { if (ArrayEncoder.isArrayOfPrimitives(arrayValue)) { encodeFirstArrayAsPrimitives(key, arrayValue, writer, depth, options); } else if (ArrayEncoder.isArrayOfObjects(arrayValue)) { @@ -90,15 +103,22 @@ private static void encodeFirstValueAsArray(final String key, final String encod } } - private static void encodeFirstArrayAsPrimitives(final String key, final ArrayNode arrayValue, final LineWriter writer, final int depth, + private static void encodeFirstArrayAsPrimitives(final String key, + final ArrayNode arrayValue, + final LineWriter writer, + final int depth, final EncodeOptions options) { final String formatted = ArrayEncoder.formatInlineArray(arrayValue, options.delimiter().toString(), key, options.lengthMarker()); writer.push(depth, LIST_ITEM_PREFIX + formatted); } - private static void encodeFirstArrayAsObjects(final String key, final String encodedKey, final ArrayNode arrayValue, - final LineWriter writer, final int depth, final EncodeOptions options) { + private static void encodeFirstArrayAsObjects(final String key, + final String encodedKey, + final ArrayNode arrayValue, + final LineWriter writer, + final int depth, + final EncodeOptions options) { final List header = TabularArrayEncoder.detectTabularHeader(arrayValue); if (!header.isEmpty()) { final String headerStr = PrimitiveEncoder.formatHeader(arrayValue.size(), key, header, @@ -118,8 +138,11 @@ private static void encodeFirstArrayAsObjects(final String key, final String enc } } - private static void encodeFirstArrayAsComplex(final String encodedKey, final ArrayNode arrayValue, final LineWriter writer, - final int depth, final EncodeOptions options) { + private static void encodeFirstArrayAsComplex(final String encodedKey, + final ArrayNode arrayValue, + final LineWriter writer, + final int depth, + final EncodeOptions options) { writer.push(depth, LIST_ITEM_PREFIX + encodedKey + OPEN_BRACKET + arrayValue.size() + CLOSE_BRACKET + COLON); for (JsonNode item : arrayValue) { @@ -136,8 +159,11 @@ private static void encodeFirstArrayAsComplex(final String encodedKey, final Arr } } - private static void encodeFirstValueAsObject(final String encodedKey, final ObjectNode nestedObj, final LineWriter writer, final int depth, - final EncodeOptions options) { + private static void encodeFirstValueAsObject(final String encodedKey, + final ObjectNode nestedObj, + final LineWriter writer, + final int depth, + final EncodeOptions options) { writer.push(depth, LIST_ITEM_PREFIX + encodedKey + COLON); if (!nestedObj.isEmpty()) { ObjectEncoder.encodeObject(nestedObj, writer, depth + 2, options, Set.of(), null, null, new HashSet<>()); diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java index b2a3417..8b9185a 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java @@ -36,9 +36,14 @@ private ObjectEncoder() { * @param remainingDepth optional override for the remaining depth * @param blockedKeys contains only keys that have undergone a successful flattening */ - public static void encodeObject(final ObjectNode value, final LineWriter writer, final int depth, final EncodeOptions options, - final Set rootLiteralKeys, final String pathPrefix, final Integer remainingDepth, - final Set blockedKeys) { + public static void encodeObject(final ObjectNode value, + final LineWriter writer, + final int depth, + final EncodeOptions options, + final Set rootLiteralKeys, + final String pathPrefix, + final Integer remainingDepth, + final Set blockedKeys) { final List> fields = value.properties().stream().toList(); // At root level (depth 0), collect all literal dotted keys for collision checking @@ -146,9 +151,15 @@ public static void encodeKeyValuePair(final String key, * @param remainingDepth the depth that remind to the limit * @return EncodeOptions changes for Case 2 */ - private static EncodeOptions flatten(final String key, final Flatten.FoldResult foldResult, final LineWriter writer, final int depth, - final EncodeOptions options, final Set rootLiteralKeys, final String pathPrefix, - final Set blockedKeys, final int remainingDepth) { + private static EncodeOptions flatten(final String key, + final Flatten.FoldResult foldResult, + final LineWriter writer, + final int depth, + final EncodeOptions options, + final Set rootLiteralKeys, + final String pathPrefix, + final Set blockedKeys, + final int remainingDepth) { final String foldedKey = foldResult.foldedKey(); EncodeOptions currentOptions = options; @@ -189,8 +200,11 @@ private static EncodeOptions flatten(final String key, final Flatten.FoldResult return currentOptions; } - private static void handleFullyFoldedLeaf(final Flatten.FoldResult foldResult, final LineWriter writer, final int depth, - final EncodeOptions options, final String encodedFoldedKey) { + private static void handleFullyFoldedLeaf(final Flatten.FoldResult foldResult, + final LineWriter writer, + final int depth, + final EncodeOptions options, + final String encodedFoldedKey) { final JsonNode leaf = foldResult.leafValue(); // Primitive From da4ff14bc79608ce61e05557a97d6076b2b222f5 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Sun, 15 Feb 2026 10:00:51 +0100 Subject: [PATCH 23/29] performents testing --- build.gradle | 33 +++- src/main/java/dev/toonformat/jtoon/JToon.java | 4 +- .../jtoon/encoder/ArrayEncoder.java | 22 ++- .../jtoon/encoder/ObjectEncoder.java | 31 +-- .../jtoon/encoder/TabularArrayEncoder.java | 58 +++--- .../jtoon/normalizer/JsonNormalizer.java | 97 +++++---- .../jtoon/util/IndentFormatter.java | 66 +++++++ .../jtoon/util/NumberFormatter.java | 130 +++++++++++++ .../jtoon/util/ObjectMapperSingleton.java | 5 +- .../toonformat/jtoon/util/StringEscaper.java | 33 ++++ .../jtoon/util/StringValidator.java | 89 +++++++++ .../jtoon/EncodingOptimizationBenchmark.java | 184 ++++++++++++++++++ .../jtoon/conformance/ConformanceTest.java | 6 +- .../toonformat/jtoon/encoder/FlattenTest.java | 159 +++++++++++++++ .../jtoon/encoder/ValueEncoderTest.java | 9 +- .../jtoon/util/IndentFormatterTest.java | 109 +++++++++++ .../jtoon/util/NumberFormatterTest.java | 110 +++++++++++ .../jtoon/util/StringEscaperEnhancedTest.java | 123 ++++++++++++ .../util/StringValidatorEnhancedTest.java | 156 +++++++++++++++ 19 files changed, 1316 insertions(+), 108 deletions(-) create mode 100644 src/main/java/dev/toonformat/jtoon/util/IndentFormatter.java create mode 100644 src/main/java/dev/toonformat/jtoon/util/NumberFormatter.java create mode 100644 src/test/java/dev/toonformat/jtoon/EncodingOptimizationBenchmark.java create mode 100644 src/test/java/dev/toonformat/jtoon/util/IndentFormatterTest.java create mode 100644 src/test/java/dev/toonformat/jtoon/util/NumberFormatterTest.java create mode 100644 src/test/java/dev/toonformat/jtoon/util/StringEscaperEnhancedTest.java create mode 100644 src/test/java/dev/toonformat/jtoon/util/StringValidatorEnhancedTest.java diff --git a/build.gradle b/build.gradle index ed82cd5..61ede39 100644 --- a/build.gradle +++ b/build.gradle @@ -51,8 +51,8 @@ pitest { spotbugs { toolVersion = '4.9.8' excludeFilter = file('spotbugs-exclude.xml') - effort = "max" - reportLevel = "low" + effort = 'MAX' + reportLevel = 'LOW' reportsDir = layout.buildDirectory.dir('spotbugs') } @@ -128,7 +128,7 @@ tasks.checkstyleTest { dependencies { implementation 'tools.jackson.core:jackson-databind:3.0.4' - implementation 'tools.jackson.module:jackson-module-afterburner:3.0.4' + implementation 'tools.jackson.module:jackson-module-blackbird:3.0.4' compileOnly 'com.github.spotbugs:spotbugs-annotations:4.9.8' testImplementation platform('org.junit:junit-bom:6.0.2') testImplementation 'org.junit.jupiter:junit-jupiter' @@ -140,12 +140,26 @@ dependencies { testAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' } +// Check if running in CI or coverage is explicitly requested +boolean isCi = System.getenv('CI') == 'true' +boolean withCoverage = project.hasProperty('withCoverage') || isCi + test { useJUnitPlatform() - finalizedBy jacocoTestReport // report is always generated after tests run + // Only run jacoco coverage in CI or when explicitly requested + if (withCoverage) { + finalizedBy jacocoTestReport + jacoco { + enabled = true + } + } else { + // Completely disable jacoco for local development + jacoco { + enabled = false + } + } } - jacocoTestReport { dependsOn test reports { @@ -156,6 +170,8 @@ jacocoTestReport { } jacocoTestCoverageVerification { + // Only enforce coverage thresholds in CI + onlyIf { isCi } violationRules { rule { enabled = true @@ -174,6 +190,11 @@ jacocoTestCoverageVerification { } } } + +// Only add coverage verification to check task in CI +if (isCi) { + check.dependsOn jacocoTestCoverageVerification +} check.dependsOn jacocoTestReport check.dependsOn spotbugsMain check.dependsOn pmdMain @@ -203,7 +224,7 @@ tasks.register('jmh', JavaExec) { group = 'verification' description = 'Run JMH benchmarks' classpath = configurations.testRuntimeClasspath + sourceSets.test.runtimeClasspath - mainClass = 'dev.toonformat.jtoon.JToonBenchmark' + mainClass = findProperty('benchmarkClass') ?: 'dev.toonformat.jtoon.JToonBenchmark' workingDir = projectDir doFirst { file('build/jmh-results').mkdirs() diff --git a/src/main/java/dev/toonformat/jtoon/JToon.java b/src/main/java/dev/toonformat/jtoon/JToon.java index 250f730..cd46f97 100644 --- a/src/main/java/dev/toonformat/jtoon/JToon.java +++ b/src/main/java/dev/toonformat/jtoon/JToon.java @@ -68,9 +68,7 @@ public static String encodeJson(final String json) { * Encodes a plain JSON string to TOON format using custom options. * *

- * Parsing is delegated to - * {@link JsonNormalizer#parse(String)} - * to maintain separation of concerns. + * Parses the JSON string to a tree structure, then encodes to TOON format. *

* * @param json The JSON string to encode (must be valid JSON) diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java index d562d46..5fe1845 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java @@ -4,7 +4,7 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; -import java.util.ArrayList; + import java.util.List; import java.util.stream.StreamSupport; import static dev.toonformat.jtoon.util.Constants.LIST_ITEM_PREFIX; @@ -147,17 +147,25 @@ private static void encodeInlinePrimitiveArray(final String prefix, final ArrayN */ public static String formatInlineArray(final ArrayNode values, final String delimiter, final String prefix, final boolean lengthMarker) { - final List valueList = new ArrayList<>(); - values.forEach(valueList::add); - final String header = PrimitiveEncoder.formatHeader(values.size(), prefix, null, delimiter, lengthMarker); - final String joinedValue = PrimitiveEncoder.joinEncodedValues(valueList, delimiter); - // Only add space if there are values + // Early return for empty arrays if (values.isEmpty()) { return header; } - return header + SPACE + joinedValue; + + // Build joined values directly without intermediate collection + final StringBuilder joinedValues = new StringBuilder(128); + boolean first = true; + for (final JsonNode value : values) { + if (!first) { + joinedValues.append(delimiter); + } + first = false; + joinedValues.append(PrimitiveEncoder.encodePrimitive(value, delimiter)); + } + + return header + SPACE + joinedValues.toString(); } /** diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java index 8b9185a..9b87dbc 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java @@ -6,10 +6,8 @@ import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import static dev.toonformat.jtoon.util.Constants.DOT; import static dev.toonformat.jtoon.util.Constants.COLON; import static dev.toonformat.jtoon.util.Constants.SPACE; @@ -44,24 +42,27 @@ public static void encodeObject(final ObjectNode value, final String pathPrefix, final Integer remainingDepth, final Set blockedKeys) { - final List> fields = value.properties().stream().toList(); + final int effectiveFlattenDepth = remainingDepth != null ? remainingDepth : options.flattenDepth(); - // At root level (depth 0), collect all literal dotted keys for collision checking + // Single-pass collection: gather sibling keys and optionally dotted keys at root level + final Set siblings = new LinkedHashSet<>(); if (depth == 0 && rootLiteralKeys != null) { rootLiteralKeys.clear(); - fields.stream() - .filter(e -> e.getKey().contains(DOT)) - .map(Map.Entry::getKey) - .forEach(rootLiteralKeys::add); + for (final Map.Entry entry : value.properties()) { + final String key = entry.getKey(); + siblings.add(key); + if (key.contains(DOT)) { + rootLiteralKeys.add(key); + } + } + } else { + for (final Map.Entry entry : value.properties()) { + siblings.add(entry.getKey()); + } } - final int effectiveFlattenDepth = remainingDepth != null ? remainingDepth : options.flattenDepth(); - - //the siblings collision do not need the absolute path - final Set siblings = fields.stream() - .map(Map.Entry::getKey) - .collect(Collectors.toCollection(LinkedHashSet::new)); - for (Map.Entry entry : fields) { + // Encode each field + for (final Map.Entry entry : value.properties()) { encodeKeyValuePair(entry.getKey(), entry.getValue(), writer, depth, options, siblings, rootLiteralKeys, pathPrefix, effectiveFlattenDepth, blockedKeys); } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java index e4ce900..efae136 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/TabularArrayEncoder.java @@ -52,11 +52,8 @@ public static List detectTabularHeader(final ArrayNode rows) { /** * Checks if all rows in the array have the same keys with primitive values. */ - private static boolean isTabularArray(final Iterable rows, final Iterable header) { - final List headerList = new ArrayList<>(); - for (String h : header) { - headerList.add(h); - } + private static boolean isTabularArray(final Iterable rows, final List header) { + final int headerSize = header.size(); for (JsonNode row : rows) { if (!row.isObject()) { @@ -64,19 +61,16 @@ private static boolean isTabularArray(final Iterable rows, final Itera } final ObjectNode obj = (ObjectNode) row; - final List keys = new ArrayList<>(obj.propertyNames()); - // All objects must have the same keys (but order can differ) - if (keys.size() != headerList.size()) { + // All objects must have the same number of keys + if (obj.size() != headerSize) { return false; } // Check that all header keys exist in the row and all values are primitives - for (String key : headerList) { - if (!obj.has(key)) { - return false; - } - if (!obj.get(key).isValueNode()) { + for (final String key : header) { + final JsonNode value = obj.get(key); + if (value == null || !value.isValueNode()) { return false; } } @@ -115,27 +109,39 @@ public static void encodeArrayOfObjectsAsTabular(final String prefix, final Arra * @param depth Indentation depth * @param options Encoding options */ - public static void writeTabularRows(final Iterable rows, final Iterable header, + public static void writeTabularRows(final Iterable rows, final List header, final LineWriter writer, final int depth, final EncodeOptions options) { - final List headerList = new ArrayList<>(); - for (String h : header) { - headerList.add(h); - } - final int headerSize = headerList.size(); - for (JsonNode row : rows) { - //skip non-object rows + // Skip non-object rows if (!row.isObject()) { continue; } final ObjectNode obj = (ObjectNode) row; - final List values = new ArrayList<>(headerSize); - for (String key : headerList) { - values.add(obj.get(key)); - } - final String joinedValue = PrimitiveEncoder.joinEncodedValues(values, options.delimiter().toString()); + final String joinedValue = joinRowValues(obj, header, options.delimiter().toString()); writer.push(depth, joinedValue); } } + + /** + * Joins values from a single row according to header order. + * Avoids creating intermediate collections. + * Missing keys are skipped. + */ + private static String joinRowValues(final ObjectNode row, final List header, final String delimiter) { + final StringBuilder sb = new StringBuilder(128); + boolean first = true; + for (final String key : header) { + final JsonNode value = row.get(key); + if (value == null) { + continue; // Skip missing keys + } + if (!first) { + sb.append(delimiter); + } + first = false; + sb.append(PrimitiveEncoder.encodePrimitive(value, delimiter)); + } + return sb.toString(); + } } diff --git a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java index 3de03c6..fde0f90 100644 --- a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java +++ b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java @@ -32,7 +32,6 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Function; -import java.util.function.IntFunction; import java.util.stream.Stream; /** @@ -260,7 +259,9 @@ private static JsonNode tryNormalizeCollection(final Object value) { */ private static ArrayNode normalizeCollection(final Collection collection) { final ArrayNode arrayNode = MAPPER.createArrayNode(); - collection.forEach(item -> arrayNode.add(normalize(item))); + for (Object item : collection) { + arrayNode.add(normalize(item)); + } return arrayNode; } @@ -269,7 +270,9 @@ private static ArrayNode normalizeCollection(final Collection collection) { */ private static ObjectNode normalizeMap(final Map map) { final ObjectNode objectNode = MAPPER.createObjectNode(); - map.forEach((key, value) -> objectNode.set(String.valueOf(key), normalize(value))); + for (Map.Entry entry : map.entrySet()) { + objectNode.set(String.valueOf(entry.getKey()), normalize(entry.getValue())); + } return objectNode; } @@ -286,58 +289,68 @@ private static JsonNode tryNormalizePojo(final Object value) { } /** - * Normalizes arrays to ArrayNode. + * Normalizes primitive arrays to ArrayNode without auto-boxing overhead. + * Uses direct array population to avoid IntFunction lambda allocations. */ private static JsonNode normalizeArray(final Object array) { if (array instanceof int[] intArr) { - return buildArrayNode(intArr.length, i -> IntNode.valueOf(intArr[i])); + final ArrayNode node = MAPPER.createArrayNode(); + for (int i = 0; i < intArr.length; i++) { + node.add(IntNode.valueOf(intArr[i])); + } + return node; } else if (array instanceof long[] longArr) { - return buildArrayNode(longArr.length, i -> LongNode.valueOf(longArr[i])); + final ArrayNode node = MAPPER.createArrayNode(); + for (int i = 0; i < longArr.length; i++) { + node.add(LongNode.valueOf(longArr[i])); + } + return node; } else if (array instanceof double[] doubleArr) { - return buildArrayNode(doubleArr.length, i -> normalizeDoubleElement(doubleArr[i])); + final ArrayNode node = MAPPER.createArrayNode(); + for (int i = 0; i < doubleArr.length; i++) { + final double val = doubleArr[i]; + node.add(Double.isFinite(val) ? DoubleNode.valueOf(val) : NullNode.getInstance()); + } + return node; } else if (array instanceof float[] floatArr) { - return buildArrayNode(floatArr.length, i -> normalizeFloatElement(floatArr[i])); + final ArrayNode node = MAPPER.createArrayNode(); + for (int i = 0; i < floatArr.length; i++) { + final float val = floatArr[i]; + node.add(Float.isFinite(val) ? FloatNode.valueOf(val) : NullNode.getInstance()); + } + return node; } else if (array instanceof boolean[] boolArr) { - return buildArrayNode(boolArr.length, i -> BooleanNode.valueOf(boolArr[i])); + final ArrayNode node = MAPPER.createArrayNode(); + for (int i = 0; i < boolArr.length; i++) { + node.add(BooleanNode.valueOf(boolArr[i])); + } + return node; } else if (array instanceof byte[] byteArr) { - return buildArrayNode(byteArr.length, i -> IntNode.valueOf(byteArr[i])); + final ArrayNode node = MAPPER.createArrayNode(); + for (int i = 0; i < byteArr.length; i++) { + node.add(IntNode.valueOf(byteArr[i])); + } + return node; } else if (array instanceof short[] shortArr) { - return buildArrayNode(shortArr.length, i -> ShortNode.valueOf(shortArr[i])); + final ArrayNode node = MAPPER.createArrayNode(); + for (int i = 0; i < shortArr.length; i++) { + node.add(ShortNode.valueOf(shortArr[i])); + } + return node; } else if (array instanceof char[] charArr) { - return buildArrayNode(charArr.length, i -> StringNode.valueOf(String.valueOf(charArr[i]))); + final ArrayNode node = MAPPER.createArrayNode(); + for (int i = 0; i < charArr.length; i++) { + node.add(StringNode.valueOf(String.valueOf(charArr[i]))); + } + return node; } else if (array instanceof Object[] objArr) { - return buildArrayNode(objArr.length, i -> normalize(objArr[i])); + final ArrayNode node = MAPPER.createArrayNode(); + for (int i = 0; i < objArr.length; i++) { + node.add(normalize(objArr[i])); + } + return node; } else { return MAPPER.createArrayNode(); } } - - /** - * Builds an ArrayNode using a functional approach. - */ - private static ArrayNode buildArrayNode(final int length, final IntFunction mapper) { - final ArrayNode arrayNode = MAPPER.createArrayNode(); - for (int i = 0; i < length; i++) { - arrayNode.add(mapper.apply(i)); - } - return arrayNode; - } - - /** - * Normalizes a single double element from an array. - */ - private static JsonNode normalizeDoubleElement(final double value) { - return Double.isFinite(value) - ? DoubleNode.valueOf(value) - : NullNode.getInstance(); - } - - /** - * Normalizes a single float element from an array. - */ - private static JsonNode normalizeFloatElement(final float value) { - return Float.isFinite(value) - ? FloatNode.valueOf(value) - : NullNode.getInstance(); - } } diff --git a/src/main/java/dev/toonformat/jtoon/util/IndentFormatter.java b/src/main/java/dev/toonformat/jtoon/util/IndentFormatter.java new file mode 100644 index 0000000..4bd9380 --- /dev/null +++ b/src/main/java/dev/toonformat/jtoon/util/IndentFormatter.java @@ -0,0 +1,66 @@ +package dev.toonformat.jtoon.util; + +/** + * Handles indentation formatting for TOON output. + * Provides consistent indentation across all encoders. + */ +public final class IndentFormatter { + + /** + * Cache of common indent strings to avoid repeated string creation. + */ + private static final int CACHE_SIZE = 20; + private static final String[] INDENT_CACHE = new String[CACHE_SIZE]; + + static { + // Pre-compute common indent sizes + for (int i = 0; i < CACHE_SIZE; i++) { + INDENT_CACHE[i] = " ".repeat(i); + } + } + + private IndentFormatter() { + throw new UnsupportedOperationException("Utility class cannot be instantiated"); + } + + /** + * Gets the indentation string for the given depth and indent size. + * + * @param depth the nesting depth (0 = root) + * @param indentSize number of spaces per indentation level + * @return the indentation string + */ + public static String getIndent(final int depth, final int indentSize) { + if (depth <= 0) { + return ""; + } + + final int totalSpaces = depth * indentSize; + + // Use cache for small indent sizes + if (totalSpaces < INDENT_CACHE.length) { + return INDENT_CACHE[totalSpaces]; + } + + return " ".repeat(totalSpaces); + } + + /** + * Gets the indentation string for a given total number of spaces. + * Useful when you already know the exact space count needed. + * + * @param spaces the number of spaces needed + * @return the indentation string + */ + public static String getSpaces(final int spaces) { + if (spaces <= 0) { + return ""; + } + + if (spaces < INDENT_CACHE.length) { + return INDENT_CACHE[spaces]; + } + + return " ".repeat(spaces); + } +} diff --git a/src/main/java/dev/toonformat/jtoon/util/NumberFormatter.java b/src/main/java/dev/toonformat/jtoon/util/NumberFormatter.java new file mode 100644 index 0000000..324ac68 --- /dev/null +++ b/src/main/java/dev/toonformat/jtoon/util/NumberFormatter.java @@ -0,0 +1,130 @@ +package dev.toonformat.jtoon.util; + +import java.math.BigDecimal; + +/** + * Formats numbers for TOON encoding. + * Handles conversion from scientific notation to plain decimal and + * strips trailing zeros. + */ +public final class NumberFormatter { + + private NumberFormatter() { + throw new UnsupportedOperationException("Utility class cannot be instantiated"); + } + + /** + * Converts a number string to plain decimal format. + * Handles scientific notation (e.g., "1e-7" -> "0.0000001"). + * + * @param value the number string to format + * @return the formatted number string without scientific notation + */ + public static String toPlainDecimal(final String value) { + if (value == null || value.isEmpty()) { + return value; + } + + // Check if it's scientific notation + if (value.contains("e") || value.contains("E")) { + try { + final double d = Double.parseDouble(value); + return formatDecimal(d); + } catch (NumberFormatException e) { + return value; + } + } + + return value; + } + + /** + * Formats a double as plain decimal without scientific notation. + * + * @param value the double value to format + * @return the formatted decimal string + */ + public static String formatDecimal(final double value) { + if (value == 0.0) { + return "0"; + } + + // Use BigDecimal to avoid scientific notation + final BigDecimal bd = BigDecimal.valueOf(value); + return bd.stripTrailingZeros().toPlainString(); + } + + /** + * Strips trailing zeros from decimal numbers while preserving necessary decimals. + * Examples: "1.500" -> "1.5", "1.0" -> "1", "0.000001" -> "0.000001" + * + * @param value the decimal string to process + * @return the string with trailing zeros removed + */ + public static String stripTrailingZeros(final String value) { + final int dotIndex = value.indexOf('.'); + if (dotIndex < 0) { + return value; + } + + int lastNonZero = value.length() - 1; + while (lastNonZero > dotIndex && value.charAt(lastNonZero) == '0') { + lastNonZero--; + } + + if (lastNonZero == dotIndex) { + return value.substring(0, dotIndex); + } + + return value.substring(0, lastNonZero + 1); + } + + /** + * Checks if a string represents a valid number. + * Handles integers, decimals, and scientific notation. + * + * @param value the string to check + * @return true if the string is a valid number representation + */ + public static boolean isNumber(final String value) { + if (value == null || value.isEmpty()) { + return false; + } + + final String trimmed = value.trim(); + if (trimmed.isEmpty() || trimmed.length() != value.length()) { + return false; + } + + try { + Double.parseDouble(trimmed); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Checks if a string represents an integer number. + * + * @param value the string to check + * @return true if the string is an integer + */ + public static boolean isInteger(final String value) { + if (value == null || value.isEmpty()) { + return false; + } + + final String trimmed = value.trim(); + if (trimmed.isEmpty() || trimmed.length() != value.length()) { + return false; + } + + try { + Long.parseLong(trimmed); + return true; + } catch (NumberFormatException e) { + return false; + } + } +} diff --git a/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java b/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java index d7c39bc..8c0d8be 100644 --- a/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java +++ b/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java @@ -4,7 +4,7 @@ import tools.jackson.databind.MapperFeature; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; -import tools.jackson.module.afterburner.AfterburnerModule; +import tools.jackson.module.blackbird.BlackbirdModule; import java.util.TimeZone; /** @@ -33,7 +33,8 @@ public static ObjectMapper getInstance() { if (result == null) { instance = result = JsonMapper.builder() .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.ALWAYS)) - .addModule(new AfterburnerModule()) // Speeds up Jackson by 20–40% in most real-world cases + .addModule(new BlackbirdModule()) // Speeds up Jackson by 20-40% + // using MethodHandles (faster than Afterburner) .defaultTimeZone(TimeZone.getTimeZone("UTC")) // set a default timezone for dates .disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) .build(); diff --git a/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java b/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java index 5f5fce2..3ca2f85 100644 --- a/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java +++ b/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java @@ -73,6 +73,39 @@ private static boolean isValidEscapeChar(final char c) { return c == 'n' || c == 'r' || c == 't' || c == '"' || c == '\\'; } + /** + * Escapes special characters in a string and wraps it in quotes. + * Convenience method that combines escape() with quote wrapping. + * + * @param value The string to escape and quote + * @return The escaped and quoted string + */ + public static String escapeAndQuote(final String value) { + if (value == null) { + return "\"null\""; + } + + final StringBuilder sb = new StringBuilder(value.length() + 2); + sb.append('"'); + + for (int i = 0; i < value.length(); i++) { + final char c = value.charAt(i); + switch (c) { + case '"' -> sb.append("\\\""); + case '\\' -> sb.append("\\\\"); + case '\b' -> sb.append("\\b"); + case '\f' -> sb.append("\\f"); + case '\n' -> sb.append("\\n"); + case '\r' -> sb.append("\\r"); + case '\t' -> sb.append("\\t"); + default -> sb.append(c); + } + } + + sb.append('"'); + return sb.toString(); + } + /** * Unescapes a string and removes surrounding quotes if present. * Reverses the escaping applied by {@link #escape(String)}. diff --git a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java index 65d6b3b..6a8e551 100644 --- a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java +++ b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java @@ -126,4 +126,93 @@ private static boolean containsDelimiter(final String value, final String delimi private static boolean startsWithListMarker(final String value) { return value.startsWith(LIST_ITEM_MARKER); } + + /** + * Checks if a value needs quotes based on delimiter-aware validation. + * More comprehensive than isSafeUnquoted, handles additional edge cases. + * + * @param value the string value to check + * @param delimiterChar the delimiter character being used + * @return true if the value needs quotes, false otherwise + */ + public static boolean needsQuotes(final String value, final char delimiterChar) { + if (value == null) { + return true; + } + + if (value.isEmpty()) { + return true; + } + + // Check for leading/trailing whitespace + if (value.charAt(0) <= ' ' || value.charAt(value.length() - 1) <= ' ') { + return true; + } + + // Check for special keyword values + final String trimmed = value.trim(); + if (trimmed.equals(TRUE_LITERAL) || trimmed.equals(FALSE_LITERAL) + || trimmed.equals(NULL_LITERAL) || looksLikeNumber(trimmed)) { + return true; + } + + // Check for structural characters and delimiter + for (int i = 0; i < value.length(); i++) { + final char c = value.charAt(i); + + // Control characters and structural chars + if (c < ' ' || c == ':' || c == '#' || c == '{' || c == '}' + || c == '[' || c == ']' || c == '"' || c == '\'' || c == '-') { + return true; + } + + // Current delimiter + if (c == delimiterChar) { + return true; + } + + // Comma when comma is the delimiter + if (delimiterChar == ',' && c == ',') { + return true; + } + } + + return false; + } + + /** + * Checks if a key consists only of numeric characters. + * Numeric keys must be quoted to avoid ambiguity. + * + * @param key the key to check + * @return true if the key is purely numeric + */ + public static boolean isNumericKey(final String key) { + if (key == null || key.isEmpty()) { + return false; + } + + for (int i = 0; i < key.length(); i++) { + if (!Character.isDigit(key.charAt(i))) { + return false; + } + } + + return true; + } + + /** + * Checks if a string contains a hyphen character. + * Hyphens often need special handling (e.g., "-" alone must be quoted). + * + * @param value the string to check + * @return true if the string contains a hyphen + */ + public static boolean containsHyphen(final String value) { + if (value == null) { + return false; + } + + return value.indexOf('-') >= 0; + } } diff --git a/src/test/java/dev/toonformat/jtoon/EncodingOptimizationBenchmark.java b/src/test/java/dev/toonformat/jtoon/EncodingOptimizationBenchmark.java new file mode 100644 index 0000000..5edf1a1 --- /dev/null +++ b/src/test/java/dev/toonformat/jtoon/EncodingOptimizationBenchmark.java @@ -0,0 +1,184 @@ +package dev.toonformat.jtoon; + +import dev.toonformat.jtoon.encoder.ValueEncoder; +import dev.toonformat.jtoon.normalizer.JsonNormalizer; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import tools.jackson.databind.JsonNode; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * JMH Benchmark measuring encoding throughput after optimizations. + * Tests both small and large objects to validate performance improvements. + */ +@State(Scope.Thread) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(2) +public class EncodingOptimizationBenchmark { + + @Param({"small", "large"}) + private String size; + + private JsonNode simpleObjectNode; + private JsonNode nestedObjectNode; + private JsonNode tabularArrayNode; + private JsonNode primitiveArrayNode; + private JsonNode complexMixedNode; + + @Setup + public void setup() { + if ("small".equals(size)) { + simpleObjectNode = createSimpleObject(10); + nestedObjectNode = createNestedObject(3, 5); + tabularArrayNode = createTabularArray(10, 5); + primitiveArrayNode = createPrimitiveArray(20); + complexMixedNode = createComplexMixedDocument(5); + } else { + simpleObjectNode = createSimpleObject(100); + nestedObjectNode = createNestedObject(5, 20); + tabularArrayNode = createTabularArray(100, 10); + primitiveArrayNode = createPrimitiveArray(500); + complexMixedNode = createComplexMixedDocument(50); + } + } + + private JsonNode createSimpleObject(int fieldCount) { + Map obj = new HashMap<>(); + for (int i = 0; i < fieldCount; i++) { + obj.put("field_" + i, i % 5 == 0 ? "string_" + i : (i % 3 == 0 ? i * 1.5 : i)); + } + return JsonNormalizer.normalize(obj); + } + + private JsonNode createNestedObject(int depth, int fieldsPerLevel) { + Map current = new HashMap<>(); + Map root = current; + + for (int d = 0; d < depth; d++) { + Map level = new HashMap<>(); + for (int i = 0; i < fieldsPerLevel; i++) { + level.put("level" + d + "_field" + i, i); + } + if (d < depth - 1) { + current.put("nested", level); + current = level; + } + } + return JsonNormalizer.normalize(root); + } + + private JsonNode createTabularArray(int rowCount, int columnCount) { + List> rows = new ArrayList<>(); + for (int r = 0; r < rowCount; r++) { + Map row = new HashMap<>(); + for (int c = 0; c < columnCount; c++) { + row.put("col" + c, r * columnCount + c); + } + rows.add(row); + } + Map obj = new HashMap<>(); + obj.put("data", rows); + return JsonNormalizer.normalize(obj); + } + + private JsonNode createPrimitiveArray(int count) { + List arr = new ArrayList<>(); + for (int i = 0; i < count; i++) { + arr.add(i % 3 == 0 ? "value" + i : i); + } + Map obj = new HashMap<>(); + obj.put("items", arr); + return JsonNormalizer.normalize(obj); + } + + private JsonNode createComplexMixedDocument(int elementCount) { + Map root = new HashMap<>(); + + // Simple fields + root.put("id", 12345); + root.put("name", "Test Document"); + root.put("active", true); + + // Nested object + Map metadata = new HashMap<>(); + for (int i = 0; i < 5; i++) { + metadata.put("meta" + i, "value" + i); + } + root.put("metadata", metadata); + + // Tabular array + List> records = new ArrayList<>(); + for (int i = 0; i < elementCount; i++) { + Map record = new HashMap<>(); + record.put("id", i); + record.put("value", i * 1.5); + record.put("label", "item" + i); + records.add(record); + } + root.put("records", records); + + // Primitive array + List tags = new ArrayList<>(); + for (int i = 0; i < elementCount / 2; i++) { + tags.add(i); + } + root.put("tags", tags); + + return JsonNormalizer.normalize(root); + } + + @Benchmark + public String encodeSimpleObject() { + return ValueEncoder.encodeValue(simpleObjectNode, EncodeOptions.DEFAULT); + } + + @Benchmark + public String encodeNestedObject() { + return ValueEncoder.encodeValue(nestedObjectNode, EncodeOptions.DEFAULT); + } + + @Benchmark + public String encodeTabularArray() { + return ValueEncoder.encodeValue(tabularArrayNode, EncodeOptions.DEFAULT); + } + + @Benchmark + public String encodePrimitiveArray() { + return ValueEncoder.encodeValue(primitiveArrayNode, EncodeOptions.DEFAULT); + } + + @Benchmark + public String encodeComplexMixed() { + return ValueEncoder.encodeValue(complexMixedNode, EncodeOptions.DEFAULT); + } + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include(EncodingOptimizationBenchmark.class.getSimpleName()) + .result("build/jmh-results/encoding-optimization-benchmark.json") + .build(); + + new Runner(opt).run(); + } +} diff --git a/src/test/java/dev/toonformat/jtoon/conformance/ConformanceTest.java b/src/test/java/dev/toonformat/jtoon/conformance/ConformanceTest.java index d3c90c9..40dc2f1 100644 --- a/src/test/java/dev/toonformat/jtoon/conformance/ConformanceTest.java +++ b/src/test/java/dev/toonformat/jtoon/conformance/ConformanceTest.java @@ -135,7 +135,7 @@ private Stream loadTestFixtures(File directory) { @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") private DecodeTestFile parseFixture(File file) { try { - var fixture = mapper.readValue(file, DecodeTestFixture.class); + DecodeTestFixture fixture = mapper.readValue(file, DecodeTestFixture.class); return new DecodeTestFile(file, fixture); } catch (Exception exception) { throw new RuntimeException("Failed to parse test fixture: " + file.getName(), exception); @@ -152,7 +152,7 @@ private DynamicContainer createTestContainer(DecodeTestFile decodeFile) { } private Stream createTestsFromFixture(DecodeTestFile decodeFile) { - var fixture = decodeFile.fixture(); + DecodeTestFixture fixture = decodeFile.fixture(); return fixture.tests().stream() .map(this::createDynamicTest); } @@ -162,7 +162,7 @@ private DynamicTest createDynamicTest(JsonDecodeTestCase testCase) { } private void executeTestCase(JsonDecodeTestCase testCase) { - var options = parseOptions(testCase.options()); + DecodeOptions options = parseOptions(testCase.options()); String toonInput = testCase.input().asString(); if (Boolean.TRUE.equals(testCase.shouldError())) { diff --git a/src/test/java/dev/toonformat/jtoon/encoder/FlattenTest.java b/src/test/java/dev/toonformat/jtoon/encoder/FlattenTest.java index 1515b74..a493bd7 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/FlattenTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/FlattenTest.java @@ -289,4 +289,163 @@ void givenEmptyObjectLeaf_whenTryFold_thenLeafIsReturned() { assertEquals(2, result.segmentCount()); } + @Test + void givenNullRootLiteralKeys_whenTryFold_thenDoesNotThrow() { + // Given + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("b"); + b.put("c", 1); + + // When + Flatten.FoldResult result = Flatten.tryFoldKeyChain( + "a", a, Set.of(), null, null, 10 + ); + + // Then + assertNotNull(result); + assertEquals("a.b.c", result.foldedKey()); + } + + @Test + void givenPathPrefixWithDot_whenTryFold_thenUsesCorrectPath() { + // Given + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("b"); + b.put("c", 1); + + // When - using pathPrefix with dot + Flatten.FoldResult result = Flatten.tryFoldKeyChain( + "a", a, Set.of(), Set.of(), "prefix.data", 10 + ); + + // Then + assertNotNull(result); + assertEquals("a.b.c", result.foldedKey()); + } + + @Test + void givenDeepSingleKeyChainWithArrayLeaf_whenTryFold_thenReturnsLeaf() { + // Given + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("b"); + ObjectNode c = b.putObject("c"); + c.putArray("items"); // array leaf + + // When + Flatten.FoldResult result = Flatten.tryFoldKeyChain( + "a", a, Set.of(), Set.of(), null, 10 + ); + + // Then + assertNotNull(result); + assertEquals("a.b.c.items", result.foldedKey()); + assertNotNull(result.leafValue()); + assertTrue(result.leafValue().isArray()); + } + + @Test + void givenSingleKeyChainAtMaxDepth_whenTryFold_thenReturnsNull() { + // Given + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("b"); + b.putObject("c").put("x", 1); + + // When - depth limit of 2 + Flatten.FoldResult result = Flatten.tryFoldKeyChain( + "a", a, Set.of(), Set.of(), null, 2 + ); + + // Then + assertNotNull(result); + assertEquals("a.b", result.foldedKey()); + } + + @Test + void givenDeeplyNestedWithEmptyIntermediate_whenTryFold_thenHandles() { + // Given + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("b"); + ObjectNode c = b.putObject("c"); + c.putObject("d"); // empty object + c.put("e", 1); + + // When + Flatten.FoldResult result = Flatten.tryFoldKeyChain( + "a", a, Set.of(), Set.of(), null, 10 + ); + + // Then + assertNotNull(result); + } + + @Test + void givenMultipleLevelsOfSingleKeyObjects_whenTryFold_thenFolds() { + // Given - deep chain + ObjectNode root = MAPPER.createObjectNode(); + ObjectNode level1 = root.putObject("a"); + ObjectNode level2 = level1.putObject("b"); + ObjectNode level3 = level2.putObject("c"); + ObjectNode level4 = level3.putObject("d"); + level4.put("value", 42); + + // When + Flatten.FoldResult result = Flatten.tryFoldKeyChain( + "a", level1, Set.of(), Set.of(), null, 10 + ); + + // Then + assertNotNull(result); + assertTrue(result.foldedKey().startsWith("a.b")); + } + + @Test + void givenSiblingCollisionWithFoldedKey_whenTryFold_thenReturnsNull() { + // Given - existing folded key that would collide + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("b"); + b.put("x", 1); + + Set siblings = Set.of("a.b.x"); + + // When + Flatten.FoldResult result = Flatten.tryFoldKeyChain( + "a", a, siblings, Set.of(), null, 10 + ); + + // Then + assertNull(result); + } + + @Test + void givenNumericKeySegment_whenTryFold_thenFolds() { + // Given + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("123"); + b.put("x", 1); + + // When + Flatten.FoldResult result = Flatten.tryFoldKeyChain( + "a", a, Set.of(), Set.of(), null, 10 + ); + + // Then - numeric keys are NOT valid identifiers, so null expected + assertNull(result); + } + + @Test + void givenUnderscoreKeySegment_whenTryFold_thenFolds() { + // Given + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("_private"); + b.put("x", 1); + + // When + Flatten.FoldResult result = Flatten.tryFoldKeyChain( + "a", a, Set.of(), Set.of(), null, 10 + ); + + // Then - underscore prefix is valid + assertNotNull(result); + } + } diff --git a/src/test/java/dev/toonformat/jtoon/encoder/ValueEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/ValueEncoderTest.java index 6a96e3c..e0cd55c 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/ValueEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/ValueEncoderTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.NumericNode; import tools.jackson.databind.node.ObjectNode; import tools.jackson.databind.node.ArrayNode; @@ -37,8 +38,8 @@ void throwsOnConstructor() throws NoSuchMethodException { @DisplayName("given primitive JsonNode when encodeValue then returns encoded primitive") void givenPrimitive_whenEncodeValue_thenReturnsEncodedPrimitive() { // Given - var number = jsonNodeFactory.numberNode(42); - var options = EncodeOptions.DEFAULT; + NumericNode number = jsonNodeFactory.numberNode(42); + EncodeOptions options = EncodeOptions.DEFAULT; // When String result = ValueEncoder.encodeValue(number, options); @@ -52,7 +53,7 @@ void givenPrimitive_whenEncodeValue_thenReturnsEncodedPrimitive() { void givenPrimitiveArray_whenEncodeValue_thenWritesInlineArray() { // Given ArrayNode array = jsonNodeFactory.arrayNode().add(1).add(2).add(3); - var options = EncodeOptions.DEFAULT; + EncodeOptions options = EncodeOptions.DEFAULT; // When String result = ValueEncoder.encodeValue(array, options); @@ -68,7 +69,7 @@ void givenObject_whenEncodeValue_thenWritesObjectLines() { ObjectNode obj = jsonNodeFactory.objectNode(); obj.put("a", 1); obj.put("b", "x"); - var options = EncodeOptions.DEFAULT; + EncodeOptions options = EncodeOptions.DEFAULT; // When String result = ValueEncoder.encodeValue(obj, options); diff --git a/src/test/java/dev/toonformat/jtoon/util/IndentFormatterTest.java b/src/test/java/dev/toonformat/jtoon/util/IndentFormatterTest.java new file mode 100644 index 0000000..d0d2a0c --- /dev/null +++ b/src/test/java/dev/toonformat/jtoon/util/IndentFormatterTest.java @@ -0,0 +1,109 @@ +package dev.toonformat.jtoon.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link IndentFormatter}. + */ +@DisplayName("IndentFormatter") +class IndentFormatterTest { + + @Test + @DisplayName("getIndent returns empty string for depth 0") + void getIndentReturnsEmptyForDepthZero() { + assertEquals("", IndentFormatter.getIndent(0, 2)); + assertEquals("", IndentFormatter.getIndent(0, 4)); + } + + @Test + @DisplayName("getIndent returns empty string for negative depth") + void getIndentReturnsEmptyForNegativeDepth() { + assertEquals("", IndentFormatter.getIndent(-1, 2)); + assertEquals("", IndentFormatter.getIndent(-5, 4)); + } + + @ParameterizedTest + @CsvSource({ + "1, 2, ' '", + "1, 4, ' '", + "2, 2, ' '", + "2, 4, ' '", + "3, 2, ' '", + "3, 4, ' '" + }) + @DisplayName("getIndent returns correct indentation") + void getIndentReturnsCorrectIndentation(int depth, int indentSize, String expected) { + assertEquals(expected, IndentFormatter.getIndent(depth, indentSize)); + } + + @Test + @DisplayName("getIndent uses cache for small indent sizes") + void getIndentUsesCache() { + // These should all use the cache + assertEquals("", IndentFormatter.getIndent(0, 2)); + assertEquals(" ", IndentFormatter.getIndent(1, 2)); + assertEquals(" ", IndentFormatter.getIndent(2, 2)); + assertEquals(" ", IndentFormatter.getIndent(3, 2)); + + // Larger sizes + assertEquals(" ", IndentFormatter.getIndent(5, 2)); + assertEquals(" ", IndentFormatter.getIndent(10, 2)); + } + + @Test + @DisplayName("getSpaces returns empty string for zero or negative") + void getSpacesReturnsEmptyForZeroOrNegative() { + assertEquals("", IndentFormatter.getSpaces(0)); + assertEquals("", IndentFormatter.getSpaces(-1)); + assertEquals("", IndentFormatter.getSpaces(-5)); + } + + @Test + @DisplayName("getSpaces returns correct number of spaces") + void getSpacesReturnsCorrectSpaces() { + assertEquals(" ", IndentFormatter.getSpaces(1)); + assertEquals(" ", IndentFormatter.getSpaces(2)); + assertEquals(" ", IndentFormatter.getSpaces(3)); + assertEquals(" ", IndentFormatter.getSpaces(4)); + assertEquals(" ", IndentFormatter.getSpaces(5)); + } + + @Test + @DisplayName("getSpaces uses cache for small sizes") + void getSpacesUsesCache() { + // These should all use the cache + for (int i = 0; i < 20; i++) { + assertEquals(" ".repeat(i), IndentFormatter.getSpaces(i)); + } + } + + @Test + @DisplayName("getSpaces handles large sizes") + void getSpacesHandlesLargeSizes() { + String large = IndentFormatter.getSpaces(100); + assertEquals(100, large.length()); + assertTrue(large.trim().isEmpty()); + } + + @Test + @DisplayName("getIndent handles large indent sizes") + void getIndentHandlesLargeSizes() { + String large = IndentFormatter.getIndent(10, 4); // 40 spaces + assertEquals(40, large.length()); + assertTrue(large.trim().isEmpty()); + } + + @Test + @DisplayName("same indent returns same instance from cache") + void sameIndentReturnsSameInstance() { + // Due to caching, identical requests should return same string + String indent1 = IndentFormatter.getSpaces(5); + String indent2 = IndentFormatter.getSpaces(5); + assertEquals(indent1, indent2); + } +} diff --git a/src/test/java/dev/toonformat/jtoon/util/NumberFormatterTest.java b/src/test/java/dev/toonformat/jtoon/util/NumberFormatterTest.java new file mode 100644 index 0000000..59f6b9e --- /dev/null +++ b/src/test/java/dev/toonformat/jtoon/util/NumberFormatterTest.java @@ -0,0 +1,110 @@ +package dev.toonformat.jtoon.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link NumberFormatter}. + */ +@DisplayName("NumberFormatter") +class NumberFormatterTest { + + @Test + @DisplayName("toPlainDecimal converts scientific notation to plain decimal") + void toPlainDecimalConvertsScientificNotation() { + assertEquals("0.0000001", NumberFormatter.toPlainDecimal("1e-7")); + assertEquals("0.000001", NumberFormatter.toPlainDecimal("1e-6")); + assertEquals("1000000", NumberFormatter.toPlainDecimal("1e6")); + assertEquals("1000000", NumberFormatter.toPlainDecimal("1E6")); + assertEquals("1230000000", NumberFormatter.toPlainDecimal("1.23e9")); + } + + @Test + @DisplayName("toPlainDecimal returns plain numbers unchanged") + void toPlainDecimalReturnsPlainNumbersUnchanged() { + assertEquals("123", NumberFormatter.toPlainDecimal("123")); + assertEquals("3.14159", NumberFormatter.toPlainDecimal("3.14159")); + assertEquals("0.5", NumberFormatter.toPlainDecimal("0.5")); + assertEquals("-42", NumberFormatter.toPlainDecimal("-42")); + } + + @Test + @DisplayName("toPlainDecimal handles null and empty") + void toPlainDecimalHandlesNullAndEmpty() { + assertNull(NumberFormatter.toPlainDecimal(null)); + assertEquals("", NumberFormatter.toPlainDecimal("")); + } + + @Test + @DisplayName("formatDecimal converts double to plain decimal") + void formatDecimalConvertsDouble() { + assertEquals("0", NumberFormatter.formatDecimal(0.0)); + assertEquals("1", NumberFormatter.formatDecimal(1.0)); + assertEquals("0.1", NumberFormatter.formatDecimal(0.1)); + assertEquals("0.0000001", NumberFormatter.formatDecimal(1e-7)); + assertEquals("1000000", NumberFormatter.formatDecimal(1000000.0)); + } + + @ParameterizedTest + @CsvSource({ + "1.500, 1.5", + "1.0, 1", + "0.000001, 0.000001", + "10.00, 10", + "3.14159000, 3.14159", + "123, 123" + }) + @DisplayName("stripTrailingZeros removes unnecessary zeros") + void stripTrailingZeros(String input, String expected) { + assertEquals(expected, NumberFormatter.stripTrailingZeros(input)); + } + + @Test + @DisplayName("isNumber identifies valid numbers") + void isNumberIdentifiesValidNumbers() { + assertTrue(NumberFormatter.isNumber("42")); + assertTrue(NumberFormatter.isNumber("-42")); + assertTrue(NumberFormatter.isNumber("3.14159")); + assertTrue(NumberFormatter.isNumber("-3.14159")); + assertTrue(NumberFormatter.isNumber("1e-7")); + assertTrue(NumberFormatter.isNumber("1E+10")); + assertTrue(NumberFormatter.isNumber("0.0")); + assertTrue(NumberFormatter.isNumber("-0")); + } + + @Test + @DisplayName("isNumber rejects non-numbers") + void isNumberRejectsNonNumbers() { + assertFalse(NumberFormatter.isNumber("abc")); + assertFalse(NumberFormatter.isNumber("12.34.56")); + assertFalse(NumberFormatter.isNumber("")); + assertFalse(NumberFormatter.isNumber(null)); + assertFalse(NumberFormatter.isNumber(" 42 ")); + assertFalse(NumberFormatter.isNumber("42px")); + } + + @Test + @DisplayName("isInteger identifies valid integers") + void isIntegerIdentifiesValidIntegers() { + assertTrue(NumberFormatter.isInteger("42")); + assertTrue(NumberFormatter.isInteger("-42")); + assertTrue(NumberFormatter.isInteger("0")); + assertTrue(NumberFormatter.isInteger("9223372036854775807")); + assertTrue(NumberFormatter.isInteger("-9223372036854775808")); + } + + @Test + @DisplayName("isInteger rejects non-integers") + void isIntegerRejectsNonIntegers() { + assertFalse(NumberFormatter.isInteger("3.14")); + assertFalse(NumberFormatter.isInteger("1e10")); + assertFalse(NumberFormatter.isInteger("abc")); + assertFalse(NumberFormatter.isInteger("")); + assertFalse(NumberFormatter.isInteger(null)); + } +} diff --git a/src/test/java/dev/toonformat/jtoon/util/StringEscaperEnhancedTest.java b/src/test/java/dev/toonformat/jtoon/util/StringEscaperEnhancedTest.java new file mode 100644 index 0000000..cc47be1 --- /dev/null +++ b/src/test/java/dev/toonformat/jtoon/util/StringEscaperEnhancedTest.java @@ -0,0 +1,123 @@ +package dev.toonformat.jtoon.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for enhanced StringEscaper methods. + */ +@DisplayName("StringEscaper - Enhanced Methods") +class StringEscaperEnhancedTest { + + @Test + @DisplayName("escapeAndQuote wraps string in quotes") + void escapeAndQuoteWrapsInQuotes() { + String result = StringEscaper.escapeAndQuote("hello"); + assertEquals("\"hello\"", result); + } + + @Test + @DisplayName("escapeAndQuote escapes backslash") + void escapeAndQuoteEscapesBackslash() { + String result = StringEscaper.escapeAndQuote("hello\\world"); + assertEquals("\"hello\\\\world\"", result); + } + + @Test + @DisplayName("escapeAndQuote escapes double quote") + void escapeAndQuoteEscapesDoubleQuote() { + String result = StringEscaper.escapeAndQuote("hello\"world"); + assertEquals("\"hello\\\"world\"", result); + } + + @Test + @DisplayName("escapeAndQuote escapes newline") + void escapeAndQuoteEscapesNewline() { + String result = StringEscaper.escapeAndQuote("hello\nworld"); + assertEquals("\"hello\\nworld\"", result); + } + + @Test + @DisplayName("escapeAndQuote escapes carriage return") + void escapeAndQuoteEscapesCarriageReturn() { + String result = StringEscaper.escapeAndQuote("hello\rworld"); + assertEquals("\"hello\\rworld\"", result); + } + + @Test + @DisplayName("escapeAndQuote escapes tab") + void escapeAndQuoteEscapesTab() { + String result = StringEscaper.escapeAndQuote("hello\tworld"); + assertEquals("\"hello\\tworld\"", result); + } + + @Test + @DisplayName("escapeAndQuote escapes backspace") + void escapeAndQuoteEscapesBackspace() { + String result = StringEscaper.escapeAndQuote("hello\bworld"); + assertEquals("\"hello\\bworld\"", result); + } + + @Test + @DisplayName("escapeAndQuote escapes form feed") + void escapeAndQuoteEscapesFormFeed() { + String result = StringEscaper.escapeAndQuote("hello\fworld"); + assertEquals("\"hello\\fworld\"", result); + } + + @Test + @DisplayName("escapeAndQuote handles multiple escape characters") + void escapeAndQuoteHandlesMultipleEscapes() { + String input = "line1\nline2\rline3\ttab"; + String result = StringEscaper.escapeAndQuote(input); + assertEquals("\"line1\\nline2\\rline3\\ttab\"", result); + } + + @Test + @DisplayName("escapeAndQuote returns quoted null for null input") + void escapeAndQuoteHandlesNull() { + String result = StringEscaper.escapeAndQuote(null); + assertEquals("\"null\"", result); + } + + @Test + @DisplayName("escapeAndQuote handles empty string") + void escapeAndQuoteHandlesEmptyString() { + String result = StringEscaper.escapeAndQuote(""); + assertEquals("\"\"", result); + } + + @Test + @DisplayName("escapeAndQuote handles string with only escapes") + void escapeAndQuoteHandlesOnlyEscapes() { + String result = StringEscaper.escapeAndQuote("\\n\\r\\t"); + assertEquals("\"\\\\n\\\\r\\\\t\"", result); + } + + @ParameterizedTest + @ValueSource(strings = {"hello", "world", "foo_bar", "123", "a b c"}) + @DisplayName("escapeAndQuote preserves safe strings (just adds quotes)") + void escapeAndQuotePreservesSafeStrings(String input) { + String result = StringEscaper.escapeAndQuote(input); + assertEquals("\"" + input + "\"", result); + } + + @Test + @DisplayName("escapeAndQuote escapes backslash at end of string") + void escapeAndQuoteEscapesTrailingBackslash() { + String result = StringEscaper.escapeAndQuote("path\\"); + assertEquals("\"path\\\\\"", result); + } + + @Test + @DisplayName("escapeAndQuote escapes all standard characters") + void escapeAndQuoteEscapesAllStandardCharacters() { + String input = "\" \\ \n \r \t \b \f"; + String result = StringEscaper.escapeAndQuote(input); + assertEquals("\"\\\" \\\\ \\n \\r \\t \\b \\f\"", result); + } +} diff --git a/src/test/java/dev/toonformat/jtoon/util/StringValidatorEnhancedTest.java b/src/test/java/dev/toonformat/jtoon/util/StringValidatorEnhancedTest.java new file mode 100644 index 0000000..6e755be --- /dev/null +++ b/src/test/java/dev/toonformat/jtoon/util/StringValidatorEnhancedTest.java @@ -0,0 +1,156 @@ +package dev.toonformat.jtoon.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for enhanced StringValidator methods. + */ +@DisplayName("StringValidator - Enhanced Methods") +class StringValidatorEnhancedTest { + + @ParameterizedTest + @ValueSource(strings = {"-", "hello-world", "foo-bar-baz"}) + @DisplayName("needsQuotes returns true for strings with hyphen") + void needsQuotesReturnsTrueForHyphen(String input) { + assertTrue(StringValidator.needsQuotes(input, ',')); + } + + @ParameterizedTest + @ValueSource(strings = {"hello", "world", "foo_bar", "abc123"}) + @DisplayName("needsQuotes returns false for safe strings") + void needsQuotesReturnsFalseForSafeStrings(String input) { + assertFalse(StringValidator.needsQuotes(input, ',')); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", " ", " "}) + @DisplayName("needsQuotes returns true for empty or whitespace-only") + void needsQuotesReturnsTrueForEmptyOrWhitespace(String input) { + assertTrue(StringValidator.needsQuotes(input, ',')); + } + + @ParameterizedTest + @ValueSource(strings = {" hello", "hello ", " hello ", "\thello", "hello\t"}) + @DisplayName("needsQuotes returns true for strings with leading/trailing whitespace") + void needsQuotesReturnsTrueForPaddedStrings(String input) { + assertTrue(StringValidator.needsQuotes(input, ',')); + } + + @ParameterizedTest + @ValueSource(strings = {"hello world", "foo bar", "a b c"}) + @DisplayName("needsQuotes returns false for internal spaces without leading/trailing") + void needsQuotesReturnsFalseForInternalSpaces(String input) { + assertFalse(StringValidator.needsQuotes(input, ',')); + } + + @ParameterizedTest + @ValueSource(strings = {"true", "false", "null"}) + @DisplayName("needsQuotes returns true for keyword-like strings (lowercase)") + void needsQuotesReturnsTrueForKeywordsLowercase(String input) { + assertTrue(StringValidator.needsQuotes(input, ',')); + } + + @ParameterizedTest + @ValueSource(strings = {"TRUE", "FALSE", "NULL", "True", "False", "Null"}) + @DisplayName("needsQuotes returns false for uppercase keywords (trim handles them differently)") + void needsQuotesReturnsFalseForUppercaseKeywords(String input) { + assertFalse(StringValidator.needsQuotes(input, ',')); + } + + @ParameterizedTest + @ValueSource(strings = {"123", "-42", "3.14", "1e10", "0.5", "-0"}) + @DisplayName("needsQuotes returns true for numeric strings") + void needsQuotesReturnsTrueForNumericStrings(String input) { + assertTrue(StringValidator.needsQuotes(input, ',')); + } + + @ParameterizedTest + @ValueSource(strings = {"a:b", "key:value", "foo:bar"}) + @DisplayName("needsQuotes returns true for strings with colon") + void needsQuotesReturnsTrueForColon(String input) { + assertTrue(StringValidator.needsQuotes(input, ',')); + } + + @ParameterizedTest + @ValueSource(strings = {"{key}", "[array]", "{foo}", "[bar]"}) + @DisplayName("needsQuotes returns true for strings with structural characters") + void needsQuotesReturnsTrueForStructuralChars(String input) { + assertTrue(StringValidator.needsQuotes(input, ',')); + } + + @Test + @DisplayName("needsQuotes returns true for null input") + void needsQuotesReturnsTrueForNull() { + assertTrue(StringValidator.needsQuotes(null, ',')); + } + + @ParameterizedTest + @ValueSource(strings = {"a|b", "foo|bar"}) + @DisplayName("needsQuotes returns true when delimiter is pipe and string contains pipe") + void needsQuotesReturnsTrueForPipeDelimiter(String input) { + assertTrue(StringValidator.needsQuotes(input, '|')); + } + + @ParameterizedTest + @ValueSource(strings = {"a,b", "foo,bar"}) + @DisplayName("needsQuotes returns true when delimiter is comma and string contains comma") + void needsQuotesReturnsTrueForCommaDelimiter(String input) { + assertTrue(StringValidator.needsQuotes(input, ',')); + } + + @ParameterizedTest + @ValueSource(strings = {"a,b", "foo,bar"}) + @DisplayName("needsQuotes returns false when delimiter is pipe and string contains comma") + void needsQuotesReturnsFalseWhenDifferentDelimiter(String input) { + assertFalse(StringValidator.needsQuotes(input, '|')); + } + + @Test + @DisplayName("isNumericKey returns true for purely numeric strings") + void isNumericKeyReturnsTrueForNumeric() { + assertTrue(StringValidator.isNumericKey("123")); + assertTrue(StringValidator.isNumericKey("0")); + assertTrue(StringValidator.isNumericKey("999999")); + } + + @ParameterizedTest + @ValueSource(strings = {"", "12a", "a12", "1a2", "abc", "hello", "123.456"}) + @DisplayName("isNumericKey returns false for non-numeric strings") + void isNumericKeyReturnsFalseForNonNumeric(String input) { + assertFalse(StringValidator.isNumericKey(input)); + } + + @Test + @DisplayName("isNumericKey returns false for null") + void isNumericKeyReturnsFalseForNull() { + assertFalse(StringValidator.isNumericKey(null)); + } + + @Test + @DisplayName("containsHyphen returns true for strings with hyphen") + void containsHyphenReturnsTrue() { + assertTrue(StringValidator.containsHyphen("-")); + assertTrue(StringValidator.containsHyphen("hello-world")); + assertTrue(StringValidator.containsHyphen("-prefix")); + assertTrue(StringValidator.containsHyphen("suffix-")); + } + + @Test + @DisplayName("containsHyphen returns false for strings without hyphen") + void containsHyphenReturnsFalse() { + assertFalse(StringValidator.containsHyphen("hello")); + assertFalse(StringValidator.containsHyphen("world")); + assertFalse(StringValidator.containsHyphen("")); + } + + @Test + @DisplayName("containsHyphen returns false for null") + void containsHyphenReturnsFalseForNull() { + assertFalse(StringValidator.containsHyphen(null)); + } +} From 38a119c91ba71eb77286339dceab35d6363afd61 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Sun, 15 Feb 2026 12:06:37 +0100 Subject: [PATCH 24/29] adding test --- .../jtoon/encoder/ArrayEncoder.java | 1 - .../dev/toonformat/jtoon/encoder/Flatten.java | 4 +- .../jtoon/util/IndentFormatter.java | 66 --------- .../jtoon/util/NumberFormatter.java | 130 ------------------ .../toonformat/jtoon/encoder/FlattenTest.java | 130 ++++++++++++++++++ .../toonformat/jtoon/util/HeadersTest.java | 48 +++++++ .../jtoon/util/IndentFormatterTest.java | 109 --------------- .../jtoon/util/NumberFormatterTest.java | 110 --------------- 8 files changed, 180 insertions(+), 418 deletions(-) delete mode 100644 src/main/java/dev/toonformat/jtoon/util/IndentFormatter.java delete mode 100644 src/main/java/dev/toonformat/jtoon/util/NumberFormatter.java create mode 100644 src/test/java/dev/toonformat/jtoon/util/HeadersTest.java delete mode 100644 src/test/java/dev/toonformat/jtoon/util/IndentFormatterTest.java delete mode 100644 src/test/java/dev/toonformat/jtoon/util/NumberFormatterTest.java diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java index 5fe1845..d1ee88f 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java @@ -4,7 +4,6 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; - import java.util.List; import java.util.stream.StreamSupport; import static dev.toonformat.jtoon.util.Constants.LIST_ITEM_PREFIX; diff --git a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java index 3126c0c..aa16f13 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/Flatten.java @@ -42,7 +42,7 @@ public record FoldResult(String foldedKey, * @param tail the tail node (if any) * @param leafValue the leaf JsonValue */ - private record ChainResult(List segments, JsonNode tail, JsonNode leafValue) { + record ChainResult(List segments, JsonNode tail, JsonNode leafValue) { } /** @@ -136,7 +136,7 @@ public static FoldResult tryFoldKeyChain(final String key, * @param maxDepth maximum number of allowed segments * @return a {@link ChainResult} containing segments, tail, and leafValue */ - private static ChainResult collectSingleKeyChain(final String startKey, + static ChainResult collectSingleKeyChain(final String startKey, final JsonNode startValue, final int maxDepth) { // normalize absolute key to its local segment diff --git a/src/main/java/dev/toonformat/jtoon/util/IndentFormatter.java b/src/main/java/dev/toonformat/jtoon/util/IndentFormatter.java deleted file mode 100644 index 4bd9380..0000000 --- a/src/main/java/dev/toonformat/jtoon/util/IndentFormatter.java +++ /dev/null @@ -1,66 +0,0 @@ -package dev.toonformat.jtoon.util; - -/** - * Handles indentation formatting for TOON output. - * Provides consistent indentation across all encoders. - */ -public final class IndentFormatter { - - /** - * Cache of common indent strings to avoid repeated string creation. - */ - private static final int CACHE_SIZE = 20; - private static final String[] INDENT_CACHE = new String[CACHE_SIZE]; - - static { - // Pre-compute common indent sizes - for (int i = 0; i < CACHE_SIZE; i++) { - INDENT_CACHE[i] = " ".repeat(i); - } - } - - private IndentFormatter() { - throw new UnsupportedOperationException("Utility class cannot be instantiated"); - } - - /** - * Gets the indentation string for the given depth and indent size. - * - * @param depth the nesting depth (0 = root) - * @param indentSize number of spaces per indentation level - * @return the indentation string - */ - public static String getIndent(final int depth, final int indentSize) { - if (depth <= 0) { - return ""; - } - - final int totalSpaces = depth * indentSize; - - // Use cache for small indent sizes - if (totalSpaces < INDENT_CACHE.length) { - return INDENT_CACHE[totalSpaces]; - } - - return " ".repeat(totalSpaces); - } - - /** - * Gets the indentation string for a given total number of spaces. - * Useful when you already know the exact space count needed. - * - * @param spaces the number of spaces needed - * @return the indentation string - */ - public static String getSpaces(final int spaces) { - if (spaces <= 0) { - return ""; - } - - if (spaces < INDENT_CACHE.length) { - return INDENT_CACHE[spaces]; - } - - return " ".repeat(spaces); - } -} diff --git a/src/main/java/dev/toonformat/jtoon/util/NumberFormatter.java b/src/main/java/dev/toonformat/jtoon/util/NumberFormatter.java deleted file mode 100644 index 324ac68..0000000 --- a/src/main/java/dev/toonformat/jtoon/util/NumberFormatter.java +++ /dev/null @@ -1,130 +0,0 @@ -package dev.toonformat.jtoon.util; - -import java.math.BigDecimal; - -/** - * Formats numbers for TOON encoding. - * Handles conversion from scientific notation to plain decimal and - * strips trailing zeros. - */ -public final class NumberFormatter { - - private NumberFormatter() { - throw new UnsupportedOperationException("Utility class cannot be instantiated"); - } - - /** - * Converts a number string to plain decimal format. - * Handles scientific notation (e.g., "1e-7" -> "0.0000001"). - * - * @param value the number string to format - * @return the formatted number string without scientific notation - */ - public static String toPlainDecimal(final String value) { - if (value == null || value.isEmpty()) { - return value; - } - - // Check if it's scientific notation - if (value.contains("e") || value.contains("E")) { - try { - final double d = Double.parseDouble(value); - return formatDecimal(d); - } catch (NumberFormatException e) { - return value; - } - } - - return value; - } - - /** - * Formats a double as plain decimal without scientific notation. - * - * @param value the double value to format - * @return the formatted decimal string - */ - public static String formatDecimal(final double value) { - if (value == 0.0) { - return "0"; - } - - // Use BigDecimal to avoid scientific notation - final BigDecimal bd = BigDecimal.valueOf(value); - return bd.stripTrailingZeros().toPlainString(); - } - - /** - * Strips trailing zeros from decimal numbers while preserving necessary decimals. - * Examples: "1.500" -> "1.5", "1.0" -> "1", "0.000001" -> "0.000001" - * - * @param value the decimal string to process - * @return the string with trailing zeros removed - */ - public static String stripTrailingZeros(final String value) { - final int dotIndex = value.indexOf('.'); - if (dotIndex < 0) { - return value; - } - - int lastNonZero = value.length() - 1; - while (lastNonZero > dotIndex && value.charAt(lastNonZero) == '0') { - lastNonZero--; - } - - if (lastNonZero == dotIndex) { - return value.substring(0, dotIndex); - } - - return value.substring(0, lastNonZero + 1); - } - - /** - * Checks if a string represents a valid number. - * Handles integers, decimals, and scientific notation. - * - * @param value the string to check - * @return true if the string is a valid number representation - */ - public static boolean isNumber(final String value) { - if (value == null || value.isEmpty()) { - return false; - } - - final String trimmed = value.trim(); - if (trimmed.isEmpty() || trimmed.length() != value.length()) { - return false; - } - - try { - Double.parseDouble(trimmed); - return true; - } catch (NumberFormatException e) { - return false; - } - } - - /** - * Checks if a string represents an integer number. - * - * @param value the string to check - * @return true if the string is an integer - */ - public static boolean isInteger(final String value) { - if (value == null || value.isEmpty()) { - return false; - } - - final String trimmed = value.trim(); - if (trimmed.isEmpty() || trimmed.length() != value.length()) { - return false; - } - - try { - Long.parseLong(trimmed); - return true; - } catch (NumberFormatException e) { - return false; - } - } -} diff --git a/src/test/java/dev/toonformat/jtoon/encoder/FlattenTest.java b/src/test/java/dev/toonformat/jtoon/encoder/FlattenTest.java index a493bd7..02d2236 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/FlattenTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/FlattenTest.java @@ -448,4 +448,134 @@ void givenUnderscoreKeySegment_whenTryFold_thenFolds() { assertNotNull(result); } + @Test + @DisplayName("given valid object but remainingDepth is 1 when tryFold then returns null") + void givenValidObjectButRemainingDepthIsOne_whenTryFold_thenReturnsNull() { + // Given - valid object chain but remainingDepth <= 1 + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("b"); + b.put("c", 123); + + // When - remainingDepth is 1 (not enough to fold) + Flatten.FoldResult result = Flatten.tryFoldKeyChain( + "a", a, Set.of(), Set.of(), null, 1 + ); + + // Then - should return null because remainingDepth <= 1 + assertNull(result); + } + + @Test + @DisplayName("given simple key without dots when collectChain then uses key directly") + void givenSimpleKeyWithoutDots_whenCollectChain_thenUsesKeyDirectly() { + // Given - simple key without dots + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("b"); + b.put("c", 123); + + // When - call collectSingleKeyChain directly with simple key + Flatten.ChainResult result = Flatten.collectSingleKeyChain("simpleKey", a, 10); + + // Then - first segment should be the key as-is (no dot processing) + assertNotNull(result); + assertEquals("simpleKey", result.segments().get(0)); + assertEquals(3, result.segments().size()); // simpleKey, b, c + } + + @Test + @DisplayName("given single key object at max depth when collectChain then treats as leaf") + void givenSingleKeyObjectAtMaxDepth_whenCollectChain_thenTreatsAsLeaf() { + // Given - single-key object at exact max depth + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("b"); + b.put("c", 123); + + // When - depth limit of 2 means we can collect "a" and "b", but "b" has 1 key "c" + // At depthCounter == maxDepth, single-key object should be treated as leaf + Flatten.ChainResult result = Flatten.collectSingleKeyChain("a", a, 2); + + // Then - should stop at "b" with single key and treat as leaf + assertNotNull(result); + assertEquals(2, result.segments().size()); // Only "a" and "b" + assertNotNull(result.leafValue()); // b is treated as leaf (single key, but at max depth) + assertNull(result.tail()); + } + + @Test + @DisplayName("given max depth reached with single key chain when collectChain then returns tail") + void givenMaxDepthReachedWithSingleKeyChain_whenCollectChain_thenReturnsTail() { + // Given - chain deeper than max depth + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("b"); + ObjectNode c = b.putObject("c"); + ObjectNode d = c.putObject("d"); + d.put("value", 42); + + // When - maxDepth of 3 means we stop at "c" which has 1 key "d" + Flatten.ChainResult result = Flatten.collectSingleKeyChain("a", a, 3); + + // Then - should have a, b, c as segments, and c (single key object) is the leaf + assertNotNull(result); + assertEquals(3, result.segments().size()); + assertEquals("c", result.segments().get(2)); + assertNotNull(result.leafValue()); + } + + @Test + @DisplayName("given empty path prefix when tryFold then uses folded key directly") + void givenEmptyPathPrefix_whenTryFold_thenUsesFoldedKeyDirectly() { + // Given - empty string pathPrefix (not null, but empty) + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("b"); + b.put("c", 123); + + // When - pathPrefix is empty string (tests line 109 branch) + Flatten.FoldResult result = Flatten.tryFoldKeyChain( + "a", a, Set.of(), Set.of(), "", 10 + ); + + // Then - should still work, using folded key directly + assertNotNull(result); + assertEquals("a.b.c", result.foldedKey()); + } + + @Test + @DisplayName("given empty object at depth when collectChain then returns leaf") + void givenEmptyObjectAtDepth_whenCollectChain_thenReturnsLeaf() { + // Given - empty object encountered during chain collection + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("b"); + b.putObject("c"); // empty object as leaf + + // When - collect chain that ends with empty object (tests line 180) + Flatten.ChainResult result = Flatten.collectSingleKeyChain("a", a, 10); + + // Then - empty object should be treated as leaf + assertNotNull(result); + assertEquals("c", result.segments().get(2)); + assertNotNull(result.leafValue()); + assertTrue(result.leafValue().isObject()); + assertTrue(result.leafValue().isEmpty()); + } + + @Test + @DisplayName("given multi key object at max depth when collectChain then returns tail") + void givenMultiKeyObjectAtMaxDepth_whenCollectChain_thenReturnsTail() { + // Given - chain where intermediate object becomes multi-key after depth reached + ObjectNode a = MAPPER.createObjectNode(); + ObjectNode b = a.putObject("b"); + b.put("x", 1); + b.put("y", 2); // b has 2 keys - should be tail when maxDepth reached + + // When - maxDepth of 2 allows processing "a" then stops, b has 2 keys (tests line 192) + Flatten.ChainResult result = Flatten.collectSingleKeyChain("a", a, 2); + + // Then - should have a, b as segments, b is tail with 2 keys + assertNotNull(result); + assertEquals(2, result.segments().size()); + assertEquals("b", result.segments().get(1)); + assertNotNull(result.tail()); + assertEquals(2, result.tail().size()); + } + } diff --git a/src/test/java/dev/toonformat/jtoon/util/HeadersTest.java b/src/test/java/dev/toonformat/jtoon/util/HeadersTest.java new file mode 100644 index 0000000..fa96040 --- /dev/null +++ b/src/test/java/dev/toonformat/jtoon/util/HeadersTest.java @@ -0,0 +1,48 @@ +package dev.toonformat.jtoon.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link Headers}. + */ +@DisplayName("Headers") +class HeadersTest { + + @Test + @DisplayName("constructor throws UnsupportedOperationException") + void constructorThrowsException() throws Exception { + Constructor constructor = Headers.class.getDeclaredConstructor(); + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, () -> constructor.newInstance()); + } + + @Test + @DisplayName("ARRAY_HEADER_PATTERN matches array headers") + void arrayHeaderPatternMatches() { + assertNotNull(Headers.ARRAY_HEADER_PATTERN.matcher("[3]").matches()); + assertNotNull(Headers.ARRAY_HEADER_PATTERN.matcher("[#2]").matches()); + assertNotNull(Headers.ARRAY_HEADER_PATTERN.matcher("[3\t]").matches()); + assertNotNull(Headers.ARRAY_HEADER_PATTERN.matcher("[2|]").matches()); + } + + @Test + @DisplayName("TABULAR_HEADER_PATTERN matches tabular headers") + void tabularHeaderPatternMatches() { + assertNotNull(Headers.TABULAR_HEADER_PATTERN.matcher("[2]{id,name,role}:").matches()); + assertNotNull(Headers.TABULAR_HEADER_PATTERN.matcher("[#3]{a,b,c}:").matches()); + } + + @Test + @DisplayName("KEYED_ARRAY_PATTERN matches keyed arrays") + void keyedArrayPatternMatches() { + assertNotNull(Headers.KEYED_ARRAY_PATTERN.matcher("items[2]{id,name}:").matches()); + assertNotNull(Headers.KEYED_ARRAY_PATTERN.matcher("tags[3]:").matches()); + assertNotNull(Headers.KEYED_ARRAY_PATTERN.matcher("data[4]{id}:").matches()); + } +} diff --git a/src/test/java/dev/toonformat/jtoon/util/IndentFormatterTest.java b/src/test/java/dev/toonformat/jtoon/util/IndentFormatterTest.java deleted file mode 100644 index d0d2a0c..0000000 --- a/src/test/java/dev/toonformat/jtoon/util/IndentFormatterTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package dev.toonformat.jtoon.util; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Unit tests for {@link IndentFormatter}. - */ -@DisplayName("IndentFormatter") -class IndentFormatterTest { - - @Test - @DisplayName("getIndent returns empty string for depth 0") - void getIndentReturnsEmptyForDepthZero() { - assertEquals("", IndentFormatter.getIndent(0, 2)); - assertEquals("", IndentFormatter.getIndent(0, 4)); - } - - @Test - @DisplayName("getIndent returns empty string for negative depth") - void getIndentReturnsEmptyForNegativeDepth() { - assertEquals("", IndentFormatter.getIndent(-1, 2)); - assertEquals("", IndentFormatter.getIndent(-5, 4)); - } - - @ParameterizedTest - @CsvSource({ - "1, 2, ' '", - "1, 4, ' '", - "2, 2, ' '", - "2, 4, ' '", - "3, 2, ' '", - "3, 4, ' '" - }) - @DisplayName("getIndent returns correct indentation") - void getIndentReturnsCorrectIndentation(int depth, int indentSize, String expected) { - assertEquals(expected, IndentFormatter.getIndent(depth, indentSize)); - } - - @Test - @DisplayName("getIndent uses cache for small indent sizes") - void getIndentUsesCache() { - // These should all use the cache - assertEquals("", IndentFormatter.getIndent(0, 2)); - assertEquals(" ", IndentFormatter.getIndent(1, 2)); - assertEquals(" ", IndentFormatter.getIndent(2, 2)); - assertEquals(" ", IndentFormatter.getIndent(3, 2)); - - // Larger sizes - assertEquals(" ", IndentFormatter.getIndent(5, 2)); - assertEquals(" ", IndentFormatter.getIndent(10, 2)); - } - - @Test - @DisplayName("getSpaces returns empty string for zero or negative") - void getSpacesReturnsEmptyForZeroOrNegative() { - assertEquals("", IndentFormatter.getSpaces(0)); - assertEquals("", IndentFormatter.getSpaces(-1)); - assertEquals("", IndentFormatter.getSpaces(-5)); - } - - @Test - @DisplayName("getSpaces returns correct number of spaces") - void getSpacesReturnsCorrectSpaces() { - assertEquals(" ", IndentFormatter.getSpaces(1)); - assertEquals(" ", IndentFormatter.getSpaces(2)); - assertEquals(" ", IndentFormatter.getSpaces(3)); - assertEquals(" ", IndentFormatter.getSpaces(4)); - assertEquals(" ", IndentFormatter.getSpaces(5)); - } - - @Test - @DisplayName("getSpaces uses cache for small sizes") - void getSpacesUsesCache() { - // These should all use the cache - for (int i = 0; i < 20; i++) { - assertEquals(" ".repeat(i), IndentFormatter.getSpaces(i)); - } - } - - @Test - @DisplayName("getSpaces handles large sizes") - void getSpacesHandlesLargeSizes() { - String large = IndentFormatter.getSpaces(100); - assertEquals(100, large.length()); - assertTrue(large.trim().isEmpty()); - } - - @Test - @DisplayName("getIndent handles large indent sizes") - void getIndentHandlesLargeSizes() { - String large = IndentFormatter.getIndent(10, 4); // 40 spaces - assertEquals(40, large.length()); - assertTrue(large.trim().isEmpty()); - } - - @Test - @DisplayName("same indent returns same instance from cache") - void sameIndentReturnsSameInstance() { - // Due to caching, identical requests should return same string - String indent1 = IndentFormatter.getSpaces(5); - String indent2 = IndentFormatter.getSpaces(5); - assertEquals(indent1, indent2); - } -} diff --git a/src/test/java/dev/toonformat/jtoon/util/NumberFormatterTest.java b/src/test/java/dev/toonformat/jtoon/util/NumberFormatterTest.java deleted file mode 100644 index 59f6b9e..0000000 --- a/src/test/java/dev/toonformat/jtoon/util/NumberFormatterTest.java +++ /dev/null @@ -1,110 +0,0 @@ -package dev.toonformat.jtoon.util; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.ValueSource; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Unit tests for {@link NumberFormatter}. - */ -@DisplayName("NumberFormatter") -class NumberFormatterTest { - - @Test - @DisplayName("toPlainDecimal converts scientific notation to plain decimal") - void toPlainDecimalConvertsScientificNotation() { - assertEquals("0.0000001", NumberFormatter.toPlainDecimal("1e-7")); - assertEquals("0.000001", NumberFormatter.toPlainDecimal("1e-6")); - assertEquals("1000000", NumberFormatter.toPlainDecimal("1e6")); - assertEquals("1000000", NumberFormatter.toPlainDecimal("1E6")); - assertEquals("1230000000", NumberFormatter.toPlainDecimal("1.23e9")); - } - - @Test - @DisplayName("toPlainDecimal returns plain numbers unchanged") - void toPlainDecimalReturnsPlainNumbersUnchanged() { - assertEquals("123", NumberFormatter.toPlainDecimal("123")); - assertEquals("3.14159", NumberFormatter.toPlainDecimal("3.14159")); - assertEquals("0.5", NumberFormatter.toPlainDecimal("0.5")); - assertEquals("-42", NumberFormatter.toPlainDecimal("-42")); - } - - @Test - @DisplayName("toPlainDecimal handles null and empty") - void toPlainDecimalHandlesNullAndEmpty() { - assertNull(NumberFormatter.toPlainDecimal(null)); - assertEquals("", NumberFormatter.toPlainDecimal("")); - } - - @Test - @DisplayName("formatDecimal converts double to plain decimal") - void formatDecimalConvertsDouble() { - assertEquals("0", NumberFormatter.formatDecimal(0.0)); - assertEquals("1", NumberFormatter.formatDecimal(1.0)); - assertEquals("0.1", NumberFormatter.formatDecimal(0.1)); - assertEquals("0.0000001", NumberFormatter.formatDecimal(1e-7)); - assertEquals("1000000", NumberFormatter.formatDecimal(1000000.0)); - } - - @ParameterizedTest - @CsvSource({ - "1.500, 1.5", - "1.0, 1", - "0.000001, 0.000001", - "10.00, 10", - "3.14159000, 3.14159", - "123, 123" - }) - @DisplayName("stripTrailingZeros removes unnecessary zeros") - void stripTrailingZeros(String input, String expected) { - assertEquals(expected, NumberFormatter.stripTrailingZeros(input)); - } - - @Test - @DisplayName("isNumber identifies valid numbers") - void isNumberIdentifiesValidNumbers() { - assertTrue(NumberFormatter.isNumber("42")); - assertTrue(NumberFormatter.isNumber("-42")); - assertTrue(NumberFormatter.isNumber("3.14159")); - assertTrue(NumberFormatter.isNumber("-3.14159")); - assertTrue(NumberFormatter.isNumber("1e-7")); - assertTrue(NumberFormatter.isNumber("1E+10")); - assertTrue(NumberFormatter.isNumber("0.0")); - assertTrue(NumberFormatter.isNumber("-0")); - } - - @Test - @DisplayName("isNumber rejects non-numbers") - void isNumberRejectsNonNumbers() { - assertFalse(NumberFormatter.isNumber("abc")); - assertFalse(NumberFormatter.isNumber("12.34.56")); - assertFalse(NumberFormatter.isNumber("")); - assertFalse(NumberFormatter.isNumber(null)); - assertFalse(NumberFormatter.isNumber(" 42 ")); - assertFalse(NumberFormatter.isNumber("42px")); - } - - @Test - @DisplayName("isInteger identifies valid integers") - void isIntegerIdentifiesValidIntegers() { - assertTrue(NumberFormatter.isInteger("42")); - assertTrue(NumberFormatter.isInteger("-42")); - assertTrue(NumberFormatter.isInteger("0")); - assertTrue(NumberFormatter.isInteger("9223372036854775807")); - assertTrue(NumberFormatter.isInteger("-9223372036854775808")); - } - - @Test - @DisplayName("isInteger rejects non-integers") - void isIntegerRejectsNonIntegers() { - assertFalse(NumberFormatter.isInteger("3.14")); - assertFalse(NumberFormatter.isInteger("1e10")); - assertFalse(NumberFormatter.isInteger("abc")); - assertFalse(NumberFormatter.isInteger("")); - assertFalse(NumberFormatter.isInteger(null)); - } -} From 17ffd61b43b58930a6d6ea4da3caf92ced2d720a Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Sun, 15 Feb 2026 16:07:36 +0100 Subject: [PATCH 25/29] cleanup --- build.gradle | 4 +- spotbugs-exclude.xml | 11 + .../jtoon/encoder/ArrayEncoder.java | 45 +++- .../jtoon/encoder/HeaderFormatter.java | 18 +- .../toonformat/jtoon/encoder/LineWriter.java | 4 +- .../toonformat/jtoon/util/StringEscaper.java | 59 ++--- .../jtoon/util/StringValidator.java | 225 +++++++----------- .../jtoon/JToonRaceConditionTest.java | 2 + .../DecodeContextRaceConditionTest.java | 14 +- .../jtoon/util/StringEscaperEnhancedTest.java | 123 ---------- .../util/StringValidatorEnhancedTest.java | 156 ------------ 11 files changed, 171 insertions(+), 490 deletions(-) delete mode 100644 src/test/java/dev/toonformat/jtoon/util/StringEscaperEnhancedTest.java delete mode 100644 src/test/java/dev/toonformat/jtoon/util/StringValidatorEnhancedTest.java diff --git a/build.gradle b/build.gradle index 61ede39..b054462 100644 --- a/build.gradle +++ b/build.gradle @@ -51,8 +51,8 @@ pitest { spotbugs { toolVersion = '4.9.8' excludeFilter = file('spotbugs-exclude.xml') - effort = 'MAX' - reportLevel = 'LOW' + effort = com.github.spotbugs.snom.Effort.valueOf("MAX") + reportLevel = com.github.spotbugs.snom.Confidence.valueOf("LOW") reportsDir = layout.buildDirectory.dir('spotbugs') } diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index b9311e6..5019218 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -11,6 +11,17 @@ + + + + + + + + + + + diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java index d1ee88f..bd27a95 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java @@ -5,7 +5,6 @@ import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; import java.util.List; -import java.util.stream.StreamSupport; import static dev.toonformat.jtoon.util.Constants.LIST_ITEM_PREFIX; import static dev.toonformat.jtoon.util.Constants.SPACE; @@ -38,26 +37,47 @@ public static void encodeArray(final String key, final ArrayNode value, return; } - // Primitive array - if (isArrayOfPrimitives(value)) { + final int size = value.size(); + boolean allPrimitives = true; + boolean allArrays = true; + boolean allObjects = true; + + for (int i = 0; i < size; i++) { + final JsonNode item = value.get(i); + if (!item.isValueNode()) { + allPrimitives = false; + } + if (!item.isArray()) { + allArrays = false; + } + if (!item.isObject()) { + allObjects = false; + } + if (!allPrimitives && !allArrays && !allObjects) { + break; + } + } + + if (allPrimitives) { encodeInlinePrimitiveArray(key, value, writer, depth, options); return; } - // Array of arrays (all primitives) - if (isArrayOfArrays(value)) { - final boolean allPrimitiveArrays = StreamSupport.stream(value.spliterator(), false) - .filter(JsonNode::isArray) - .allMatch(ArrayEncoder::isArrayOfPrimitives); - + if (allArrays) { + boolean allPrimitiveArrays = true; + for (int i = 0; i < size; i++) { + if (!isArrayOfPrimitives(value.get(i))) { + allPrimitiveArrays = false; + break; + } + } if (allPrimitiveArrays) { encodeArrayOfArraysAsListItems(key, value, writer, depth, options); return; } } - // Array of objects - if (isArrayOfObjects(value)) { + if (allObjects) { final List header = TabularArrayEncoder.detectTabularHeader(value); if (!header.isEmpty()) { TabularArrayEncoder.encodeArrayOfObjectsAsTabular(key, value, header, writer, depth, options); @@ -67,7 +87,6 @@ public static void encodeArray(final String key, final ArrayNode value, return; } - // Mixed array: fallback to expanded format encodeMixedArrayAsListItems(key, value, writer, depth, options); } @@ -164,7 +183,7 @@ public static String formatInlineArray(final ArrayNode values, final String deli joinedValues.append(PrimitiveEncoder.encodePrimitive(value, delimiter)); } - return header + SPACE + joinedValues.toString(); + return header + SPACE + joinedValues; } /** diff --git a/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java b/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java index 2baec22..403c28b 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java @@ -115,10 +115,20 @@ private static void appendFieldsIfPresent( } private static String formatFields(final Collection fields, final String delimiter) { - return fields.stream() - .map(PrimitiveEncoder::encodeKey) - .reduce((a, b) -> a + delimiter + b) - .orElse(""); + if (fields.isEmpty()) { + return ""; + } + + final StringBuilder sb = new StringBuilder(); + boolean first = true; + for (final String field : fields) { + if (!first) { + sb.append(delimiter); + } + first = false; + sb.append(PrimitiveEncoder.encodeKey(field)); + } + return sb.toString(); } } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java b/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java index 8ab1631..1d22e02 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java @@ -51,7 +51,9 @@ public void push(final int depth, final String content) { if (depth < indentCache.length) { stringBuilder.append(indentCache[depth]); } else { - stringBuilder.append(String.valueOf(indentationString).repeat(depth)); + for (int i = 0; i < depth; i++) { + stringBuilder.append(indentationString); + } } } stringBuilder.append(content); diff --git a/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java b/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java index 3ca2f85..20abf4d 100644 --- a/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java +++ b/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java @@ -18,12 +18,26 @@ private StringEscaper() { * @return The escaped string */ public static String escape(final String value) { - return value - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + if (value == null || value.isEmpty()) { + return value; + } + + final int len = value.length(); + final StringBuilder sb = new StringBuilder(len + 16); + + for (int i = 0; i < len; i++) { + final char c = value.charAt(i); + switch (c) { + case '\\' -> sb.append("\\\\"); + case '"' -> sb.append("\\\""); + case '\n' -> sb.append("\\n"); + case '\r' -> sb.append("\\r"); + case '\t' -> sb.append("\\t"); + default -> sb.append(c); + } + } + + return sb.toString(); } /** @@ -73,39 +87,6 @@ private static boolean isValidEscapeChar(final char c) { return c == 'n' || c == 'r' || c == 't' || c == '"' || c == '\\'; } - /** - * Escapes special characters in a string and wraps it in quotes. - * Convenience method that combines escape() with quote wrapping. - * - * @param value The string to escape and quote - * @return The escaped and quoted string - */ - public static String escapeAndQuote(final String value) { - if (value == null) { - return "\"null\""; - } - - final StringBuilder sb = new StringBuilder(value.length() + 2); - sb.append('"'); - - for (int i = 0; i < value.length(); i++) { - final char c = value.charAt(i); - switch (c) { - case '"' -> sb.append("\\\""); - case '\\' -> sb.append("\\\\"); - case '\b' -> sb.append("\\b"); - case '\f' -> sb.append("\\f"); - case '\n' -> sb.append("\\n"); - case '\r' -> sb.append("\\r"); - case '\t' -> sb.append("\\t"); - default -> sb.append(c); - } - } - - sb.append('"'); - return sb.toString(); - } - /** * Unescapes a string and removes surrounding quotes if present. * Reverses the escaping applied by {@link #escape(String)}. diff --git a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java index 6a8e551..de94f46 100644 --- a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java +++ b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java @@ -1,6 +1,5 @@ package dev.toonformat.jtoon.util; -import java.util.regex.Pattern; import static dev.toonformat.jtoon.util.Constants.BACKSLASH; import static dev.toonformat.jtoon.util.Constants.COLON; import static dev.toonformat.jtoon.util.Constants.DOUBLE_QUOTE; @@ -11,18 +10,9 @@ /** * Validates strings for safe unquoted usage in TOON format. - * Follows Object Calisthenics principles with guard clauses and single-level - * indentation. + * Uses char-by-char validation for performance instead of regex. */ public final class StringValidator { - private static final Pattern NUMERIC_PATTERN = Pattern.compile("^-?\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?$", - Pattern.CASE_INSENSITIVE); - - private static final Pattern OCTAL_PATTERN = Pattern.compile("^0[0-7]+$"); - private static final Pattern LEADING_ZERO_PATTERN = Pattern.compile("^0\\d+$"); - private static final Pattern UNQUOTED_KEY_PATTERN = Pattern.compile("^[A-Z_][\\w.]*$", Pattern.CASE_INSENSITIVE); - private static final Pattern STRUCTURAL_CHARS = Pattern.compile("[\\[\\]{}]"); - private static final Pattern CONTROL_CHARS = Pattern.compile("[\\n\\r\\t]"); private StringValidator() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); @@ -30,46 +20,53 @@ private StringValidator() { /** * Checks if a string can be safely written without quotes. - * Uses guard clauses and early returns for clarity. + * Uses char-by-char validation for performance. * * @param value the string value to check * @param delimiter the delimiter being used (for validation) * @return true if the string can be safely written without quotes, false otherwise */ public static boolean isSafeUnquoted(final String value, final String delimiter) { - if (isNullOrEmpty(value)) { - return false; - } - - if (isPaddedWithWhitespace(value)) { - return false; - } - - if (looksLikeKeyword(value)) { + if (value == null || value.isEmpty()) { return false; } - if (looksLikeNumber(value)) { - return false; - } + final int len = value.length(); - if (containsColon(value)) { + if (value.charAt(0) == ' ' || value.charAt(len - 1) == ' ') { return false; } - if (containsQuotesOrBackslash(value)) { + if (isKeyword(value)) { return false; } - if (containsStructuralCharacters(value)) { + if (isNumericLike(value)) { return false; } - if (containsControlCharacters(value)) { - return false; + for (int i = 0; i < len; i++) { + final char c = value.charAt(i); + switch (c) { + case ':': + case '"': + case '\\': + case '[': + case ']': + case '{': + case '}': + case '\n': + case '\r': + case '\t': + return false; + default: + if (delimiter.length() == 1 && c == delimiter.charAt(0)) { + return false; + } + } } - return !containsDelimiter(value, delimiter) && !startsWithListMarker(value); + return !value.startsWith(LIST_ITEM_MARKER); } /** @@ -79,140 +76,86 @@ public static boolean isSafeUnquoted(final String value, final String delimiter) * @return true if the key can be used without quotes, false otherwise */ public static boolean isValidUnquotedKey(final String key) { - return UNQUOTED_KEY_PATTERN.matcher(key).matches(); - } + if (key == null || key.isEmpty()) { + return false; + } - private static boolean isNullOrEmpty(final String value) { - return value == null || value.isEmpty(); - } + final int len = key.length(); + final char first = key.charAt(0); + + if (!Character.isJavaIdentifierStart(first) && first != '_') { + return false; + } + + for (int i = 1; i < len; i++) { + final char c = key.charAt(i); + if (!Character.isJavaIdentifierPart(c) && c != '.') { + return false; + } + } - private static boolean isPaddedWithWhitespace(final String value) { - return !value.equals(value.trim()); + return true; } - private static boolean looksLikeKeyword(final String value) { + private static boolean isKeyword(final String value) { return TRUE_LITERAL.equals(value) || FALSE_LITERAL.equals(value) || NULL_LITERAL.equals(value); } - private static boolean looksLikeNumber(final String value) { - return OCTAL_PATTERN.matcher(value).matches() - || LEADING_ZERO_PATTERN.matcher(value).matches() - || NUMERIC_PATTERN.matcher(value).matches(); - } - - private static boolean containsColon(final String value) { - return value.contains(COLON); - } - - static boolean containsQuotesOrBackslash(final String value) { - return value.indexOf(DOUBLE_QUOTE) >= 0 - || value.indexOf(BACKSLASH) >= 0; - } - - private static boolean containsStructuralCharacters(final String value) { - return STRUCTURAL_CHARS.matcher(value).find(); - } - - private static boolean containsControlCharacters(final String value) { - return CONTROL_CHARS.matcher(value).find(); - } - - private static boolean containsDelimiter(final String value, final String delimiter) { - return value.contains(delimiter); - } - - private static boolean startsWithListMarker(final String value) { - return value.startsWith(LIST_ITEM_MARKER); - } - - /** - * Checks if a value needs quotes based on delimiter-aware validation. - * More comprehensive than isSafeUnquoted, handles additional edge cases. - * - * @param value the string value to check - * @param delimiterChar the delimiter character being used - * @return true if the value needs quotes, false otherwise - */ - public static boolean needsQuotes(final String value, final char delimiterChar) { - if (value == null) { - return true; - } - + private static boolean isNumericLike(final String value) { if (value.isEmpty()) { - return true; - } - - // Check for leading/trailing whitespace - if (value.charAt(0) <= ' ' || value.charAt(value.length() - 1) <= ' ') { - return true; - } - - // Check for special keyword values - final String trimmed = value.trim(); - if (trimmed.equals(TRUE_LITERAL) || trimmed.equals(FALSE_LITERAL) - || trimmed.equals(NULL_LITERAL) || looksLikeNumber(trimmed)) { - return true; + return false; } - // Check for structural characters and delimiter - for (int i = 0; i < value.length(); i++) { - final char c = value.charAt(i); - - // Control characters and structural chars - if (c < ' ' || c == ':' || c == '#' || c == '{' || c == '}' - || c == '[' || c == ']' || c == '"' || c == '\'' || c == '-') { - return true; - } - - // Current delimiter - if (c == delimiterChar) { - return true; - } + final int len = value.length(); + int i = 0; - // Comma when comma is the delimiter - if (delimiterChar == ',' && c == ',') { - return true; + if (value.charAt(0) == '-') { + if (len < 2) { + return false; } + i = 1; } - return false; - } + boolean hasDigit = false; + boolean hasDot = false; + boolean hasExponent = false; - /** - * Checks if a key consists only of numeric characters. - * Numeric keys must be quoted to avoid ambiguity. - * - * @param key the key to check - * @return true if the key is purely numeric - */ - public static boolean isNumericKey(final String key) { - if (key == null || key.isEmpty()) { - return false; - } + while (i < len) { + final char c = value.charAt(i); - for (int i = 0; i < key.length(); i++) { - if (!Character.isDigit(key.charAt(i))) { + if (c >= '0' && c <= '9') { + hasDigit = true; + } else if (c == '.') { + if (hasDot || hasExponent || !hasDigit) { + return false; + } + hasDot = true; + hasDigit = false; + } else if (c == 'e' || c == 'E') { + if (!hasDigit || hasExponent) { + return false; + } + hasExponent = true; + hasDigit = false; + if (i + 1 < len) { + final char next = value.charAt(i + 1); + if (next == '+' || next == '-') { + i++; + } + } + } else { return false; } + i++; } - return true; + return hasDigit; } - /** - * Checks if a string contains a hyphen character. - * Hyphens often need special handling (e.g., "-" alone must be quoted). - * - * @param value the string to check - * @return true if the string contains a hyphen - */ - public static boolean containsHyphen(final String value) { - if (value == null) { - return false; - } - - return value.indexOf('-') >= 0; + static boolean containsQuotesOrBackslash(final String value) { + return value.indexOf(DOUBLE_QUOTE) >= 0 + || value.indexOf(BACKSLASH) >= 0; } } diff --git a/src/test/java/dev/toonformat/jtoon/JToonRaceConditionTest.java b/src/test/java/dev/toonformat/jtoon/JToonRaceConditionTest.java index 926bd25..adbfb5e 100644 --- a/src/test/java/dev/toonformat/jtoon/JToonRaceConditionTest.java +++ b/src/test/java/dev/toonformat/jtoon/JToonRaceConditionTest.java @@ -15,6 +15,7 @@ class JToonRaceConditionTest { @Test @DisplayName("Should be thread-safe when encoding and decoding concurrently") + @SuppressWarnings("unchecked") void concurrentEncodeDecode() throws InterruptedException, ExecutionException { int threadCount = 20; int iterationsPerThread = 100; @@ -66,6 +67,7 @@ void concurrentEncodeDecode() throws InterruptedException, ExecutionException { @Test @DisplayName("Should handle different objects concurrently without interference") + @SuppressWarnings("unchecked") void concurrentDifferentObjects() throws InterruptedException, ExecutionException { int threadCount = 10; int iterations = 1000; diff --git a/src/test/java/dev/toonformat/jtoon/decoder/DecodeContextRaceConditionTest.java b/src/test/java/dev/toonformat/jtoon/decoder/DecodeContextRaceConditionTest.java index c86aa32..24219df 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/DecodeContextRaceConditionTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/DecodeContextRaceConditionTest.java @@ -20,22 +20,13 @@ class DecodeContextRaceConditionTest { @Test @DisplayName("Should be thread-safe when decoding multiple inputs concurrently") + @SuppressWarnings("unchecked") void concurrentDecoding() throws InterruptedException, ExecutionException { int threadCount = 20; int iterationsPerThread = 100; ExecutorService executor = Executors.newFixedThreadPool(threadCount); - String toonInput = new StringBuilder() - .append("name: JToon\n") - .append("version: 1.0.0\n") - .append("tags[3]:\n") - .append(" - java\n") - .append(" - json\n") - .append(" - toon\n") - .append("metadata:\n") - .append(" author: dev\n") - .append(" active: true") - .toString(); + String toonInput = "name: JToon\nversion: 1.0.0\ntags[3]:\n - java\n - json\n - toon\nmetadata:\n author: dev\n active: true".formatted(); final List> futures = new ArrayList<>(); @@ -62,6 +53,7 @@ void concurrentDecoding() throws InterruptedException, ExecutionException { @Test @DisplayName("Should handle different inputs concurrently without interference") + @SuppressWarnings("unchecked") void concurrentDifferentInputs() throws InterruptedException, ExecutionException { int threadCount = 10; ExecutorService executor = Executors.newFixedThreadPool(threadCount); diff --git a/src/test/java/dev/toonformat/jtoon/util/StringEscaperEnhancedTest.java b/src/test/java/dev/toonformat/jtoon/util/StringEscaperEnhancedTest.java deleted file mode 100644 index cc47be1..0000000 --- a/src/test/java/dev/toonformat/jtoon/util/StringEscaperEnhancedTest.java +++ /dev/null @@ -1,123 +0,0 @@ -package dev.toonformat.jtoon.util; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Unit tests for enhanced StringEscaper methods. - */ -@DisplayName("StringEscaper - Enhanced Methods") -class StringEscaperEnhancedTest { - - @Test - @DisplayName("escapeAndQuote wraps string in quotes") - void escapeAndQuoteWrapsInQuotes() { - String result = StringEscaper.escapeAndQuote("hello"); - assertEquals("\"hello\"", result); - } - - @Test - @DisplayName("escapeAndQuote escapes backslash") - void escapeAndQuoteEscapesBackslash() { - String result = StringEscaper.escapeAndQuote("hello\\world"); - assertEquals("\"hello\\\\world\"", result); - } - - @Test - @DisplayName("escapeAndQuote escapes double quote") - void escapeAndQuoteEscapesDoubleQuote() { - String result = StringEscaper.escapeAndQuote("hello\"world"); - assertEquals("\"hello\\\"world\"", result); - } - - @Test - @DisplayName("escapeAndQuote escapes newline") - void escapeAndQuoteEscapesNewline() { - String result = StringEscaper.escapeAndQuote("hello\nworld"); - assertEquals("\"hello\\nworld\"", result); - } - - @Test - @DisplayName("escapeAndQuote escapes carriage return") - void escapeAndQuoteEscapesCarriageReturn() { - String result = StringEscaper.escapeAndQuote("hello\rworld"); - assertEquals("\"hello\\rworld\"", result); - } - - @Test - @DisplayName("escapeAndQuote escapes tab") - void escapeAndQuoteEscapesTab() { - String result = StringEscaper.escapeAndQuote("hello\tworld"); - assertEquals("\"hello\\tworld\"", result); - } - - @Test - @DisplayName("escapeAndQuote escapes backspace") - void escapeAndQuoteEscapesBackspace() { - String result = StringEscaper.escapeAndQuote("hello\bworld"); - assertEquals("\"hello\\bworld\"", result); - } - - @Test - @DisplayName("escapeAndQuote escapes form feed") - void escapeAndQuoteEscapesFormFeed() { - String result = StringEscaper.escapeAndQuote("hello\fworld"); - assertEquals("\"hello\\fworld\"", result); - } - - @Test - @DisplayName("escapeAndQuote handles multiple escape characters") - void escapeAndQuoteHandlesMultipleEscapes() { - String input = "line1\nline2\rline3\ttab"; - String result = StringEscaper.escapeAndQuote(input); - assertEquals("\"line1\\nline2\\rline3\\ttab\"", result); - } - - @Test - @DisplayName("escapeAndQuote returns quoted null for null input") - void escapeAndQuoteHandlesNull() { - String result = StringEscaper.escapeAndQuote(null); - assertEquals("\"null\"", result); - } - - @Test - @DisplayName("escapeAndQuote handles empty string") - void escapeAndQuoteHandlesEmptyString() { - String result = StringEscaper.escapeAndQuote(""); - assertEquals("\"\"", result); - } - - @Test - @DisplayName("escapeAndQuote handles string with only escapes") - void escapeAndQuoteHandlesOnlyEscapes() { - String result = StringEscaper.escapeAndQuote("\\n\\r\\t"); - assertEquals("\"\\\\n\\\\r\\\\t\"", result); - } - - @ParameterizedTest - @ValueSource(strings = {"hello", "world", "foo_bar", "123", "a b c"}) - @DisplayName("escapeAndQuote preserves safe strings (just adds quotes)") - void escapeAndQuotePreservesSafeStrings(String input) { - String result = StringEscaper.escapeAndQuote(input); - assertEquals("\"" + input + "\"", result); - } - - @Test - @DisplayName("escapeAndQuote escapes backslash at end of string") - void escapeAndQuoteEscapesTrailingBackslash() { - String result = StringEscaper.escapeAndQuote("path\\"); - assertEquals("\"path\\\\\"", result); - } - - @Test - @DisplayName("escapeAndQuote escapes all standard characters") - void escapeAndQuoteEscapesAllStandardCharacters() { - String input = "\" \\ \n \r \t \b \f"; - String result = StringEscaper.escapeAndQuote(input); - assertEquals("\"\\\" \\\\ \\n \\r \\t \\b \\f\"", result); - } -} diff --git a/src/test/java/dev/toonformat/jtoon/util/StringValidatorEnhancedTest.java b/src/test/java/dev/toonformat/jtoon/util/StringValidatorEnhancedTest.java deleted file mode 100644 index 6e755be..0000000 --- a/src/test/java/dev/toonformat/jtoon/util/StringValidatorEnhancedTest.java +++ /dev/null @@ -1,156 +0,0 @@ -package dev.toonformat.jtoon.util; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Unit tests for enhanced StringValidator methods. - */ -@DisplayName("StringValidator - Enhanced Methods") -class StringValidatorEnhancedTest { - - @ParameterizedTest - @ValueSource(strings = {"-", "hello-world", "foo-bar-baz"}) - @DisplayName("needsQuotes returns true for strings with hyphen") - void needsQuotesReturnsTrueForHyphen(String input) { - assertTrue(StringValidator.needsQuotes(input, ',')); - } - - @ParameterizedTest - @ValueSource(strings = {"hello", "world", "foo_bar", "abc123"}) - @DisplayName("needsQuotes returns false for safe strings") - void needsQuotesReturnsFalseForSafeStrings(String input) { - assertFalse(StringValidator.needsQuotes(input, ',')); - } - - @ParameterizedTest - @ValueSource(strings = {"", " ", " ", " "}) - @DisplayName("needsQuotes returns true for empty or whitespace-only") - void needsQuotesReturnsTrueForEmptyOrWhitespace(String input) { - assertTrue(StringValidator.needsQuotes(input, ',')); - } - - @ParameterizedTest - @ValueSource(strings = {" hello", "hello ", " hello ", "\thello", "hello\t"}) - @DisplayName("needsQuotes returns true for strings with leading/trailing whitespace") - void needsQuotesReturnsTrueForPaddedStrings(String input) { - assertTrue(StringValidator.needsQuotes(input, ',')); - } - - @ParameterizedTest - @ValueSource(strings = {"hello world", "foo bar", "a b c"}) - @DisplayName("needsQuotes returns false for internal spaces without leading/trailing") - void needsQuotesReturnsFalseForInternalSpaces(String input) { - assertFalse(StringValidator.needsQuotes(input, ',')); - } - - @ParameterizedTest - @ValueSource(strings = {"true", "false", "null"}) - @DisplayName("needsQuotes returns true for keyword-like strings (lowercase)") - void needsQuotesReturnsTrueForKeywordsLowercase(String input) { - assertTrue(StringValidator.needsQuotes(input, ',')); - } - - @ParameterizedTest - @ValueSource(strings = {"TRUE", "FALSE", "NULL", "True", "False", "Null"}) - @DisplayName("needsQuotes returns false for uppercase keywords (trim handles them differently)") - void needsQuotesReturnsFalseForUppercaseKeywords(String input) { - assertFalse(StringValidator.needsQuotes(input, ',')); - } - - @ParameterizedTest - @ValueSource(strings = {"123", "-42", "3.14", "1e10", "0.5", "-0"}) - @DisplayName("needsQuotes returns true for numeric strings") - void needsQuotesReturnsTrueForNumericStrings(String input) { - assertTrue(StringValidator.needsQuotes(input, ',')); - } - - @ParameterizedTest - @ValueSource(strings = {"a:b", "key:value", "foo:bar"}) - @DisplayName("needsQuotes returns true for strings with colon") - void needsQuotesReturnsTrueForColon(String input) { - assertTrue(StringValidator.needsQuotes(input, ',')); - } - - @ParameterizedTest - @ValueSource(strings = {"{key}", "[array]", "{foo}", "[bar]"}) - @DisplayName("needsQuotes returns true for strings with structural characters") - void needsQuotesReturnsTrueForStructuralChars(String input) { - assertTrue(StringValidator.needsQuotes(input, ',')); - } - - @Test - @DisplayName("needsQuotes returns true for null input") - void needsQuotesReturnsTrueForNull() { - assertTrue(StringValidator.needsQuotes(null, ',')); - } - - @ParameterizedTest - @ValueSource(strings = {"a|b", "foo|bar"}) - @DisplayName("needsQuotes returns true when delimiter is pipe and string contains pipe") - void needsQuotesReturnsTrueForPipeDelimiter(String input) { - assertTrue(StringValidator.needsQuotes(input, '|')); - } - - @ParameterizedTest - @ValueSource(strings = {"a,b", "foo,bar"}) - @DisplayName("needsQuotes returns true when delimiter is comma and string contains comma") - void needsQuotesReturnsTrueForCommaDelimiter(String input) { - assertTrue(StringValidator.needsQuotes(input, ',')); - } - - @ParameterizedTest - @ValueSource(strings = {"a,b", "foo,bar"}) - @DisplayName("needsQuotes returns false when delimiter is pipe and string contains comma") - void needsQuotesReturnsFalseWhenDifferentDelimiter(String input) { - assertFalse(StringValidator.needsQuotes(input, '|')); - } - - @Test - @DisplayName("isNumericKey returns true for purely numeric strings") - void isNumericKeyReturnsTrueForNumeric() { - assertTrue(StringValidator.isNumericKey("123")); - assertTrue(StringValidator.isNumericKey("0")); - assertTrue(StringValidator.isNumericKey("999999")); - } - - @ParameterizedTest - @ValueSource(strings = {"", "12a", "a12", "1a2", "abc", "hello", "123.456"}) - @DisplayName("isNumericKey returns false for non-numeric strings") - void isNumericKeyReturnsFalseForNonNumeric(String input) { - assertFalse(StringValidator.isNumericKey(input)); - } - - @Test - @DisplayName("isNumericKey returns false for null") - void isNumericKeyReturnsFalseForNull() { - assertFalse(StringValidator.isNumericKey(null)); - } - - @Test - @DisplayName("containsHyphen returns true for strings with hyphen") - void containsHyphenReturnsTrue() { - assertTrue(StringValidator.containsHyphen("-")); - assertTrue(StringValidator.containsHyphen("hello-world")); - assertTrue(StringValidator.containsHyphen("-prefix")); - assertTrue(StringValidator.containsHyphen("suffix-")); - } - - @Test - @DisplayName("containsHyphen returns false for strings without hyphen") - void containsHyphenReturnsFalse() { - assertFalse(StringValidator.containsHyphen("hello")); - assertFalse(StringValidator.containsHyphen("world")); - assertFalse(StringValidator.containsHyphen("")); - } - - @Test - @DisplayName("containsHyphen returns false for null") - void containsHyphenReturnsFalseForNull() { - assertFalse(StringValidator.containsHyphen(null)); - } -} From b7d8c8aca3bc6b74ecc9912966f651b9ca22e08a Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Sun, 15 Feb 2026 18:51:31 +0100 Subject: [PATCH 26/29] harding --- src/main/java/dev/toonformat/jtoon/JToon.java | 9 +++++++++ .../java/dev/toonformat/jtoon/encoder/ValueEncoder.java | 7 ++++++- .../dev/toonformat/jtoon/normalizer/JsonNormalizer.java | 9 ++++++--- .../toonformat/jtoon/normalizer/JsonNormalizerTest.java | 4 ++-- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/java/dev/toonformat/jtoon/JToon.java b/src/main/java/dev/toonformat/jtoon/JToon.java index cd46f97..d0c2157 100644 --- a/src/main/java/dev/toonformat/jtoon/JToon.java +++ b/src/main/java/dev/toonformat/jtoon/JToon.java @@ -4,6 +4,7 @@ import dev.toonformat.jtoon.encoder.ValueEncoder; import dev.toonformat.jtoon.normalizer.JsonNormalizer; import tools.jackson.databind.JsonNode; +import java.util.Objects; /** * Main entry point for encoding and decoding TOON (Token-Oriented Object Notation) format. @@ -41,8 +42,10 @@ public static String encode(final Object input) { * @param input The object to encode (can be null) * @param options Encoding options (indent, delimiter, length marker) * @return The JToon-formatted string + * @throws NullPointerException if options is null */ public static String encode(final Object input, final EncodeOptions options) { + Objects.requireNonNull(options, "EncodeOptions cannot be null"); final JsonNode normalizedValue = JsonNormalizer.normalize(input); return ValueEncoder.encodeValue(normalizedValue, options); } @@ -75,8 +78,10 @@ public static String encodeJson(final String json) { * @param options Encoding options (indent, delimiter, length marker) * @return The TOON-formatted string * @throws IllegalArgumentException if the input is not valid JSON + * @throws NullPointerException if options is null */ public static String encodeJson(final String json, final EncodeOptions options) { + Objects.requireNonNull(options, "EncodeOptions cannot be null"); final JsonNode parsed = JsonNormalizer.parse(json); return ValueEncoder.encodeValue(parsed, options); } @@ -111,8 +116,10 @@ public static Object decode(final String toon) { * @return Parsed object (Map, List, primitive, or null) * @throws IllegalArgumentException if strict mode is enabled and input is * invalid + * @throws NullPointerException if options is null */ public static Object decode(final String toon, final DecodeOptions options) { + Objects.requireNonNull(options, "DecodeOptions cannot be null"); return ValueDecoder.decode(toon, options); } @@ -148,8 +155,10 @@ public static String decodeToJson(final String toon) { * @return JSON string representation * @throws IllegalArgumentException if strict mode is enabled and input is * invalid + * @throws NullPointerException if options is null */ public static String decodeToJson(final String toon, final DecodeOptions options) { + Objects.requireNonNull(options, "DecodeOptions cannot be null"); return ValueDecoder.decodeToJson(toon, options); } } diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ValueEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ValueEncoder.java index 8c00180..2b558f4 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ValueEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ValueEncoder.java @@ -20,11 +20,16 @@ private ValueEncoder() { /** * Encodes a normalized JsonNode value to TOON format. * - * @param value The JsonNode to encode + * @param value The JsonNode to encode (can be null) * @param options Encoding options (indent, delimiter, length marker) * @return The TOON-formatted string */ public static String encodeValue(final JsonNode value, final EncodeOptions options) { + // Handle null values + if (value == null || value.isNull()) { + return "null"; + } + // Handle primitive values directly if (value.isValueNode()) { return PrimitiveEncoder.encodePrimitive(value, options.delimiter().toString()); diff --git a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java index fde0f90..c1cf3d6 100644 --- a/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java +++ b/src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java @@ -70,13 +70,16 @@ private JsonNormalizer() { * @throws IllegalArgumentException if the input is blank or not valid JSON */ public static JsonNode parse(final String json) { - if (json == null || json.isBlank()) { - throw new IllegalArgumentException("Invalid JSON"); + if (json == null) { + throw new IllegalArgumentException("JSON string cannot be null"); + } + if (json.isBlank()) { + throw new IllegalArgumentException("JSON string cannot be blank"); } try { return MAPPER.readTree(json); } catch (Exception e) { - throw new IllegalArgumentException("Invalid JSON", e); + throw new IllegalArgumentException("Invalid JSON: " + e.getMessage(), e); } } diff --git a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java index 0c5bcfa..a13c328 100644 --- a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java +++ b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java @@ -1920,7 +1920,7 @@ void parseNullAsString() { // Then - assertEquals("Invalid JSON", thrown.getMessage()); + assertEquals("JSON string cannot be null", thrown.getMessage()); } @@ -1935,7 +1935,7 @@ void parseEmptyString() { // Then - assertEquals("Invalid JSON", thrown.getMessage()); + assertEquals("JSON string cannot be blank", thrown.getMessage()); } } From dfd889d04ef8829dd0c595cee918e271fe280151 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Fri, 20 Feb 2026 21:38:23 +0100 Subject: [PATCH 27/29] fix PMD warnings --- pmd-rules.xml | 3 +++ src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java | 4 ++-- src/main/java/dev/toonformat/jtoon/util/StringValidator.java | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pmd-rules.xml b/pmd-rules.xml index 4c41fed..de98618 100644 --- a/pmd-rules.xml +++ b/pmd-rules.xml @@ -11,6 +11,9 @@ + + + diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java index 33f37fe..507d87a 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java @@ -4,9 +4,9 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; -import java.util.ArrayList; + import java.util.List; -import java.util.stream.StreamSupport; + import static dev.toonformat.jtoon.util.Constants.LIST_ITEM_PREFIX; import static dev.toonformat.jtoon.util.Constants.SPACE; diff --git a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java index de94f46..935bed5 100644 --- a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java +++ b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java @@ -1,7 +1,6 @@ package dev.toonformat.jtoon.util; import static dev.toonformat.jtoon.util.Constants.BACKSLASH; -import static dev.toonformat.jtoon.util.Constants.COLON; import static dev.toonformat.jtoon.util.Constants.DOUBLE_QUOTE; import static dev.toonformat.jtoon.util.Constants.FALSE_LITERAL; import static dev.toonformat.jtoon.util.Constants.LIST_ITEM_MARKER; From a9b5f4bbce351f0dd406c382d4bf16a7b3fecc2e Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Fri, 20 Feb 2026 21:52:03 +0100 Subject: [PATCH 28/29] update PMD to 7.21.0 adn rerun --- build.gradle | 2 +- pmd-rules.xml | 7 ++----- .../java/dev/toonformat/jtoon/encoder/ObjectEncoder.java | 4 +++- .../dev/toonformat/jtoon/util/ObjectMapperSingleton.java | 3 ++- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index b054462..eff703f 100644 --- a/build.gradle +++ b/build.gradle @@ -84,7 +84,7 @@ tasks.spotbugsTest { } pmd { - toolVersion = '7.0.0' + toolVersion = '7.21.0' ruleSetFiles = files('pmd-rules.xml') ruleSets = [] // Disable default rulesets, use custom file only consoleOutput = true diff --git a/pmd-rules.xml b/pmd-rules.xml index de98618..b5d8556 100644 --- a/pmd-rules.xml +++ b/pmd-rules.xml @@ -12,11 +12,7 @@ - - - - @@ -43,13 +39,13 @@ - + @@ -67,6 +63,7 @@ + diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java index 9b87dbc..d5d3732 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ObjectEncoder.java @@ -105,11 +105,13 @@ public static void encodeKeyValuePair(final String key, EncodeOptions currentOptions = options; // Attempt key folding when enabled + final KeyFolding flattenMode = currentOptions.flatten(); if (remainingDepth > 0 && !siblings.isEmpty() && blockedKeys != null && !blockedKeys.contains(key) - && KeyFolding.SAFE.equals(currentOptions.flatten())) { + && flattenMode != null + && flattenMode == KeyFolding.SAFE) { final Flatten.FoldResult foldResult = Flatten.tryFoldKeyChain(key, value, siblings, rootLiteralKeys, pathPrefix, remainingDepth); if (foldResult != null) { diff --git a/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java b/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java index 8c0d8be..3b41338 100644 --- a/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java +++ b/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java @@ -31,13 +31,14 @@ public static ObjectMapper getInstance() { synchronized (ObjectMapperSingleton.class) { result = instance; if (result == null) { - instance = result = JsonMapper.builder() + result = JsonMapper.builder() .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.ALWAYS)) .addModule(new BlackbirdModule()) // Speeds up Jackson by 20-40% // using MethodHandles (faster than Afterburner) .defaultTimeZone(TimeZone.getTimeZone("UTC")) // set a default timezone for dates .disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) .build(); + instance = result; } } } From 062d23ce818e3e56056489c673100a5de60898c4 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Fri, 20 Feb 2026 22:21:51 +0100 Subject: [PATCH 29/29] update PMD to 7.21.0 adn rerun --- pmd-rules-test.xml | 14 +++++++++++--- .../dev/toonformat/jtoon/encoder/ArrayEncoder.java | 2 -- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pmd-rules-test.xml b/pmd-rules-test.xml index 2811908..0935b71 100644 --- a/pmd-rules-test.xml +++ b/pmd-rules-test.xml @@ -11,13 +11,15 @@ - - - + + + + + @@ -28,6 +30,7 @@ + @@ -70,6 +73,7 @@ + @@ -81,6 +85,7 @@ + @@ -102,6 +107,8 @@ + + @@ -120,6 +127,7 @@ + diff --git a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java index 507d87a..bd27a95 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/ArrayEncoder.java @@ -4,9 +4,7 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; - import java.util.List; - import static dev.toonformat.jtoon.util.Constants.LIST_ITEM_PREFIX; import static dev.toonformat.jtoon.util.Constants.SPACE;