ISSUE-849: Use a map between topics and message-names when using ProtobufFile (#854)

* Use a map between topics and message-names when using ProtobufFile
* Validate the given message names for the topics in ProtobufFileRecordSerDe
This commit is contained in:
Meysam Zarezadeh 2021-09-13 10:50:40 +04:30 committed by GitHub
parent 68cad6c76c
commit 64f957771c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 192 additions and 12 deletions

View file

@ -2,6 +2,7 @@ package com.provectus.kafka.ui.config;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Properties; import java.util.Properties;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
@ -26,6 +27,7 @@ public class ClustersProperties {
String keySchemaNameTemplate = "%s-key"; String keySchemaNameTemplate = "%s-key";
String protobufFile; String protobufFile;
String protobufMessageName; String protobufMessageName;
Map<String, String> protobufMessageNameByTopic;
List<ConnectCluster> kafkaConnect; List<ConnectCluster> kafkaConnect;
int jmxPort; int jmxPort;
boolean jmxSsl; boolean jmxSsl;

View file

@ -35,6 +35,7 @@ public class KafkaCluster {
private final Throwable lastZookeeperException; private final Throwable lastZookeeperException;
private final Path protobufFile; private final Path protobufFile;
private final String protobufMessageName; private final String protobufMessageName;
private final Map<String, String> protobufMessageNameByTopic;
private final Properties properties; private final Properties properties;
private final Boolean readOnly; private final Boolean readOnly;
private final Boolean disableLogDirsCollection; private final Boolean disableLogDirsCollection;

View file

@ -35,7 +35,8 @@ public class DeserializationService {
if (cluster.getProtobufFile() != null) { if (cluster.getProtobufFile() != null) {
log.info("Using ProtobufFileRecordSerDe for cluster '{}'", cluster.getName()); log.info("Using ProtobufFileRecordSerDe for cluster '{}'", cluster.getName());
return new ProtobufFileRecordSerDe(cluster.getProtobufFile(), return new ProtobufFileRecordSerDe(cluster.getProtobufFile(),
cluster.getProtobufMessageName(), objectMapper); cluster.getProtobufMessageNameByTopic(), cluster.getProtobufMessageName(),
objectMapper);
} else { } else {
log.info("Using SchemaRegistryAwareRecordSerDe for cluster '{}'", cluster.getName()); log.info("Using SchemaRegistryAwareRecordSerDe for cluster '{}'", cluster.getName());
return new SchemaRegistryAwareRecordSerDe(cluster); return new SchemaRegistryAwareRecordSerDe(cluster);

View file

@ -1,6 +1,7 @@
package com.provectus.kafka.ui.serde; package com.provectus.kafka.ui.serde;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.DynamicMessage; import com.google.protobuf.DynamicMessage;
import com.google.protobuf.util.JsonFormat; import com.google.protobuf.util.JsonFormat;
import com.provectus.kafka.ui.model.MessageSchema; import com.provectus.kafka.ui.model.MessageSchema;
@ -14,6 +15,8 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -30,15 +33,35 @@ public class ProtobufFileRecordSerDe implements RecordSerDe {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final Path protobufSchemaPath; private final Path protobufSchemaPath;
private final ProtobufSchemaConverter schemaConverter = new ProtobufSchemaConverter(); private final ProtobufSchemaConverter schemaConverter = new ProtobufSchemaConverter();
private final Map<String, Descriptor> messageDescriptorMap;
private final Descriptor defaultMessageDescriptor;
public ProtobufFileRecordSerDe(Path protobufSchemaPath, String messageName, public ProtobufFileRecordSerDe(Path protobufSchemaPath, Map<String, String> messageNameMap,
ObjectMapper objectMapper) throws IOException { String defaultMessageName, ObjectMapper objectMapper)
throws IOException {
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.protobufSchemaPath = protobufSchemaPath; this.protobufSchemaPath = protobufSchemaPath;
try (final Stream<String> lines = Files.lines(protobufSchemaPath)) { try (final Stream<String> lines = Files.lines(protobufSchemaPath)) {
this.protobufSchema = new ProtobufSchema( var schema = new ProtobufSchema(
lines.collect(Collectors.joining("\n")) lines.collect(Collectors.joining("\n"))
).copy(messageName); );
if (defaultMessageName != null) {
this.protobufSchema = schema.copy(defaultMessageName);
} else {
this.protobufSchema = schema;
}
this.messageDescriptorMap = new HashMap<>();
if (messageNameMap != null) {
for (Map.Entry<String, String> entry : messageNameMap.entrySet()) {
var descriptor = Objects.requireNonNull(protobufSchema.toDescriptor(entry.getValue()),
"The given message type is not found in protobuf definition: "
+ entry.getValue());
messageDescriptorMap.put(entry.getKey(), descriptor);
}
}
defaultMessageDescriptor = Objects.requireNonNull(protobufSchema.toDescriptor(),
"The given message type is not found in protobuf definition: "
+ defaultMessageName);
} }
} }
@ -51,7 +74,7 @@ public class ProtobufFileRecordSerDe implements RecordSerDe {
builder.keyFormat(MessageFormat.UNKNOWN); builder.keyFormat(MessageFormat.UNKNOWN);
} }
if (msg.value() != null) { if (msg.value() != null) {
builder.value(parse(msg.value().get())); builder.value(parse(msg.value().get(), getDescriptor(msg.topic())));
builder.valueFormat(MessageFormat.PROTOBUF); builder.valueFormat(MessageFormat.PROTOBUF);
} }
return builder.build(); return builder.build();
@ -60,10 +83,14 @@ public class ProtobufFileRecordSerDe implements RecordSerDe {
} }
} }
private Descriptor getDescriptor(String topic) {
return messageDescriptorMap.getOrDefault(topic, defaultMessageDescriptor);
}
@SneakyThrows @SneakyThrows
private String parse(byte[] value) { private String parse(byte[] value, Descriptor descriptor) {
DynamicMessage protoMsg = DynamicMessage.parseFrom( DynamicMessage protoMsg = DynamicMessage.parseFrom(
protobufSchema.toDescriptor(), descriptor,
new ByteArrayInputStream(value) new ByteArrayInputStream(value)
); );
byte[] jsonFromProto = ProtobufSchemaUtils.toJson(protoMsg); byte[] jsonFromProto = ProtobufSchemaUtils.toJson(protoMsg);
@ -78,7 +105,7 @@ public class ProtobufFileRecordSerDe implements RecordSerDe {
if (data == null) { if (data == null) {
return new ProducerRecord<>(topic, partition, Objects.requireNonNull(key).getBytes(), null); return new ProducerRecord<>(topic, partition, Objects.requireNonNull(key).getBytes(), null);
} }
DynamicMessage.Builder builder = protobufSchema.newMessageBuilder(); DynamicMessage.Builder builder = DynamicMessage.newBuilder(getDescriptor(topic));
try { try {
JsonFormat.parser().merge(data, builder); JsonFormat.parser().merge(data, builder);
final DynamicMessage message = builder.build(); final DynamicMessage message = builder.build();
@ -98,7 +125,7 @@ public class ProtobufFileRecordSerDe implements RecordSerDe {
final JsonSchema jsonSchema = schemaConverter.convert( final JsonSchema jsonSchema = schemaConverter.convert(
protobufSchemaPath.toUri(), protobufSchemaPath.toUri(),
protobufSchema.toDescriptor() getDescriptor(topic)
); );
final MessageSchema keySchema = new MessageSchema() final MessageSchema keySchema = new MessageSchema()
.name(protobufSchema.fullName()) .name(protobufSchema.fullName())

View file

@ -19,6 +19,14 @@ kafka:
# address: http://localhost:8083 # address: http://localhost:8083
# jmxPort: 9998 # jmxPort: 9998
# read-only: true # read-only: true
# -
# name: localUsingProtobufFile
# bootstrapServers: localhost:9092
# protobufFile: messages.proto
# protobufMessageName: GenericMessage
# protobufMessageNameByTopic:
# input-topic: InputMessage
# output-topic: OutputMessage
admin-client-timeout: 5000 admin-client-timeout: 5000
zookeeper: zookeeper:
connection-timeout: 1000 connection-timeout: 1000

View file

@ -0,0 +1,102 @@
package com.provectus.kafka.ui.serde;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.DynamicMessage;
import com.google.protobuf.util.JsonFormat;
import com.provectus.kafka.ui.serde.schemaregistry.MessageFormat;
import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Map;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.common.utils.Bytes;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
class ProtobufFileRecordSerDeTest {
// Sample message of type `test.Person`
private static byte[] personMessage;
// Sample message of type `test.AddressBook`
private static byte[] addressBookMessage;
private static Path protobufSchemaPath;
@BeforeAll
static void setUp() throws URISyntaxException, IOException {
protobufSchemaPath = Paths.get(ProtobufFileRecordSerDeTest.class.getClassLoader()
.getResource("address-book.proto").toURI());
ProtobufSchema protobufSchema = new ProtobufSchema(Files.readString(protobufSchemaPath));
DynamicMessage.Builder builder = protobufSchema.newMessageBuilder("test.Person");
JsonFormat.parser().merge(
"{ \"name\": \"My Name\",\"id\": 101, \"email\": \"user1@example.com\" }", builder);
personMessage = builder.build().toByteArray();
builder = protobufSchema.newMessageBuilder("test.AddressBook");
JsonFormat.parser().merge(
"{\"version\": 1, \"people\": ["
+ "{ \"name\": \"My Name\",\"id\": 102, \"email\": \"user2@example.com\" }]}", builder);
addressBookMessage = builder.build().toByteArray();
}
@Test
void testDeserialize() throws IOException {
var messageNameMap = Map.of(
"topic1", "test.Person",
"topic2", "test.AddressBook");
var deserializer =
new ProtobufFileRecordSerDe(protobufSchemaPath, messageNameMap, null, new ObjectMapper());
var msg1 = deserializer
.deserialize(new ConsumerRecord<>("topic1", 1, 0, Bytes.wrap("key".getBytes()),
Bytes.wrap(personMessage)));
assertEquals(MessageFormat.PROTOBUF, msg1.getValueFormat());
assertTrue(msg1.getValue().contains("user1@example.com"));
var msg2 = deserializer
.deserialize(new ConsumerRecord<>("topic2", 1, 1, Bytes.wrap("key".getBytes()),
Bytes.wrap(addressBookMessage)));
assertTrue(msg2.getValue().contains("user2@example.com"));
}
@Test
void testNoDefaultMessageName() throws IOException {
// by default the first message type defined in proto definition is used
var deserializer =
new ProtobufFileRecordSerDe(protobufSchemaPath, Collections.emptyMap(), null,
new ObjectMapper());
var msg = deserializer
.deserialize(new ConsumerRecord<>("topic", 1, 0, Bytes.wrap("key".getBytes()),
Bytes.wrap(personMessage)));
assertTrue(msg.getValue().contains("user1@example.com"));
}
@Test
void testDefaultMessageName() throws IOException {
var messageNameMap = Map.of("topic1", "test.Person");
var deserializer =
new ProtobufFileRecordSerDe(protobufSchemaPath, messageNameMap, "test.AddressBook",
new ObjectMapper());
var msg = deserializer
.deserialize(new ConsumerRecord<>("a_random_topic", 1, 0, Bytes.wrap("key".getBytes()),
Bytes.wrap(addressBookMessage)));
assertTrue(msg.getValue().contains("user2@example.com"));
}
@Test
void testSerialize() throws IOException {
var messageNameMap = Map.of("topic1", "test.Person");
var serializer =
new ProtobufFileRecordSerDe(protobufSchemaPath, messageNameMap, "test.AddressBook",
new ObjectMapper());
var serialized = serializer.serialize("topic1", "key1", "{\"name\":\"MyName\"}", 0);
assertNotNull(serialized.value());
}
}

View file

@ -0,0 +1,39 @@
// [START declaration]
syntax = "proto3";
package test;
// [END declaration]
// [START java_declaration]
option java_multiple_files = true;
option java_package = "com.example.tutorial.protos";
option java_outer_classname = "AddressBookProtos";
// [END java_declaration]
// [START messages]
message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
// Our address book file is just one of these.
message AddressBook {
int32 version = 1;
repeated Person people = 2;
}
// [END messages]