diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bec247b..dde6c74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,27 +8,25 @@ on: branches: - main workflow_dispatch: - branches: - - main jobs: build: runs-on: ubuntu-latest strategy: matrix: - java: [ '11' ] + java: [ '21' ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: java-version: ${{ matrix.java }} - distribution: 'adopt' + distribution: 'temurin' - name: Print Java version run: java -version - - name: Gradle wrapper validation - uses: gradle/wrapper-validation-action@v1 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 - name: Run tests - run: ./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --stacktrace clean build + run: ./gradlew --no-daemon --stacktrace clean build diff --git a/README.md b/README.md index 7598a74..a62173f 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Add the following to your `pom.xml`: com.arakelian json-filter - 4.0.1 + 5.0.0 compile ``` @@ -136,7 +136,7 @@ repositories { } dependencies { - compile 'com.arakelian:json-filter:4.0.1' + compile 'com.arakelian:json-filter:5.0.0' } ``` diff --git a/build.gradle b/build.gradle index 278fd50..aba66b0 100644 --- a/build.gradle +++ b/build.gradle @@ -2,47 +2,45 @@ plugins { id 'java-library' - id 'maven-publish' - id 'signing' + id 'java-test-fixtures' id 'eclipse' id 'idea' // keep dependencies up-to-date! - id 'com.github.ben-manes.versions' version '0.46.0' - - // useful for creating immutable java beans - id 'org.inferred.processors' version '3.7.0' + id 'com.github.ben-manes.versions' version '0.53.0' // to ensure clean code - id "net.ltgt.errorprone" version "3.1.0" + id "net.ltgt.errorprone" version "5.0.0" // for deployment to Maven Central - id "io.codearte.nexus-staging" version "0.30.0" + id "com.vanniktech.maven.publish" version "0.36.0" } group = 'com.arakelian' -version = '4.0.1' +version = '5.0.0' apply from: "core.gradle" -wrapper { - gradleVersion = '8.1.1' +wrapper { + gradleVersion = '9.3.1' } -publishing.publications.mavenJava { +mavenPublishing { + coordinates(project.group.toString(), project.name, project.version.toString()) + pom { name = "Json Filter" description = "High-speed stream-based filtering of arbitrarily large JSON documents." url = "https://github.com/arakelian/json-filter" - + licenses { license { name = 'The Apache License, Version 2.0' url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' } } - + developers { developer { id = 'arakelian' @@ -50,7 +48,7 @@ publishing.publications.mavenJava { email = 'greg@arakelian.com' } } - + scm { connection = 'scm:git:https://github.com/arakelian/json-filter.git' developerConnection = 'scm:git:git@github.com:arakelian/json-filter.git' @@ -60,28 +58,29 @@ publishing.publications.mavenJava { } dependencies { - processor 'org.immutables:value:2.9.3' + annotationProcessor 'org.immutables:value:2.10.1' // annotations - api 'org.immutables:value-annotations:2.9.3' + api 'org.immutables:value-annotations:2.10.1' // for date utils - api 'com.arakelian:more-commons:4.0.1' - + api 'com.arakelian:more-commons:5.1.0' + // configure errorprone version - errorprone 'com.google.errorprone:error_prone_core:2.18.0' + errorprone 'com.google.errorprone:error_prone_core:2.36.0' - // we shadow Guava due to frequent downstream compatibility issues - api 'com.google.guava:guava:31.1-jre' + // we use Guava directly + api 'com.google.guava:guava:33.4.0-jre' // logging - testImplementation 'org.apache.logging.log4j:log4j-api:2.20.0' - testImplementation 'org.apache.logging.log4j:log4j-core:2.20.0' - testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.20.0' - testImplementation 'org.slf4j:jcl-over-slf4j:2.0.7' - testImplementation 'org.slf4j:jul-to-slf4j:2.0.7' - api 'org.slf4j:slf4j-api:2.0.7' + testImplementation 'org.apache.logging.log4j:log4j-api:2.24.3' + testImplementation 'org.apache.logging.log4j:log4j-core:2.24.3' + testImplementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.24.3' + testImplementation 'org.slf4j:jcl-over-slf4j:2.0.16' + testImplementation 'org.slf4j:jul-to-slf4j:2.0.16' + api 'org.slf4j:slf4j-api:2.0.16' // for unit testing - testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/core.gradle b/core.gradle index a32e0fc..1eea844 100644 --- a/core.gradle +++ b/core.gradle @@ -1,14 +1,4 @@ -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'com.guardsquare:proguard-gradle:7.2.1' // The ProGuard Gradle plugin. - } -} - - ext { // useful macros, you can add your own macros = [ @@ -16,18 +6,7 @@ ext { 'clean', 'classpath', 'build', - 'minify', - 'generatePomFileForInternalPublication', // needed to generate internal POM - 'publishMavenJavaPublicationToMavenLocal' // real publication to Maven Local (~/.m2/repository) - ], - 'sonatype' : [ - 'generatePomFileForInternalPublication', - 'publishMavenJavaPublicationToOssrhRepository', - 'closeAndReleaseRepository' - ], - 'deploy' : [ - 'uploadArchives', - 'closeAndReleaseRepository' + 'publishToMavenLocal' ], 'classpath' : [ 'cleanEclipseClasspath', @@ -37,14 +16,14 @@ ext { 'ideaModule' ], ] - + // package patterns to exclude from Eclipse excludeFromEclipse = [] } // ------------------------------------------- -// REPOSITORIES / PUBLISHING +// REPOSITORIES // ------------------------------------------- repositories { @@ -55,22 +34,7 @@ repositories { mavenCentral() } -task sourcesJar(type: Jar) { - archiveClassifier.set('sources') - from sourceSets.main.allJava -} - -task javadocJar(type: Jar, dependsOn: classes) { - archiveClassifier.set('javadoc') - from javadoc -} - -task testsJar(type:Jar, dependsOn: testClasses) { - archiveClassifier.set('tests') - from sourceSets.test.output -} - -test { +tasks.named('test') { // enable JUnit 5 tests useJUnitPlatform() } @@ -80,108 +44,37 @@ test { // JAVA COMPILER // ------------------------------------------- -tasks.withType(JavaCompile) { task -> - sourceCompatibility = 11 - targetCompatibility = 11 +tasks.withType(JavaCompile).configureEach { task -> + sourceCompatibility = 21 + targetCompatibility = 21 // always UTF-8 options.encoding = 'UTF-8' - // java 8 option which export names of constructor and method parameter names; no longer - // have to declare parameter names with @JsonCreator + // export names of constructor and method parameter names options.compilerArgs << "-parameters" - + options.compilerArgs << '-Xlint:unchecked' if (project.plugins.hasPlugin('net.ltgt.errorprone')) { // Eclipse code formatting removes extraneous parenthesis which errorprone complains about options.errorprone.disable 'OperatorPrecedence' - + // we don't need to check return value always options.errorprone.disable 'FutureReturnValueIgnored' - + // generated code can have lots of bogus warnings options.errorprone.disableWarningsInGeneratedCode = true - + // bogus warning options.errorprone.disable 'StringSplitter' - + // ignore all generated source folders options.errorprone.excludedPaths = '.*generated.*' } } -// ------------------------------------------- -// SHADOW JAR -// ------------------------------------------- - -if(plugins.hasPlugin("com.github.johnrengelman.shadow")) { - jar { - archiveClassifier.set('original') - } - - shadowJar { - archiveClassifier.set('shadow') - } - - sourceSets { - // shadow configuration is added by Shadow plugin, but it's only configured for the main sourceset - test.compileClasspath += configurations.shadow - test.runtimeClasspath += configurations.shadow - } -} - - -publishing { - publications { - mavenJava(MavenPublication) { - from components.java - } - } -} - -publishing { - if(project.hasProperty('nexusUsername')) { - repositories { - maven { - name = "ossrh" - url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - credentials { - username = project.nexusUsername - password = project.nexusPassword - } - } - } - } - - publications { - mavenJava { - artifact sourcesJar - artifact javadocJar - artifact testsJar - - versionMapping { - usage('java-api') { - fromResolutionOf('runtimeClasspath') - } - usage('java-runtime') { - fromResolutionResult() - } - } - } - } -} - -signing { - sign publishing.publications.mavenJava -} - -nexusStaging { - repositoryDescription project.name -} - - // ------------------------------------------- // ECLIPSE // ------------------------------------------- @@ -248,15 +141,15 @@ eclipse { // final adjustments to .classpath file before it is saved withXml { xml -> def node = xml.asNode() - + project_refs.unique(false).each { name -> println "Creating Eclipse project dependency: " + name node.appendNode('classpathentry', [ combineaccessrules: false, exported: true, kind: 'src', path: '/' + name ]) } - + def apt = ['.apt_generated_test': 'bin/test', '.apt_generated': 'bin/main'] - + apt.each { path, output -> def atts = node.appendNode('classpathentry', [kind:'src', output: output, path: path]).appendNode('attributes') atts.appendNode('attribute', [name: 'ignore_optional_problems', value: true]) @@ -273,12 +166,14 @@ eclipse { // README // ------------------------------------------- -task readme { - ant.replaceregexp(match:'\\([0-9\\.]+)\\<\\/version\\>', replace:"${version}", flags:'g', byline:true) { - fileset(dir: '.', includes: 'README.md') - } - ant.replaceregexp(match:'com\\.arakelian\\:' + project.name + ':([0-9\\.]+)', replace:"com.arakelian:${project.name}:${version}", flags:'g', byline:true) { - fileset(dir: '.', includes: 'README.md') +tasks.register('readme') { + doLast { + ant.replaceregexp(match:'\\([0-9\\.]+)\\<\\/version\\>', replace:"${version}", flags:'g', byline:true) { + fileset(dir: '.', includes: 'README.md') + } + ant.replaceregexp(match:'com\\.arakelian\\:' + project.name + ':([0-9\\.]+)', replace:"com.arakelian:${project.name}:${version}", flags:'g', byline:true) { + fileset(dir: '.', includes: 'README.md') + } } } @@ -310,4 +205,3 @@ gradle.startParameter.taskNames.each { param -> // replace command line arguments gradle.startParameter.taskNames = newTasks.flatten() - \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37aef8d..78dfb56 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/com/arakelian/json/JsonFilter.java b/src/main/java/com/arakelian/json/JsonFilter.java index f7b6a3b..70c7ae7 100644 --- a/src/main/java/com/arakelian/json/JsonFilter.java +++ b/src/main/java/com/arakelian/json/JsonFilter.java @@ -34,6 +34,10 @@ * where only a small number of fields may actually be used. */ public class JsonFilter { + /** + * Predicate that tests whether a given JSON field path should be included in the output based + * on the configured include and exclude patterns. + */ private static final class PathPredicate implements Predicate { /** Path patterns which are included **/ private final Set includes; @@ -165,6 +169,15 @@ private static boolean equals(final CharSequence lhs, final CharSequence rhs) { return true; } + /** + * Filters the given JSON string according to the specified options, returning a new JSON string + * with only the included fields. + * + * @param json the input JSON string + * @param options the filtering options specifying includes, excludes, and formatting + * @return the filtered JSON string, or the original string if no filtering is needed + * @throws IOException if the JSON is invalid or an I/O error occurs + */ public static CharSequence filter(final CharSequence json, final JsonFilterOptions options) throws IOException { if (json == null || json.length() == 0) { @@ -298,6 +311,13 @@ public static CharSequence prettyifyQuietly(final CharSequence str) { */ private final JsonWriter writer; + /** + * Constructs a new {@code JsonFilter} with the given reader, writer, and options. + * + * @param reader the JSON reader to read input from + * @param writer the JSON writer to write filtered output to + * @param options the filtering options specifying includes, excludes, and formatting + */ public JsonFilter(final JsonReader reader, final JsonWriter writer, final JsonFilterOptions options) { Preconditions.checkArgument(reader != null, "reader must be non-null"); Preconditions.checkArgument(writer != null, "writer must be non-null"); @@ -309,22 +329,49 @@ public JsonFilter(final JsonReader reader, final JsonWriter writer, final JsonFi this.predicate = new PathPredicate(options.getIncludes(), options.getExcludes()); } + /** + * Returns the current field path being processed. + * + * @return the current field path + */ public CharSequence getCurrentPath() { return currentPath; } + /** + * Returns the current nesting depth within the JSON document. + * + * @return the current nesting depth + */ public final int getDepth() { return depth; } + /** + * Returns the filtering options used by this filter. + * + * @return the filtering options + */ public final JsonFilterOptions getOptions() { return options; } + /** + * Returns the JSON writer used by this filter. + * + * @return the JSON writer + */ public final JsonWriter getWriter() { return writer; } + /** + * Processes the input JSON, applying the configured filtering rules and writing the result to + * the output writer. + * + * @return this filter instance for chaining + * @throws IOException if the JSON is invalid or an I/O error occurs + */ public JsonFilter process() throws IOException { depth = 0; JsonToken token = reader.nextEvent(); diff --git a/src/main/java/com/arakelian/json/JsonFilterCallback.java b/src/main/java/com/arakelian/json/JsonFilterCallback.java index 94083c7..93ffb39 100644 --- a/src/main/java/com/arakelian/json/JsonFilterCallback.java +++ b/src/main/java/com/arakelian/json/JsonFilterCallback.java @@ -19,11 +19,27 @@ import java.io.IOException; +/** + * Callback interface for receiving notifications during JSON filtering. Implementations can inject + * custom logic at the start and end of JSON objects. + */ public interface JsonFilterCallback { + /** + * Called immediately after the start of a JSON object is written to the output. + * + * @param filter the filter currently processing the JSON + * @throws IOException if an I/O error occurs + */ @SuppressWarnings("unused") public default void afterStartObject(final JsonFilter filter) throws IOException { } + /** + * Called immediately before the end of a JSON object is written to the output. + * + * @param filter the filter currently processing the JSON + * @throws IOException if an I/O error occurs + */ @SuppressWarnings("unused") public default void beforeEndObject(final JsonFilter filter) throws IOException { } diff --git a/src/main/java/com/arakelian/json/JsonFilterOptions.java b/src/main/java/com/arakelian/json/JsonFilterOptions.java index 0def746..41cc157 100644 --- a/src/main/java/com/arakelian/json/JsonFilterOptions.java +++ b/src/main/java/com/arakelian/json/JsonFilterOptions.java @@ -22,33 +22,77 @@ import org.immutables.value.Value; +/** + * Immutable configuration options for {@link JsonFilter}, specifying which fields to include or + * exclude, whether to pretty-print the output, and an optional callback for custom processing. + */ @Value.Immutable(copy = false) public abstract class JsonFilterOptions { + /** + * Returns the optional callback for custom processing during filtering. + * + * @return the callback, or {@code null} if none + */ @Nullable public abstract JsonFilterCallback getCallback(); + /** + * Returns the set of field paths to exclude from the output. + * + * @return the set of excluded paths, or {@code null} if none + */ @Nullable public abstract Set getExcludes(); + /** + * Returns the set of field paths to include in the output. + * + * @return the set of included paths, or {@code null} if none + */ @Nullable public abstract Set getIncludes(); + /** + * Returns an optional flag indicating whether the output should be pretty-printed. + * + * @return an {@link Optional} containing the pretty-print flag + */ public abstract Optional getPretty(); + /** + * Returns {@code true} if a callback has been configured. + * + * @return {@code true} if a callback is set + */ public final boolean hasCallback() { return getCallback() != null; } + /** + * Returns {@code true} if any exclude paths have been configured. + * + * @return {@code true} if excludes are set + */ public final boolean hasExcludes() { final Set excludes = getExcludes(); return excludes != null && excludes.size() != 0; } + /** + * Returns {@code true} if any include paths have been configured. + * + * @return {@code true} if includes are set + */ public final boolean hasIncludes() { final Set includes = getIncludes(); return includes != null && includes.size() != 0; } + /** + * Returns {@code true} if no includes, excludes, or callback have been configured. + * + * @return {@code true} if no filtering options are set + */ public final boolean isEmpty() { return !hasIncludes() && !hasExcludes() && !hasCallback(); } diff --git a/src/main/java/com/arakelian/json/JsonReader.java b/src/main/java/com/arakelian/json/JsonReader.java index 0e15234..800609d 100644 --- a/src/main/java/com/arakelian/json/JsonReader.java +++ b/src/main/java/com/arakelian/json/JsonReader.java @@ -20,13 +20,29 @@ import java.io.IOException; import java.io.Reader; +/** + * High-performance streaming JSON reader that minimizes memory allocations by operating directly on + * character buffers. Supports reading from {@link CharSequence}, {@link Reader}, or raw character + * arrays. + */ public final class JsonReader { + /** + * Exception thrown when an error is encountered while parsing JSON input. + */ public static class JsonParseException extends IOException { + /** + * Constructs a new parse exception with the given message. + * + * @param msg the detail message + */ public JsonParseException(final String msg) { super(msg); } } + /** + * Enumeration of JSON token types that can be encountered during parsing. + */ public enum JsonToken { // Event indicating a JSON string value, including member names of objects STRING, @@ -146,6 +162,13 @@ enum ParserState { /** current state while reading a number **/ private int numberState; + /** + * Constructs a reader that reads from the given character array. + * + * @param data the character array containing JSON + * @param start the start index (inclusive) + * @param end the end index (exclusive) + */ public JsonReader(final char[] data, final int start, final int end) { this.in = null; this.buf = data; @@ -153,21 +176,44 @@ public JsonReader(final char[] data, final int start, final int end) { this.end = end; } + /** + * Constructs a reader that reads from the given {@link Reader} with a default buffer size. + * + * @param in the reader to obtain JSON data from + */ public JsonReader(final Reader in) { // 8192 matches the default buffer size of a BufferedReader so double // buffering of the data is avoided. this(in, new char[8192]); } + /** + * Constructs a reader that reads from the given {@link Reader} using the specified buffer. + * + * @param in the reader to obtain JSON data from + * @param buffer the buffer to use for reading + */ public JsonReader(final Reader in, final char[] buffer) { this.in = in; this.buf = buffer; } + /** + * Constructs a reader that reads from the given {@link CharSequence}. + * + * @param data the character sequence containing JSON + */ public JsonReader(final CharSequence data) { this(data, 0, data.length()); } + /** + * Constructs a reader that reads from a subsequence of the given {@link CharSequence}. + * + * @param data the character sequence containing JSON + * @param start the start index (inclusive) + * @param end the end index (exclusive) + */ public JsonReader(final CharSequence data, final int start, final int end) { this.in = null; this.start = start; @@ -453,6 +499,11 @@ public CharSequence getNumberChars() throws IOException { } } + /** + * Returns the current position in the input stream. + * + * @return the current position + */ public long getPosition() { return gpos + start; } @@ -521,6 +572,11 @@ private final boolean isWhitespace(final int ch) { return Character.isWhitespace(ch) || ch == 0x00a0; } + /** + * Returns the most recently read token. + * + * @return the last token read + */ public JsonToken lastEvent() { return event; } @@ -996,11 +1052,20 @@ public String toString() { return "start=" + start + ",end=" + end + ",state=" + state + "valstate=" + valueState; } + /** + * Returns {@code true} if the last string read was an object member name (key). + * + * @return {@code true} if the last string was a key + */ public boolean wasKey() { return state == ParserState.DID_MEMNAME; } } +/** + * A no-op character array that discards all writes. Used internally by {@link JsonReader} to skip + * over values without allocating memory. + */ class NullCharArr extends SimpleCharArr { public NullCharArr() { super(new char[1], 0, 0); @@ -1028,6 +1093,10 @@ public void write(final char b[], final int off, final int len) { } } +/** + * A simple, resizable character array that implements {@link CharSequence}. Used internally by + * {@link JsonReader} as a lightweight buffer to avoid unnecessary {@link String} allocations. + */ class SimpleCharArr implements CharSequence { protected char[] buf; protected int start; diff --git a/src/main/java/com/arakelian/json/JsonWriter.java b/src/main/java/com/arakelian/json/JsonWriter.java index 27b829c..39ecac0 100644 --- a/src/main/java/com/arakelian/json/JsonWriter.java +++ b/src/main/java/com/arakelian/json/JsonWriter.java @@ -39,7 +39,11 @@ import com.google.common.io.BaseEncoding; /** - * Fast JSON writer + * High-performance JSON writer that serializes Java objects to JSON with support for pretty-printing + * and options to skip null or empty values. Operates on any {@link Writer} implementation and + * minimizes intermediate {@link String} allocations. + * + * @param the type of {@link Writer} to write JSON output to */ public class JsonWriter implements Closeable { private static enum Container { @@ -365,10 +369,18 @@ public static String toString(final Object value, final boolean pretty) throws I /** True to skip empty values **/ private boolean skipEmpty = false; + /** + * Constructs a new {@code JsonWriter} with no underlying writer set. + */ public JsonWriter() { reset(); } + /** + * Constructs a new {@code JsonWriter} that writes to the given writer. + * + * @param writer the writer to output JSON to + */ public JsonWriter(final W writer) { setWriter(writer); reset(); @@ -486,6 +498,13 @@ private void internalWriteUnescapedString(final CharSequence csq) throws IOExcep } } + /** + * Returns {@code true} if the given value is considered empty. Null values, empty strings, empty + * collections, empty maps, and non-finite floating point values are considered empty. + * + * @param value the value to test + * @return {@code true} if the value is empty + */ public final boolean isEmpty(final Object value) { if (value == null) { return true; @@ -610,6 +629,13 @@ public final JsonWriter withWriter(final W writer) { return this; } + /** + * Writes the given byte array as a Base64-encoded JSON string value. + * + * @param data the byte array to encode, or {@code null} to write a JSON null + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeBase64String(final byte[] data) throws IOException { if (data == null) { return writeNull(); @@ -625,6 +651,14 @@ public final JsonWriter writeBase64String(final byte[] data) throws IOExcepti return this; } + /** + * Writes the given {@link BigDecimal} as a JSON number value using its plain string + * representation. + * + * @param val the value to write, or {@code null} to write a JSON null + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public JsonWriter writeBigDecimal(final BigDecimal val) throws IOException { if (val == null) { return writeNull(); @@ -633,11 +667,26 @@ public JsonWriter writeBigDecimal(final BigDecimal val) throws IOException { return this; } + /** + * Writes the given boolean as a JSON boolean value. + * + * @param val the boolean value + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeBoolean(final boolean val) throws IOException { writeChars(val ? "true" : "false"); return this; } + /** + * Writes the given {@link Boolean} as a JSON boolean value, or a JSON null if the value is + * {@code null}. + * + * @param val the boolean value, or {@code null} to write a JSON null + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeBoolean(final Boolean val) throws IOException { if (val == null) { return writeNull(); @@ -654,14 +703,35 @@ private final void writeChars(final CharSequence csq) throws IOException { afterValue(); } + /** + * Writes the given {@link Date} as an ISO-8601 formatted JSON string in UTC. + * + * @param val the date value + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeDate(final Date val) throws IOException { return writeDate(DateUtils.toZonedDateTimeUtc(val)); } + /** + * Writes the given {@link Instant} as an ISO-8601 formatted JSON string in UTC. + * + * @param val the instant value + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeDate(final Instant val) throws IOException { return writeDate(DateUtils.toZonedDateTimeUtc(val)); } + /** + * Writes the given {@link ZonedDateTime} as an ISO-8601 formatted JSON string. + * + * @param val the zoned date time value + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeDate(final ZonedDateTime val) throws IOException { final String iso = DateUtils.toStringIsoFormat(val); writeString(iso); @@ -690,6 +760,12 @@ public final JsonWriter writeDouble(final Double val) throws IOException { return this; } + /** + * Writes the end of the current JSON array. + * + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeEndArray() throws IOException { final boolean notEmpty = this.state[indent--].writeEndArray(); if (notEmpty) { @@ -698,6 +774,12 @@ public final JsonWriter writeEndArray() throws IOException { return this; } + /** + * Writes the end of the current JSON object. + * + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeEndObject() throws IOException { final boolean notEmpty = this.state[indent--].writeEndObject(); if (notEmpty) { @@ -728,11 +810,25 @@ public final JsonWriter writeFloat(final Float val) throws IOException { return this; } + /** + * Sets the key for the next value to be written within a JSON object. + * + * @param key the key name + * @return this writer for chaining + */ public final JsonWriter writeKey(final Object key) { this.state[indent].writeKey(key); return this; } + /** + * Writes a key-value pair where the value is written without JSON string escaping. + * + * @param key the key name + * @param value the value to write unescaped + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeKeyUnescapedValue(final Object key, final Object value) throws IOException { if (skipEmpty && isEmpty(value) || skipNulls && isNull(value)) { @@ -743,6 +839,14 @@ public final JsonWriter writeKeyUnescapedValue(final Object key, final Object return this; } + /** + * Writes a key-value pair within a JSON object. + * + * @param key the key name + * @param value the value to write + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeKeyValue(final Object key, final Object value) throws IOException { if (skipEmpty && isEmpty(value) || skipNulls && isNull(value)) { return this; @@ -752,6 +856,13 @@ public final JsonWriter writeKeyValue(final Object key, final Object value) t return this; } + /** + * Writes the given {@link Collection} as a JSON array. + * + * @param collection the collection to write, or {@code null} to write a JSON null + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeList(final Collection collection) throws IOException { if (collection == null) { return writeNull(); @@ -775,6 +886,13 @@ public final JsonWriter writeList(final Collection collection) throws IOExcep return this; } + /** + * Writes the given array of objects as a JSON array. + * + * @param objects the objects to write, or {@code null} to write a JSON null + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeList(final Object... objects) throws IOException { if (objects == null) { return writeNull(); @@ -796,6 +914,13 @@ public final JsonWriter writeList(final Object... objects) throws IOException return this; } + /** + * Writes the given {@link Map} as a JSON object. + * + * @param map the map to write, or {@code null} to write a JSON null + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeMap(final Map map) throws IOException { if (map == null) { return writeNull(); @@ -823,6 +948,12 @@ private final JsonWriter writeNewline() throws IOException { return this; } + /** + * Writes a JSON null value. If skip-nulls or skip-empty is enabled, the null may be suppressed. + * + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeNull() throws IOException { if (skipEmpty || skipNulls) { return this; @@ -872,6 +1003,14 @@ public final JsonWriter writeNumber(final Number val) throws IOException { return this; } + /** + * Writes the given Java object as the appropriate JSON type, dispatching based on the object's + * runtime type. + * + * @param val the object to write, or {@code null} to write a JSON null + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeObject(final Object val) throws IOException { if (val == null) { return writeNull(); @@ -925,16 +1064,33 @@ public final JsonWriter writeObject(final Object val) throws IOException { return this; } + /** + * Begins writing a new JSON array. + * + * @return this writer for chaining + */ public final JsonWriter writeStartArray() { this.state[++indent].startArray(); return this; } + /** + * Begins writing a new JSON object. + * + * @return this writer for chaining + */ public final JsonWriter writeStartObject() { this.state[++indent].startObject(); return this; } + /** + * Writes the given {@link CharSequence} as a properly escaped JSON string value. + * + * @param csq the string to write, or {@code null} to write a JSON null + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeString(final CharSequence csq) throws IOException { if (csq == null) { return writeNull(); @@ -949,6 +1105,14 @@ public final JsonWriter writeString(final CharSequence csq) throws IOExceptio return this; } + /** + * Writes the given value directly to the output without JSON string escaping. This is useful for + * writing pre-formatted JSON or raw numeric values. + * + * @param val the value to write without escaping + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeUnescapedString(final Object val) throws IOException { beforeValue(); if (val instanceof CharSequence) { diff --git a/src/main/java/com/arakelian/json/Nullable.java b/src/main/java/com/arakelian/json/Nullable.java index 4bf3cca..6713008 100644 --- a/src/main/java/com/arakelian/json/Nullable.java +++ b/src/main/java/com/arakelian/json/Nullable.java @@ -22,6 +22,10 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Annotation indicating that a field, method, parameter, or type may hold a {@code null} value. + * Used with Immutables to mark optional properties. + */ @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) diff --git a/src/test/java/com/arakelian/json/JsonFilterTest.java b/src/test/java/com/arakelian/json/JsonFilterTest.java index 653962d..9e5108e 100644 --- a/src/test/java/com/arakelian/json/JsonFilterTest.java +++ b/src/test/java/com/arakelian/json/JsonFilterTest.java @@ -22,6 +22,10 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +/** + * Tests for {@link JsonFilter}, verifying include/exclude filtering, pretty-printing, and + * compaction of JSON documents. + */ public class JsonFilterTest { @Test public void testExcludeComplex() throws IOException { diff --git a/src/test/java/com/arakelian/json/JsonReaderTest.java b/src/test/java/com/arakelian/json/JsonReaderTest.java index aa642f4..4680b93 100644 --- a/src/test/java/com/arakelian/json/JsonReaderTest.java +++ b/src/test/java/com/arakelian/json/JsonReaderTest.java @@ -36,6 +36,9 @@ import com.arakelian.json.JsonReader.JsonToken; +/** + * Tests for {@link JsonReader}, verifying correct tokenization and parsing of JSON input. + */ public class JsonReaderTest { private Object[] parse(final String json) throws IOException { final List tokens = new ArrayList<>(); diff --git a/src/test/java/com/arakelian/json/JsonWriterTest.java b/src/test/java/com/arakelian/json/JsonWriterTest.java index 5ed31f9..c916f1a 100644 --- a/src/test/java/com/arakelian/json/JsonWriterTest.java +++ b/src/test/java/com/arakelian/json/JsonWriterTest.java @@ -20,6 +20,10 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +/** + * Tests for {@link JsonWriter}, verifying correct serialization of various data types, formatting + * options, and error handling. + */ public class JsonWriterTest { @FunctionalInterface public interface JsonTest {