You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iggy.apache.org by ma...@apache.org on 2025/11/25 13:18:59 UTC

(iggy) 01/01: chore(java): update project dependencies

This is an automated email from the ASF dual-hosted git repository.

maciej pushed a commit to branch java-update-deps
in repository https://gitbox.apache.org/repos/asf/iggy.git

commit 8b9244f7d9a7e750d808a6aa7b658e08cdbebff9
Author: Maciej Modzelewski <ma...@modzelewski.dev>
AuthorDate: Tue Nov 25 11:11:31 2025 +0100

    chore(java): update project dependencies
---
 foreign/java/.gitattributes                        |   3 +
 foreign/java/build.gradle.kts                      |   4 +-
 .../Permissions.java => buildSrc/build.gradle.kts} |  15 ++-
 .../settings.gradle.kts}                           |  10 +-
 .../iggy.java-application-conventions.gradle.kts}  |  20 ++--
 .../iggy.java-common-conventions.gradle.kts}       |  28 ++++--
 .../iggy.java-library-conventions.gradle.kts}      |  17 ++--
 foreign/java/examples/build.gradle.kts             |  13 +--
 .../iggy-connector-library/build.gradle.kts        |  51 ++--------
 .../serialization/JsonDeserializationSchema.java   |  41 ++------
 .../serialization/JsonSerializationSchema.java     |  44 +++------
 .../iggy-flink-examples/build.gradle.kts           |  58 +++--------
 foreign/java/gradle/libs.versions.toml             | 106 +++++++++++++++++++++
 foreign/java/gradle/wrapper/gradle-wrapper.jar     | Bin 43764 -> 45633 bytes
 .../java/gradle/wrapper/gradle-wrapper.properties  |   2 +-
 foreign/java/gradlew                               |   5 +-
 foreign/java/gradlew.bat                           |   3 +-
 foreign/java/java-sdk/build.gradle.kts             |  49 +++-------
 .../blocking/http/ConsumerGroupsHttpClient.java    |   2 +-
 .../client/blocking/http/InternalHttpClient.java   |  23 ++---
 .../client/blocking/http/ObjectMapperFactory.java  |  20 ++--
 .../http/PersonalAccessTokensHttpClient.java       |   2 +-
 .../client/blocking/http/StreamsHttpClient.java    |   2 +-
 .../client/blocking/http/SystemHttpClient.java     |   2 +-
 .../client/blocking/http/TopicsHttpClient.java     |   2 +-
 .../iggy/client/blocking/http/UsersHttpClient.java |   2 +-
 .../java/org/apache/iggy/user/Permissions.java     |   3 +-
 27 files changed, 250 insertions(+), 277 deletions(-)

diff --git a/foreign/java/.gitattributes b/foreign/java/.gitattributes
new file mode 100644
index 000000000..8af972cde
--- /dev/null
+++ b/foreign/java/.gitattributes
@@ -0,0 +1,3 @@
+/gradlew text eol=lf
+*.bat text eol=crlf
+*.jar binary
diff --git a/foreign/java/build.gradle.kts b/foreign/java/build.gradle.kts
index 415fa3f0b..d1d66fb3b 100644
--- a/foreign/java/build.gradle.kts
+++ b/foreign/java/build.gradle.kts
@@ -20,7 +20,7 @@
 import com.diffplug.gradle.spotless.SpotlessExtension
 
 plugins {
-    id("com.diffplug.spotless") version "8.0.0" apply false
+    alias(libs.plugins.spotless) apply false
 }
 
 subprojects {
@@ -42,7 +42,7 @@ subprojects {
         apply(plugin = "checkstyle")
 
         configure<CheckstyleExtension> {
-            toolVersion = "10.23.1"
+            toolVersion = libs.versions.checkstyle.get()
             configFile = file("${project.rootDir}/dev-support/checkstyle/checkstyle.xml")
             configDirectory = file("${project.rootDir}/dev-support/checkstyle")
         }
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java b/foreign/java/buildSrc/build.gradle.kts
similarity index 73%
copy from foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
copy to foreign/java/buildSrc/build.gradle.kts
index 4fc72e9f8..920b25bc7 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
+++ b/foreign/java/buildSrc/build.gradle.kts
@@ -17,12 +17,11 @@
  * under the License.
  */
 
-package org.apache.iggy.user;
+plugins {
+    `kotlin-dsl`
+}
 
-import com.fasterxml.jackson.annotation.JsonSetter;
-import com.fasterxml.jackson.annotation.Nulls;
-
-import java.util.Map;
-
-public record Permissions(
-        GlobalPermissions global, @JsonSetter(nulls = Nulls.AS_EMPTY) Map<Long, StreamPermissions> streams) {}
+repositories {
+    mavenCentral()
+    gradlePluginPortal()
+}
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java b/foreign/java/buildSrc/settings.gradle.kts
similarity index 73%
copy from foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
copy to foreign/java/buildSrc/settings.gradle.kts
index 4fc72e9f8..076f6863a 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
+++ b/foreign/java/buildSrc/settings.gradle.kts
@@ -17,12 +17,4 @@
  * under the License.
  */
 
-package org.apache.iggy.user;
-
-import com.fasterxml.jackson.annotation.JsonSetter;
-import com.fasterxml.jackson.annotation.Nulls;
-
-import java.util.Map;
-
-public record Permissions(
-        GlobalPermissions global, @JsonSetter(nulls = Nulls.AS_EMPTY) Map<Long, StreamPermissions> streams) {}
+rootProject.name = "buildSrc"
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java b/foreign/java/buildSrc/src/main/kotlin/iggy.java-application-conventions.gradle.kts
similarity index 73%
copy from foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
copy to foreign/java/buildSrc/src/main/kotlin/iggy.java-application-conventions.gradle.kts
index 4fc72e9f8..4ac7210d6 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
+++ b/foreign/java/buildSrc/src/main/kotlin/iggy.java-application-conventions.gradle.kts
@@ -17,12 +17,16 @@
  * under the License.
  */
 
-package org.apache.iggy.user;
+plugins {
+    id("iggy.java-common-conventions")
+    application
+}
 
-import com.fasterxml.jackson.annotation.JsonSetter;
-import com.fasterxml.jackson.annotation.Nulls;
-
-import java.util.Map;
-
-public record Permissions(
-        GlobalPermissions global, @JsonSetter(nulls = Nulls.AS_EMPTY) Map<Long, StreamPermissions> streams) {}
+tasks.withType<Jar> {
+    manifest {
+        val mainClassName = application.mainClass.orNull
+        if (mainClassName != null) {
+            attributes["Main-Class"] = mainClassName
+        }
+    }
+}
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java b/foreign/java/buildSrc/src/main/kotlin/iggy.java-common-conventions.gradle.kts
similarity index 68%
copy from foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
copy to foreign/java/buildSrc/src/main/kotlin/iggy.java-common-conventions.gradle.kts
index 4fc72e9f8..bb86082b9 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
+++ b/foreign/java/buildSrc/src/main/kotlin/iggy.java-common-conventions.gradle.kts
@@ -17,12 +17,28 @@
  * under the License.
  */
 
-package org.apache.iggy.user;
+plugins {
+    java
+}
 
-import com.fasterxml.jackson.annotation.JsonSetter;
-import com.fasterxml.jackson.annotation.Nulls;
+repositories {
+    mavenCentral()
+}
 
-import java.util.Map;
+java {
+    sourceCompatibility = JavaVersion.VERSION_17
+    targetCompatibility = JavaVersion.VERSION_17
+}
 
-public record Permissions(
-        GlobalPermissions global, @JsonSetter(nulls = Nulls.AS_EMPTY) Map<Long, StreamPermissions> streams) {}
+tasks.withType<JavaCompile> {
+    options.encoding = "UTF-8"
+    options.compilerArgs.add("-parameters")
+}
+
+tasks.withType<Javadoc> {
+    options.encoding = "UTF-8"
+}
+
+tasks.withType<Test> {
+    useJUnitPlatform()
+}
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java b/foreign/java/buildSrc/src/main/kotlin/iggy.java-library-conventions.gradle.kts
similarity index 73%
copy from foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
copy to foreign/java/buildSrc/src/main/kotlin/iggy.java-library-conventions.gradle.kts
index 4fc72e9f8..e50072dbb 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
+++ b/foreign/java/buildSrc/src/main/kotlin/iggy.java-library-conventions.gradle.kts
@@ -17,12 +17,13 @@
  * under the License.
  */
 
-package org.apache.iggy.user;
+plugins {
+    id("iggy.java-common-conventions")
+    `java-library`
+    `maven-publish`
+}
 
-import com.fasterxml.jackson.annotation.JsonSetter;
-import com.fasterxml.jackson.annotation.Nulls;
-
-import java.util.Map;
-
-public record Permissions(
-        GlobalPermissions global, @JsonSetter(nulls = Nulls.AS_EMPTY) Map<Long, StreamPermissions> streams) {}
+java {
+    withJavadocJar()
+    withSourcesJar()
+}
diff --git a/foreign/java/examples/build.gradle.kts b/foreign/java/examples/build.gradle.kts
index ed5cba4f9..d92ebb06a 100644
--- a/foreign/java/examples/build.gradle.kts
+++ b/foreign/java/examples/build.gradle.kts
@@ -18,12 +18,7 @@
  */
 
 plugins {
-    id("java")
-    id("application")
-}
-
-repositories {
-    mavenCentral()
+    id("iggy.java-application-conventions")
 }
 
 application {
@@ -32,9 +27,9 @@ application {
 
 dependencies {
     implementation(project(":iggy"))
-    implementation("org.slf4j:slf4j-api:2.0.17")
-    runtimeOnly("ch.qos.logback:logback-classic:1.5.21")
-    runtimeOnly("io.netty:netty-resolver-dns-native-macos:4.2.7.Final:osx-aarch_64")
+    implementation(libs.slf4j.api)
+    runtimeOnly(libs.logback.classic)
+    runtimeOnly(libs.netty.dns.macos) { artifact { classifier = "osx-aarch_64" } }
 }
 
 // Task for running async consumer example
diff --git a/foreign/java/external-processors/iggy-connector-flink/iggy-connector-library/build.gradle.kts b/foreign/java/external-processors/iggy-connector-flink/iggy-connector-library/build.gradle.kts
index b614f3ff4..43a66d4c9 100644
--- a/foreign/java/external-processors/iggy-connector-flink/iggy-connector-library/build.gradle.kts
+++ b/foreign/java/external-processors/iggy-connector-flink/iggy-connector-library/build.gradle.kts
@@ -18,60 +18,29 @@
  */
 
 plugins {
-    id("java-library")
-    id("maven-publish")
+    id("iggy.java-library-conventions")
 }
 
-repositories {
-    mavenCentral()
-}
-
-java {
-    // Target Java 17 for CI compatibility (Java 21 Flink Docker can run Java 17 bytecode)
-    sourceCompatibility = JavaVersion.VERSION_17
-    targetCompatibility = JavaVersion.VERSION_17
-
-    withJavadocJar()
-    withSourcesJar()
-}
-
-val flinkVersion = "2.1.0"
-
 dependencies {
     // Iggy SDK - use local project when building within Iggy repository
     api(project(":iggy"))
 
     // Flink dependencies (provided - not bundled with connector)
-    compileOnly("org.apache.flink:flink-connector-base:${flinkVersion}")
-    compileOnly("org.apache.flink:flink-streaming-java:${flinkVersion}")
+    compileOnly(libs.flink.connector.base)
+    compileOnly(libs.flink.streaming.java)
 
     // Serialization support
-    implementation("com.fasterxml.jackson.core:jackson-databind:2.18.0")
-    implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.0")
+    implementation(libs.jackson.databind)
 
     // Logging
-    compileOnly("org.slf4j:slf4j-api:2.0.16")
+    compileOnly(libs.slf4j.api)
 
     // Testing
-    testImplementation("org.apache.flink:flink-test-utils:${flinkVersion}")
-    testImplementation("org.apache.flink:flink-runtime:${flinkVersion}:tests")
-    testImplementation("org.junit.jupiter:junit-jupiter:5.14.1")
-    testImplementation("org.assertj:assertj-core:3.27.6")
-    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
-    testRuntimeOnly("org.slf4j:slf4j-simple:2.0.16")
-}
-
-tasks.withType<Test> {
-    useJUnitPlatform()
-}
-
-tasks.withType<JavaCompile> {
-    options.encoding = "UTF-8"
-    options.compilerArgs.add("-parameters")
-}
-
-tasks.withType<Javadoc> {
-    options.encoding = "UTF-8"
+    testImplementation(libs.flink.test.utils)
+    testImplementation(libs.flink.runtime.tests) { artifact { classifier = "tests" } }
+    testImplementation(platform(libs.junit.bom))
+    testImplementation(libs.bundles.testing)
+    testRuntimeOnly(libs.slf4j.simple)
 }
 
 publishing {
diff --git a/foreign/java/external-processors/iggy-connector-flink/iggy-connector-library/src/main/java/org/apache/iggy/connector/serialization/JsonDeserializationSchema.java b/foreign/java/external-processors/iggy-connector-flink/iggy-connector-library/src/main/java/org/apache/iggy/connector/serialization/JsonDeserializationSchema.java
index 61251ddbe..ff15e6b6e 100644
--- a/foreign/java/external-processors/iggy-connector-flink/iggy-connector-library/src/main/java/org/apache/iggy/connector/serialization/JsonDeserializationSchema.java
+++ b/foreign/java/external-processors/iggy-connector-flink/iggy-connector-library/src/main/java/org/apache/iggy/connector/serialization/JsonDeserializationSchema.java
@@ -19,12 +19,9 @@
 
 package org.apache.iggy.connector.serialization;
 
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-
-import java.io.IOException;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
 
 /**
  * Deserialization schema for JSON using Jackson.
@@ -45,13 +42,13 @@ public class JsonDeserializationSchema<T> implements DeserializationSchema<T> {
      * @param typeClass the class to deserialize to
      */
     public JsonDeserializationSchema(Class<T> typeClass) {
-        this(typeClass, createDefaultObjectMapper());
+        this(typeClass, new JsonMapper());
     }
 
     /**
      * Creates a new JSON deserializer with a custom ObjectMapper.
      *
-     * @param typeClass the class to deserialize to
+     * @param typeClass    the class to deserialize to
      * @param objectMapper the Jackson ObjectMapper to use
      */
     public JsonDeserializationSchema(Class<T> typeClass, ObjectMapper objectMapper) {
@@ -66,15 +63,15 @@ public class JsonDeserializationSchema<T> implements DeserializationSchema<T> {
     }
 
     @Override
-    public T deserialize(byte[] data, RecordMetadata metadata) throws IOException {
+    public T deserialize(byte[] data, RecordMetadata metadata) {
         if (data == null || data.length == 0) {
             return null;
         }
 
         try {
             return getObjectMapper().readValue(data, typeClass);
-        } catch (IOException e) {
-            throw new IOException("Failed to deserialize JSON for type " + typeClass.getName(), e);
+        } catch (JacksonException e) {
+            throw new RuntimeException("Failed to deserialize JSON for type " + typeClass.getName(), e);
         }
     }
 
@@ -98,27 +95,7 @@ public class JsonDeserializationSchema<T> implements DeserializationSchema<T> {
             return objectMapper;
         }
         // Fallback for deserialized instances
-        return createDefaultObjectMapper();
-    }
-
-    /**
-     * Creates a default ObjectMapper configured for common use cases.
-     *
-     * @return configured ObjectMapper
-     */
-    private static ObjectMapper createDefaultObjectMapper() {
-        ObjectMapper mapper = new ObjectMapper();
-
-        // Register Java 8 time module for LocalDateTime, Instant, etc.
-        mapper.registerModule(new JavaTimeModule());
-
-        // Don't fail on unknown properties
-        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
-
-        // Write dates as ISO-8601 strings, not timestamps
-        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
-
-        return mapper;
+        return new JsonMapper();
     }
 
     /**
diff --git a/foreign/java/external-processors/iggy-connector-flink/iggy-connector-library/src/main/java/org/apache/iggy/connector/serialization/JsonSerializationSchema.java b/foreign/java/external-processors/iggy-connector-flink/iggy-connector-library/src/main/java/org/apache/iggy/connector/serialization/JsonSerializationSchema.java
index 8dcdb1ed8..2fb872e88 100644
--- a/foreign/java/external-processors/iggy-connector-flink/iggy-connector-library/src/main/java/org/apache/iggy/connector/serialization/JsonSerializationSchema.java
+++ b/foreign/java/external-processors/iggy-connector-flink/iggy-connector-library/src/main/java/org/apache/iggy/connector/serialization/JsonSerializationSchema.java
@@ -19,14 +19,12 @@
 
 package org.apache.iggy.connector.serialization;
 
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
 
-import java.io.IOException;
 import java.util.Optional;
 import java.util.function.Function;
 
@@ -49,7 +47,7 @@ public class JsonSerializationSchema<T> implements SerializationSchema<T> {
      * Creates a new JSON serializer.
      */
     public JsonSerializationSchema() {
-        this(createDefaultObjectMapper(), null);
+        this(new JsonMapper(), null);
     }
 
     /**
@@ -58,7 +56,7 @@ public class JsonSerializationSchema<T> implements SerializationSchema<T> {
      * @param partitionKeyExtractor function to extract partition key from elements
      */
     public JsonSerializationSchema(Function<T, Integer> partitionKeyExtractor) {
-        this(createDefaultObjectMapper(), partitionKeyExtractor);
+        this((ObjectMapper) new JsonMapper(), partitionKeyExtractor);
     }
 
     /**
@@ -73,7 +71,7 @@ public class JsonSerializationSchema<T> implements SerializationSchema<T> {
     /**
      * Creates a new JSON serializer with custom ObjectMapper and partition key extractor.
      *
-     * @param objectMapper the Jackson ObjectMapper to use
+     * @param objectMapper          the Jackson ObjectMapper to use
      * @param partitionKeyExtractor function to extract partition key from elements
      */
     public JsonSerializationSchema(ObjectMapper objectMapper, Function<T, Integer> partitionKeyExtractor) {
@@ -89,7 +87,7 @@ public class JsonSerializationSchema<T> implements SerializationSchema<T> {
      * Static factory method to avoid constructor ambiguity.
      *
      * @param partitionKeyExtractor function to extract partition key from elements
-     * @param <T> the type to serialize
+     * @param <T>                   the type to serialize
      * @return new JsonSerializationSchema instance
      */
     public static <T> JsonSerializationSchema<T> withPartitionKeyExtractor(Function<T, Integer> partitionKeyExtractor) {
@@ -97,15 +95,15 @@ public class JsonSerializationSchema<T> implements SerializationSchema<T> {
     }
 
     @Override
-    public byte[] serialize(T element) throws IOException {
+    public byte[] serialize(T element) {
         if (element == null) {
             return null;
         }
 
         try {
             return getObjectMapper().writeValueAsBytes(element);
-        } catch (IOException e) {
-            throw new IOException(
+        } catch (JacksonException e) {
+            throw new RuntimeException(
                     "Failed to serialize object of type " + element.getClass().getName(), e);
         }
     }
@@ -140,26 +138,6 @@ public class JsonSerializationSchema<T> implements SerializationSchema<T> {
             return objectMapper;
         }
         // Fallback for deserialized instances
-        return createDefaultObjectMapper();
-    }
-
-    /**
-     * Creates a default ObjectMapper configured for common use cases.
-     *
-     * @return configured ObjectMapper
-     */
-    private static ObjectMapper createDefaultObjectMapper() {
-        ObjectMapper mapper = new ObjectMapper();
-
-        // Register Java 8 time module for LocalDateTime, Instant, etc.
-        mapper.registerModule(new JavaTimeModule());
-
-        // Don't fail on unknown properties
-        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
-
-        // Write dates as ISO-8601 strings, not timestamps
-        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
-
-        return mapper;
+        return new JsonMapper();
     }
 }
diff --git a/foreign/java/external-processors/iggy-connector-flink/iggy-flink-examples/build.gradle.kts b/foreign/java/external-processors/iggy-connector-flink/iggy-flink-examples/build.gradle.kts
index 802e594bf..6108df8f1 100644
--- a/foreign/java/external-processors/iggy-connector-flink/iggy-flink-examples/build.gradle.kts
+++ b/foreign/java/external-processors/iggy-connector-flink/iggy-flink-examples/build.gradle.kts
@@ -18,16 +18,10 @@
  */
 
 plugins {
-    application
-    id("com.github.johnrengelman.shadow") version "8.1.1"
+    id("iggy.java-application-conventions")
+    alias(libs.plugins.shadow)
 }
 
-repositories {
-    mavenCentral()
-}
-
-val flinkVersion = "2.1.0"
-
 application {
     mainClass.set("org.apache.iggy.flink.example.StreamTransformJob")
 }
@@ -37,30 +31,27 @@ dependencies {
     implementation(project(":iggy-connector-library"))
 
     // Flink runtime (provided by cluster in production)
-    compileOnly("org.apache.flink:flink-streaming-java:${flinkVersion}")
-    compileOnly("org.apache.flink:flink-clients:${flinkVersion}")
+    compileOnly(libs.flink.streaming.java)
+    compileOnly(libs.flink.clients)
 
     // For local development/testing, include Flink at runtime
-    runtimeOnly("org.apache.flink:flink-streaming-java:${flinkVersion}")
-    runtimeOnly("org.apache.flink:flink-clients:${flinkVersion}")
+    runtimeOnly(libs.flink.streaming.java)
+    runtimeOnly(libs.flink.clients)
 
     // Jackson for JSON serialization in model classes
-    implementation("com.fasterxml.jackson.core:jackson-databind:2.20.1")
-    implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.1")
+    implementation(libs.jackson.databind)
 
     // Include in fat jar for standalone execution
-    implementation("org.slf4j:slf4j-simple:2.0.16")
-    implementation("com.typesafe:config:1.4.3")
+    implementation(libs.slf4j.simple)
+    implementation(libs.typesafe.config)
 
     // Testing
-    testImplementation("org.junit.jupiter:junit-jupiter:5.14.1")
-    testImplementation("org.assertj:assertj-core:3.27.6")
-    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
-    testRuntimeOnly("org.slf4j:slf4j-simple:2.0.16")
+    testImplementation(platform(libs.junit.bom))
+    testImplementation(libs.bundles.testing)
+    testRuntimeOnly(libs.slf4j.simple)
 }
 
 tasks.withType<Test> {
-    useJUnitPlatform()
     // Exclude integration tests that require a running Iggy server
     // These should be run separately with: ./gradlew integrationTest
     exclude("**/AsyncTcp*Test.class", "**/SendTextDataTest.class")
@@ -77,28 +68,3 @@ tasks.shadowJar {
     // Relocate conflicting dependencies if needed
     // relocate("com.google", "org.apache.iggy.shaded.com.google")
 }
-
-java {
-    // Target Java 17 for CI compatibility (Java 21 Flink Docker can run Java 17 bytecode)
-    sourceCompatibility = JavaVersion.VERSION_17
-    targetCompatibility = JavaVersion.VERSION_17
-}
-
-tasks.withType<JavaCompile> {
-    options.encoding = "UTF-8"
-    options.compilerArgs.add("-parameters")
-}
-
-tasks.withType<Javadoc> {
-    options.encoding = "UTF-8"
-    (options as StandardJavadocDocletOptions).apply {
-        addStringOption("Xdoclint:none", "-quiet")
-        addBooleanOption("html5", true)
-    }
-}
-
-tasks.withType<Jar> {
-    manifest {
-        attributes["Main-Class"] = application.mainClass.get()
-    }
-}
diff --git a/foreign/java/gradle/libs.versions.toml b/foreign/java/gradle/libs.versions.toml
new file mode 100644
index 000000000..63cd5b3bd
--- /dev/null
+++ b/foreign/java/gradle/libs.versions.toml
@@ -0,0 +1,106 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[versions]
+# Flink
+flink = "2.1.1"
+
+# Jackson
+jackson = "3.0.2"
+
+# Apache Commons
+commons-lang3 = "3.20.0"
+
+# HTTP Client
+httpclient5 = "5.5.1"
+
+# Reactor
+reactor-core = "3.8.0"
+reactor-netty = "1.3.0"
+
+# Logging
+slf4j = "2.0.17"
+logback = "1.5.21"
+
+# Testing
+junit = "6.0.1"
+assertj = "3.27.6"
+testcontainers = "2.0.2"
+
+# Netty
+netty = "4.2.7.Final"
+
+# Spotbugs
+spotbugs = "4.9.8"
+
+# Config
+typesafe-config = "1.4.5"
+
+# Build plugins
+spotless = "8.1.0"
+shadow = "9.2.2"
+checkstyle = "12.1.2"
+
+[libraries]
+# Jackson
+jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" }
+
+# Apache HTTP Client
+httpclient5 = { module = "org.apache.httpcomponents.client5:httpclient5", version.ref = "httpclient5" }
+
+# Apache Commons
+commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang3" }
+
+# Reactor
+reactor-core = { module = "io.projectreactor:reactor-core", version.ref = "reactor-core" }
+reactor-netty-core = { module = "io.projectreactor.netty:reactor-netty-core", version.ref = "reactor-netty" }
+
+# Logging
+slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
+slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
+logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
+
+# Flink
+flink-connector-base = { module = "org.apache.flink:flink-connector-base", version.ref = "flink" }
+flink-streaming-java = { module = "org.apache.flink:flink-streaming-java", version.ref = "flink" }
+flink-clients = { module = "org.apache.flink:flink-clients", version.ref = "flink" }
+flink-test-utils = { module = "org.apache.flink:flink-test-utils", version.ref = "flink" }
+flink-runtime-tests = { module = "org.apache.flink:flink-runtime", version.ref = "flink" }
+
+# Testing
+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" }
+assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
+testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
+testcontainers-junit = { module = "org.testcontainers:testcontainers-junit-jupiter", version.ref = "testcontainers" }
+
+# Netty
+netty-dns-macos = { module = "io.netty:netty-resolver-dns-native-macos", version.ref = "netty" }
+
+# Spotbugs
+spotbugs-annotations = { module = "com.github.spotbugs:spotbugs-annotations", version.ref = "spotbugs" }
+
+# Config
+typesafe-config = { module = "com.typesafe:config", version.ref = "typesafe-config" }
+
+[bundles]
+testing = ["junit-jupiter", "junit-platform-launcher", "assertj-core"]
+
+[plugins]
+spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
+shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
diff --git a/foreign/java/gradle/wrapper/gradle-wrapper.jar b/foreign/java/gradle/wrapper/gradle-wrapper.jar
index 1b33c55ba..f8e1ee312 100644
Binary files a/foreign/java/gradle/wrapper/gradle-wrapper.jar and b/foreign/java/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/foreign/java/gradle/wrapper/gradle-wrapper.properties b/foreign/java/gradle/wrapper/gradle-wrapper.properties
index d4081da47..23449a2b5 100644
--- a/foreign/java/gradle/wrapper/gradle-wrapper.properties
+++ b/foreign/java/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
 networkTimeout=10000
 validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
diff --git a/foreign/java/gradlew b/foreign/java/gradlew
index 23d15a936..adff685a0 100755
--- a/foreign/java/gradlew
+++ b/foreign/java/gradlew
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 #
-# Copyright © 2015-2021 the original authors.
+# Copyright © 2015 the original authors.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -114,7 +114,6 @@ case "$( uname )" in                #(
   NONSTOP* )        nonstop=true ;;
 esac
 
-CLASSPATH="\\\"\\\""
 
 
 # Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
 # For Cygwin or MSYS, switch paths to Windows format before running java
 if "$cygwin" || "$msys" ; then
     APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
-    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
 
     JAVACMD=$( cygpath --unix "$JAVACMD" )
 
@@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
 
 set -- \
         "-Dorg.gradle.appname=$APP_BASE_NAME" \
-        -classpath "$CLASSPATH" \
         -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
         "$@"
 
diff --git a/foreign/java/gradlew.bat b/foreign/java/gradlew.bat
index db3a6ac20..c4bdd3ab8 100644
--- a/foreign/java/gradlew.bat
+++ b/foreign/java/gradlew.bat
@@ -70,11 +70,10 @@ goto fail
 :execute
 @rem Setup the command line
 
-set CLASSPATH=
 
 
 @rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
 
 :end
 @rem End local scope for the variables with windows NT shell
diff --git a/foreign/java/java-sdk/build.gradle.kts b/foreign/java/java-sdk/build.gradle.kts
index a03cbde84..6197e5e07 100644
--- a/foreign/java/java-sdk/build.gradle.kts
+++ b/foreign/java/java-sdk/build.gradle.kts
@@ -18,44 +18,23 @@
  */
 
 plugins {
-    id("java-library")
-    id("maven-publish")
-}
-
-repositories {
-    mavenCentral()
-}
-
-java {
-    // Keep Java 17 for SDK compatibility with broader ecosystem
-    sourceCompatibility = JavaVersion.VERSION_17
-    targetCompatibility = JavaVersion.VERSION_17
-
-    withJavadocJar()
-    withSourcesJar()
+    id("iggy.java-library-conventions")
 }
 
 dependencies {
-    implementation("org.apache.httpcomponents.client5:httpclient5:5.5.1")
-    implementation("com.fasterxml.jackson.core:jackson-databind:2.20.1")
-    implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.20.1")
-    implementation("org.apache.commons:commons-lang3:3.20.0")
-    implementation("org.slf4j:slf4j-api:2.0.17")
-    implementation("com.github.spotbugs:spotbugs-annotations:4.9.8")
-    implementation("io.projectreactor:reactor-core:3.8.0")
-    implementation("io.projectreactor.netty:reactor-netty-core:1.3.0")
-    testImplementation("org.testcontainers:testcontainers:1.21.3")
-    testImplementation("org.testcontainers:junit-jupiter:1.21.3")
-    testImplementation(platform("org.junit:junit-bom:5.14.1"))
-    testImplementation("org.junit.jupiter:junit-jupiter")
-    testImplementation("org.assertj:assertj-core:3.27.6")
-    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
-    testRuntimeOnly("ch.qos.logback:logback-classic:1.5.21")
-    testRuntimeOnly("io.netty:netty-resolver-dns-native-macos:4.2.7.Final:osx-aarch_64")
-}
-
-tasks.withType<Test> {
-    useJUnitPlatform()
+    implementation(libs.httpclient5)
+    implementation(libs.jackson.databind)
+    implementation(libs.commons.lang3)
+    implementation(libs.slf4j.api)
+    implementation(libs.spotbugs.annotations)
+    implementation(libs.reactor.core)
+    implementation(libs.reactor.netty.core)
+    testImplementation(libs.testcontainers)
+    testImplementation(libs.testcontainers.junit)
+    testImplementation(platform(libs.junit.bom))
+    testImplementation(libs.bundles.testing)
+    testRuntimeOnly(libs.logback.classic)
+    testRuntimeOnly(libs.netty.dns.macos) { artifact { classifier = "osx-aarch_64" } }
 }
 
 publishing {
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ConsumerGroupsHttpClient.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ConsumerGroupsHttpClient.java
index 95bd4c609..fec8f8ddf 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ConsumerGroupsHttpClient.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ConsumerGroupsHttpClient.java
@@ -19,13 +19,13 @@
 
 package org.apache.iggy.client.blocking.http;
 
-import com.fasterxml.jackson.core.type.TypeReference;
 import org.apache.iggy.client.blocking.ConsumerGroupsClient;
 import org.apache.iggy.consumergroup.ConsumerGroup;
 import org.apache.iggy.consumergroup.ConsumerGroupDetails;
 import org.apache.iggy.identifier.ConsumerId;
 import org.apache.iggy.identifier.StreamId;
 import org.apache.iggy.identifier.TopicId;
+import tools.jackson.core.type.TypeReference;
 
 import java.util.List;
 import java.util.Optional;
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/InternalHttpClient.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/InternalHttpClient.java
index 368d1a06d..70a2379a0 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/InternalHttpClient.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/InternalHttpClient.java
@@ -19,10 +19,6 @@
 
 package org.apache.iggy.client.blocking.http;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.hc.client5.http.impl.classic.HttpClients;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
@@ -33,6 +29,9 @@ import org.apache.iggy.client.blocking.http.error.IggyHttpError;
 import org.apache.iggy.client.blocking.http.error.IggyHttpException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.ObjectMapper;
 
 import java.io.IOException;
 import java.util.Optional;
@@ -126,16 +125,12 @@ final class InternalHttpClient {
     }
 
     private ClassicHttpRequest addRequestBody(ClassicRequestBuilder requestBuilder, Object body) {
-        try {
-            var encodedBody = objectMapper.writeValueAsString(body);
-            log.debug("Request body: {}", encodedBody);
-            return requestBuilder
-                    .setHeader("Content-Type", "application/json")
-                    .setEntity(encodedBody)
-                    .build();
-        } catch (JsonProcessingException e) {
-            throw new RuntimeException(e);
-        }
+        var encodedBody = objectMapper.writeValueAsString(body);
+        log.debug("Request body: {}", encodedBody);
+        return requestBuilder
+                .setHeader("Content-Type", "application/json")
+                .setEntity(encodedBody)
+                .build();
     }
 
     private <T> T handleTypedResponse(ClassicHttpResponse response, JavaType type) throws IOException {
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ObjectMapperFactory.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ObjectMapperFactory.java
index 533c642b6..324378780 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ObjectMapperFactory.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ObjectMapperFactory.java
@@ -19,23 +19,19 @@
 
 package org.apache.iggy.client.blocking.http;
 
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.MapperFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.PropertyNamingStrategies;
-import com.fasterxml.jackson.databind.json.JsonMapper;
-import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.MapperFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.PropertyNamingStrategies;
+import tools.jackson.databind.json.JsonMapper;
 
 final class ObjectMapperFactory {
 
     private static final ObjectMapper INSTANCE = JsonMapper.builder()
             .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
-            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
-            .configure(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES, true)
-            .configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true)
-            .build()
-            .registerModule(new Jdk8Module())
-            .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
+            .enable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)
+            .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
+            .build();
 
     private ObjectMapperFactory() {}
 
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/PersonalAccessTokensHttpClient.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/PersonalAccessTokensHttpClient.java
index 15a7dd993..dff57cbf2 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/PersonalAccessTokensHttpClient.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/PersonalAccessTokensHttpClient.java
@@ -19,12 +19,12 @@
 
 package org.apache.iggy.client.blocking.http;
 
-import com.fasterxml.jackson.core.type.TypeReference;
 import org.apache.iggy.client.blocking.PersonalAccessTokensClient;
 import org.apache.iggy.personalaccesstoken.PersonalAccessTokenInfo;
 import org.apache.iggy.personalaccesstoken.RawPersonalAccessToken;
 import org.apache.iggy.user.IdentityInfo;
 import org.apache.iggy.user.TokenInfo;
+import tools.jackson.core.type.TypeReference;
 
 import java.math.BigInteger;
 import java.util.List;
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/StreamsHttpClient.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/StreamsHttpClient.java
index 7b38895bd..6929bc293 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/StreamsHttpClient.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/StreamsHttpClient.java
@@ -19,11 +19,11 @@
 
 package org.apache.iggy.client.blocking.http;
 
-import com.fasterxml.jackson.core.type.TypeReference;
 import org.apache.iggy.client.blocking.StreamsClient;
 import org.apache.iggy.identifier.StreamId;
 import org.apache.iggy.stream.StreamBase;
 import org.apache.iggy.stream.StreamDetails;
+import tools.jackson.core.type.TypeReference;
 
 import java.util.List;
 import java.util.Optional;
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/SystemHttpClient.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/SystemHttpClient.java
index 17122ec22..af6edb30c 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/SystemHttpClient.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/SystemHttpClient.java
@@ -19,11 +19,11 @@
 
 package org.apache.iggy.client.blocking.http;
 
-import com.fasterxml.jackson.core.type.TypeReference;
 import org.apache.iggy.client.blocking.SystemClient;
 import org.apache.iggy.system.ClientInfo;
 import org.apache.iggy.system.ClientInfoDetails;
 import org.apache.iggy.system.Stats;
+import tools.jackson.core.type.TypeReference;
 
 import java.util.List;
 
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/TopicsHttpClient.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/TopicsHttpClient.java
index 0c91a1c2a..e343058a8 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/TopicsHttpClient.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/TopicsHttpClient.java
@@ -19,13 +19,13 @@
 
 package org.apache.iggy.client.blocking.http;
 
-import com.fasterxml.jackson.core.type.TypeReference;
 import org.apache.iggy.client.blocking.TopicsClient;
 import org.apache.iggy.identifier.StreamId;
 import org.apache.iggy.identifier.TopicId;
 import org.apache.iggy.topic.CompressionAlgorithm;
 import org.apache.iggy.topic.Topic;
 import org.apache.iggy.topic.TopicDetails;
+import tools.jackson.core.type.TypeReference;
 
 import java.math.BigInteger;
 import java.util.List;
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UsersHttpClient.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UsersHttpClient.java
index b56f8bf2c..51deb4d87 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UsersHttpClient.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UsersHttpClient.java
@@ -19,7 +19,6 @@
 
 package org.apache.iggy.client.blocking.http;
 
-import com.fasterxml.jackson.core.type.TypeReference;
 import org.apache.iggy.client.blocking.UsersClient;
 import org.apache.iggy.identifier.UserId;
 import org.apache.iggy.user.IdentityInfo;
@@ -28,6 +27,7 @@ import org.apache.iggy.user.TokenInfo;
 import org.apache.iggy.user.UserInfo;
 import org.apache.iggy.user.UserInfoDetails;
 import org.apache.iggy.user.UserStatus;
+import tools.jackson.core.type.TypeReference;
 
 import java.util.List;
 import java.util.Optional;
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
index 4fc72e9f8..f3480e30b 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
@@ -25,4 +25,5 @@ import com.fasterxml.jackson.annotation.Nulls;
 import java.util.Map;
 
 public record Permissions(
-        GlobalPermissions global, @JsonSetter(nulls = Nulls.AS_EMPTY) Map<Long, StreamPermissions> streams) {}
+        GlobalPermissions global,
+        @JsonSetter(nulls = Nulls.AS_EMPTY) Map<Long, StreamPermissions> streams) {}