Skip to content

Commit 08a7873

Browse files
authored
Merge pull request #248 from Aiven-Open/eliax1996/generalize-the-convert-logic
generalize the convert logic
2 parents fbe2c21 + 6b15652 commit 08a7873

2 files changed

Lines changed: 92 additions & 11 deletions

File tree

src/main/java/io/aiven/kafka/connect/http/converter/RecordValueConverter.java

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,98 @@
1616

1717
package io.aiven.kafka.connect.http.converter;
1818

19-
import java.util.HashMap;
20-
import java.util.LinkedHashMap;
19+
import java.util.HashSet;
20+
import java.util.List;
2121
import java.util.Map;
22+
import java.util.Set;
23+
import java.util.concurrent.ConcurrentHashMap;
24+
import java.util.stream.Collectors;
2225

2326
import org.apache.kafka.connect.data.Struct;
2427
import org.apache.kafka.connect.errors.DataException;
2528
import org.apache.kafka.connect.sink.SinkRecord;
2629

2730
public class RecordValueConverter {
28-
31+
private static final ConcurrentHashMap<Class<?>, Converter> RUNTIME_CLASS_TO_CONVERTER_CACHE =
32+
new ConcurrentHashMap<>();
2933
private final JsonRecordValueConverter jsonRecordValueConverter = new JsonRecordValueConverter();
3034

3135
private final Map<Class<?>, Converter> converters = Map.of(
32-
String.class, record -> (String) record.value(),
33-
HashMap.class, jsonRecordValueConverter,
34-
LinkedHashMap.class, jsonRecordValueConverter,
35-
Struct.class, jsonRecordValueConverter
36+
String.class, record -> (String) record.value(),
37+
Map.class, jsonRecordValueConverter,
38+
Struct.class, jsonRecordValueConverter
3639
);
3740

3841
interface Converter {
3942
String convert(final SinkRecord record);
4043
}
4144

4245
public String convert(final SinkRecord record) {
43-
if (!converters.containsKey(record.value().getClass())) {
46+
final Converter converter = getConverter(record);
47+
return converter.convert(record);
48+
}
49+
50+
private Converter getConverter(final SinkRecord record) {
51+
return RUNTIME_CLASS_TO_CONVERTER_CACHE.computeIfAbsent(record.value().getClass(), clazz -> {
52+
final boolean directlyConvertible = converters.containsKey(clazz);
53+
final List<Class<?>> convertibleByImplementedTypes = getAllSerializableImplementedInterfaces(clazz);
54+
validateConvertibility(clazz, directlyConvertible, convertibleByImplementedTypes);
55+
56+
Class<?> implementedClazz = clazz;
57+
if (!directlyConvertible) {
58+
implementedClazz = convertibleByImplementedTypes.get(0);
59+
}
60+
return converters.get(implementedClazz);
61+
});
62+
}
63+
64+
private List<Class<?>> getAllSerializableImplementedInterfaces(final Class<?> recordClazz) {
65+
// caching the computation since querying implemented interfaces is expensive.
66+
// The size of the cache is unlimited, but I don't think it's a problem
67+
// since the number of different record classes is limited.
68+
return getAllInterfaces(recordClazz).stream()
69+
.filter(converters::containsKey)
70+
.collect(Collectors.toList());
71+
}
72+
73+
public static Set<Class<?>> getAllInterfaces(final Class<?> clazz) {
74+
final Set<Class<?>> interfaces = new HashSet<>();
75+
76+
for (final Class<?> implementation : clazz.getInterfaces()) {
77+
interfaces.add(implementation);
78+
interfaces.addAll(getAllInterfaces(implementation));
79+
}
80+
81+
if (clazz.getSuperclass() != null) {
82+
interfaces.addAll(getAllInterfaces(clazz.getSuperclass()));
83+
}
84+
85+
return interfaces;
86+
}
87+
88+
private static void validateConvertibility(
89+
final Class<?> recordClazz,
90+
final boolean directlyConvertible,
91+
final List<Class<?>> convertibleByImplementedTypes
92+
) {
93+
final boolean isConvertibleType = directlyConvertible || !convertibleByImplementedTypes.isEmpty();
94+
95+
if (!isConvertibleType) {
96+
throw new DataException(
97+
String.format(
98+
"Record value must be a String, a Schema Struct or implement "
99+
+ "`java.util.Map`, but %s is given",
100+
recordClazz));
101+
}
102+
if (!directlyConvertible && convertibleByImplementedTypes.size() > 1) {
103+
final String implementedTypes = convertibleByImplementedTypes.stream().map(Class::getSimpleName)
104+
.collect(Collectors.joining(", ", "[", "]"));
44105
throw new DataException(
45-
"Record value must be String, Schema Struct, LinkedHashMap or HashMap,"
46-
+ " but " + record.value().getClass() + " is given");
106+
String.format(
107+
"Record value must be only one of String, Schema Struct or implement "
108+
+ "`java.util.Map`, but %s matches multiple types: %s",
109+
recordClazz, implementedTypes));
47110
}
48-
return converters.get(record.value().getClass()).convert(record);
49111
}
50112

51113
}

src/test/java/io/aiven/kafka/connect/http/converter/RecordValueConverterTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616

1717
package io.aiven.kafka.connect.http.converter;
1818

19+
import javax.swing.UIDefaults;
20+
1921
import java.util.HashMap;
2022
import java.util.LinkedHashMap;
2123
import java.util.Map;
2224

25+
2326
import org.apache.kafka.connect.data.Field;
2427
import org.apache.kafka.connect.data.Schema;
2528
import org.apache.kafka.connect.data.SchemaBuilder;
@@ -68,6 +71,22 @@ void convertStringRecord() {
6871
assertThat(recordValueConverter.convert(sinkRecord)).isEqualTo("some-str-value");
6972
}
7073

74+
@Test
75+
void convertWeirdMapRecord() {
76+
final var recordSchema = SchemaBuilder.map(Schema.STRING_SCHEMA, Schema.STRING_SCHEMA);
77+
78+
final UIDefaults value = new UIDefaults(
79+
new String[] {"Font", "BeautifulFont"}
80+
);
81+
82+
final var sinkRecord = new SinkRecord(
83+
"some-topic", 0,
84+
SchemaBuilder.string(),
85+
"some-key", recordSchema, value, 1L);
86+
87+
assertThat(recordValueConverter.convert(sinkRecord)).isEqualTo("{\"Font\":\"BeautifulFont\"}");
88+
}
89+
7190
@Test
7291
void convertHashMapRecord() {
7392
final var recordSchema = SchemaBuilder.map(Schema.STRING_SCHEMA, Schema.STRING_SCHEMA);

0 commit comments

Comments
 (0)