Skip to content

Commit 588d53d

Browse files
committed
feat: use Values to parse value_and_schemas
1 parent 6452aeb commit 588d53d

File tree

2 files changed

+110
-89
lines changed

2 files changed

+110
-89
lines changed

src/main/java/io/aiven/kafka/connect/transforms/FilterByFieldValue.java

Lines changed: 78 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
import java.util.Optional;
2121
import java.util.function.Predicate;
2222

23+
import java.util.regex.Pattern;
2324
import org.apache.kafka.common.config.AbstractConfig;
2425
import org.apache.kafka.common.config.ConfigDef;
2526
import org.apache.kafka.common.config.ConfigException;
2627
import org.apache.kafka.connect.connector.ConnectRecord;
2728
import org.apache.kafka.connect.data.Field;
2829
import org.apache.kafka.connect.data.Schema;
30+
import org.apache.kafka.connect.data.SchemaAndValue;
2931
import org.apache.kafka.connect.data.Struct;
3032
import org.apache.kafka.connect.data.Values;
3133
import org.apache.kafka.connect.transforms.Transformation;
@@ -35,7 +37,68 @@ public abstract class FilterByFieldValue<R extends ConnectRecord<R>> implements
3537
private String fieldName;
3638
private Optional<String> fieldExpectedValue;
3739
private Optional<String> fieldValuePattern;
38-
private Predicate<String> filterCondition;
40+
41+
@Override
42+
public ConfigDef config() {
43+
return new ConfigDef()
44+
.define("field.name",
45+
ConfigDef.Type.STRING,
46+
null,
47+
ConfigDef.Importance.HIGH,
48+
"The field name to filter by."
49+
+ "Schema-based records (Avro), schemaless (e.g. JSON), and raw values are supported."
50+
+ "If empty, the whole key/value record will be filtered.")
51+
.define("field.value",
52+
ConfigDef.Type.STRING,
53+
null,
54+
ConfigDef.Importance.HIGH,
55+
"Expected value to match. Either define this, or a regex pattern")
56+
.define("field.value.pattern",
57+
ConfigDef.Type.STRING,
58+
null,
59+
ConfigDef.Importance.HIGH,
60+
"The pattern to match. Either define this, or an expected value")
61+
.define("field.value.matches",
62+
ConfigDef.Type.BOOLEAN,
63+
true,
64+
ConfigDef.Importance.MEDIUM,
65+
"The filter mode, 'true' for matching or 'false' for non-matching");
66+
}
67+
68+
@Override
69+
public void configure(final Map<String, ?> configs) {
70+
final AbstractConfig config = new AbstractConfig(config(), configs);
71+
this.fieldName = config.getString("field.name");
72+
this.fieldExpectedValue = Optional.ofNullable(config.getString("field.value"));
73+
this.fieldValuePattern = Optional.ofNullable(config.getString("field.value.pattern"));
74+
final boolean expectedValuePresent = fieldExpectedValue.isPresent();
75+
final boolean regexPatternPresent = fieldValuePattern.map(s -> !s.isEmpty()).orElse(false);
76+
if (expectedValuePresent == regexPatternPresent) {
77+
throw new ConfigException(
78+
"Either field.value or field.value.pattern have to be set to apply filter transform");
79+
}
80+
final Predicate<SchemaAndValue> matchCondition;
81+
82+
if (expectedValuePresent) {
83+
final SchemaAndValue expectedSchemaAndValue = Values.parseString(fieldExpectedValue.get());
84+
matchCondition = schemaAndValue -> expectedSchemaAndValue.value().equals(schemaAndValue.value());
85+
} else {
86+
final String pattern = fieldValuePattern.get();
87+
final Predicate<String> regexPredicate = Pattern.compile(pattern).asPredicate();
88+
matchCondition = schemaAndValue ->
89+
schemaAndValue != null
90+
&& regexPredicate.test(Values.convertToString(schemaAndValue.schema(), schemaAndValue.value()));
91+
}
92+
93+
this.filterCondition = config.getBoolean("field.value.matches")
94+
? matchCondition
95+
: (result -> !matchCondition.test(result));
96+
}
97+
private Predicate<SchemaAndValue> filterCondition;
98+
99+
protected abstract Schema operatingSchema(R record);
100+
101+
protected abstract Object operatingValue(R record);
39102

40103
@Override
41104
public R apply(final R record) {
@@ -50,102 +113,46 @@ public R apply(final R record) {
50113
}
51114
}
52115

53-
54-
protected abstract Schema operatingSchema(R record);
55-
56-
protected abstract Object operatingValue(R record);
57-
58116
private R applyWithSchema(final R record) {
59117
final Struct struct = (Struct) operatingValue(record);
60-
final String fieldValue = getStructFieldValue(struct, fieldName).orElse(null);
61-
return filterCondition.test(fieldValue) ? record : null;
118+
final SchemaAndValue schemaAndValue = getStructFieldValue(struct, fieldName).orElse(null);
119+
return filterCondition.test(schemaAndValue) ? record : null;
62120
}
63121

64-
private Optional<String> getStructFieldValue(final Struct struct, final String fieldName) {
122+
private Optional<SchemaAndValue> getStructFieldValue(final Struct struct, final String fieldName) {
65123
final Schema schema = struct.schema();
66124
final Field field = schema.field(fieldName);
67125
final Object fieldValue = struct.get(field);
68-
return Optional.ofNullable(Values.convertToString(field.schema(), fieldValue));
126+
if (fieldValue == null) {
127+
return Optional.empty();
128+
} else {
129+
return Optional.of(new SchemaAndValue(field.schema(), struct.get(field)));
130+
}
69131
}
70132

71133
@SuppressWarnings("unchecked")
72134
private R applySchemaless(final R record) {
73135
if (fieldName == null || fieldName.isEmpty()) {
74-
final String value = getSchemalessFieldValue(operatingValue(record)).orElse(null);
75-
return filterCondition.test(value) ? record : null;
136+
final SchemaAndValue schemaAndValue = getSchemalessFieldValue(operatingValue(record)).orElse(null);
137+
return filterCondition.test(schemaAndValue) ? record : null;
76138
} else {
77139
final Map<String, Object> map = (Map<String, Object>) operatingValue(record);
78-
final String fieldValue = getSchemalessFieldValue(map.get(fieldName)).orElse(null);
79-
return filterCondition.test(fieldValue) ? record : null;
140+
final SchemaAndValue schemaAndValue = getSchemalessFieldValue(map.get(fieldName)).orElse(null);
141+
return filterCondition.test(schemaAndValue) ? record : null;
80142
}
81143
}
82144

83-
private Optional<String> getSchemalessFieldValue(final Object fieldValue) {
145+
private Optional<SchemaAndValue> getSchemalessFieldValue(final Object fieldValue) {
84146
if (fieldValue == null) {
85147
return Optional.empty();
86148
}
87-
return Optional.ofNullable(Values.convertToString(null, fieldValue));
88-
}
89-
90-
@Override
91-
public ConfigDef config() {
92-
return new ConfigDef()
93-
.define("field.name",
94-
ConfigDef.Type.STRING,
95-
null,
96-
ConfigDef.Importance.HIGH,
97-
"The field name to filter by."
98-
+ "Schema-based records (Avro), schemaless (e.g. JSON), and raw values are supported."
99-
+ "If empty, the whole key/value record will be filtered.")
100-
.define("field.value",
101-
ConfigDef.Type.STRING,
102-
null,
103-
ConfigDef.Importance.HIGH,
104-
"Expected value to match. Either define this, or a regex pattern")
105-
.define("field.value.pattern",
106-
ConfigDef.Type.STRING,
107-
null,
108-
ConfigDef.Importance.HIGH,
109-
"The pattern to match. Either define this, or an expected value")
110-
.define("field.value.matches",
111-
ConfigDef.Type.BOOLEAN,
112-
true,
113-
ConfigDef.Importance.MEDIUM,
114-
"The filter mode, 'true' for matching or 'false' for non-matching");
149+
return Optional.of(new SchemaAndValue(Values.inferSchema(fieldValue), fieldValue));
115150
}
116151

117152
@Override
118153
public void close() {
119154
}
120155

121-
@Override
122-
public void configure(final Map<String, ?> configs) {
123-
final AbstractConfig config = new AbstractConfig(config(), configs);
124-
this.fieldName = config.getString("field.name");
125-
this.fieldExpectedValue = Optional.ofNullable(config.getString("field.value"));
126-
this.fieldValuePattern = Optional.ofNullable(config.getString("field.value.pattern"));
127-
final boolean expectedValuePresent = fieldExpectedValue.map(s -> !s.isEmpty()).orElse(false);
128-
final boolean regexPatternPresent = fieldValuePattern.map(s -> !s.isEmpty()).orElse(false);
129-
if (expectedValuePresent == regexPatternPresent) {
130-
throw new ConfigException(
131-
"Either field.value or field.value.pattern have to be set to apply filter transform");
132-
}
133-
final Predicate<String> matchCondition;
134-
135-
if (expectedValuePresent) {
136-
final String expectedValue = fieldExpectedValue.get();
137-
matchCondition = fieldValue -> fieldValue != null && fieldValue.equals(expectedValue);
138-
} else {
139-
final String pattern = fieldValuePattern.get();
140-
matchCondition = fieldValue -> fieldValue != null && fieldValue.matches(pattern);
141-
}
142-
143-
this.filterCondition = config.getBoolean("field.value.matches")
144-
? matchCondition
145-
: (result -> !matchCondition.test(result));
146-
}
147-
148-
149156
public static final class Key<R extends ConnectRecord<R>> extends FilterByFieldValue<R> {
150157

151158
@Override

src/test/java/io/aiven/kafka/connect/transforms/FilterByFieldValueTest.java

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,19 @@ void shouldFilterOutKeyRecordsEqualsToId() {
6565
final FilterByFieldValue<SourceRecord> filter = new FilterByFieldValue.Key<>();
6666
filter.configure(Map.of(
6767
"field.name", "id",
68-
"field.value", "123",
68+
"field.value", "A123",
6969
"field.value.matches", "false"
7070
));
7171

7272
assertNull(filter.apply(prepareStructRecord(
73-
struct -> struct.put("id", "123"),
73+
struct -> struct.put("id", "A123"),
7474
struct -> {
75-
})), "Record with id '123' should be filtered out");
75+
})), "Record with id 'A132' should be filtered out");
7676
final SourceRecord record = prepareStructRecord(
77-
struct -> struct.put("id", "111"),
77+
struct -> struct.put("id", "A111"),
7878
struct -> {
7979
});
80-
assertEquals(record, filter.apply(record), "Record with id not '123' should not be filtered out");
80+
assertEquals(record, filter.apply(record), "Record with id not 'A132' should not be filtered out");
8181
}
8282

8383
@Test
@@ -110,7 +110,7 @@ void shouldFilterOutKeyRecordsNotEqualsToId() {
110110
final FilterByFieldValue<SourceRecord> filter = new FilterByFieldValue.Key<>();
111111
filter.configure(Map.of(
112112
"field.name", "id",
113-
"field.value", "123",
113+
"field.value", "A123",
114114
"field.value.matches", "true"
115115
));
116116

@@ -121,13 +121,13 @@ void shouldFilterOutKeyRecordsNotEqualsToId() {
121121
struct -> {
122122
}
123123
)),
124-
"Record with id not equal to '123' should be filtered out");
124+
"Record with id not equal to 'A132' should be filtered out");
125125
final SourceRecord record = prepareStructRecord(
126-
struct -> struct.put("id", "123"),
126+
struct -> struct.put("id", "A123"),
127127
struct -> {
128128
}
129129
);
130-
assertEquals(record, filter.apply(record), "Record with id equal to '123' should not be filtered out");
130+
assertEquals(record, filter.apply(record), "Record with id equal to 'A132' should not be filtered out");
131131
}
132132

133133
@Test
@@ -139,9 +139,9 @@ void shouldFilterOutMapValueRecordsWithRegex() {
139139
configs.put("field.value.matches", "false");
140140
filterByFieldValue.configure(configs);
141141

142-
assertNull(filterByFieldValue.apply(prepareRecord(() -> "42", () -> Map.of("language", "Javascript"))),
142+
assertNull(filterByFieldValue.apply(prepareRecord(() -> "A42", () -> Map.of("language", "Javascript"))),
143143
"The record should be filtered out");
144-
final SourceRecord record = prepareRecord(() -> "42", () -> Map.of("language", "Rust"));
144+
final SourceRecord record = prepareRecord(() -> "A42", () -> Map.of("language", "Rust"));
145145
assertEquals(record, filterByFieldValue.apply(record), "The record should not be filtered out");
146146
}
147147

@@ -154,21 +154,21 @@ void shouldFilterOutMapKeyRecordsWithRegex() {
154154
configs.put("field.value.matches", "false");
155155
filterByFieldValue.configure(configs);
156156

157-
assertNull(filterByFieldValue.apply(prepareRecord(() -> Map.of("language", "Javascript"), () -> "42")),
157+
assertNull(filterByFieldValue.apply(prepareRecord(() -> Map.of("language", "Javascript"), () -> "A42")),
158158
"The record should be filtered out");
159-
final SourceRecord record = prepareRecord(() -> Map.of("language", "Rust"), () -> "42");
159+
final SourceRecord record = prepareRecord(() -> Map.of("language", "Rust"), () -> "A42");
160160
assertEquals(record, filterByFieldValue.apply(record), "The record should not be filtered out");
161161
}
162162

163163
@Test
164164
void shouldFilterOutRawKeyRecords() {
165165
final FilterByFieldValue<SourceRecord> filterByFieldValue = new FilterByFieldValue.Key<>();
166166
final Map<String, String> configs = new HashMap<>();
167-
configs.put("field.value", "42");
167+
configs.put("field.value", "A42");
168168
configs.put("field.value.matches", "false");
169169
filterByFieldValue.configure(configs);
170170

171-
assertNull(filterByFieldValue.apply(prepareRecord(() -> "42", () -> Map.of("language", "Javascript"))),
171+
assertNull(filterByFieldValue.apply(prepareRecord(() -> "A42", () -> Map.of("language", "Javascript"))),
172172
"The record should be filtered out");
173173
final SourceRecord record = prepareRecord(() -> "43", () -> Map.of("language", "Rust"));
174174
assertEquals(record, filterByFieldValue.apply(record), "The record should be filtered out");
@@ -178,16 +178,30 @@ void shouldFilterOutRawKeyRecords() {
178178
void shouldFilterOutRawValueRecords() {
179179
final FilterByFieldValue<SourceRecord> filterByFieldValue = new FilterByFieldValue.Value<>();
180180
final Map<String, String> configs = new HashMap<>();
181-
configs.put("field.value", "42");
181+
configs.put("field.value", "A42");
182182
configs.put("field.value.matches", "false");
183183
filterByFieldValue.configure(configs);
184184

185-
assertNull(filterByFieldValue.apply(prepareRecord(() -> Map.of("language", "Javascript"), () -> "42")),
185+
assertNull(filterByFieldValue.apply(prepareRecord(() -> Map.of("language", "Javascript"), () -> "A42")),
186186
"The record should be filtered out");
187187
final SourceRecord record = prepareRecord(() -> Map.of("language", "Rust"), () -> "43");
188188
assertEquals(record, filterByFieldValue.apply(record), "The record should be filtered out");
189189
}
190190

191+
@Test
192+
void shouldFilterOutRawNumericValueRecords() {
193+
final FilterByFieldValue<SourceRecord> filterByFieldValue = new FilterByFieldValue.Value<>();
194+
final Map<String, String> configs = new HashMap<>();
195+
configs.put("field.value", "42");
196+
configs.put("field.value.matches", "false");
197+
filterByFieldValue.configure(configs);
198+
199+
assertNull(filterByFieldValue.apply(prepareRecord(() -> Map.of("language", "Javascript"), () -> (byte) 42)),
200+
"The record should be filtered out");
201+
final SourceRecord record = prepareRecord(() -> Map.of("language", "Rust"), () -> (byte) 43);
202+
assertEquals(record, filterByFieldValue.apply(record), "The record should be filtered out");
203+
}
204+
191205
private SourceRecord prepareRecord(
192206
final Supplier<Object> keySupplier,
193207
final Supplier<Object> valueSupplier
@@ -217,7 +231,7 @@ private SourceRecord prepareStructRecord(
217231
.build();
218232

219233
final Struct key = new Struct(keySchema)
220-
.put("id", "123");
234+
.put("id", "A123");
221235
keyChanges.accept(key);
222236

223237
final Struct after = new Struct(valueSchema.field("after").schema())

0 commit comments

Comments
 (0)