Skip to content

Commit bb1beed

Browse files
committed
feat: implement multi stream append.
1 parent 26c72a2 commit bb1beed

4 files changed

Lines changed: 338 additions & 0 deletions

File tree

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ dependencies {
6464
implementation "org.slf4j:slf4j-api:2.0.17"
6565
implementation "org.bouncycastle:bcprov-jdk18on:1.80"
6666
implementation "org.bouncycastle:bcpkix-jdk18on:1.80"
67+
implementation "org.apache.commons:commons-lang3:3.17.0"
6768

6869
implementation platform("io.opentelemetry:opentelemetry-bom:${openTelemetryVersion}")
6970
implementation "io.opentelemetry:opentelemetry-api"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.kurrent.dbclient.v2;
2+
3+
import io.kurrent.dbclient.StreamMetadata;
4+
5+
/**
6+
* Interface for decoding metadata from a byte array.
7+
*/
8+
public interface IMetadataDecoder {
9+
/**
10+
* Decodes metadata from a byte array.
11+
*
12+
* @param bytes The byte array containing the encoded metadata.
13+
* @return The decoded metadata.
14+
*/
15+
StreamMetadata decode(byte[] bytes);
16+
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package io.kurrent.dbclient.v2;
2+
3+
import org.apache.commons.lang3.BooleanUtils;
4+
import org.apache.commons.lang3.math.NumberUtils;
5+
6+
import java.nio.ByteBuffer;
7+
import java.time.DateTimeException;
8+
import java.time.Instant;
9+
import java.time.LocalDate;
10+
import java.time.LocalTime;
11+
import java.time.format.DateTimeParseException;
12+
import java.util.HashMap;
13+
import java.util.Map;
14+
import java.util.Optional;
15+
import java.util.UUID;
16+
17+
/**
18+
* Represents a collection of metadata as key-value pairs with additional helper methods.
19+
*/
20+
public class Metadata extends HashMap<String, Object> {
21+
/**
22+
* Initializes a new, empty instance of the Metadata class.
23+
*/
24+
public Metadata() {
25+
super();
26+
}
27+
28+
/**
29+
* Initializes a new instance of the Metadata class from an existing metadata instance.
30+
*
31+
* @param metadata The metadata to copy from.
32+
*/
33+
public Metadata(Metadata metadata) {
34+
super(metadata);
35+
}
36+
37+
/**
38+
* Initializes a new instance of the Metadata class from a dictionary.
39+
*
40+
* @param dictionary The dictionary to copy from.
41+
*/
42+
public Metadata(Map<String, Object> dictionary) {
43+
super(dictionary);
44+
}
45+
46+
/**
47+
* Adds or updates a key-value pair in the metadata and returns the metadata instance.
48+
*
49+
* @param key The key to add or update.
50+
* @param value The value to set.
51+
* @param <T> The type of the value.
52+
* @return The metadata instance (this) to enable method chaining.
53+
*/
54+
public <T> Metadata set(String key, T value) {
55+
this.put(key, value);
56+
return this;
57+
}
58+
59+
/**
60+
* Gets a typed value from the metadata.
61+
*
62+
* @param key The key to retrieve.
63+
* @param type The class of the type T.
64+
* @param <T> The type to cast the value to.
65+
* @return The value cast to type T, or null if the key is not found or the value can't be cast to type T.
66+
*/
67+
@SuppressWarnings("unchecked")
68+
public <T> T get(String key, Class<T> type) {
69+
Object value = get(key);
70+
if (containsKey(key) && type.isInstance(value))
71+
return (T) value;
72+
73+
return null;
74+
}
75+
76+
/**
77+
* Gets a typed value from the metadata, with automatic type conversion where appropriate.
78+
*
79+
* @param metadata The metadata object.
80+
* @param key The key to retrieve.
81+
* @param defaultValue The default value to return if the key is not found or the value can't be cast to type T.
82+
* @param type The class of the type T.
83+
* @param <T> The type to cast the value to.
84+
* @return The value cast to type T, or the default value if the key is not found or the value can't be cast to type T.
85+
*/
86+
@SuppressWarnings("unchecked")
87+
public <T> T get(String key, T defaultValue, Class<T> type) {
88+
Object value = get(key);
89+
if (containsKey(key) && type.isInstance(value)) {
90+
return (T) value;
91+
}
92+
return defaultValue;
93+
}
94+
95+
/**
96+
* Tries to get a typed value from the metadata, with automatic type conversion where appropriate.
97+
*
98+
* @param key The key to retrieve.
99+
* @param type The class of the type T.
100+
* @param <T> The type to get or convert to.
101+
*/
102+
@SuppressWarnings("unchecked")
103+
public <T> Optional<T> tryGet(String key, Class<T> type) {
104+
Object obj = get(key);
105+
if (!containsKey(key)) {
106+
return Optional.empty();
107+
}
108+
109+
// Direct type match
110+
if (type.isInstance(obj)) {
111+
return Optional.of((T) obj);
112+
}
113+
114+
// Handle byte array conversions
115+
if (type == byte[].class || type == ByteBuffer.class) {
116+
if (obj instanceof byte[]) {
117+
if (type == byte[].class)
118+
return Optional.of((T) obj);
119+
else
120+
return Optional.of((T) ByteBuffer.wrap((byte[]) obj));
121+
}
122+
123+
if (obj instanceof ByteBuffer) {
124+
if (type == byte[].class)
125+
return Optional.of((T) ((ByteBuffer) obj).array());
126+
else
127+
return Optional.of((T) obj);
128+
}
129+
130+
return Optional.empty();
131+
}
132+
133+
// Convert string representation for various types
134+
String stringValue = obj.toString();
135+
if (stringValue == null)
136+
return Optional.empty();
137+
138+
// Handle common value types with parsing
139+
try {
140+
if (type == Boolean.class || type == boolean.class) {
141+
Boolean value = BooleanUtils.toBooleanObject(stringValue);
142+
143+
if (value == null)
144+
return Optional.empty();
145+
146+
return Optional.of((T) value);
147+
}
148+
149+
if (type == Byte.class || type == byte.class) {
150+
byte value = NumberUtils.toByte(stringValue);
151+
if (value == 0 && !stringValue.equalsIgnoreCase("0"))
152+
return Optional.empty();
153+
154+
return Optional.of((T) Byte.valueOf(value));
155+
}
156+
157+
if (type == Short.class || type == short.class) {
158+
short value = NumberUtils.toShort(stringValue);
159+
if (value == 0 && !stringValue.equalsIgnoreCase("0"))
160+
return Optional.empty();
161+
162+
return Optional.of((T) Short.valueOf(stringValue));
163+
}
164+
165+
if (type == Integer.class || type == int.class) {
166+
int value = NumberUtils.toInt(stringValue);
167+
if (value == 0 && !stringValue.equalsIgnoreCase("0"))
168+
return Optional.empty();
169+
170+
return Optional.of((T) Integer.valueOf(stringValue));
171+
}
172+
173+
if (type == Long.class || type== long.class) {
174+
long value = NumberUtils.toLong(stringValue);
175+
if (value == 0 && !stringValue.equalsIgnoreCase("0"))
176+
return Optional.empty();
177+
178+
return Optional.of((T) Long.valueOf(stringValue));
179+
}
180+
181+
if (type == Float.class || type == float.class) {
182+
float value = NumberUtils.toFloat(stringValue, Float.NaN);
183+
if (Float.isNaN(value))
184+
return Optional.empty();
185+
186+
return Optional.of((T) Float.valueOf(value));
187+
}
188+
189+
if (type == Double.class || type == double.class) {
190+
double value = NumberUtils.toDouble(stringValue, Double.NaN);
191+
if (Double.isNaN(value))
192+
return Optional.empty();
193+
194+
return Optional.of((T) Double.valueOf(value));
195+
196+
}
197+
198+
if (type == Character.class || type == char.class) {
199+
if (stringValue.length() == 1) {
200+
return Optional.of((T) Character.valueOf(stringValue.charAt(0)));
201+
}
202+
}
203+
204+
if (type == UUID.class)
205+
return Optional.of((T) UUID.fromString(stringValue));
206+
207+
if (type == Instant.class)
208+
return Optional.of((T) Instant.parse(stringValue));
209+
210+
if (type == LocalDate.class)
211+
return Optional.of((T) LocalDate.parse(stringValue));
212+
213+
if (type == LocalTime.class)
214+
return Optional.of((T) LocalTime.parse(stringValue));
215+
216+
} catch (DateTimeParseException | IllegalArgumentException e) {
217+
return Optional.empty();
218+
}
219+
220+
return Optional.empty();
221+
}
222+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package io.kurrent.dbclient.v2;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.time.Instant;
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
import java.util.Optional;
10+
import java.util.UUID;
11+
12+
public class MetadataTests {
13+
@Test
14+
public void testMetadataConstructors() {
15+
// Test default constructor
16+
Metadata metadata1 = new Metadata();
17+
Assertions.assertEquals(0, metadata1.size());
18+
19+
// Test constructor with existing metadata
20+
metadata1.put("key1", "value1");
21+
Metadata metadata2 = new Metadata(metadata1);
22+
Assertions.assertEquals(1, metadata2.size());
23+
Assertions.assertEquals("value1", metadata2.get("key1"));
24+
25+
// Test constructor with dictionary
26+
Map<String, Object> dictionary = new HashMap<>();
27+
dictionary.put("key2", "value2");
28+
Metadata metadata3 = new Metadata(dictionary);
29+
Assertions.assertEquals(1, metadata3.size());
30+
Assertions.assertEquals("value2", metadata3.get("key2"));
31+
}
32+
33+
@Test
34+
public void testMetadataSetMethod() {
35+
Metadata metadata = new Metadata();
36+
37+
// Test set method with chaining
38+
metadata.set("key1", "value1").set("key2", 123);
39+
40+
Assertions.assertEquals(2, metadata.size());
41+
Assertions.assertEquals("value1", metadata.get("key1"));
42+
Assertions.assertEquals(123, metadata.get("key2"));
43+
}
44+
45+
@Test
46+
public void testMetadataGetMethod() {
47+
Metadata metadata = new Metadata();
48+
metadata.put("stringKey", "stringValue");
49+
metadata.put("intKey", 123);
50+
metadata.put("boolKey", true);
51+
52+
// Test get method with type parameter
53+
String stringValue = metadata.get("stringKey", String.class);
54+
Integer intValue = metadata.get("intKey", Integer.class);
55+
Boolean boolValue = metadata.get("boolKey", Boolean.class);
56+
57+
Assertions.assertEquals("stringValue", stringValue);
58+
Assertions.assertEquals(123, intValue);
59+
Assertions.assertEquals(true, boolValue);
60+
61+
// Test get method with a default value
62+
String nonExistentValue = metadata.get("nonExistentKey", "defaultValue", String.class);
63+
Assertions.assertEquals("defaultValue", nonExistentValue);
64+
}
65+
66+
@Test
67+
public void testMetadataTryGetMethod() {
68+
Metadata metadata = new Metadata();
69+
metadata.put("stringKey", "stringValue");
70+
metadata.put("intKey", 123);
71+
metadata.put("boolKey", true);
72+
metadata.put("uuidKey", UUID.fromString("00000000-0000-0000-0000-000000000001"));
73+
metadata.put("instantKey", Instant.parse("2023-01-01T00:00:00Z"));
74+
75+
// Test tryGet method with direct type match
76+
Optional<String> stringResult = metadata.tryGet("stringKey", String.class);
77+
Assertions.assertTrue(stringResult.isPresent());
78+
Assertions.assertEquals("stringValue", stringResult.get());
79+
80+
// Test tryGet method with type conversion
81+
Optional<Integer> intResult = metadata.tryGet("intKey", Integer.class);
82+
Assertions.assertTrue(intResult.isPresent());
83+
Assertions.assertEquals(123, intResult.get());
84+
85+
// Test tryGet method with string conversion
86+
metadata.put("stringIntKey", "456");
87+
Optional<Integer> stringIntResult = metadata.tryGet("stringIntKey", Integer.class);
88+
Assertions.assertTrue(stringIntResult.isPresent());
89+
Assertions.assertEquals(456, stringIntResult.get());
90+
91+
// Test tryGet method with a non-existent key
92+
Optional<String> nonExistentResult = metadata.tryGet("nonExistentKey", String.class);
93+
Assertions.assertFalse(nonExistentResult.isPresent());
94+
95+
// Test tryGet method with incompatible type
96+
Optional<UUID> incompatibleResult = metadata.tryGet("stringKey", UUID.class);
97+
Assertions.assertFalse(incompatibleResult.isPresent());
98+
}
99+
}

0 commit comments

Comments
 (0)