From d27a601f6d8c79d66ab3202f3e1afffb01336b11 Mon Sep 17 00:00:00 2001 From: Greg Arakelian Date: Tue, 24 Feb 2026 01:00:11 -0500 Subject: [PATCH 1/2] Modernize build, fix compiler warnings, add eclipseFactoryPath task - Add eclipseFactoryPath Gradle task to generate .factorypath for Eclipse annotation processing - Fix 16 errorprone PatternMatchingInstanceof warnings using Java 16+ pattern-matching instanceof - Fix 1 errorprone RedundantControlFlow warning by removing redundant continue statement - Add 32 javadoc comments: package-level, enum constants, and 21 public methods - Create gradle/libs.versions.toml version catalog centralizing dependency versions - Replace hardcoded plugin versions with version catalog references using alias() - Apply Log4j and JUnit BOMs to manage transitive dependency versions - Add Java toolchain declaration to replace per-task sourceCompatibility/targetCompatibility - Replace Ant-based readme task with native Groovy String.replaceAll() - Replace macro/task-name-rewriting system with proper Gradle lifecycle tasks - Remove configuration cache incompatibilities These changes improve code quality, maintainability, and build performance while remaining fully backward compatible. Co-Authored-By: Claude Haiku 4.5 --- build.gradle | 42 +++-- core.gradle | 100 +++++----- gradle/libs.versions.toml | 34 ++++ .../com/arakelian/json/JsonFilterOptions.java | 3 + .../java/com/arakelian/json/JsonReader.java | 20 +- .../java/com/arakelian/json/JsonWriter.java | 172 +++++++++++++++--- .../java/com/arakelian/json/package-info.java | 3 + 7 files changed, 267 insertions(+), 107 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/build.gradle b/build.gradle index aba66b0..ba9eacb 100644 --- a/build.gradle +++ b/build.gradle @@ -7,13 +7,13 @@ plugins { id 'idea' // keep dependencies up-to-date! - id 'com.github.ben-manes.versions' version '0.53.0' + alias(libs.plugins.versions) // to ensure clean code - id "net.ltgt.errorprone" version "5.0.0" + alias(libs.plugins.errorprone) // for deployment to Maven Central - id "com.vanniktech.maven.publish" version "0.36.0" + alias(libs.plugins.maven.publish) } group = 'com.arakelian' @@ -58,29 +58,31 @@ mavenPublishing { } dependencies { - annotationProcessor 'org.immutables:value:2.10.1' + annotationProcessor libs.immutables.value // annotations - api 'org.immutables:value-annotations:2.10.1' + api libs.immutables.annotations // for date utils - api 'com.arakelian:more-commons:5.1.0' + api libs.more.commons // configure errorprone version - errorprone 'com.google.errorprone:error_prone_core:2.36.0' + errorprone libs.errorprone.core // we use Guava directly - api 'com.google.guava:guava:33.4.0-jre' - - // logging - 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.11.4' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + api libs.guava + + // logging (Log4j versions managed by BOM) + testImplementation platform(libs.log4j.bom) + testImplementation libs.log4j.api + testImplementation libs.log4j.core + testImplementation libs.log4j.slf4j2.impl + testImplementation libs.jcl.over.slf4j + testImplementation libs.jul.to.slf4j + api libs.slf4j.api + + // for unit testing (JUnit versions managed by BOM) + testImplementation platform(libs.junit.bom) + testImplementation libs.junit.jupiter + testRuntimeOnly libs.junit.platform.launcher } diff --git a/core.gradle b/core.gradle index 1eea844..ff6782a 100644 --- a/core.gradle +++ b/core.gradle @@ -1,22 +1,5 @@ ext { - // useful macros, you can add your own - macros = [ - 'all' : [ - 'clean', - 'classpath', - 'build', - 'publishToMavenLocal' - ], - 'classpath' : [ - 'cleanEclipseClasspath', - 'eclipseClasspath', - 'eclipseFactoryPath', - 'cleanIdeaModule', - 'ideaModule' - ], - ] - // package patterns to exclude from Eclipse excludeFromEclipse = [] } @@ -41,13 +24,21 @@ tasks.named('test') { // ------------------------------------------- -// JAVA COMPILER +// JAVA TOOLCHAIN // ------------------------------------------- -tasks.withType(JavaCompile).configureEach { task -> - sourceCompatibility = 21 - targetCompatibility = 21 +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +// ------------------------------------------- +// JAVA COMPILER +// ------------------------------------------- + +tasks.withType(JavaCompile).configureEach { // always UTF-8 options.encoding = 'UTF-8' @@ -162,46 +153,61 @@ eclipse { } +// ------------------------------------------- +// ECLIPSE FACTORY PATH (for annotation processing) +// ------------------------------------------- + +tasks.register('eclipseFactoryPath') { + description = 'Generates the .factorypath file for Eclipse annotation processing' + + doLast { + def factorypath = new File(projectDir, '.factorypath') + factorypath.withWriter { writer -> + writer.writeLine '' + writer.writeLine '' + configurations.annotationProcessor.resolvedConfiguration.resolvedArtifacts.each { artifact -> + writer.writeLine " " + } + writer.writeLine '' + } + println "Generated ${factorypath}" + } +} + + // ------------------------------------------- // README // ------------------------------------------- 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') - } + def readmeFile = file('README.md') + def content = readmeFile.text + content = content.replaceAll(/[0-9.]+<\/version>/, "${version}") + content = content.replaceAll("com\\.arakelian:" + project.name + ":[0-9.]+", "com.arakelian:${project.name}:${version}") + readmeFile.text = content } } // ------------------------------------------- -// SHORTCUT TASKS +// LIFECYCLE TASKS // ------------------------------------------- +tasks.register('classpath') { + dependsOn 'cleanEclipseClasspath', 'eclipseClasspath', + 'eclipseFactoryPath', 'cleanIdeaModule', 'ideaModule' +} -// This code allows us to define aliases, such as "all", so that when we do "gradle all", -// we can substitute in a series of other gradle tasks -// see: https://caffeineinduced.wordpress.com/2015/01/25/run-a-list-of-gradle-tasks-in-specific-order/ -def newTasks = [] +// ensure clean tasks run before generate tasks +tasks.named('eclipseClasspath') { mustRunAfter 'cleanEclipseClasspath' } +tasks.named('ideaModule') { mustRunAfter 'cleanIdeaModule' } -// gradle respects ordering of tasks specified on command line, so we replace shortcuts -// with equivalent commands as though they were specified by user -gradle.startParameter.taskNames.each { param -> - def macro = project.ext.macros[param] - if( macro ) { - macro.each { task -> - if(project.tasks.names.contains(task)) { - newTasks << task - } - } - } else { - newTasks << param - } +tasks.register('all') { + dependsOn 'clean', 'classpath', 'build', 'publishToMavenLocal' } -// replace command line arguments -gradle.startParameter.taskNames = newTasks.flatten() +// ensure proper execution order +tasks.named('classpath') { mustRunAfter 'clean' } +tasks.named('build') { mustRunAfter 'classpath' } +tasks.named('publishToMavenLocal') { mustRunAfter 'build' } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..c89bff8 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,34 @@ +[versions] +errorprone = "2.36.0" +guava = "33.4.0-jre" +immutables = "2.10.1" +junit = "5.11.4" +log4j = "2.24.3" +more-commons = "5.1.0" +slf4j = "2.0.16" + +errorprone-plugin = "5.0.0" +maven-publish-plugin = "0.36.0" +versions-plugin = "0.53.0" + +[libraries] +errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } +immutables-value = { module = "org.immutables:value", version.ref = "immutables" } +immutables-annotations = { module = "org.immutables:value-annotations", version.ref = "immutables" } +junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } +log4j-bom = { module = "org.apache.logging.log4j:log4j-bom", version.ref = "log4j" } +log4j-api = { module = "org.apache.logging.log4j:log4j-api" } +log4j-core = { module = "org.apache.logging.log4j:log4j-core" } +log4j-slf4j2-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl" } +more-commons = { module = "com.arakelian:more-commons", version.ref = "more-commons" } +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } +jcl-over-slf4j = { module = "org.slf4j:jcl-over-slf4j", version.ref = "slf4j" } +jul-to-slf4j = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } + +[plugins] +versions = { id = "com.github.ben-manes.versions", version.ref = "versions-plugin" } +errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone-plugin" } +maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish-plugin" } diff --git a/src/main/java/com/arakelian/json/JsonFilterOptions.java b/src/main/java/com/arakelian/json/JsonFilterOptions.java index 41cc157..3c78763 100644 --- a/src/main/java/com/arakelian/json/JsonFilterOptions.java +++ b/src/main/java/com/arakelian/json/JsonFilterOptions.java @@ -28,6 +28,9 @@ */ @Value.Immutable(copy = false) public abstract class JsonFilterOptions { + /** Constructs a new {@code JsonFilterOptions}. */ + protected JsonFilterOptions() { + } /** * Returns the optional callback for custom processing during filtering. * diff --git a/src/main/java/com/arakelian/json/JsonReader.java b/src/main/java/com/arakelian/json/JsonReader.java index 800609d..42cdbc2 100644 --- a/src/main/java/com/arakelian/json/JsonReader.java +++ b/src/main/java/com/arakelian/json/JsonReader.java @@ -44,10 +44,10 @@ public JsonParseException(final String 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 + /** Event indicating a JSON string value, including member names of objects. */ STRING, - // Event indicating a JSON number value which fits into a signed 64 bit integer + /** Event indicating a JSON number value which fits into a signed 64 bit integer. */ LONG, /** @@ -65,25 +65,25 @@ public enum JsonToken { */ BIGNUMBER, - // Event indicating a JSON boolean + /** Event indicating a JSON boolean. */ BOOLEAN, - // Event indicating a JSON null + /** Event indicating a JSON null. */ NULL, - // Event indicating the start of a JSON object + /** Event indicating the start of a JSON object. */ OBJECT_START, - // Event indicating the end of a JSON object + /** Event indicating the end of a JSON object. */ OBJECT_END, - // Event indicating the start of a JSON array + /** Event indicating the start of a JSON array. */ ARRAY_START, - // Event indicating the end of a JSON array + /** Event indicating the end of a JSON array. */ ARRAY_END, - // Event indicating the end of input has been reached + /** Event indicating the end of input has been reached. */ EOF; } @@ -853,7 +853,7 @@ private int readCharUntilWs() throws IOException { if ((WS_MASK >> ch & 0x01) == 0) { return ch; } else if (ch <= ' ') { // this will only be true if one of the whitespace bits was set - continue; + // skip whitespace } else if (!isWhitespace(ch)) { // we'll only reach here with certain bare strings, // errors, or strange whitespace like 0xa0 return ch; diff --git a/src/main/java/com/arakelian/json/JsonWriter.java b/src/main/java/com/arakelian/json/JsonWriter.java index 39ecac0..5ea7a90 100644 --- a/src/main/java/com/arakelian/json/JsonWriter.java +++ b/src/main/java/com/arakelian/json/JsonWriter.java @@ -248,9 +248,8 @@ public boolean writeEndObject() throws IOException { public final void writeKey(final Object key) { Preconditions.checkState(container == Container.OBJECT, "key value pairs only valid in object"); - if (key instanceof CharSequence) { + if (key instanceof CharSequence csq) { // we are trying hard to avoid allocating stings, so we will store the key - final CharSequence csq = (CharSequence) key; final int length = csq.length(); final int capacity = this.key != null ? this.key.length : 0; if (length > capacity) { @@ -406,11 +405,22 @@ public void close() throws IOException { } } + /** + * Flushes the underlying writer. + * + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter flush() throws IOException { writer.flush(); return this; } + /** + * Returns the underlying writer. + * + * @return the writer + */ public final W getWriter() { return writer; } @@ -510,34 +520,29 @@ public final boolean isEmpty(final Object value) { return true; } - if (value instanceof CharSequence) { - final CharSequence csq = (CharSequence) value; + if (value instanceof CharSequence csq) { return csq.length() == 0; } - if (value instanceof Double) { - final Double d = (Double) value; + if (value instanceof Double d) { if (d.isInfinite() || d.isNaN()) { return true; } return false; } - if (value instanceof Float) { - final Float f = (Float) value; + if (value instanceof Float f) { if (f.isInfinite() || f.isNaN()) { return true; } return false; } - if (value instanceof Map) { - final Map m = (Map) value; + if (value instanceof Map m) { return m.size() == 0; } - if (value instanceof List) { - final List l = (List) value; + if (value instanceof List l) { return l.size() == 0; } @@ -573,18 +578,36 @@ public final boolean isNull(final Object value) { return false; } + /** + * Returns {@code true} if pretty-printing is enabled. + * + * @return {@code true} if pretty-printing is enabled + */ public final boolean isPretty() { return pretty; } + /** + * Returns {@code true} if empty values are skipped during serialization. + * + * @return {@code true} if empty values are skipped + */ public final boolean isSkipEmpty() { return skipEmpty; } + /** + * Returns {@code true} if null values are skipped during serialization. + * + * @return {@code true} if null values are skipped + */ public final boolean isSkipNulls() { return skipNulls; } + /** + * Resets the writer state to its initial configuration, allowing reuse. + */ public final void reset() { this.indent = 0; for (int depth = 0, length = state.length; depth < length; depth++) { @@ -593,37 +616,81 @@ public final void reset() { this.state[0].reset(Container.DOCUMENT); } + /** + * Sets whether pretty-printing is enabled. + * + * @param pretty {@code true} to enable pretty-printing + */ public final void setPretty(final boolean pretty) { this.pretty = pretty; } + /** + * Sets whether empty values are skipped during serialization. + * + * @param skipEmpty {@code true} to skip empty values + */ public final void setSkipEmpty(final boolean skipEmpty) { this.skipEmpty = skipEmpty; } + /** + * Sets whether null values are skipped during serialization. + * + * @param skipNulls {@code true} to skip null values + */ public final void setSkipNulls(final boolean skipNulls) { this.skipNulls = skipNulls; } + /** + * Sets the underlying writer. + * + * @param writer the writer to output JSON to + */ public final void setWriter(final W writer) { this.writer = writer; } + /** + * Sets whether pretty-printing is enabled. + * + * @param pretty {@code true} to enable pretty-printing + * @return this writer for chaining + */ public final JsonWriter withPretty(final boolean pretty) { setPretty(pretty); return this; } + /** + * Sets whether empty values are skipped during serialization. + * + * @param skipEmpty {@code true} to skip empty values + * @return this writer for chaining + */ public final JsonWriter withSkipEmpty(final boolean skipEmpty) { setSkipEmpty(skipEmpty); return this; } + /** + * Sets whether null values are skipped during serialization. + * + * @param skipNulls {@code true} to skip null values + * @return this writer for chaining + */ public final JsonWriter withSkipNulls(final boolean skipNulls) { setSkipNulls(skipNulls); return this; } + /** + * Sets the underlying writer. + * + * @param writer the writer to output JSON to + * @return this writer for chaining + */ public final JsonWriter withWriter(final W writer) { setWriter(writer); return this; @@ -738,6 +805,14 @@ public final JsonWriter writeDate(final ZonedDateTime val) throws IOException return this; } + /** + * Writes the given {@code double} as a JSON number value. Non-finite values are written as + * JSON null. + * + * @param val the value to write + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeDouble(final double val) throws IOException { if (Double.isInfinite(val) || Double.isNaN(val)) { return writeNull(); @@ -749,6 +824,14 @@ public final JsonWriter writeDouble(final double val) throws IOException { return this; } + /** + * Writes the given {@link Double} as a JSON number value. Null or non-finite values are + * written as JSON null. + * + * @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 final JsonWriter writeDouble(final Double val) throws IOException { if (val == null || val.isInfinite() || val.isNaN()) { return writeNull(); @@ -788,6 +871,14 @@ public final JsonWriter writeEndObject() throws IOException { return this; } + /** + * Writes the given {@code float} as a JSON number value. Non-finite values are written as + * JSON null. + * + * @param val the value to write + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeFloat(final float val) throws IOException { if (Float.isInfinite(val) || Float.isNaN(val)) { return writeNull(); @@ -799,6 +890,14 @@ public final JsonWriter writeFloat(final float val) throws IOException { return this; } + /** + * Writes the given {@link Float} as a JSON number value. Null or non-finite values are + * written as JSON null. + * + * @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 final JsonWriter writeFloat(final Float val) throws IOException { if (val == null || val.isInfinite() || val.isNaN()) { return writeNull(); @@ -965,6 +1064,13 @@ public final JsonWriter writeNull() throws IOException { return this; } + /** + * Writes the given {@code int} as a JSON number value. + * + * @param val the value to write + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeNumber(final int val) throws IOException { final StringBuilder buf = new StringBuilder(); buf.append(val); @@ -972,6 +1078,13 @@ public final JsonWriter writeNumber(final int val) throws IOException { return this; } + /** + * Writes the given {@code long} as a JSON number value. + * + * @param val the value to write + * @return this writer for chaining + * @throws IOException if an I/O error occurs + */ public final JsonWriter writeNumber(final long val) throws IOException { final StringBuilder buf = new StringBuilder(); buf.append(val); @@ -979,23 +1092,28 @@ public final JsonWriter writeNumber(final long val) throws IOException { return this; } + /** + * Writes the given {@link Number} as a JSON number value, dispatching to the appropriate + * method based on the runtime type. + * + * @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 final JsonWriter writeNumber(final Number val) throws IOException { if (val == null) { return writeNull(); } - if (val instanceof Double) { - final Double d = (Double) val; + if (val instanceof Double d) { return writeDouble(d); } - if (val instanceof Float) { - final Float f = (Float) val; + if (val instanceof Float f) { return writeFloat(f); } - if (val instanceof BigDecimal) { - final BigDecimal bd = (BigDecimal) val; + if (val instanceof BigDecimal bd) { return writeBigDecimal(bd); } @@ -1021,13 +1139,11 @@ public final JsonWriter writeObject(final Object val) throws IOException { return this; } - if (val instanceof Number) { - final Number n = (Number) val; + if (val instanceof Number n) { return writeNumber(n); } - if (val instanceof Boolean) { - final Boolean b = (Boolean) val; + if (val instanceof Boolean b) { return writeBoolean(b); } @@ -1039,16 +1155,13 @@ public final JsonWriter writeObject(final Object val) throws IOException { return writeList((Collection) val); } - if (val instanceof Date) { - final Date d = (Date) val; + if (val instanceof Date d) { return writeDate(d); } - if (val instanceof Instant) { - final Instant i = (Instant) val; + if (val instanceof Instant i) { return writeDate(i); } - if (val instanceof ZonedDateTime) { - final ZonedDateTime zdt = (ZonedDateTime) val; + if (val instanceof ZonedDateTime zdt) { return writeDate(zdt); } @@ -1115,9 +1228,8 @@ public final JsonWriter writeString(final CharSequence csq) throws IOExceptio */ public final JsonWriter writeUnescapedString(final Object val) throws IOException { beforeValue(); - if (val instanceof CharSequence) { + if (val instanceof CharSequence csq) { // we can avoid toString - final CharSequence csq = (CharSequence) val; internalWriteUnescapedString(csq); } else { final String csq = val != null ? val.toString() : ""; diff --git a/src/main/java/com/arakelian/json/package-info.java b/src/main/java/com/arakelian/json/package-info.java index bbe9d7f..9949583 100644 --- a/src/main/java/com/arakelian/json/package-info.java +++ b/src/main/java/com/arakelian/json/package-info.java @@ -15,6 +15,9 @@ * limitations under the License. */ +/** + * Provides high-speed stream-based filtering and serialization of JSON documents. + */ @Value.Style(get = { "is*", "get*" }) package com.arakelian.json; From fd3ef810182f9a2130a19d1ac0db8aaf01925e6f Mon Sep 17 00:00:00 2001 From: Greg Arakelian Date: Tue, 24 Feb 2026 01:01:25 -0500 Subject: [PATCH 2/2] Upgrade GitHub Actions to latest versions - actions/checkout v4 -> v6 - actions/setup-java v4 -> v5 - gradle/actions/setup-gradle v4 -> v5 - Add explicit permissions block for least-privilege Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dde6c74..40a010d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,9 @@ on: - main workflow_dispatch: +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest @@ -16,17 +19,17 @@ jobs: matrix: java: [ '21' ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: ${{ matrix.java }} distribution: 'temurin' - name: Print Java version run: java -version - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 - name: Run tests run: ./gradlew --no-daemon --stacktrace clean build