diff --git a/ .gitattributes b/.gitattributes similarity index 100% rename from .gitattributes rename to .gitattributes 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' 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/.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 diff --git a/build.gradle b/build.gradle index 03323a5..ed82cd5 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,10 @@ plugins { id 'maven-publish' id 'signing' id 'jacoco' + 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' @@ -35,13 +39,105 @@ 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.9.8' + 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") + } + } + ignoreFailures = true +} + +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 + } +} + +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 + ignoreFailures = true +} + 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.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' + 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 { @@ -79,6 +175,9 @@ 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' @@ -100,3 +199,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() + } +} + diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..b8f2e90 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pmd-rules-test.xml b/pmd-rules-test.xml new file mode 100644 index 0000000..2811908 --- /dev/null +++ b/pmd-rules-test.xml @@ -0,0 +1,127 @@ + + + + + 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml new file mode 100644 index 0000000..b9311e6 --- /dev/null +++ b/spotbugs-exclude.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/dev/toonformat/jtoon/DecodeOptions.java b/src/main/java/dev/toonformat/jtoon/DecodeOptions.java index 8163247..452676e 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); @@ -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 4e5ebe0..6825120 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(final String delimiterValue) { + this.value = delimiterValue; } /** @@ -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/EncodeOptions.java b/src/main/java/dev/toonformat/jtoon/EncodeOptions.java index 43ae51d..c522826 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. @@ -39,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); } @@ -50,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); } @@ -61,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); } @@ -72,18 +73,19 @@ 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) { - return new EncodeOptions(2, Delimiter.COMMA, false, flatten ? KeyFolding.SAFE : KeyFolding.OFF, Integer.MAX_VALUE); + public static EncodeOptions withFlatten(final boolean flatten) { + 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. */ - 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 518de7f..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,8 +42,8 @@ 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) { - JsonNode normalizedValue = JsonNormalizer.normalize(input); + 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,8 +78,8 @@ 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) { - JsonNode parsed = JsonNormalizer.parse(json); + 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/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..4c4accf 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"); } @@ -32,8 +32,8 @@ 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) { - Delimiter arrayDelimiter = extractDelimiterFromHeader(header, 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,10 +46,10 @@ 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) { - Matcher matcher = ARRAY_HEADER_PATTERN.matcher(header); + static Delimiter extractDelimiterFromHeader(final String header, final DecodeContext context) { + 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(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); 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; } @@ -137,8 +138,8 @@ static List parseArrayWithDelimiter(String header, int depth, Delimiter * @param header header * @param actualLength actual length */ - static void validateArrayLength(String header, int actualLength) { - Integer declaredLength = extractLengthFromHeader(header); + static void validateArrayLength(final String header, final int actualLength) { + final Integer declaredLength = extractLengthFromHeader(header); if (declaredLength != null && declaredLength != actualLength) { throw new IllegalArgumentException( String.format("Array length mismatch: declared %d, found %d", declaredLength, actualLength)); @@ -150,12 +151,12 @@ static void validateArrayLength(String header, 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(String header) { - Matcher matcher = ARRAY_HEADER_PATTERN.matcher(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)); + return Integer.valueOf(matcher.group(2)); } return null; } @@ -167,10 +168,10 @@ private static Integer extractLengthFromHeader(String header) { * @param arrayDelimiter array delimiter * @return parsed array values */ - static List parseArrayValues(String values, Delimiter arrayDelimiter) { - List result = new ArrayList<>(); - List rawValues = parseDelimitedValues(values, arrayDelimiter); - for (String value : rawValues) { + static List parseArrayValues(final String values, final Delimiter arrayDelimiter) { + final List rawValues = parseDelimitedValues(values, arrayDelimiter); + final List result = new ArrayList<>(rawValues.size()); + for (final String value : rawValues) { result.add(PrimitiveDecoder.parse(value)); } return result; @@ -184,16 +185,16 @@ static List parseArrayValues(String values, Delimiter arrayDelimiter) { * @param arrayDelimiter array delimiter * @return parsed delimited values */ - static List parseDelimitedValues(String input, Delimiter arrayDelimiter) { - List result = new ArrayList<>(); - StringBuilder stringBuilder = new StringBuilder(); + static List parseDelimitedValues(final String input, final Delimiter arrayDelimiter) { + 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++; @@ -234,20 +235,20 @@ 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) { - List result = new ArrayList<>(); + private static List parseListArray(final int depth, final String header, final DecodeContext context) { + 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 { @@ -270,14 +271,14 @@ 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) { - int nextNonBlankLine = DecodeHelper.findNextNonBlankLine(context.currentLine + 1, 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) { 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 } @@ -299,13 +300,14 @@ 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 } // 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 f319efe..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 { @@ -23,10 +23,10 @@ public class DecodeContext { /** * Current line being decoded. */ - protected int currentLine = 0; + 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 f0db743..cfc294a 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; @@ -28,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; @@ -44,14 +42,14 @@ 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) { - int indentSize = context.options.indent(); + private static int computeLeadingSpaces(final String line, final DecodeContext context) { + 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()) { @@ -67,7 +65,7 @@ private static int computeLeadingSpaces(String line, DecodeContext context) { 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)); @@ -83,8 +81,8 @@ 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) { - return line.trim().isEmpty(); + static boolean isBlankLine(final String line) { + return line.isBlank(); } /** @@ -94,21 +92,21 @@ 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; for (int i = 0; i < content.length(); i++) { - char c = content.charAt(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; } } @@ -122,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++; @@ -139,7 +137,8 @@ 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)) { @@ -164,12 +163,13 @@ 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; } - Object existing = map.get(key); + final Object existing = map.get(key); checkFinalValueConflict(key, existing, value, context); } @@ -179,7 +179,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++; @@ -198,13 +198,13 @@ 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++; } 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 8d90be5..7dd4938 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/KeyDecoder.java @@ -3,12 +3,11 @@ 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 java.util.regex.MatchResult; import static dev.toonformat.jtoon.util.Constants.DOT; import static dev.toonformat.jtoon.util.Headers.KEYED_ARRAY_PATTERN; @@ -30,11 +29,11 @@ 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) { - String key = StringEscaper.unescape(originalKey); - String arrayHeader = content.substring(originalKey.length()); - List arrayValue = ArrayDecoder.parseArray(arrayHeader, parentDepth + 1, 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); // Handle path expansion for array keys if (shouldExpandKey(originalKey, context)) { @@ -54,24 +53,26 @@ 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(final Map current, final String dottedKey, final Object value, + final 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 = current.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<>(); - current.put(segment, nested); - current = nested; + 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; - current = existingMap; + final Map existingMap = (Map) existing; + currentMap = existingMap; } else { // Conflict: existing is not a Map if (context.options.strict()) { @@ -80,23 +81,25 @@ static void expandPathIntoMap(Map current, String dottedKey, Obj segment, existing.getClass().getSimpleName())); } // LWW: overwrite with new nested object - Map nested = new LinkedHashMap<>(); - current.put(segment, nested); - current = nested; + final Map nested = new LinkedHashMap<>(); + currentMap.put(segment, nested); + currentMap = nested; } } // Set the final value - String finalSegment = segments[segments.length - 1]; - Object existing = current.get(finalSegment); + final String finalSegment = segments[segments.length - 1]; + final 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"). * @@ -105,12 +108,13 @@ 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) { - int colonIdx = DecodeHelper.findUnquotedColon(content); + static void processKeyValueLine(final Map result, final String content, + final int depth, final DecodeContext context) { + 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 @@ -131,11 +135,11 @@ 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) { - String unescapedKey = StringEscaper.unescape(key); + static void parseKeyValuePairIntoMap(final Map map, final String key, final String value, + final int depth, final DecodeContext context) { + 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); } @@ -148,7 +152,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; } @@ -163,7 +167,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; @@ -180,18 +184,18 @@ 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) { - 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; - if (value.trim().isEmpty()) { + final Object parsedValue; + if (value.isBlank()) { parsedValue = new LinkedHashMap<>(); } else { parsedValue = PrimitiveDecoder.parse(value); @@ -201,8 +205,8 @@ 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()) { + final Object parsedValue; + if (value.isBlank()) { parsedValue = new LinkedHashMap<>(); } else { parsedValue = PrimitiveDecoder.parse(value); @@ -221,8 +225,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); @@ -242,9 +246,9 @@ 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) { - Map obj = new LinkedHashMap<>(); + 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); if (parseRootFields) { @@ -262,13 +266,15 @@ 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) { - String originalKey = keyedArray.group(1).trim(); - String key = StringEscaper.unescape(originalKey); - String arrayHeader = content.substring(keyedArray.group(1).length()); + 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(group1.length()); - var 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)) { @@ -296,19 +302,22 @@ 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(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; } - String originalKey = keyedArray.group(1).trim(); - String key = StringEscaper.unescape(originalKey); - String arrayHeader = fieldContent.substring(keyedArray.group(1).length()); + final String group1 = keyedArray.group(1); + final String originalKey = group1.trim(); + final String key = StringEscaper.unescape(originalKey); + final String arrayHeader = fieldContent.substring(group1.length()); // 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); + 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)) { @@ -330,16 +339,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(final String fieldContent, final Map item, final int depth, + final 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 2d5c118..42b3806 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ListItemDecoder.java @@ -2,12 +2,11 @@ import dev.toonformat.jtoon.Delimiter; import dev.toonformat.jtoon.util.StringEscaper; - +import java.util.Collection; 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; @@ -17,7 +16,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. @@ -28,10 +29,10 @@ public final class 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 Collection result, final 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)); @@ -52,9 +53,9 @@ 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) { + static Object parseListItem(final String content, final int depth, final DecodeContext context) { // Handle empty item: just "-" or "- " - String itemContent; + final String itemContent; if (content.length() > 2) { itemContent = content.substring(2).trim(); } else { @@ -70,7 +71,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 +82,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 +107,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,16 +116,16 @@ 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.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); @@ -138,19 +141,20 @@ public static Object parseListItem(String content, int depth, DecodeContext cont * * @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(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) { - 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/ObjectDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java index c1f23a1..6ff16b2 100644 --- a/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java +++ b/src/main/java/dev/toonformat/jtoon/decoder/ObjectDecoder.java @@ -1,11 +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; /** @@ -24,11 +23,11 @@ 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) { - Map result = new LinkedHashMap<>(); + static Map parseNestedObject(final int parentDepth, final DecodeContext context) { + 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)) { @@ -36,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; @@ -57,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(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); if (keyedArray.find()) { KeyDecoder.processKeyedArrayLine(result, content, keyedArray.group(1), parentDepth, context); @@ -75,10 +75,10 @@ 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) { - 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; @@ -90,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 { @@ -118,13 +118,14 @@ 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) { - String originalKeyTrimmed = originalKey.trim(); - String key = StringEscaper.unescape(originalKey); - String arrayHeader = content.substring(originalKey.length()); + 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()); - var 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)) { @@ -144,12 +145,12 @@ 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) { - Object result = PrimitiveDecoder.parse(content); + static Object parseBareScalarValue(final String content, final int depth, final DecodeContext context) { + final Object result = PrimitiveDecoder.parse(content); 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); } @@ -164,17 +165,17 @@ 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) { - 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 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 { @@ -204,11 +205,11 @@ 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) { - boolean isEmpty = value.trim().isEmpty(); + 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 - 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..d7a2c3a 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; @@ -46,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 ""; } @@ -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; } @@ -83,17 +82,17 @@ static Object parse(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")) { - double parsed = Double.parseDouble(value); + 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 60a3dc3..c75a321 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; @@ -34,22 +32,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(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(); } - 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); } @@ -74,15 +73,16 @@ 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); } - List result = new ArrayList<>(); - List rawValues = ArrayDecoder.parseDelimitedValues(keysStr, arrayDelimiter); - for (String key : rawValues) { + final List rawValues = ArrayDecoder.parseDelimitedValues(keysStr, arrayDelimiter); + final List result = new ArrayList<>(rawValues.size()); + for (final String key : rawValues) { result.add(StringEscaper.unescape(key)); } return result; @@ -94,13 +94,13 @@ 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) { - char expectedChar = expectedDelimiter.toString().charAt(0); + private static void validateKeysDelimiter(final String keysStr, final Delimiter expectedDelimiter) { + 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) { @@ -119,17 +119,17 @@ 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"); + 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, brace fields use comma"); + 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,15 +145,16 @@ 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) { - String line = context.lines[context.currentLine]; + 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)) { 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; } @@ -171,13 +172,13 @@ private static boolean processTabularArrayLine(int expectedRowDepth, List 0) { return true; // Key-value pair at the same depth-terminate an array } @@ -219,8 +221,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 } @@ -239,11 +241,12 @@ private static boolean shouldTerminateTabularArray(String line, int lineDepth, i * @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(String line, int lineDepth, 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) { - 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) { @@ -264,9 +267,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(final String rowContent, final List keys, + final Delimiter arrayDelimiter, final 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/decoder/ValueDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java index 129b3c6..b147060 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; @@ -47,20 +45,22 @@ private ValueDecoder() { * @throws IllegalArgumentException if strict mode is enabled and input is * invalid */ - public static Object decode(String toon, DecodeOptions options) { - if (toon == null || toon.trim().isEmpty()) { + public static Object decode(final String toon, final DecodeOptions options) { + if (toon == null || toon.isBlank()) { return new LinkedHashMap<>(); } // 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); } @@ -116,9 +116,9 @@ 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 { - 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 1e3404f..d562d46 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; @@ -32,9 +30,10 @@ 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()) { - 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 +47,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 +59,7 @@ public static void encodeArray(String key, ArrayNode value, LineWriter writer, i // Array of objects if (isArrayOfObjects(value)) { - var header = TabularArrayEncoder.detectTabularHeader(value); + final List header = TabularArrayEncoder.detectTabularHeader(value); if (!header.isEmpty()) { TabularArrayEncoder.encodeArrayOfObjectsAsTabular(key, value, header, writer, depth, options); } else { @@ -79,7 +78,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; } @@ -97,7 +96,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) { + static boolean isArrayOfArrays(final JsonNode array) { if (!array.isArray()) { return false; } @@ -115,7 +114,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,11 +127,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()); + 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); } @@ -145,12 +145,13 @@ 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) { - List valueList = new ArrayList<>(); + 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); - 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()) { @@ -162,16 +163,16 @@ 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) { - String header = PrimitiveEncoder.formatHeader(values.size(), prefix, null, options.delimiter().toString(), - options.lengthMarker()); + 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); 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); } } @@ -180,13 +181,13 @@ 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) { - String header = PrimitiveEncoder.formatHeader(items.size(), prefix, null, options.delimiter().toString(), - options.lengthMarker()); + 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); for (JsonNode item : items) { @@ -197,23 +198,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 fc8bc7a..3126c0c 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; /** @@ -17,12 +15,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. * @@ -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) @@ -65,19 +63,19 @@ 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; } // 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)) { @@ -107,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; @@ -127,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,9 +136,11 @@ 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 - String localStartKey = startKey.contains(DOT) + final String localStartKey = startKey.contains(DOT) ? startKey.substring(startKey.lastIndexOf(DOT.charAt(0)) + 1) : startKey; @@ -151,16 +151,16 @@ private static ChainResult collectSingleKeyChain(String startKey, JsonNode start // track depth of folding int depthCounter = 1; - while (currentValue.isObject() && depthCounter < maxDepth) { + while (depthCounter < maxDepth && currentValue.isObject()) { 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 164e3d6..2baec22 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/HeaderFormatter.java @@ -1,7 +1,7 @@ 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; import static dev.toonformat.jtoon.util.Constants.COMMA; @@ -42,8 +42,8 @@ public record HeaderConfig( * @param config Header configuration * @return Formatted header string */ - public static String format(HeaderConfig config) { - StringBuilder header = new StringBuilder(); + static String format(final HeaderConfig config) { + final StringBuilder header = new StringBuilder(); appendKeyIfPresent(header, config.key()); appendArrayLength(header, config.length(), config.delimiter(), config.lengthMarker()); @@ -64,26 +64,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) { - HeaderConfig config = new HeaderConfig(length, key, fields, delimiter, 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) { @@ -95,27 +95,26 @@ private static void appendArrayLength( header.append(CLOSE_BRACKET); } - private static void appendDelimiterIfNotDefault(StringBuilder header, String delimiter) { - if (!delimiter.equals(COMMA)) { + 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 Collection fields, + final String delimiter) { if (fields == null || fields.isEmpty()) { return; } header.append(OPEN_BRACE); - String quotedFields = formatFields(fields, delimiter); - header.append(quotedFields); + header.append(formatFields(fields, delimiter)); header.append(CLOSE_BRACE); } - private static String formatFields(List fields, 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/LineWriter.java b/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java index 59850b5..8ab1631 100644 --- a/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java +++ b/src/main/java/dev/toonformat/jtoon/encoder/LineWriter.java @@ -1,44 +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) { - 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 2424f05..9a1bf75 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; @@ -37,8 +35,11 @@ private ListItemEncoder() { * @param depth Indentation depth * @param options Encoding options */ - public static void encodeObjectAsListItem(ObjectNode obj, LineWriter writer, int depth, EncodeOptions options) { - List keys = new ArrayList<>(obj.propertyNames()); + 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()) { writer.push(depth, LIST_ITEM_MARKER); @@ -46,14 +47,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<>()); } } @@ -61,9 +63,12 @@ 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) { - String encodedKey = PrimitiveEncoder.encodeKey(key); + 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()) { encodeFirstValueAsPrimitive(encodedKey, value, writer, depth, options); @@ -74,14 +79,21 @@ 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)) { @@ -91,19 +103,27 @@ 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()); + 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) { - List header = TabularArrayEncoder.detectTabularHeader(arrayValue); + 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()) { - 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 +138,11 @@ 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) { @@ -127,8 +150,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); @@ -136,8 +159,11 @@ 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 fabf737..8b9185a 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,15 @@ 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(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 if (depth == 0 && rootLiteralKeys != null) { @@ -49,15 +54,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); } } @@ -75,51 +81,58 @@ 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; } - 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 - if (KeyFolding.SAFE.equals(options.flatten()) + if (remainingDepth > 0 && !siblings.isEmpty() - && remainingDepth > 0 && blockedKeys != null - && !blockedKeys.contains(key)) { - Flatten.FoldResult foldResult = Flatten.tryFoldKeyChain(key, value, siblings, rootLiteralKeys, pathPrefix, remainingDepth); + && !blockedKeys.contains(key) + && KeyFolding.SAFE.equals(currentOptions.flatten())) { + final 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; + final 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); } } } @@ -138,53 +151,68 @@ 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(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; // 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) { - 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; + 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; - 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) { - JsonNode leaf = foldResult.leafValue(); + 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 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; } @@ -199,13 +227,12 @@ 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); } } } - 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 8675bd2..e3f50cf 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.Collection; 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; @@ -18,17 +16,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); @@ -42,14 +43,14 @@ 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(); } - 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); } @@ -59,28 +60,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) { + static String encodeStringLiteral(final String value, final String delimiter) { if (StringValidator.isSafeUnquoted(value, delimiter)) { return value; } @@ -91,10 +97,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; } @@ -104,22 +111,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 Collection 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 @@ -128,11 +148,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 0a9552f..e4ce900 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; @@ -26,18 +25,18 @@ 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(); } - 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(); @@ -53,22 +52,27 @@ 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 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; } - 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()) { + 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; } @@ -91,10 +95,11 @@ 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) { - String headerStr = PrimitiveEncoder.formatHeader(rows.size(), prefix, header, options.delimiter().toString(), - options.lengthMarker()); + 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); writeTabularRows(rows, header, writer, depth + 1, options); @@ -110,19 +115,25 @@ 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 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; } - ObjectNode obj = (ObjectNode) row; - List values = new ArrayList<>(); - for (String key : header) { + final ObjectNode obj = (ObjectNode) row; + final List values = new ArrayList<>(headerSize); + for (String key : headerList) { 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..8c00180 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; @@ -25,19 +24,19 @@ 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()); } // 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 52581f5..3de03c6 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; @@ -15,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; @@ -72,8 +70,8 @@ private JsonNormalizer() { * @return Parsed JsonNode * @throws IllegalArgumentException if the input is blank or not valid JSON */ - public static JsonNode parse(String json) { - if (json == null || json.trim().isEmpty()) { + public static JsonNode parse(final String json) { + if (json == null || json.isBlank()) { throw new IllegalArgumentException("Invalid JSON"); } try { @@ -89,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) { @@ -108,19 +106,19 @@ 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) .findFirst() - .orElse(NullNode.getInstance()); + .orElseGet(NullNode::getInstance); } /** * 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) { @@ -145,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(); } @@ -153,13 +151,13 @@ private static JsonNode normalizeDouble(Double value) { return IntNode.valueOf(0); } return tryConvertToLong(value) - .orElse(DoubleNode.valueOf(value)); + .orElseGet(() -> DoubleNode.valueOf(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(); @@ -168,14 +166,14 @@ 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(); } 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)); } @@ -183,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) { @@ -196,8 +194,8 @@ 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 + 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 ? LongNode.valueOf(value.longValue()) @@ -208,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) { @@ -239,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)); } @@ -247,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) { @@ -260,8 +258,8 @@ private static JsonNode tryNormalizeCollection(Object value) { /** * Normalizes a Collection to an ArrayNode. */ - private static ArrayNode normalizeCollection(Collection collection) { - ArrayNode arrayNode = MAPPER.createArrayNode(); + private static ArrayNode normalizeCollection(final Collection collection) { + final ArrayNode arrayNode = MAPPER.createArrayNode(); collection.forEach(item -> arrayNode.add(normalize(item))); return arrayNode; } @@ -269,8 +267,8 @@ private static ArrayNode normalizeCollection(Collection collection) { /** * Normalizes a Map to an ObjectNode. */ - private static ObjectNode normalizeMap(Map map) { - ObjectNode objectNode = MAPPER.createObjectNode(); + 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; } @@ -279,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) { @@ -290,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) { @@ -317,8 +315,8 @@ 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(); + 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)); } @@ -328,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(); @@ -337,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/Headers.java b/src/main/java/dev/toonformat/jtoon/util/Headers.java index dadf18f..b2b6d16 100644 --- a/src/main/java/dev/toonformat/jtoon/util/Headers.java +++ b/src/main/java/dev/toonformat/jtoon/util/Headers.java @@ -3,28 +3,32 @@ 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 { +public final 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 */ public static final Pattern KEYED_ARRAY_PATTERN = Pattern.compile("^(.+?)\\[(#?)\\d+([\\t|])?](\\{[^}]+})?:.*$"); + private Headers() { + throw new UnsupportedOperationException("Utility class cannot be instantiated"); + } + } diff --git a/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java b/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java index 8d24086..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; /** @@ -15,7 +14,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 +26,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/StringEscaper.java b/src/main/java/dev/toonformat/jtoon/util/StringEscaper.java index de49057..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; } @@ -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()) { @@ -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; } @@ -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()) { @@ -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 9ecee67..65d6b3b 100644 --- a/src/main/java/dev/toonformat/jtoon/util/StringValidator.java +++ b/src/main/java/dev/toonformat/jtoon/util/StringValidator.java @@ -1,8 +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. @@ -31,7 +36,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; } @@ -64,11 +69,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); } /** @@ -77,50 +78,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) { - return value.equals(TRUE_LITERAL) - || value.equals(FALSE_LITERAL) - || value.equals(NULL_LITERAL); + 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) { - return OCTAL_PATTERN.matcher(value).matches() || LEADING_ZERO_PATTERN.matcher(value).matches() || NUMERIC_PATTERN.matcher(value).matches(); + 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); } } 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(); + } +} 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"); } }