This is an automated email from the ASF dual-hosted git repository.
piotr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 8827588e8 feat(java): improve tests and clean up json mappings (#2658)
8827588e8 is described below
commit 8827588e8e50845ef9fb088c81bac09f8db05220
Author: Maciej Modzelewski <[email protected]>
AuthorDate: Mon Feb 2 12:22:35 2026 +0100
feat(java): improve tests and clean up json mappings (#2658)
---
.../java/dev-support/checkstyle/suppressions.xml | 3 +
.../blocking/http/MessageMixin.java} | 32 +--
.../client/blocking/http/ObjectMapperFactory.java | 11 +-
.../blocking/http}/UserHeadersSerializer.java | 4 +-
.../java/org/apache/iggy/message/HeaderKey.java | 34 +--
.../java/org/apache/iggy/message/HeaderKind.java | 16 --
.../java/org/apache/iggy/message/HeaderValue.java | 74 +++---
.../main/java/org/apache/iggy/message/Message.java | 33 +--
.../java/org/apache/iggy/message/Partitioning.java | 6 +-
.../org/apache/iggy/serde/Base64Serializer.java | 35 ---
.../java/org/apache/iggy/user/Permissions.java | 7 +-
.../org/apache/iggy/user/StreamPermissions.java | 5 +-
.../iggy/client/blocking/IntegrationTest.java | 6 +
.../blocking/http/HeaderKindSerializationTest.java | 120 +++++++++
.../client/blocking/http/ObjectMapperTest.java | 286 +++++++++++++++++++++
.../apache/iggy/serde/BytesDeserializerTest.java | 76 ------
16 files changed, 510 insertions(+), 238 deletions(-)
diff --git a/foreign/java/dev-support/checkstyle/suppressions.xml
b/foreign/java/dev-support/checkstyle/suppressions.xml
index ea771eb79..b155ae09c 100644
--- a/foreign/java/dev-support/checkstyle/suppressions.xml
+++ b/foreign/java/dev-support/checkstyle/suppressions.xml
@@ -27,4 +27,7 @@
<!-- Allow non-private fields in test code (e.g., @Inject, test base
classes) -->
<suppress checks="VisibilityModifier" files=".*[\\/]src[\\/]test[\\/].*"/>
+
+ <!-- Allow inner types before methods in test code (e.g., test data
classes) -->
+ <suppress checks="InnerTypeLast" files=".*[\\/]src[\\/]test[\\/].*"/>
</suppressions>
diff --git
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/MessageMixin.java
similarity index 55%
copy from
foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java
copy to
foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/MessageMixin.java
index 878ad8670..8ff4306fe 100644
---
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java
+++
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/MessageMixin.java
@@ -17,28 +17,30 @@
* under the License.
*/
-package org.apache.iggy.serde;
+package org.apache.iggy.client.blocking.http;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import org.apache.iggy.exception.IggyOperationNotSupportedException;
import org.apache.iggy.message.HeaderEntry;
import org.apache.iggy.message.HeaderKey;
import org.apache.iggy.message.HeaderValue;
-import tools.jackson.core.JacksonException;
-import tools.jackson.core.JsonGenerator;
-import tools.jackson.databind.SerializationContext;
-import tools.jackson.databind.ValueSerializer;
+import org.apache.iggy.message.Message;
+import org.apache.iggy.message.MessageHeader;
+import tools.jackson.databind.annotation.JsonSerialize;
+import java.util.List;
import java.util.Map;
-public class UserHeadersSerializer extends ValueSerializer<Map<HeaderKey,
HeaderValue>> {
+/**
+ * Jackson mixin for {@link Message} to keep the domain object free of
serialization annotations.
+ */
+abstract class MessageMixin {
- @Override
- public void serialize(Map<HeaderKey, HeaderValue> headers, JsonGenerator
gen, SerializationContext ctxt)
- throws JacksonException {
- gen.writeStartArray();
- for (Map.Entry<HeaderKey, HeaderValue> entry : headers.entrySet()) {
- ctxt.findValueSerializer(HeaderEntry.class)
- .serialize(new HeaderEntry(entry.getKey(),
entry.getValue()), gen, ctxt);
- }
- gen.writeEndArray();
+ @JsonCreator
+ static Message of(MessageHeader header, byte[] payload, List<HeaderEntry>
userHeaders) {
+ throw new IggyOperationNotSupportedException("Mixin method should not
be called directly");
}
+
+ @JsonSerialize(using = UserHeadersSerializer.class)
+ abstract Map<HeaderKey, HeaderValue> userHeaders();
}
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 cd7a8e9b9..167db5d36 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
@@ -21,26 +21,33 @@ package org.apache.iggy.client.blocking.http;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
+import org.apache.iggy.message.Message;
import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.EnumNamingStrategies;
import tools.jackson.databind.MapperFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.PropertyNamingStrategies;
import tools.jackson.databind.json.JsonMapper;
+import java.util.List;
import java.util.Map;
-public final class ObjectMapperFactory {
+final class ObjectMapperFactory {
private static final ObjectMapper INSTANCE = JsonMapper.builder()
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
.enable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
+ .enumNamingStrategy(EnumNamingStrategies.LOWER_CASE)
.withConfigOverride(Map.class, map ->
map.setNullHandling(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)))
+ .withConfigOverride(
+ List.class, list ->
list.setNullHandling(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)))
+ .addMixIn(Message.class, MessageMixin.class)
.build();
private ObjectMapperFactory() {}
- public static ObjectMapper getInstance() {
+ static ObjectMapper getInstance() {
return INSTANCE;
}
}
diff --git
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UserHeadersSerializer.java
similarity index 92%
rename from
foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java
rename to
foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UserHeadersSerializer.java
index 878ad8670..6f8aa16d6 100644
---
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java
+++
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UserHeadersSerializer.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.apache.iggy.serde;
+package org.apache.iggy.client.blocking.http;
import org.apache.iggy.message.HeaderEntry;
import org.apache.iggy.message.HeaderKey;
@@ -29,7 +29,7 @@ import tools.jackson.databind.ValueSerializer;
import java.util.Map;
-public class UserHeadersSerializer extends ValueSerializer<Map<HeaderKey,
HeaderValue>> {
+class UserHeadersSerializer extends ValueSerializer<Map<HeaderKey,
HeaderValue>> {
@Override
public void serialize(Map<HeaderKey, HeaderValue> headers, JsonGenerator
gen, SerializationContext ctxt)
diff --git
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKey.java
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKey.java
index ad65857a9..c46bdc78d 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKey.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKey.java
@@ -19,25 +19,24 @@
package org.apache.iggy.message;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.iggy.exception.IggyInvalidArgumentException;
import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Base64;
public record HeaderKey(HeaderKind kind, byte[] value) {
- @JsonCreator
- public static HeaderKey fromJson(@JsonProperty("kind") HeaderKind kind,
@JsonProperty("value") String base64Value) {
- byte[] decodedValue = Base64.getDecoder().decode(base64Value);
- return new HeaderKey(kind, decodedValue);
- }
- public static HeaderKey fromString(String val) {
- if (val.isEmpty() || val.length() > 255) {
- throw new IllegalArgumentException("Value has incorrect size, must
be between 1 and 255");
+ public static HeaderKey fromString(String value) {
+ if (StringUtils.isBlank(value)) {
+ throw new IggyInvalidArgumentException("Value cannot be null or
empty");
+ }
+ var bytes = value.getBytes(StandardCharsets.UTF_8);
+ if (bytes.length > 255) {
+ throw new IggyInvalidArgumentException("Value has incorrect size,
must be between 1 and 255 bytes");
}
- return new HeaderKey(HeaderKind.String,
val.getBytes(StandardCharsets.UTF_8));
+ return new HeaderKey(HeaderKind.String, bytes);
}
@Override
@@ -49,14 +48,15 @@ public record HeaderKey(HeaderKind kind, byte[] value) {
return false;
}
HeaderKey headerKey = (HeaderKey) o;
- return kind == headerKey.kind && Arrays.equals(value, headerKey.value);
+ return new EqualsBuilder()
+ .append(value, headerKey.value)
+ .append(kind, headerKey.kind)
+ .isEquals();
}
@Override
public int hashCode() {
- int result = kind.hashCode();
- result = 31 * result + Arrays.hashCode(value);
- return result;
+ return new HashCodeBuilder(17,
37).append(kind).append(value).toHashCode();
}
@Override
diff --git
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKind.java
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKind.java
index 753a9ee5a..00f024c25 100644
---
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKind.java
+++
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKind.java
@@ -19,39 +19,23 @@
package org.apache.iggy.message;
-import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.iggy.exception.IggyInvalidArgumentException;
public enum HeaderKind {
- @JsonProperty("raw")
Raw(1),
- @JsonProperty("string")
String(2),
- @JsonProperty("bool")
Bool(3),
- @JsonProperty("int8")
Int8(4),
- @JsonProperty("int16")
Int16(5),
- @JsonProperty("int32")
Int32(6),
- @JsonProperty("int64")
Int64(7),
- @JsonProperty("int128")
Int128(8),
- @JsonProperty("uint8")
Uint8(9),
- @JsonProperty("uint16")
Uint16(10),
- @JsonProperty("uint32")
Uint32(11),
- @JsonProperty("uint64")
Uint64(12),
- @JsonProperty("uint128")
Uint128(13),
- @JsonProperty("float32")
Float32(14),
- @JsonProperty("float64")
Float64(15);
private final int code;
diff --git
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderValue.java
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderValue.java
index f4fc6a4c2..ee4177da0 100644
---
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderValue.java
+++
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderValue.java
@@ -19,28 +19,27 @@
package org.apache.iggy.message;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.iggy.exception.IggyInvalidArgumentException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
import java.util.Base64;
public record HeaderValue(HeaderKind kind, byte[] value) {
- @JsonCreator
- public static HeaderValue fromJson(
- @JsonProperty("kind") HeaderKind kind, @JsonProperty("value")
String base64Value) {
- byte[] decodedValue = Base64.getDecoder().decode(base64Value);
- return new HeaderValue(kind, decodedValue);
- }
- public static HeaderValue fromString(String val) {
- if (val.isEmpty() || val.length() > 255) {
- throw new IllegalArgumentException("Value has incorrect size, must
be between 1 and 255");
+ public static HeaderValue fromString(String value) {
+ if (StringUtils.isBlank(value)) {
+ throw new IggyInvalidArgumentException("Value cannot be null or
empty");
+ }
+ var bytes = value.getBytes(StandardCharsets.UTF_8);
+ if (bytes.length > 255) {
+ throw new IggyInvalidArgumentException("Value has incorrect size,
must be between 1 and 255 bytes");
}
- return new HeaderValue(HeaderKind.String,
val.getBytes(StandardCharsets.UTF_8));
+ return new HeaderValue(HeaderKind.String, bytes);
}
public static HeaderValue fromBool(boolean val) {
@@ -71,14 +70,14 @@ public record HeaderValue(HeaderKind kind, byte[] value) {
public static HeaderValue fromUint8(short val) {
if (val < 0 || val > 255) {
- throw new IllegalArgumentException("Value must be between 0 and
255");
+ throw new IggyInvalidArgumentException("Value must be between 0
and 255");
}
return new HeaderValue(HeaderKind.Uint8, new byte[] {(byte) val});
}
public static HeaderValue fromUint16(int val) {
if (val < 0 || val > 65535) {
- throw new IllegalArgumentException("Value must be between 0 and
65535");
+ throw new IggyInvalidArgumentException("Value must be between 0
and 65535");
}
ByteBuffer buffer =
ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN);
buffer.putShort((short) val);
@@ -87,7 +86,7 @@ public record HeaderValue(HeaderKind kind, byte[] value) {
public static HeaderValue fromUint32(long val) {
if (val < 0 || val > 4294967295L) {
- throw new IllegalArgumentException("Value must be between 0 and
4294967295");
+ throw new IggyInvalidArgumentException("Value must be between 0
and 4294967295");
}
ByteBuffer buffer =
ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt((int) val);
@@ -108,84 +107,84 @@ public record HeaderValue(HeaderKind kind, byte[] value) {
public static HeaderValue fromRaw(byte[] val) {
if (val.length == 0 || val.length > 255) {
- throw new IllegalArgumentException("Value has incorrect size, must
be between 1 and 255");
+ throw new IggyInvalidArgumentException("Value has incorrect size,
must be between 1 and 255 bytes");
}
return new HeaderValue(HeaderKind.Raw, val);
}
public String asString() {
if (kind != HeaderKind.String) {
- throw new IllegalStateException("Header value is not a string,
kind: " + kind);
+ throw new IggyInvalidArgumentException("Header value is not a
string, kind: " + kind);
}
return new String(value, StandardCharsets.UTF_8);
}
public boolean asBool() {
if (kind != HeaderKind.Bool) {
- throw new IllegalStateException("Header value is not a bool, kind:
" + kind);
+ throw new IggyInvalidArgumentException("Header value is not a
bool, kind: " + kind);
}
return value[0] == 1;
}
public byte asInt8() {
if (kind != HeaderKind.Int8) {
- throw new IllegalStateException("Header value is not an int8,
kind: " + kind);
+ throw new IggyInvalidArgumentException("Header value is not an
int8, kind: " + kind);
}
return value[0];
}
public short asInt16() {
if (kind != HeaderKind.Int16) {
- throw new IllegalStateException("Header value is not an int16,
kind: " + kind);
+ throw new IggyInvalidArgumentException("Header value is not an
int16, kind: " + kind);
}
return
ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getShort();
}
public int asInt32() {
if (kind != HeaderKind.Int32) {
- throw new IllegalStateException("Header value is not an int32,
kind: " + kind);
+ throw new IggyInvalidArgumentException("Header value is not an
int32, kind: " + kind);
}
return ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getInt();
}
public long asInt64() {
if (kind != HeaderKind.Int64) {
- throw new IllegalStateException("Header value is not an int64,
kind: " + kind);
+ throw new IggyInvalidArgumentException("Header value is not an
int64, kind: " + kind);
}
return ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getLong();
}
public short asUint8() {
if (kind != HeaderKind.Uint8) {
- throw new IllegalStateException("Header value is not a uint8,
kind: " + kind);
+ throw new IggyInvalidArgumentException("Header value is not a
uint8, kind: " + kind);
}
return (short) (value[0] & 0xFF);
}
public int asUint16() {
if (kind != HeaderKind.Uint16) {
- throw new IllegalStateException("Header value is not a uint16,
kind: " + kind);
+ throw new IggyInvalidArgumentException("Header value is not a
uint16, kind: " + kind);
}
return
(ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF);
}
public long asUint32() {
if (kind != HeaderKind.Uint32) {
- throw new IllegalStateException("Header value is not a uint32,
kind: " + kind);
+ throw new IggyInvalidArgumentException("Header value is not a
uint32, kind: " + kind);
}
return (ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getInt()
& 0xFFFFFFFFL);
}
public float asFloat32() {
if (kind != HeaderKind.Float32) {
- throw new IllegalStateException("Header value is not a float32,
kind: " + kind);
+ throw new IggyInvalidArgumentException("Header value is not a
float32, kind: " + kind);
}
return
ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getFloat();
}
public double asFloat64() {
if (kind != HeaderKind.Float64) {
- throw new IllegalStateException("Header value is not a float64,
kind: " + kind);
+ throw new IggyInvalidArgumentException("Header value is not a
float64, kind: " + kind);
}
return
ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getDouble();
}
@@ -194,6 +193,11 @@ public record HeaderValue(HeaderKind kind, byte[] value) {
return value;
}
+ @Override
+ public String toString() {
+ return toStringValue();
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -203,19 +207,15 @@ public record HeaderValue(HeaderKind kind, byte[] value) {
return false;
}
HeaderValue that = (HeaderValue) o;
- return kind == that.kind && Arrays.equals(value, that.value);
+ return new EqualsBuilder()
+ .append(value, that.value)
+ .append(kind, that.kind)
+ .isEquals();
}
@Override
public int hashCode() {
- int result = kind.hashCode();
- result = 31 * result + Arrays.hashCode(value);
- return result;
- }
-
- @Override
- public String toString() {
- return toStringValue();
+ return new HashCodeBuilder(17,
37).append(kind).append(value).toHashCode();
}
private String toStringValue() {
diff --git
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Message.java
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Message.java
index 144103d10..de84fc3a2 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Message.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Message.java
@@ -19,39 +19,26 @@
package org.apache.iggy.message;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonSetter;
-import com.fasterxml.jackson.annotation.Nulls;
-import org.apache.iggy.serde.Base64Serializer;
-import org.apache.iggy.serde.UserHeadersSerializer;
-import tools.jackson.databind.annotation.JsonSerialize;
-
+import javax.annotation.Nullable;
import java.math.BigInteger;
-import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-public record Message(
- MessageHeader header,
- @JsonSerialize(using = Base64Serializer.class) byte[] payload,
- @JsonSerialize(using = UserHeadersSerializer.class) Map<HeaderKey,
HeaderValue> userHeaders) {
- @JsonCreator
- public static Message fromJson(
- @JsonProperty("header") MessageHeader header,
- @JsonProperty("payload") String base64Payload,
- @JsonProperty(value = "user_headers", required = false)
@JsonSetter(nulls = Nulls.AS_EMPTY)
- List<HeaderEntry> userHeadersList) {
- byte[] decodedPayload = Base64.getDecoder().decode(base64Payload);
+public record Message(MessageHeader header, byte[] payload, Map<HeaderKey,
HeaderValue> userHeaders) {
+
+ /**
+ * Creates a Message from JSON deserialization. Used by Jackson mixin.
+ */
+ public static Message of(MessageHeader header, byte[] payload, @Nullable
List<HeaderEntry> userHeaders) {
Map<HeaderKey, HeaderValue> headersMap = new HashMap<>();
- if (userHeadersList != null) {
- for (HeaderEntry entry : userHeadersList) {
+ if (userHeaders != null) {
+ for (HeaderEntry entry : userHeaders) {
headersMap.put(entry.key(), entry.value());
}
}
- return new Message(header, decodedPayload, headersMap);
+ return new Message(header, payload, headersMap);
}
public static Message of(String payload) {
diff --git
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Partitioning.java
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Partitioning.java
index c451510d2..51f071fc3 100644
---
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Partitioning.java
+++
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Partitioning.java
@@ -21,14 +21,10 @@ package org.apache.iggy.message;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.iggy.exception.IggyInvalidArgumentException;
-import org.apache.iggy.serde.Base64Serializer;
-import tools.jackson.databind.annotation.JsonSerialize;
import java.nio.ByteBuffer;
-public record Partitioning(
- PartitioningKind kind,
- @JsonSerialize(using = Base64Serializer.class) byte[] value) {
+public record Partitioning(PartitioningKind kind, byte[] value) {
public static Partitioning balanced() {
return new Partitioning(PartitioningKind.Balanced, new byte[] {});
}
diff --git
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/Base64Serializer.java
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/Base64Serializer.java
deleted file mode 100644
index 17bda8b39..000000000
---
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/Base64Serializer.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.iggy.serde;
-
-import tools.jackson.core.JacksonException;
-import tools.jackson.core.JsonGenerator;
-import tools.jackson.databind.SerializationContext;
-import tools.jackson.databind.ValueSerializer;
-
-import java.util.Base64;
-
-public class Base64Serializer extends ValueSerializer<byte[]> {
-
- @Override
- public void serialize(byte[] value, JsonGenerator gen,
SerializationContext ctxt) throws JacksonException {
- gen.writeString(Base64.getEncoder().encodeToString(value));
- }
-}
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 f3480e30b..c96dbdc12 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
@@ -19,11 +19,6 @@
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) {}
+public record Permissions(GlobalPermissions global, Map<Long,
StreamPermissions> streams) {}
diff --git
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/StreamPermissions.java
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/StreamPermissions.java
index 1b64e9ada..54c2488d8 100644
---
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/StreamPermissions.java
+++
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/StreamPermissions.java
@@ -19,9 +19,6 @@
package org.apache.iggy.user;
-import com.fasterxml.jackson.annotation.JsonSetter;
-import com.fasterxml.jackson.annotation.Nulls;
-
import java.util.Map;
public record StreamPermissions(
@@ -31,4 +28,4 @@ public record StreamPermissions(
boolean readTopics,
boolean pollMessages,
boolean sendMessages,
- @JsonSetter(nulls = Nulls.AS_EMPTY) Map<Long, TopicPermissions>
topics) {}
+ Map<Long, TopicPermissions> topics) {}
diff --git
a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java
index 8420872d6..46afa9a9c 100644
---
a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java
+++
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java
@@ -19,6 +19,8 @@
package org.apache.iggy.client.blocking;
+import com.github.dockerjava.api.model.Capability;
+import com.github.dockerjava.api.model.Ulimit;
import org.apache.iggy.stream.StreamDetails;
import org.apache.iggy.topic.CompressionAlgorithm;
import org.junit.jupiter.api.AfterAll;
@@ -64,6 +66,10 @@ public abstract class IntegrationTest {
.withEnv("IGGY_ROOT_PASSWORD", "iggy")
.withEnv("IGGY_TCP_ADDRESS", "0.0.0.0:8090")
.withEnv("IGGY_HTTP_ADDRESS", "0.0.0.0:3000")
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig()
+ .withCapAdd(Capability.SYS_NICE)
+ .withSecurityOpts(List.of("seccomp:unconfined"))
+ .withUlimits(List.of(new Ulimit("memlock", -1L,
-1L))))
.withLogConsumer(frame ->
System.out.print(frame.getUtf8String()));
iggyServer.start();
} else {
diff --git
a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/HeaderKindSerializationTest.java
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/HeaderKindSerializationTest.java
new file mode 100644
index 000000000..cb3ff5558
--- /dev/null
+++
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/HeaderKindSerializationTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+
+package org.apache.iggy.client.blocking.http;
+
+import org.apache.iggy.message.HeaderKind;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import tools.jackson.databind.ObjectMapper;
+
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class HeaderKindSerializationTest {
+
+ private final ObjectMapper objectMapper =
ObjectMapperFactory.getInstance();
+
+ @Nested
+ class Serialization {
+
+ @ParameterizedTest
+
@MethodSource("org.apache.iggy.client.blocking.http.HeaderKindSerializationTest#headerKindMappings")
+ void shouldSerializeToLowercase(HeaderKind kind, String expectedJson) {
+ // given
+ // kind provided by parameter
+
+ // when
+ String json = objectMapper.writeValueAsString(kind);
+
+ // then
+ assertThat(json).isEqualTo("\"" + expectedJson + "\"");
+ }
+ }
+
+ @Nested
+ class Deserialization {
+
+ @ParameterizedTest
+
@MethodSource("org.apache.iggy.client.blocking.http.HeaderKindSerializationTest#headerKindMappings")
+ void shouldDeserializeFromLowercase(HeaderKind expectedKind, String
jsonValue) {
+ // given
+ String json = "\"" + jsonValue + "\"";
+
+ // when
+ HeaderKind result = objectMapper.readValue(json, HeaderKind.class);
+
+ // then
+ assertThat(result).isEqualTo(expectedKind);
+ }
+
+ @ParameterizedTest
+
@MethodSource("org.apache.iggy.client.blocking.http.HeaderKindSerializationTest#headerKindMappings")
+ void shouldDeserializeCaseInsensitive(HeaderKind expectedKind, String
jsonValue) {
+ // given
+ String json = "\"" + jsonValue.toUpperCase() + "\"";
+
+ // when
+ HeaderKind result = objectMapper.readValue(json, HeaderKind.class);
+
+ // then
+ assertThat(result).isEqualTo(expectedKind);
+ }
+ }
+
+ @Nested
+ class Roundtrip {
+
+ @ParameterizedTest
+
@MethodSource("org.apache.iggy.client.blocking.http.HeaderKindSerializationTest#headerKindMappings")
+ void shouldRoundtripAllHeaderKinds(HeaderKind kind, String ignored) {
+ // given
+ // kind provided by parameter
+
+ // when
+ String json = objectMapper.writeValueAsString(kind);
+ HeaderKind result = objectMapper.readValue(json, HeaderKind.class);
+
+ // then
+ assertThat(result).isEqualTo(kind);
+ }
+ }
+
+ static Stream<Arguments> headerKindMappings() {
+ return Stream.of(
+ Arguments.of(HeaderKind.Raw, "raw"),
+ Arguments.of(HeaderKind.String, "string"),
+ Arguments.of(HeaderKind.Bool, "bool"),
+ Arguments.of(HeaderKind.Int8, "int8"),
+ Arguments.of(HeaderKind.Int16, "int16"),
+ Arguments.of(HeaderKind.Int32, "int32"),
+ Arguments.of(HeaderKind.Int64, "int64"),
+ Arguments.of(HeaderKind.Int128, "int128"),
+ Arguments.of(HeaderKind.Uint8, "uint8"),
+ Arguments.of(HeaderKind.Uint16, "uint16"),
+ Arguments.of(HeaderKind.Uint32, "uint32"),
+ Arguments.of(HeaderKind.Uint64, "uint64"),
+ Arguments.of(HeaderKind.Uint128, "uint128"),
+ Arguments.of(HeaderKind.Float32, "float32"),
+ Arguments.of(HeaderKind.Float64, "float64"));
+ }
+}
diff --git
a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/ObjectMapperTest.java
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/ObjectMapperTest.java
new file mode 100644
index 000000000..eefda1659
--- /dev/null
+++
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/ObjectMapperTest.java
@@ -0,0 +1,286 @@
+/*
+ * 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.
+ */
+
+package org.apache.iggy.client.blocking.http;
+
+import org.apache.iggy.message.Message;
+import org.apache.iggy.message.PolledMessages;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import tools.jackson.databind.ObjectMapper;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ObjectMapperTest {
+
+ private final ObjectMapper objectMapper =
ObjectMapperFactory.getInstance();
+
+ @Nested
+ class Deserialization {
+
+ @Nested
+ @DisplayName("PolledMessages")
+ class PolledMessagesDeserialization {
+
+ @Test
+ void shouldDeserializePolledMessagesWithEmptyUserHeaders() {
+ // given
+ String json = """
+ {
+ "partition_id": 1,
+ "current_offset": 10,
+ "count": 1,
+ "messages": [
+ {
+ "header": {
+ "checksum": 0,
+ "id": 42,
+ "offset": 0,
+ "timestamp": 0,
+ "origin_timestamp": 1000,
+ "user_headers_length": 0,
+ "payload_length": 4
+ },
+ "payload": "dGVzdA==",
+ "user_headers": []
+ }
+ ]
+ }
+ """;
+
+ // when
+ var polledMessages = objectMapper.readValue(json,
PolledMessages.class);
+
+ // then
+ assertThat(polledMessages).isNotNull();
+ assertThat(polledMessages.messages()).hasSize(1);
+
assertThat(polledMessages.messages().get(0).userHeaders()).isEmpty();
+ }
+
+ @Test
+ void shouldDeserializePolledMessagesWithUserHeaders() {
+ // given
+ String json = """
+ {
+ "partition_id": 1,
+ "current_offset": 10,
+ "count": 1,
+ "messages": [
+ {
+ "header": {
+ "checksum": 0,
+ "id": 42,
+ "offset": 0,
+ "timestamp": 0,
+ "origin_timestamp": 1000,
+ "user_headers_length": 62,
+ "payload_length": 4
+ },
+ "payload": "dGVzdA==",
+ "user_headers": [
+ {
+ "key": {"kind": "string", "value":
"Y29udGVudC10eXBl"},
+ "value": {"kind": "string", "value":
"dGV4dC9wbGFpbg=="}
+ }
+ ]
+ }
+ ]
+ }
+ """;
+
+ // when
+ var polledMessages = objectMapper.readValue(json,
PolledMessages.class);
+
+ // then
+ assertThat(polledMessages).isNotNull();
+ assertThat(polledMessages.messages()).hasSize(1);
+ var headers = polledMessages.messages().get(0).userHeaders();
+ assertThat(headers).hasSize(1);
+ var header = headers.entrySet().iterator().next();
+
assertThat(header.getKey().toString()).isEqualTo("content-type");
+
assertThat(header.getValue().toString()).isEqualTo("text/plain");
+ }
+ }
+
+ @Nested
+ @DisplayName("Payload")
+ class PayloadDeserialization {
+
+ @Test
+ void shouldDeserializeBase64EncodedPayloadToBytes() {
+ // given
+ String expectedPayload = "test";
+ String base64Payload =
+
Base64.getEncoder().encodeToString(expectedPayload.getBytes(StandardCharsets.UTF_8));
+ String json = createMessageJson(base64Payload);
+
+ // when
+ var polledMessages = objectMapper.readValue(json,
PolledMessages.class);
+
+ // then
+ byte[] actualPayload =
polledMessages.messages().get(0).payload();
+
assertThat(actualPayload).isEqualTo(expectedPayload.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Test
+ void shouldDeserializeEmptyPayload() {
+ // given
+ String base64Payload = Base64.getEncoder().encodeToString(new
byte[0]);
+ String json = createMessageJson(base64Payload);
+
+ // when
+ var polledMessages = objectMapper.readValue(json,
PolledMessages.class);
+
+ // then
+ byte[] actualPayload =
polledMessages.messages().get(0).payload();
+ assertThat(actualPayload).isEmpty();
+ }
+
+ @Test
+ void shouldDeserializeBinaryPayload() {
+ // given
+ byte[] binaryData = new byte[] {0x00, 0x01, 0x02, (byte) 0xFF,
(byte) 0xFE};
+ String base64Payload =
Base64.getEncoder().encodeToString(binaryData);
+ String json = createMessageJson(base64Payload);
+
+ // when
+ var polledMessages = objectMapper.readValue(json,
PolledMessages.class);
+
+ // then
+ byte[] actualPayload =
polledMessages.messages().get(0).payload();
+ assertThat(actualPayload).isEqualTo(binaryData);
+ }
+
+ private String createMessageJson(String base64Payload) {
+ return """
+ {
+ "partition_id": 1,
+ "current_offset": 0,
+ "count": 1,
+ "messages": [
+ {
+ "header": {
+ "checksum": 0,
+ "id": 1,
+ "offset": 0,
+ "timestamp": 0,
+ "origin_timestamp": 0,
+ "user_headers_length": 0,
+ "payload_length": 4
+ },
+ "payload": "%s",
+ "user_headers": []
+ }
+ ]
+ }
+ """.formatted(base64Payload);
+ }
+ }
+ }
+
+ @Nested
+ class Serialization {
+
+ @Nested
+ @DisplayName("Payload")
+ class PayloadSerialization {
+
+ @Test
+ void shouldSerializePayloadToBase64() {
+ // given
+ String payloadContent = "test";
+ Message message = Message.of(payloadContent);
+
+ // when
+ String json = objectMapper.writeValueAsString(message);
+
+ // then
+ String expectedBase64 =
+
Base64.getEncoder().encodeToString(payloadContent.getBytes(StandardCharsets.UTF_8));
+ assertThat(json).contains("\"payload\":\"" + expectedBase64 +
"\"");
+ }
+
+ @Test
+ void shouldSerializeEmptyPayloadToBase64() {
+ // given
+ String payloadContent = "";
+ Message message = Message.of(payloadContent);
+
+ // when
+ String json = objectMapper.writeValueAsString(message);
+
+ // then
+ String expectedBase64 = Base64.getEncoder().encodeToString(new
byte[0]);
+ assertThat(json).contains("\"payload\":\"" + expectedBase64 +
"\"");
+ }
+
+ @Test
+ void shouldSerializeBinaryPayloadToBase64() {
+ // given
+ byte[] binaryData = new byte[] {0x00, 0x01, 0x02, (byte) 0xFF,
(byte) 0xFE};
+ Message message = Message.of("placeholder");
+ Message binaryMessage = new Message(message.header(),
binaryData, message.userHeaders());
+
+ // when
+ String json = objectMapper.writeValueAsString(binaryMessage);
+
+ // then
+ String expectedBase64 =
Base64.getEncoder().encodeToString(binaryData);
+ assertThat(json).contains("\"payload\":\"" + expectedBase64 +
"\"");
+ }
+ }
+ }
+
+ @Nested
+ class Roundtrip {
+
+ @Test
+ void shouldRoundtripTextPayload() {
+ // given
+ String payloadContent = "Hello, World!";
+ Message originalMessage = Message.of(payloadContent);
+
+ // when
+ String json = objectMapper.writeValueAsString(originalMessage);
+ Message deserializedMessage = objectMapper.readValue(json,
Message.class);
+
+ // then
+
assertThat(deserializedMessage.payload()).isEqualTo(originalMessage.payload());
+ }
+
+ @Test
+ void shouldRoundtripBinaryPayload() {
+ // given
+ byte[] binaryData = new byte[] {0x00, 0x01, 0x02, (byte) 0x80,
(byte) 0xFF};
+ Message originalMessage = Message.of("placeholder");
+ Message binaryMessage = new Message(originalMessage.header(),
binaryData, originalMessage.userHeaders());
+
+ // when
+ String json = objectMapper.writeValueAsString(binaryMessage);
+ Message deserializedMessage = objectMapper.readValue(json,
Message.class);
+
+ // then
+ assertThat(deserializedMessage.payload()).isEqualTo(binaryData);
+ }
+ }
+}
diff --git
a/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
index 36a6d5ea3..496a428e5 100644
---
a/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
+++
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
@@ -752,80 +752,4 @@ class BytesDeserializerTest {
assertThat(tokenInfo.expiryAt()).isEmpty();
}
}
-
- @Nested
- class JsonDeserialization {
-
- private static final tools.jackson.databind.ObjectMapper MAPPER =
-
org.apache.iggy.client.blocking.http.ObjectMapperFactory.getInstance();
-
- @Test
- void shouldDeserializePolledMessagesWithEmptyUserHeaders() throws
Exception {
- String json = """
- {
- "partition_id": 1,
- "current_offset": 10,
- "count": 1,
- "messages": [
- {
- "header": {
- "checksum": 0,
- "id": 42,
- "offset": 0,
- "timestamp": 0,
- "origin_timestamp": 1000,
- "user_headers_length": 0,
- "payload_length": 4
- },
- "payload": "dGVzdA==",
- "user_headers": []
- }
- ]
- }
- """;
-
- var polledMessages = MAPPER.readValue(json,
org.apache.iggy.message.PolledMessages.class);
-
- assertThat(polledMessages).isNotNull();
- assertThat(polledMessages.messages()).hasSize(1);
-
assertThat(polledMessages.messages().get(0).userHeaders()).isEmpty();
- }
-
- @Test
- void shouldDeserializePolledMessagesWithUserHeaders() throws Exception
{
- String json = """
- {
- "partition_id": 1,
- "current_offset": 10,
- "count": 1,
- "messages": [
- {
- "header": {
- "checksum": 0,
- "id": 42,
- "offset": 0,
- "timestamp": 0,
- "origin_timestamp": 1000,
- "user_headers_length": 62,
- "payload_length": 4
- },
- "payload": "dGVzdA==",
- "user_headers": [
- {
- "key": {"kind": "string", "value":
"Y29udGVudC10eXBl"},
- "value": {"kind": "string", "value":
"dGV4dC9wbGFpbg=="}
- }
- ]
- }
- ]
- }
- """;
-
- var polledMessages = MAPPER.readValue(json,
org.apache.iggy.message.PolledMessages.class);
-
- assertThat(polledMessages).isNotNull();
- assertThat(polledMessages.messages()).hasSize(1);
-
assertThat(polledMessages.messages().get(0).userHeaders()).hasSize(1);
- }
- }
}