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
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;