Skip to content

Commit 2866a69

Browse files
GoMati-MUstheppi
andauthored
KCQL validation proposal (#73)
* KCQL validation proposal * PR comments pt.1 * PR comments pt.1 --------- Co-authored-by: Stefan Bocutiu <stefan.bocutiu@gmail.com>
1 parent 76221de commit 2866a69

19 files changed

Lines changed: 1004 additions & 94 deletions

java-connectors/kafka-connect-azure-eventhubs/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ project(':kafka-connect-azure-eventhubs') {
88
dependencies {
99
implementation project(':kafka-connect-common')
1010
implementation project(':kafka-connect-query-language')
11+
testImplementation(project(path: ':test-utils', configuration: 'testArtifacts'))
1112

1213
// //azure-specific dependencies in case we want to change from kafka protocol
1314
// implementation group: 'com.azure', name: 'azure-identity', version: '1.11.2'

java-connectors/kafka-connect-azure-eventhubs/src/main/java/io/lenses/streamreactor/connect/azure/eventhubs/source/AzureEventHubsSourceConnector.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,21 @@
1818
import static io.lenses.streamreactor.common.util.AsciiArtPrinter.printAsciiHeader;
1919
import static io.lenses.streamreactor.common.util.EitherUtils.unpackOrThrow;
2020

21+
import io.lenses.streamreactor.common.exception.ConnectorStartupException;
22+
import io.lenses.streamreactor.common.util.JarManifest;
23+
import io.lenses.streamreactor.connect.azure.eventhubs.config.AzureEventHubsConfigConstants;
24+
import io.lenses.streamreactor.connect.azure.eventhubs.config.AzureEventHubsSourceConfig;
25+
import io.lenses.streamreactor.connect.azure.eventhubs.util.EventHubsKcqlMappingsValidator;
2126
import java.util.ArrayList;
2227
import java.util.List;
2328
import java.util.Map;
2429
import java.util.stream.IntStream;
25-
30+
import lombok.extern.slf4j.Slf4j;
2631
import org.apache.kafka.common.config.ConfigDef;
2732
import org.apache.kafka.connect.connector.Task;
2833
import org.apache.kafka.connect.source.ExactlyOnceSupport;
2934
import org.apache.kafka.connect.source.SourceConnector;
3035

31-
import io.lenses.streamreactor.common.util.JarManifest;
32-
import io.lenses.streamreactor.connect.azure.eventhubs.config.AzureEventHubsConfigConstants;
33-
import io.lenses.streamreactor.connect.azure.eventhubs.config.AzureEventHubsSourceConfig;
34-
import io.lenses.streamreactor.connect.azure.eventhubs.util.KcqlConfigTopicMapper;
35-
import lombok.extern.slf4j.Slf4j;
36-
3736
/**
3837
* Implementation of {@link SourceConnector} for Microsoft Azure EventHubs.
3938
*/
@@ -90,6 +89,8 @@ public String version() {
9089
private static void parseAndValidateConfigs(Map<String, String> props) {
9190
AzureEventHubsSourceConfig azureEventHubsSourceConfig = new AzureEventHubsSourceConfig(props);
9291
String kcqlMappings = azureEventHubsSourceConfig.getString(AzureEventHubsConfigConstants.KCQL_CONFIG);
93-
KcqlConfigTopicMapper.mapInputToOutputsFromConfig(kcqlMappings);
92+
EventHubsKcqlMappingsValidator.mapInputToOutputsFromConfig(kcqlMappings).mapLeft(e -> {
93+
throw new ConnectorStartupException(e);
94+
});
9495
}
9596
}

java-connectors/kafka-connect-azure-eventhubs/src/main/java/io/lenses/streamreactor/connect/azure/eventhubs/source/AzureEventHubsSourceTask.java

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,26 @@
1818
import static io.lenses.streamreactor.common.util.EitherUtils.unpackOrThrow;
1919
import static java.util.Optional.ofNullable;
2020

21+
import cyclops.control.Either;
22+
import io.lenses.kcql.Kcql;
23+
import io.lenses.streamreactor.common.exception.StreamReactorException;
24+
import io.lenses.streamreactor.common.util.EitherUtils;
25+
import io.lenses.streamreactor.common.util.JarManifest;
26+
import io.lenses.streamreactor.connect.azure.eventhubs.config.AzureEventHubsConfigConstants;
27+
import io.lenses.streamreactor.connect.azure.eventhubs.config.AzureEventHubsSourceConfig;
28+
import io.lenses.streamreactor.connect.azure.eventhubs.util.EventHubsKcqlMappingsValidator;
2129
import java.time.Duration;
2230
import java.time.temporal.ChronoUnit;
2331
import java.util.List;
2432
import java.util.Map;
2533
import java.util.concurrent.ArrayBlockingQueue;
26-
34+
import java.util.stream.Collectors;
35+
import lombok.extern.slf4j.Slf4j;
2736
import org.apache.kafka.clients.consumer.ConsumerRecords;
2837
import org.apache.kafka.connect.source.SourceRecord;
2938
import org.apache.kafka.connect.source.SourceTask;
3039
import org.apache.kafka.connect.storage.OffsetStorageReader;
3140

32-
import io.lenses.streamreactor.common.util.JarManifest;
33-
import io.lenses.streamreactor.connect.azure.eventhubs.config.AzureEventHubsConfigConstants;
34-
import io.lenses.streamreactor.connect.azure.eventhubs.config.AzureEventHubsSourceConfig;
35-
import io.lenses.streamreactor.connect.azure.eventhubs.util.KcqlConfigTopicMapper;
36-
import lombok.extern.slf4j.Slf4j;
37-
3841
/**
3942
* Implementation of {@link SourceTask} for Microsoft Azure EventHubs.
4043
*/
@@ -73,18 +76,19 @@ public void start(Map<String, String> props) {
7376
TopicPartitionOffsetProvider topicPartitionOffsetProvider = new TopicPartitionOffsetProvider(offsetStorageReader);
7477

7578
ArrayBlockingQueue<ConsumerRecords<byte[], byte[]>> recordsQueue =
76-
new ArrayBlockingQueue<>(
77-
RECORDS_QUEUE_DEFAULT_SIZE);
78-
Map<String, String> inputToOutputTopics =
79-
KcqlConfigTopicMapper.mapInputToOutputsFromConfig(
79+
new ArrayBlockingQueue<>(RECORDS_QUEUE_DEFAULT_SIZE);
80+
Either<StreamReactorException, List<Kcql>> mappedInputsOutputsEither =
81+
EventHubsKcqlMappingsValidator.mapInputToOutputsFromConfig(
8082
azureEventHubsSourceConfig.getString(AzureEventHubsConfigConstants.KCQL_CONFIG));
83+
Map<String, String> inputToOutputTopics =
84+
EitherUtils.unpackOrThrow(mappedInputsOutputsEither)
85+
.stream().collect(Collectors.toUnmodifiableMap(Kcql::getSource, Kcql::getTarget));
86+
8187
blockingQueueProducerProvider = new BlockingQueueProducerProvider(topicPartitionOffsetProvider);
8288
KafkaByteBlockingQueuedProducer producer =
83-
blockingQueueProducerProvider.createProducer(
84-
azureEventHubsSourceConfig, recordsQueue, inputToOutputTopics);
89+
blockingQueueProducerProvider.createProducer(azureEventHubsSourceConfig, recordsQueue, inputToOutputTopics);
8590
EventHubsKafkaConsumerController kafkaConsumerController =
86-
new EventHubsKafkaConsumerController(
87-
producer, recordsQueue, inputToOutputTopics);
91+
new EventHubsKafkaConsumerController(producer, recordsQueue, inputToOutputTopics);
8892
initialize(kafkaConsumerController, azureEventHubsSourceConfig);
8993
}
9094

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2017-2024 Lenses.io Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.lenses.streamreactor.connect.azure.eventhubs.util;
17+
18+
import cyclops.control.Either;
19+
import io.lenses.kcql.Kcql;
20+
import io.lenses.streamreactor.common.exception.StreamReactorException;
21+
import io.lenses.streamreactor.common.validation.ConnectorConfigKcqlValidator;
22+
import io.lenses.streamreactor.common.validation.validators.DistinctSourceNamesValidator;
23+
import io.lenses.streamreactor.common.validation.validators.DistinctTargetNamesValidator;
24+
import io.lenses.streamreactor.common.validation.validators.PatternMatchingSourceNameValidator;
25+
import io.lenses.streamreactor.common.validation.validators.PatternMatchingTargetNameValidator;
26+
import java.util.List;
27+
import java.util.function.UnaryOperator;
28+
29+
public class EventHubsKcqlMappingsValidator {
30+
31+
private static final String TOPIC_NAME_REGEX = "^[\\w][\\w\\-\\_\\.]*$";
32+
33+
public static final String TOPIC_NAME_ERROR_MESSAGE =
34+
"%s topic %s, name is not correctly specified (It can contain only letters, numbers and hyphens,"
35+
+ " underscores and dots and has to start with number or letter)";
36+
37+
public static final UnaryOperator<String> OUTPUT_TOPIC_ERROR_MESSAGE_FUNCTION =
38+
topicName -> String.format(TOPIC_NAME_ERROR_MESSAGE, "Output", topicName);
39+
40+
public static final UnaryOperator<String> INPUT_TOPIC_ERROR_MESSAGE_FUNCTION =
41+
topicName -> String.format(TOPIC_NAME_ERROR_MESSAGE, "Input", topicName);
42+
43+
private static final ConnectorConfigKcqlValidator EVENT_HUBS_KCQL_VALIDATOR =
44+
ConnectorConfigKcqlValidator.builder()
45+
.singleKcqlValidators(List.of(
46+
new PatternMatchingSourceNameValidator(TOPIC_NAME_REGEX, INPUT_TOPIC_ERROR_MESSAGE_FUNCTION),
47+
new PatternMatchingTargetNameValidator(TOPIC_NAME_REGEX, OUTPUT_TOPIC_ERROR_MESSAGE_FUNCTION)
48+
))
49+
.allKcqlValidators(List.of(
50+
new DistinctSourceNamesValidator(),
51+
new DistinctTargetNamesValidator()
52+
))
53+
.build();
54+
55+
/**
56+
* This method parses KCQL statements and fetches input and output topics checking against regex for invalid topic
57+
* names in input and output.
58+
*
59+
* @param kcqlString string to parse
60+
* @return map of input to output topic names
61+
*/
62+
public static Either<StreamReactorException, List<Kcql>> mapInputToOutputsFromConfig(String kcqlString) {
63+
return EVENT_HUBS_KCQL_VALIDATOR.validateKcqlString(kcqlString);
64+
}
65+
66+
}

java-connectors/kafka-connect-azure-eventhubs/src/main/java/io/lenses/streamreactor/connect/azure/eventhubs/util/KcqlConfigTopicMapper.java

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,62 +15,88 @@
1515
*/
1616
package io.lenses.streamreactor.connect.azure.eventhubs.util;
1717

18-
import java.util.ArrayList;
19-
import java.util.HashMap;
18+
import static io.lenses.streamreactor.common.util.StringUtils.getSystemsNewLineChar;
19+
20+
import cyclops.control.Either;
21+
import io.lenses.kcql.Kcql;
22+
import io.lenses.streamreactor.common.exception.ConnectorStartupException;
23+
import java.util.Collection;
2024
import java.util.List;
2125
import java.util.Map;
26+
import java.util.Set;
27+
import java.util.function.Function;
2228
import java.util.regex.Matcher;
2329
import java.util.regex.Pattern;
24-
25-
import org.apache.kafka.common.config.ConfigException;
26-
27-
import io.lenses.kcql.Kcql;
30+
import java.util.stream.Collectors;
31+
import java.util.stream.Stream;
32+
import lombok.AccessLevel;
33+
import lombok.NoArgsConstructor;
2834

2935
/**
3036
* Class that represents methods around KCQL topic handling.
3137
*/
38+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
3239
public class KcqlConfigTopicMapper {
3340

3441
private static final String TOPIC_NAME_REGEX = "^[\\w][\\w\\-\\_\\.]*$";
3542
private static final Pattern TOPIC_NAME_PATTERN = Pattern.compile(TOPIC_NAME_REGEX);
43+
44+
private static final String ERROR_DELIMITER = ";" + getSystemsNewLineChar();
3645
public static final String TOPIC_NAME_ERROR_MESSAGE =
3746
"%s topic %s, name is not correctly specified (It can contain only letters, numbers and hyphens,"
38-
+ " underscores and dots and has to start with number or letter";
47+
+ " underscores and dots and has to start with number or letter)";
3948

4049
/**
41-
* This method parses KCQL statements and fetches input and output topics checking against
42-
* regex for invalid topic names in input and output.
43-
*
50+
* This method parses KCQL statements and fetches input and output topics checking against regex for invalid topic
51+
* names in input and output.
52+
*
4453
* @param kcqlString string to parse
4554
* @return map of input to output topic names
4655
*/
47-
public static Map<String, String> mapInputToOutputsFromConfig(String kcqlString) {
56+
public static Either<ConnectorStartupException, List<Kcql>> mapInputToOutputsFromConfig(String kcqlString) {
4857
List<Kcql> kcqls = Kcql.parseMultiple(kcqlString);
49-
Map<String, String> inputToOutputTopics = new HashMap<>(kcqls.size());
50-
List<String> outputTopics = new ArrayList<>(kcqls.size());
5158

52-
for (Kcql kcql : kcqls) {
53-
String inputTopic = kcql.getSource();
54-
String outputTopic = kcql.getTarget();
59+
List<String> inputTopics = kcqls.stream().map(Kcql::getSource).collect(Collectors.toUnmodifiableList());
60+
List<String> outputTopics = kcqls.stream().map(Kcql::getTarget).collect(Collectors.toUnmodifiableList());
5561

56-
if (!topicNameMatchesAgainstRegex(inputTopic)) {
57-
throw new ConfigException(String.format(TOPIC_NAME_ERROR_MESSAGE, "Input", inputTopic));
58-
}
59-
if (!topicNameMatchesAgainstRegex(outputTopic)) {
60-
throw new ConfigException(String.format(TOPIC_NAME_ERROR_MESSAGE, "Output", outputTopic));
61-
}
62-
if (inputToOutputTopics.containsKey(inputTopic)) {
63-
throw new ConfigException(String.format("Input %s cannot be mapped twice.", inputTopic));
64-
}
65-
if (outputTopics.contains(outputTopic)) {
66-
throw new ConfigException(String.format("Output %s cannot be mapped twice.", outputTopic));
67-
}
62+
Set<String> allErrors =
63+
Stream.of(
64+
validateTopicMappings(inputTopics, "Input"),
65+
validateTopicMappings(outputTopics, "Output"),
66+
validateTopicName(inputTopics, "Input"),
67+
validateTopicName(outputTopics, "Output")
68+
).flatMap(Collection::stream).collect(Collectors.toUnmodifiableSet());
6869

69-
inputToOutputTopics.put(inputTopic, outputTopic);
70-
outputTopics.add(outputTopic);
70+
if (!allErrors.isEmpty()) {
71+
return Either.left(new ConnectorStartupException(
72+
String.format("The following errors occurred during validation: %s", String.join(ERROR_DELIMITER,
73+
allErrors))));
7174
}
7275

73-
return inputToOutputTopics;
76+
return Either.right(List.copyOf(kcqls));
77+
}
78+
79+
private static List<String> validateTopicName(List<String> topicNames, String description) {
80+
return topicNames.stream()
81+
.filter(topicName -> !topicNameMatchesAgainstRegex(topicName))
82+
.map(topicName -> String.format(TOPIC_NAME_ERROR_MESSAGE, description, topicName))
83+
.collect(Collectors.toUnmodifiableList());
84+
}
85+
86+
private static List<String> detectDuplicateTopicMappings(List<String> topics) {
87+
return topics.stream()
88+
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
89+
.entrySet().stream()
90+
.filter(entry -> entry.getValue() > 1)
91+
.map(Map.Entry::getKey)
92+
.collect(Collectors.toUnmodifiableList());
93+
}
94+
95+
private static List<String> validateTopicMappings(List<String> topics, String description) {
96+
return detectDuplicateTopicMappings(topics).stream()
97+
.map(
98+
dupe -> String.format("%s '%s' cannot be mapped twice.", description, dupe)
99+
).collect(Collectors.toUnmodifiableList());
74100
}
75101

76102
private static boolean topicNameMatchesAgainstRegex(String topicName) {

0 commit comments

Comments
 (0)