Schema Registry Migration
Initial attempt at migrating schema registry from https://github.com/spring-cloud/spring-cloud-schema-registry to Spring Cloud Stream. Schema Registry within the scope of Spring Cloud Stream will only support Spring Cloud Stream specific use cases in the context of binders. Resolves https://github.com/spring-cloud/spring-cloud-stream/issues/2491 Resolves #2492
This commit is contained in:
committed by
Oleg Zhurakousky
parent
0daa58c48a
commit
e1f173e139
1
pom.xml
1
pom.xml
@@ -34,6 +34,7 @@
|
||||
<modules>
|
||||
<module>core</module>
|
||||
<module>binders</module>
|
||||
<module>schema-registry</module>
|
||||
<module>bom</module>
|
||||
<module>docs</module>
|
||||
<module>samples</module>
|
||||
|
||||
56
schema-registry/pom.xml
Normal file
56
schema-registry/pom.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>spring-cloud-stream-schema-registry</artifactId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
<name>schema-registry</name>
|
||||
<description>Spring Cloud Stream Schema Registry Components</description>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-parent</artifactId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<avro.version>1.9.2</avro.version>
|
||||
<h2.version>1.4.192</h2.version>
|
||||
<jackson-bom.version>2.13.2</jackson-bom.version>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
<module>spring-cloud-stream-schema-registry-core</module>
|
||||
<module>spring-cloud-stream-schema-registry-server</module>
|
||||
<module>spring-cloud-stream-schema-registry-client</module>
|
||||
</modules>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson</groupId>
|
||||
<artifactId>jackson-bom</artifactId>
|
||||
<version>${jackson-bom.version}</version>
|
||||
<scope>import</scope>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.avro</groupId>
|
||||
<artifactId>avro</artifactId>
|
||||
<version>${avro.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>${h2.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
</project>
|
||||
Binary file not shown.
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>spring-cloud-stream-schema-registry</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-stream-schema-registry-client</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-messaging</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.avro</groupId>
|
||||
<artifactId>avro</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>test-jar</type>
|
||||
<scope>test</scope>
|
||||
<classifier>test-binder</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-schema-registry-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-avro</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.avro</groupId>
|
||||
<artifactId>avro</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- <dependency>-->
|
||||
<!-- <!– used for testing https://github.com/spring-cloud/spring-cloud-schema-registry/issues/19 –>-->
|
||||
<!-- <groupId>com.fasterxml.jackson.dataformat</groupId>-->
|
||||
<!-- <artifactId>jackson-dataformat-xml</artifactId>-->
|
||||
<!-- <scope>tests</scope>-->
|
||||
<!-- </dependency>-->
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.avro</groupId>
|
||||
<artifactId>avro-maven-plugin</artifactId>
|
||||
<version>${avro.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>generate-test-sources</phase>
|
||||
<goals>
|
||||
<goal>schema</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<outputDirectory>${project.basedir}/target/generated-test-sources
|
||||
</outputDirectory>
|
||||
<testOutputDirectory>
|
||||
${project.basedir}/target/generated-test-sources
|
||||
</testOutputDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/resources/schemas
|
||||
</testSourceDirectory>
|
||||
<testIncludes>
|
||||
<testInclude>**/*.avsc</testInclude>
|
||||
</testIncludes>
|
||||
<imports>
|
||||
<import>
|
||||
${project.basedir}/src/test/resources/schemas/imports/Email.avsc
|
||||
</import>
|
||||
<import>
|
||||
${project.basedir}/src/test/resources/schemas/imports/Sms.avsc
|
||||
</import>
|
||||
<import>
|
||||
${project.basedir}/src/test/resources/schemas/imports/PushNotification.avsc
|
||||
</import>
|
||||
</imports>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry;
|
||||
|
||||
import org.apache.avro.Schema;
|
||||
|
||||
/**
|
||||
* Stores a {@link Schema} together with its String representation.
|
||||
*
|
||||
* Helps to avoid unnecessary parsing of schema textual representation, as well as calls
|
||||
* to {@link org.apache.avro.Schema} toString method which is very expensive due the
|
||||
* utilization of {@link com.fasterxml.jackson.databind.ObjectMapper} to output a JSON
|
||||
* representation of the schema.
|
||||
*
|
||||
* Once a schema is found for any Class, be it a POJO or a
|
||||
* {@link org.apache.avro.generic.GenericContainer}, both textual representation as well
|
||||
* as the {@link org.apache.avro.Schema} will be stored within this class.
|
||||
*
|
||||
* @author Vinicius Carvalho
|
||||
*
|
||||
*/
|
||||
public class ParsedSchema {
|
||||
|
||||
private final Schema schema;
|
||||
|
||||
private final String representation;
|
||||
|
||||
private SchemaRegistrationResponse registration;
|
||||
|
||||
public ParsedSchema(Schema schema) {
|
||||
this.schema = schema;
|
||||
this.representation = schema.toString();
|
||||
}
|
||||
|
||||
public Schema getSchema() {
|
||||
return this.schema;
|
||||
}
|
||||
|
||||
public String getRepresentation() {
|
||||
return this.representation;
|
||||
}
|
||||
|
||||
public SchemaRegistrationResponse getRegistration() {
|
||||
return this.registration;
|
||||
}
|
||||
|
||||
public void setRegistration(SchemaRegistrationResponse registration) {
|
||||
this.registration = registration;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
public class SchemaNotFoundException extends RuntimeException {
|
||||
|
||||
public SchemaNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* References a schema through its subject and version.
|
||||
*
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
public class SchemaReference {
|
||||
|
||||
private String subject;
|
||||
|
||||
private int version;
|
||||
|
||||
private String format;
|
||||
|
||||
public SchemaReference(String subject, int version, String format) {
|
||||
Assert.hasText(subject, "cannot be empty");
|
||||
Assert.isTrue(version > 0, "must be a positive integer");
|
||||
Assert.hasText(format, "cannot be empty");
|
||||
this.subject = subject;
|
||||
this.version = version;
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return this.subject;
|
||||
}
|
||||
|
||||
public void setSubject(String subject) {
|
||||
Assert.hasText(subject, "cannot be empty");
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public void setVersion(int version) {
|
||||
Assert.isTrue(version > 0, "must be a positive integer");
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getFormat() {
|
||||
return this.format;
|
||||
}
|
||||
|
||||
public void setFormat(String format) {
|
||||
Assert.hasText(format, "cannot be empty");
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SchemaReference that = (SchemaReference) o;
|
||||
|
||||
if (this.version != that.version) {
|
||||
return false;
|
||||
}
|
||||
if (!this.subject.equals(that.subject)) {
|
||||
return false;
|
||||
}
|
||||
return this.format.equals(that.format);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = this.subject.hashCode();
|
||||
result = 31 * result + this.version;
|
||||
result = 31 * result + this.format.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SchemaReference{" + "subject='" + this.subject + '\'' + ", version="
|
||||
+ this.version + ", format='" + this.format + '\'' + '}';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
public class SchemaRegistrationResponse {
|
||||
|
||||
private int id;
|
||||
|
||||
private SchemaReference schemaReference;
|
||||
|
||||
public int getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public SchemaReference getSchemaReference() {
|
||||
return this.schemaReference;
|
||||
}
|
||||
|
||||
public void setSchemaReference(SchemaReference schemaReference) {
|
||||
this.schemaReference = schemaReference;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2016-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.avro;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.apache.avro.Schema;
|
||||
import org.apache.avro.io.DatumWriter;
|
||||
import org.apache.avro.io.Encoder;
|
||||
import org.apache.avro.io.EncoderFactory;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.converter.AbstractMessageConverter;
|
||||
import org.springframework.messaging.converter.MessageConversionException;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* Base class for Apache Avro
|
||||
* {@link org.springframework.messaging.converter.MessageConverter} implementations.
|
||||
*
|
||||
* @author Marius Bogoevici
|
||||
* @author Vinicius Carvalho
|
||||
* @author Sercan Karaoglu
|
||||
* @author Ish Mahajan
|
||||
*/
|
||||
public abstract class AbstractAvroMessageConverter extends AbstractMessageConverter {
|
||||
|
||||
/**
|
||||
* common parser will let user to import external schemas.
|
||||
*/
|
||||
private Schema.Parser schemaParser = new Schema.Parser();
|
||||
private AvroSchemaServiceManager avroSchemaServiceManager;
|
||||
|
||||
@Deprecated
|
||||
protected AbstractAvroMessageConverter(MimeType supportedMimeType) {
|
||||
this(Collections.singletonList(supportedMimeType), new AvroSchemaServiceManagerImpl());
|
||||
}
|
||||
|
||||
protected AbstractAvroMessageConverter(MimeType supportedMimeType, AvroSchemaServiceManager avroSchemaServiceManager) {
|
||||
this(Collections.singletonList(supportedMimeType), avroSchemaServiceManager);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
protected AbstractAvroMessageConverter(Collection<MimeType> supportedMimeTypes) {
|
||||
this(supportedMimeTypes, new AvroSchemaServiceManagerImpl());
|
||||
setContentTypeResolver(new OriginalContentTypeResolver());
|
||||
}
|
||||
|
||||
protected AbstractAvroMessageConverter(Collection<MimeType> supportedMimeTypes, AvroSchemaServiceManager manager) {
|
||||
super(supportedMimeTypes);
|
||||
setContentTypeResolver(new OriginalContentTypeResolver());
|
||||
this.avroSchemaServiceManager = manager;
|
||||
}
|
||||
|
||||
protected AvroSchemaServiceManager avroSchemaServiceManager() {
|
||||
return this.avroSchemaServiceManager;
|
||||
}
|
||||
|
||||
protected Schema parseSchema(Resource r) throws IOException {
|
||||
return this.schemaParser.parse(r.getInputStream());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canConvertFrom(Message<?> message, Class<?> targetClass) {
|
||||
return super.canConvertFrom(message, targetClass)
|
||||
&& (message.getPayload() instanceof byte[]);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object convertFromInternal(Message<?> message, Class<?> targetClass, Object conversionHint) {
|
||||
Object result;
|
||||
try {
|
||||
byte[] payload = (byte[]) message.getPayload();
|
||||
|
||||
MimeType mimeType = getContentTypeResolver().resolve(message.getHeaders());
|
||||
if (mimeType == null) {
|
||||
if (conversionHint instanceof MimeType) {
|
||||
mimeType = (MimeType) conversionHint;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Schema writerSchema = resolveWriterSchemaForDeserialization(mimeType);
|
||||
Schema readerSchema = resolveReaderSchemaForDeserialization(targetClass);
|
||||
|
||||
result = avroSchemaServiceManager().readData(targetClass, payload, readerSchema, writerSchema);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new MessageConversionException(message, "Failed to read payload", e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object convertToInternal(Object payload, MessageHeaders headers, Object conversionHint) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try {
|
||||
MimeType hintedContentType = null;
|
||||
if (conversionHint instanceof MimeType) {
|
||||
hintedContentType = (MimeType) conversionHint;
|
||||
}
|
||||
Schema schema = resolveSchemaForWriting(payload, headers, hintedContentType);
|
||||
@SuppressWarnings("unchecked")
|
||||
DatumWriter<Object> writer = avroSchemaServiceManager().getDatumWriter(payload.getClass(), schema);
|
||||
Encoder encoder = EncoderFactory.get().binaryEncoder(baos, null);
|
||||
writer.write(payload, encoder);
|
||||
encoder.flush();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new MessageConversionException("Failed to write payload", e);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
protected abstract Schema resolveSchemaForWriting(Object payload, MessageHeaders headers, MimeType hintedContentType);
|
||||
|
||||
protected abstract Schema resolveWriterSchemaForDeserialization(MimeType mimeType);
|
||||
|
||||
protected abstract Schema resolveReaderSchemaForDeserialization(Class<?> targetClass);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.avro;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.cloud.stream.schema.registry.client.SchemaRegistryClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
* @author Vinicius Carvalho
|
||||
* @author Sercan Karaoglu
|
||||
* @author Ish Mahajan
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(name = "org.apache.avro.Schema")
|
||||
@ConditionalOnProperty(value = "spring.cloud.schemaRegistryClient.enabled", matchIfMissing = true)
|
||||
@ConditionalOnBean(type = "org.springframework.cloud.stream.schema.registry.client.SchemaRegistryClient")
|
||||
@EnableConfigurationProperties({ AvroMessageConverterProperties.class })
|
||||
@Import(AvroSchemaServiceManagerImpl.class)
|
||||
public class AvroMessageConverterAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(AvroSchemaRegistryClientMessageConverter.class)
|
||||
public AvroSchemaRegistryClientMessageConverter avroSchemaMessageConverter1(
|
||||
SchemaRegistryClient schemaRegistryClient,
|
||||
AvroSchemaServiceManager avroSchemaServiceManager,
|
||||
AvroMessageConverterProperties avroMessageConverterProperties) {
|
||||
|
||||
AvroSchemaRegistryClientMessageConverter avroSchemaRegistryClientMessageConverter =
|
||||
new AvroSchemaRegistryClientMessageConverter(schemaRegistryClient, cacheManager(), avroSchemaServiceManager);
|
||||
|
||||
avroSchemaRegistryClientMessageConverter.setDynamicSchemaGenerationEnabled(
|
||||
avroMessageConverterProperties.isDynamicSchemaGenerationEnabled());
|
||||
|
||||
if (avroMessageConverterProperties.getReaderSchema() != null) {
|
||||
avroSchemaRegistryClientMessageConverter.setReaderSchema(avroMessageConverterProperties.getReaderSchema());
|
||||
}
|
||||
if (!ObjectUtils.isEmpty(avroMessageConverterProperties.getSchemaLocations())) {
|
||||
avroSchemaRegistryClientMessageConverter.setSchemaLocations(avroMessageConverterProperties.getSchemaLocations());
|
||||
}
|
||||
if (!ObjectUtils.isEmpty(avroMessageConverterProperties.getSchemaImports())) {
|
||||
avroSchemaRegistryClientMessageConverter.setSchemaImports(avroMessageConverterProperties.getSchemaImports());
|
||||
}
|
||||
avroSchemaRegistryClientMessageConverter.setPrefix(avroMessageConverterProperties.getPrefix());
|
||||
|
||||
try {
|
||||
Class<?> clazz = avroMessageConverterProperties.getSubjectNamingStrategy();
|
||||
Constructor constructor = ReflectionUtils.accessibleConstructor(clazz);
|
||||
avroSchemaRegistryClientMessageConverter.setSubjectNamingStrategy(
|
||||
(SubjectNamingStrategy) constructor.newInstance());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Unable to create SubjectNamingStrategy "
|
||||
+ avroMessageConverterProperties.getSubjectNamingStrategy().toString(), ex);
|
||||
}
|
||||
avroSchemaRegistryClientMessageConverter.setSubjectNamePrefix(avroMessageConverterProperties.getSubjectNamePrefix());
|
||||
|
||||
return avroSchemaRegistryClientMessageConverter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public CacheManager cacheManager() {
|
||||
return new ConcurrentMapCacheManager();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.avro;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
* @author Sercan Karaoglu
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "spring.cloud.schema.avro")
|
||||
public class AvroMessageConverterProperties {
|
||||
|
||||
private boolean dynamicSchemaGenerationEnabled;
|
||||
|
||||
private Resource readerSchema;
|
||||
|
||||
/**
|
||||
* The source directory of Apache Avro schema. This schema is used by this converter.
|
||||
* If this schema depends on other schemas consider defining those those dependent
|
||||
* ones in the {@link #schemaImports}
|
||||
* @parameter
|
||||
*/
|
||||
private Resource[] schemaLocations;
|
||||
|
||||
/**
|
||||
* A list of files or directories that should be loaded first thus making them
|
||||
* importable by subsequent schemas. Note that imported files should not reference
|
||||
* each other.
|
||||
* @parameter
|
||||
*/
|
||||
private Resource[] schemaImports;
|
||||
|
||||
private String prefix = "vnd";
|
||||
|
||||
private String subjectNamePrefix;
|
||||
|
||||
private Class<? extends SubjectNamingStrategy> subjectNamingStrategy = DefaultSubjectNamingStrategy.class;
|
||||
|
||||
public Resource getReaderSchema() {
|
||||
return this.readerSchema;
|
||||
}
|
||||
|
||||
public void setReaderSchema(Resource readerSchema) {
|
||||
Assert.notNull(readerSchema, "cannot be null");
|
||||
this.readerSchema = readerSchema;
|
||||
}
|
||||
|
||||
public Resource[] getSchemaLocations() {
|
||||
return this.schemaLocations;
|
||||
}
|
||||
|
||||
public void setSchemaLocations(Resource[] schemaLocations) {
|
||||
Assert.notEmpty(schemaLocations, "cannot be null");
|
||||
this.schemaLocations = schemaLocations;
|
||||
}
|
||||
|
||||
public boolean isDynamicSchemaGenerationEnabled() {
|
||||
return this.dynamicSchemaGenerationEnabled;
|
||||
}
|
||||
|
||||
public void setDynamicSchemaGenerationEnabled(boolean dynamicSchemaGenerationEnabled) {
|
||||
this.dynamicSchemaGenerationEnabled = dynamicSchemaGenerationEnabled;
|
||||
}
|
||||
|
||||
public String getPrefix() {
|
||||
return this.prefix;
|
||||
}
|
||||
|
||||
public void setPrefix(String prefix) {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
public Class<?> getSubjectNamingStrategy() {
|
||||
return this.subjectNamingStrategy;
|
||||
}
|
||||
|
||||
public void setSubjectNamingStrategy(Class<? extends SubjectNamingStrategy> subjectNamingStrategy) {
|
||||
Assert.notNull(subjectNamingStrategy, "cannot be null");
|
||||
this.subjectNamingStrategy = subjectNamingStrategy;
|
||||
}
|
||||
|
||||
public Resource[] getSchemaImports() {
|
||||
return this.schemaImports;
|
||||
}
|
||||
|
||||
public void setSchemaImports(Resource[] schemaImports) {
|
||||
this.schemaImports = schemaImports;
|
||||
}
|
||||
|
||||
public String getSubjectNamePrefix() {
|
||||
return subjectNamePrefix;
|
||||
}
|
||||
|
||||
public void setSubjectNamePrefix(String subjectNamePrefix) {
|
||||
this.subjectNamePrefix = subjectNamePrefix;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.avro;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.apache.avro.Schema;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* A {@link org.springframework.messaging.converter.MessageConverter} using Apache Avro.
|
||||
* The schema for serializing and deserializing will be automatically inferred from the
|
||||
* class for {@link org.apache.avro.specific.SpecificRecord} and regular classes, unless a
|
||||
* specific schema is set, case in which that schema will be used instead. For converting
|
||||
* to {@link org.apache.avro.generic.GenericRecord} targets, a schema must be set.s
|
||||
*
|
||||
* @author Marius Bogoevici
|
||||
* @author Ish Mahajan
|
||||
*/
|
||||
|
||||
public class AvroSchemaMessageConverter extends AbstractAvroMessageConverter {
|
||||
|
||||
private Schema schema;
|
||||
|
||||
/**
|
||||
* Create a {@link AvroSchemaMessageConverter}. Uses the default {@link MimeType} of
|
||||
* {@code "application/avro"}.
|
||||
*/
|
||||
@Deprecated
|
||||
public AvroSchemaMessageConverter() {
|
||||
super(new MimeType("application", "avro"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link AvroSchemaMessageConverter}. Uses the default {@link MimeType} of
|
||||
* {@code "application/avro"}.
|
||||
* @param manager for schema management
|
||||
*/
|
||||
public AvroSchemaMessageConverter(AvroSchemaServiceManager manager) {
|
||||
super(new MimeType("application", "avro"), manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link AvroSchemaMessageConverter}. The converter will be used for the
|
||||
* provided {@link MimeType}.
|
||||
* @param supportedMimeType mime type to be supported by
|
||||
* {@link AvroSchemaMessageConverter}
|
||||
*/
|
||||
@Deprecated
|
||||
public AvroSchemaMessageConverter(MimeType supportedMimeType) {
|
||||
super(supportedMimeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link AvroSchemaMessageConverter}. The converter will be used for the
|
||||
* provided {@link MimeType}.
|
||||
* @param supportedMimeType mime type to be supported by
|
||||
* {@link AvroSchemaMessageConverter}
|
||||
* @param manager for schema management
|
||||
*/
|
||||
public AvroSchemaMessageConverter(MimeType supportedMimeType, AvroSchemaServiceManager manager) {
|
||||
super(supportedMimeType, manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link AvroSchemaMessageConverter}. The converter will be used for the
|
||||
* provided {@link MimeType}s.
|
||||
* @param supportedMimeTypes the mime types supported by this converter
|
||||
*/
|
||||
@Deprecated
|
||||
public AvroSchemaMessageConverter(Collection<MimeType> supportedMimeTypes) {
|
||||
super(supportedMimeTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link AvroSchemaMessageConverter}. The converter will be used for the
|
||||
* provided {@link MimeType}s.
|
||||
* @param supportedMimeTypes the mime types supported by this converter
|
||||
* @param manager for schema management
|
||||
*/
|
||||
public AvroSchemaMessageConverter(Collection<MimeType> supportedMimeTypes, AvroSchemaServiceManager manager) {
|
||||
super(supportedMimeTypes, manager);
|
||||
}
|
||||
|
||||
public Schema getSchema() {
|
||||
return this.schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Apache Avro schema to be used by this converter.
|
||||
* @param schema schema to be used by this converter
|
||||
*/
|
||||
public void setSchema(Schema schema) {
|
||||
Assert.notNull(schema, "schema cannot be null");
|
||||
this.schema = schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* The location of the Apache Avro schema to be used by this converter.
|
||||
* @param schemaLocation the location of the schema used by this converter.
|
||||
*/
|
||||
public void setSchemaLocation(Resource schemaLocation) {
|
||||
Assert.notNull(schemaLocation, "schema cannot be null");
|
||||
try {
|
||||
this.schema = parseSchema(schemaLocation);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException("Schema cannot be parsed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Schema resolveWriterSchemaForDeserialization(MimeType mimeType) {
|
||||
return this.schema;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Schema resolveReaderSchemaForDeserialization(Class<?> targetClass) {
|
||||
return this.schema;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Schema resolveSchemaForWriting(Object payload, MessageHeaders headers,
|
||||
MimeType hintedContentType) {
|
||||
return this.schema;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
/*
|
||||
* Copyright 2016-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.avro;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.avro.Schema;
|
||||
import org.apache.avro.generic.GenericContainer;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.beans.factory.BeanInitializationException;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.support.NoOpCacheManager;
|
||||
import org.springframework.cloud.stream.schema.registry.ParsedSchema;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaNotFoundException;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaReference;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaRegistrationResponse;
|
||||
import org.springframework.cloud.stream.schema.registry.client.SchemaRegistryClient;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* A {@link org.springframework.messaging.converter.MessageConverter} for Apache Avro,
|
||||
* with the ability to publish and retrieve schemas stored in a schema server, allowing
|
||||
* for schema evolution in applications. The supported content types are in the form
|
||||
* `application/*+avro`.
|
||||
*
|
||||
* During the conversion to a message, the converter will set the 'contentType' header to
|
||||
* 'application/[prefix].[subject].v[version]+avro', where:
|
||||
*
|
||||
* <li>
|
||||
* <ul>
|
||||
* <i>prefix</i> is a configurable prefix (default 'vnd');
|
||||
* </ul>
|
||||
* <ul>
|
||||
* <i>subject</i> is a subject derived from the type of the outgoing object - typically
|
||||
* the class name;
|
||||
* </ul>
|
||||
* <ul>
|
||||
* <i>version</i> is the schema version for the given subject;
|
||||
* </ul>
|
||||
* </li>
|
||||
*
|
||||
* When converting from a message, the converter will parse the content-type and use it to
|
||||
* fetch and cache the writer schema using the provided {@link SchemaRegistryClient}.
|
||||
*
|
||||
* @author Marius Bogoevici
|
||||
* @author Vinicius Carvalho
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Sercan Karaoglu
|
||||
* @author Ish Mahajan
|
||||
*/
|
||||
public class AvroSchemaRegistryClientMessageConverter extends AbstractAvroMessageConverter
|
||||
implements InitializingBean {
|
||||
|
||||
/**
|
||||
* Avro format defined in the Mime type.
|
||||
*/
|
||||
public static final String AVRO_FORMAT = "avro";
|
||||
|
||||
/**
|
||||
* Pattern for validating the prefix to be used in the publised subtype.
|
||||
*/
|
||||
public static final Pattern PREFIX_VALIDATION_PATTERN = Pattern.compile("[\\p{Alnum}]");
|
||||
|
||||
/**
|
||||
* Spring Cloud Stream schema property prefix.
|
||||
*/
|
||||
public static final String CACHE_PREFIX = "org.springframework.cloud.stream.schema";
|
||||
|
||||
/**
|
||||
* Property for reflection cache.
|
||||
*/
|
||||
public static final String REFLECTION_CACHE_NAME = CACHE_PREFIX + ".reflectionCache";
|
||||
|
||||
/**
|
||||
* Property for schema cache.
|
||||
*/
|
||||
public static final String SCHEMA_CACHE_NAME = CACHE_PREFIX + ".schemaCache";
|
||||
|
||||
/**
|
||||
* Property for reference cache.
|
||||
*/
|
||||
public static final String REFERENCE_CACHE_NAME = CACHE_PREFIX + ".referenceCache";
|
||||
|
||||
/**
|
||||
* Default Mime type for Avro.
|
||||
*/
|
||||
public static final MimeType DEFAULT_AVRO_MIME_TYPE = new MimeType("application", "*+" + AVRO_FORMAT);
|
||||
|
||||
private static final AvroSchemaServiceManager defaultAvroSchemaServiceManager =
|
||||
new AvroSchemaServiceManagerImpl();
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
protected Resource[] schemaImports = new Resource[] {};
|
||||
|
||||
private Pattern versionedSchema;
|
||||
|
||||
private boolean dynamicSchemaGenerationEnabled;
|
||||
|
||||
private Schema readerSchema;
|
||||
|
||||
private Resource[] schemaLocations;
|
||||
|
||||
private SchemaRegistryClient schemaRegistryClient;
|
||||
|
||||
private String prefix = "vnd";
|
||||
|
||||
private String subjectNamePrefix;
|
||||
|
||||
private SubjectNamingStrategy subjectNamingStrategy;
|
||||
|
||||
/**
|
||||
* Creates a new instance, configuring it with {@link SchemaRegistryClient} and
|
||||
* {@link CacheManager}.
|
||||
* @param schemaRegistryClient the {@link SchemaRegistryClient} used to interact with
|
||||
* the schema registry server.
|
||||
* @param cacheManager instance of {@link CacheManager} to cache parsed schemas. If
|
||||
* caching is not required use {@link NoOpCacheManager}
|
||||
*/
|
||||
@Deprecated
|
||||
public AvroSchemaRegistryClientMessageConverter(
|
||||
SchemaRegistryClient schemaRegistryClient, CacheManager cacheManager) {
|
||||
super(Collections.singletonList(DEFAULT_AVRO_MIME_TYPE), defaultAvroSchemaServiceManager);
|
||||
Assert.notNull(schemaRegistryClient, "cannot be null");
|
||||
Assert.notNull(cacheManager, "'cacheManager' cannot be null");
|
||||
this.schemaRegistryClient = schemaRegistryClient;
|
||||
this.cacheManager = cacheManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance, configuring it with {@link SchemaRegistryClient} and
|
||||
* {@link CacheManager}.
|
||||
* @param schemaRegistryClient the {@link SchemaRegistryClient} used to interact with
|
||||
* the schema registry server.
|
||||
* @param cacheManager instance of {@link CacheManager} to cache parsed schemas. If
|
||||
* caching is not required use {@link NoOpCacheManager}
|
||||
* @param manager instance of {@link AvroSchemaServiceManager} to manage schemas.
|
||||
*/
|
||||
public AvroSchemaRegistryClientMessageConverter(
|
||||
SchemaRegistryClient schemaRegistryClient, CacheManager cacheManager, AvroSchemaServiceManager manager) {
|
||||
super(Collections.singletonList(DEFAULT_AVRO_MIME_TYPE), manager);
|
||||
Assert.notNull(schemaRegistryClient, "cannot be null");
|
||||
Assert.notNull(cacheManager, "'cacheManager' cannot be null");
|
||||
Assert.notNull(manager, "'avroSchemaServiceManager' cannot be null");
|
||||
this.schemaRegistryClient = schemaRegistryClient;
|
||||
this.cacheManager = cacheManager;
|
||||
}
|
||||
|
||||
public boolean isDynamicSchemaGenerationEnabled() {
|
||||
return this.dynamicSchemaGenerationEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the converter to generate and register schemas automatically. If set to
|
||||
* false, it only allows the converter to use pre-registered schemas. Default 'true'.
|
||||
* @param dynamicSchemaGenerationEnabled true if dynamic schema generation is enabled
|
||||
*/
|
||||
public void setDynamicSchemaGenerationEnabled(boolean dynamicSchemaGenerationEnabled) {
|
||||
this.dynamicSchemaGenerationEnabled = dynamicSchemaGenerationEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of locations where the converter can load schemas from. Schemas provided at
|
||||
* these locations will be registered automatically.
|
||||
* @param schemaLocations array of locations
|
||||
*/
|
||||
public void setSchemaLocations(Resource[] schemaLocations) {
|
||||
Assert.notEmpty(schemaLocations, "cannot be empty");
|
||||
this.schemaLocations = schemaLocations;
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of schema locations where should be imported first. Schemas provided at these
|
||||
* locations will be reference, thus they should not reference each other.
|
||||
* @param schemaImports array of schema imports
|
||||
*/
|
||||
public void setSchemaImports(Resource[] schemaImports) {
|
||||
this.schemaImports = schemaImports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the prefix to be used in the published subtype. Default 'vnd'.
|
||||
* @param prefix prefix to be set
|
||||
*/
|
||||
public void setPrefix(String prefix) {
|
||||
Assert.hasText(prefix, "Prefix cannot be empty");
|
||||
Assert.isTrue(!PREFIX_VALIDATION_PATTERN.matcher(this.prefix).matches(), "Invalid prefix:" + this.prefix);
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
public void setReaderSchema(Resource readerSchema) {
|
||||
Assert.notNull(readerSchema, "cannot be null");
|
||||
try {
|
||||
this.readerSchema = parseSchema(readerSchema);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new BeanInitializationException("Cannot initialize reader schema", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSubjectNamingStrategy(SubjectNamingStrategy subjectNamingStrategy) {
|
||||
this.subjectNamingStrategy = subjectNamingStrategy;
|
||||
}
|
||||
|
||||
public void setSubjectNamePrefix(String subjectNamePrefix) {
|
||||
this.subjectNamePrefix = subjectNamePrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
this.versionedSchema = Pattern.compile("application/" + this.prefix
|
||||
+ "\\.([\\p{Alnum}\\$\\.]+)\\.v(\\p{Digit}+)\\+" + AVRO_FORMAT);
|
||||
|
||||
Stream.of(this.schemaImports, this.schemaLocations)
|
||||
.filter(arr -> !ObjectUtils.isEmpty(arr))
|
||||
.distinct()
|
||||
.peek(resources -> {
|
||||
if (this.logger.isInfoEnabled()) {
|
||||
this.logger.info("Scanning avro schema resources on classpath");
|
||||
this.logger.info("Parsing " + this.schemaImports.length + " schemas");
|
||||
}
|
||||
})
|
||||
.flatMap(Arrays::stream)
|
||||
.forEach(resource -> {
|
||||
try {
|
||||
Schema schema = parseSchema(resource);
|
||||
if (schema.getType().equals(Schema.Type.UNION)) {
|
||||
schema.getTypes().forEach(innerSchema -> registerSchema(resource, innerSchema));
|
||||
}
|
||||
else {
|
||||
registerSchema(resource, schema);
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
if (this.logger.isWarnEnabled()) {
|
||||
this.logger.warn("Failed to parse schema at " + resource.getFilename(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.cacheManager instanceof NoOpCacheManager) {
|
||||
this.logger.warn("Schema caching is effectively disabled "
|
||||
+ "since configured cache manager is a NoOpCacheManager. If this was not "
|
||||
+ "the intention, please provide the appropriate instance of CacheManager "
|
||||
+ "(i.e., ConcurrentMapCacheManager).");
|
||||
}
|
||||
}
|
||||
|
||||
protected String toSubject(String subjectNamePrefix, Schema schema) {
|
||||
return this.subjectNamingStrategy.toSubject(subjectNamePrefix, schema);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
// we support all types
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supportsMimeType(MessageHeaders headers) {
|
||||
if (super.supportsMimeType(headers)) {
|
||||
return true;
|
||||
}
|
||||
MimeType mimeType = getContentTypeResolver().resolve(headers);
|
||||
return DEFAULT_AVRO_MIME_TYPE.includes(mimeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Schema resolveSchemaForWriting(Object payload, MessageHeaders headers,
|
||||
MimeType hintedContentType) {
|
||||
|
||||
Schema schema;
|
||||
schema = extractSchemaForWriting(payload);
|
||||
ParsedSchema parsedSchema = this.getCache(REFERENCE_CACHE_NAME).get(schema, ParsedSchema.class);
|
||||
|
||||
if (parsedSchema == null) {
|
||||
parsedSchema = new ParsedSchema(schema);
|
||||
this.getCache(REFERENCE_CACHE_NAME).putIfAbsent(schema, parsedSchema);
|
||||
}
|
||||
|
||||
if (parsedSchema.getRegistration() == null) {
|
||||
SchemaRegistrationResponse response = this.schemaRegistryClient.register(toSubject(this.subjectNamePrefix, schema),
|
||||
AVRO_FORMAT, parsedSchema.getRepresentation());
|
||||
parsedSchema.setRegistration(response);
|
||||
|
||||
}
|
||||
|
||||
SchemaReference schemaReference = parsedSchema.getRegistration().getSchemaReference();
|
||||
|
||||
DirectFieldAccessor dfa = new DirectFieldAccessor(headers);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> _headers = (Map<String, Object>) dfa.getPropertyValue("headers");
|
||||
_headers.put(MessageHeaders.CONTENT_TYPE, "application/" + this.prefix + "." + schemaReference.getSubject()
|
||||
+ ".v" + schemaReference.getVersion() + "+" + AVRO_FORMAT);
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Schema resolveWriterSchemaForDeserialization(MimeType mimeType) {
|
||||
SchemaReference schemaReference = extractSchemaReference(mimeType);
|
||||
if (schemaReference != null) {
|
||||
ParsedSchema parsedSchema = this.getCache(REFERENCE_CACHE_NAME).get(schemaReference, ParsedSchema.class);
|
||||
if (parsedSchema == null) {
|
||||
String schemaContent = this.schemaRegistryClient.fetch(schemaReference);
|
||||
if (schemaContent != null) {
|
||||
Schema schema = new Schema.Parser().parse(schemaContent);
|
||||
parsedSchema = new ParsedSchema(schema);
|
||||
this.getCache(REFERENCE_CACHE_NAME).putIfAbsent(schemaReference, parsedSchema);
|
||||
}
|
||||
}
|
||||
if (parsedSchema != null) {
|
||||
return parsedSchema.getSchema();
|
||||
}
|
||||
}
|
||||
return this.readerSchema;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Schema resolveReaderSchemaForDeserialization(Class<?> targetClass) {
|
||||
return this.readerSchema;
|
||||
}
|
||||
|
||||
private Schema extractSchemaForWriting(Object payload) {
|
||||
Schema schema = null;
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug("Obtaining schema for class " + payload.getClass());
|
||||
}
|
||||
if (GenericContainer.class.isAssignableFrom(payload.getClass())) {
|
||||
schema = ((GenericContainer) payload).getSchema();
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug("Avro type detected, using schema from object");
|
||||
}
|
||||
}
|
||||
else {
|
||||
schema = this.getCache(REFLECTION_CACHE_NAME).get(payload.getClass().getName(), Schema.class);
|
||||
if (schema == null) {
|
||||
if (!isDynamicSchemaGenerationEnabled()) {
|
||||
throw new SchemaNotFoundException(String.format(
|
||||
"No schema found in the local cache for %s, and dynamic schema generation is not enabled",
|
||||
payload.getClass()));
|
||||
}
|
||||
else {
|
||||
schema = super.avroSchemaServiceManager().getSchema(payload.getClass());
|
||||
}
|
||||
this.getCache(REFLECTION_CACHE_NAME).put(payload.getClass().getName(), schema);
|
||||
}
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
private void registerSchema(Resource schemaLocation, Schema schema) {
|
||||
if (this.logger.isInfoEnabled()) {
|
||||
this.logger.info("Resource " + schemaLocation.getFilename() + " parsed into schema "
|
||||
+ schema.getNamespace() + "." + schema.getName());
|
||||
}
|
||||
|
||||
this.schemaRegistryClient.register(toSubject(this.subjectNamePrefix, schema), AVRO_FORMAT, schema.toString());
|
||||
|
||||
if (this.logger.isInfoEnabled()) {
|
||||
this.logger.info("Schema " + schema.getName() + " registered with id " + schema);
|
||||
}
|
||||
|
||||
this.getCache(REFLECTION_CACHE_NAME).put(schema.getNamespace() + "." + schema.getName(), schema);
|
||||
}
|
||||
|
||||
private SchemaReference extractSchemaReference(MimeType mimeType) {
|
||||
SchemaReference schemaReference = null;
|
||||
Matcher schemaMatcher = this.versionedSchema.matcher(mimeType.toString());
|
||||
if (schemaMatcher.find()) {
|
||||
String subject = schemaMatcher.group(1);
|
||||
Integer version = Integer.parseInt(schemaMatcher.group(2));
|
||||
schemaReference = new SchemaReference(subject, version, AVRO_FORMAT);
|
||||
}
|
||||
return schemaReference;
|
||||
}
|
||||
|
||||
private Cache getCache(String name) {
|
||||
Cache cache = this.cacheManager.getCache(name);
|
||||
Assert.notNull(cache, "Cache by the name '" + name + "' is not present in this CacheManager - '"
|
||||
+ this.cacheManager + "'. Typically caches are auto-created by the CacheManagers. "
|
||||
+ "Consider reporting it as an issue to the developer of this CacheManager.");
|
||||
return cache;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.avro;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.avro.Schema;
|
||||
import org.apache.avro.io.DatumReader;
|
||||
import org.apache.avro.io.DatumWriter;
|
||||
|
||||
/**
|
||||
* Manage a {@link Schema} together with its String representation.
|
||||
*
|
||||
* Helps to substitute the default implementation of {@link org.apache.avro.Schema}
|
||||
* Generation using Custom Avro schema generator
|
||||
*
|
||||
* Provide a custom bean definition of {@link AvroSchemaServiceManager} and mark
|
||||
* it as @Primary to override the default implementation
|
||||
*
|
||||
* @author Ish Mahajan
|
||||
*
|
||||
*/
|
||||
public interface AvroSchemaServiceManager {
|
||||
|
||||
/**
|
||||
* get {@link Schema}.
|
||||
* @param clazz {@link Class} for which schema generation is required
|
||||
* @return returns avro schema for given class
|
||||
*/
|
||||
Schema getSchema(Class<?> clazz);
|
||||
|
||||
/**
|
||||
* get {@link DatumWriter}.
|
||||
* @param type {@link Class} of java object which needs to be serialized
|
||||
* @param schema {@link Schema} of object which needs to be serialized
|
||||
* @return datum writer which can be used to write Avro payload
|
||||
*/
|
||||
DatumWriter<Object> getDatumWriter(Class<? extends Object> type, Schema schema);
|
||||
|
||||
/**
|
||||
* get {@link DatumReader}.
|
||||
* @param type {@link Class} of java object which needs to be serialized
|
||||
* @param schema {@link Schema} default schema of object which needs to be de-serialized
|
||||
* @param writerSchema {@link Schema} writerSchema provided at run time
|
||||
* @return datum reader which can be used to read Avro payload
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
DatumReader<Object> getDatumReader(Class<? extends Object> type, Schema schema, Schema writerSchema);
|
||||
|
||||
/**
|
||||
* read data from avro type payload {@link DatumReader}.
|
||||
* @param targetClass {@link Class} of java object which needs to be serialized
|
||||
* @param payload {@link byte} serialized payload of object which needs to be de-serialized
|
||||
* @param readerSchema {@link Schema} readerSchema of object which needs to be de-serialized
|
||||
* @param writerSchema {@link Schema} writerSchema used to while serializing payload
|
||||
* @return java object after reading Avro Payload
|
||||
* @throws IOException in case of error
|
||||
*/
|
||||
Object readData(Class<? extends Object> targetClass, byte[] payload, Schema readerSchema, Schema writerSchema)
|
||||
throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.avro;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.avro.Schema;
|
||||
import org.apache.avro.generic.GenericDatumReader;
|
||||
import org.apache.avro.generic.GenericDatumWriter;
|
||||
import org.apache.avro.generic.GenericRecord;
|
||||
import org.apache.avro.io.DatumReader;
|
||||
import org.apache.avro.io.DatumWriter;
|
||||
import org.apache.avro.io.Decoder;
|
||||
import org.apache.avro.io.DecoderFactory;
|
||||
import org.apache.avro.reflect.ReflectData;
|
||||
import org.apache.avro.reflect.ReflectDatumReader;
|
||||
import org.apache.avro.reflect.ReflectDatumWriter;
|
||||
import org.apache.avro.specific.SpecificDatumReader;
|
||||
import org.apache.avro.specific.SpecificDatumWriter;
|
||||
import org.apache.avro.specific.SpecificRecord;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.messaging.converter.MessageConversionException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Default Concrete implementation of {@link AvroSchemaServiceManager}.
|
||||
*
|
||||
* Helps to substitute the default implementation of {@link org.apache.avro.Schema} Generation using Custom Avro
|
||||
* schema generator
|
||||
*
|
||||
* Provide a custom bean definition of {@link AvroSchemaServiceManager} and mark it as @Primary to override this
|
||||
* default implementation
|
||||
*
|
||||
* @author Ish Mahajan
|
||||
*
|
||||
*/
|
||||
|
||||
@Component
|
||||
public class AvroSchemaServiceManagerImpl implements AvroSchemaServiceManager {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(this.getClass());
|
||||
|
||||
/**
|
||||
* get {@link Schema}.
|
||||
* @param clazz {@link Class} for which schema generation is required
|
||||
* @return returns avro schema for given class
|
||||
*/
|
||||
@Override
|
||||
public Schema getSchema(Class<?> clazz) {
|
||||
return ReflectData.get().getSchema(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* get {@link DatumWriter}.
|
||||
* @param type {@link Class} of java object which needs to be serialized
|
||||
* @param schema {@link Schema} of object which needs to be serialized
|
||||
* @return datum writer which can be used to write Avro payload
|
||||
*/
|
||||
@Override
|
||||
public DatumWriter<Object> getDatumWriter(Class<?> type, Schema schema) {
|
||||
DatumWriter<Object> writer;
|
||||
this.logger.debug("Finding correct DatumWriter for type " + type.getName());
|
||||
if (SpecificRecord.class.isAssignableFrom(type)) {
|
||||
if (schema != null) {
|
||||
writer = new SpecificDatumWriter<>(schema);
|
||||
}
|
||||
else {
|
||||
writer = new SpecificDatumWriter(type);
|
||||
}
|
||||
}
|
||||
else if (GenericRecord.class.isAssignableFrom(type)) {
|
||||
writer = new GenericDatumWriter<>(schema);
|
||||
}
|
||||
else {
|
||||
if (schema != null) {
|
||||
writer = new ReflectDatumWriter<>(schema);
|
||||
}
|
||||
else {
|
||||
writer = new ReflectDatumWriter(type);
|
||||
}
|
||||
}
|
||||
return writer;
|
||||
}
|
||||
|
||||
/**
|
||||
* get {@link DatumReader}.
|
||||
* @param type {@link Class} of java object which needs to be serialized
|
||||
* @param readerSchema {@link Schema} default schema of object which needs to be de-serialized
|
||||
* @param writerSchema {@link Schema} writerSchema provided at run time
|
||||
* @return datum reader which can be used to read Avro payload
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Override
|
||||
public DatumReader<Object> getDatumReader(Class<?> type, Schema readerSchema, Schema writerSchema) {
|
||||
DatumReader<Object> reader = null;
|
||||
if (SpecificRecord.class.isAssignableFrom(type)) {
|
||||
if (readerSchema != null) {
|
||||
if (writerSchema != null) {
|
||||
reader = new SpecificDatumReader<>(writerSchema, readerSchema);
|
||||
}
|
||||
else {
|
||||
reader = new SpecificDatumReader<>(readerSchema);
|
||||
}
|
||||
}
|
||||
else {
|
||||
reader = new SpecificDatumReader(type);
|
||||
if (writerSchema != null) {
|
||||
reader.setSchema(writerSchema);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (GenericRecord.class.isAssignableFrom(type)) {
|
||||
if (readerSchema != null) {
|
||||
if (writerSchema != null) {
|
||||
reader = new GenericDatumReader<>(writerSchema, readerSchema);
|
||||
}
|
||||
else {
|
||||
reader = new GenericDatumReader<>(readerSchema);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (writerSchema != null) {
|
||||
reader = new GenericDatumReader(writerSchema);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
reader = new ReflectDatumReader(type);
|
||||
if (writerSchema != null) {
|
||||
reader.setSchema(writerSchema);
|
||||
}
|
||||
}
|
||||
if (reader == null) {
|
||||
throw new MessageConversionException("No schema can be inferred from type "
|
||||
+ type.getName() + " and no schema has been explicitly configured.");
|
||||
}
|
||||
return reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* read data from avro type payload {@link DatumReader}.
|
||||
* @param clazz {@link Class} of java object which needs to be serialized
|
||||
* @param payload {@link byte} serialized payload of object which needs to be de-serialized
|
||||
* @param readerSchema {@link Schema} readerSchema of object which needs to be de-serialized
|
||||
* @param writerSchema {@link Schema} writerSchema used to while serializing payload
|
||||
* @return java object after reading Avro Payload
|
||||
* @throws IOException is thrown in case of error
|
||||
*/
|
||||
@Override
|
||||
public Object readData(Class<? extends Object> clazz, byte[] payload, Schema readerSchema, Schema writerSchema)
|
||||
throws IOException {
|
||||
DatumReader<Object> reader = this.getDatumReader(clazz, readerSchema, writerSchema);
|
||||
Decoder decoder = DecoderFactory.get().binaryDecoder(payload, null);
|
||||
return reader.read(null, decoder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2016-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.avro;
|
||||
|
||||
import org.apache.avro.Schema;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author David Kalosi
|
||||
*/
|
||||
public class DefaultSubjectNamingStrategy implements SubjectNamingStrategy {
|
||||
|
||||
@Override
|
||||
public String toSubject(String subjectNamePrefix, Schema schema) {
|
||||
return StringUtils.hasText(subjectNamePrefix) ?
|
||||
subjectNamePrefix + "-" + schema.getName().toLowerCase() :
|
||||
schema.getName().toLowerCase();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2017-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.avro;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.converter.ContentTypeResolver;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*
|
||||
* Resolves contentType looking for a originalContentType header first. If not found
|
||||
* returns the contentType
|
||||
*
|
||||
*/
|
||||
class OriginalContentTypeResolver implements ContentTypeResolver {
|
||||
|
||||
private static final String BINDER_ORIGINAL_CONTENT_TYPE = "originalContentType";
|
||||
|
||||
private ConcurrentMap<String, MimeType> mimeTypeCache = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public MimeType resolve(MessageHeaders headers) {
|
||||
Object contentType = headers
|
||||
.get(BINDER_ORIGINAL_CONTENT_TYPE) != null
|
||||
? headers.get(BINDER_ORIGINAL_CONTENT_TYPE)
|
||||
: headers.get(MessageHeaders.CONTENT_TYPE);
|
||||
MimeType mimeType = null;
|
||||
if (contentType instanceof MimeType) {
|
||||
mimeType = (MimeType) contentType;
|
||||
}
|
||||
else if (contentType instanceof String) {
|
||||
mimeType = this.mimeTypeCache.get(contentType);
|
||||
if (mimeType == null) {
|
||||
String valueAsString = (String) contentType;
|
||||
mimeType = MimeType.valueOf(valueAsString);
|
||||
this.mimeTypeCache.put(valueAsString, mimeType);
|
||||
}
|
||||
}
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.avro;
|
||||
|
||||
import org.apache.avro.Schema;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author José A. Íñigo
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class QualifiedSubjectNamingStrategy implements SubjectNamingStrategy {
|
||||
|
||||
@Override
|
||||
public String toSubject(String subjectNamePrefix, Schema schema) {
|
||||
return StringUtils.hasText(subjectNamePrefix) ?
|
||||
subjectNamePrefix + "-" + schema.getFullName().toLowerCase() :
|
||||
schema.getFullName().toLowerCase();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2016-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.avro;
|
||||
|
||||
import org.apache.avro.Schema;
|
||||
|
||||
/**
|
||||
* Provides function towards naming schema registry subjects for Avro files.
|
||||
*
|
||||
* @author David Kalosi
|
||||
*/
|
||||
public interface SubjectNamingStrategy {
|
||||
|
||||
/**
|
||||
* Takes the Avro schema on input and returns the generated subject under which the
|
||||
* schema should be registered.
|
||||
* @param subjectNamePrefix optional subject name prefix
|
||||
* @param schema schema to register
|
||||
* @return subject name
|
||||
*/
|
||||
String toSubject(String subjectNamePrefix, Schema schema);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2020-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.avro;
|
||||
|
||||
import org.apache.avro.Schema;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class SubjectPrefixOnlyNamingStrategy implements SubjectNamingStrategy {
|
||||
|
||||
@Override
|
||||
public String toSubject(String subjectNamePrefix, Schema schema) {
|
||||
return subjectNamePrefix;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.client;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaReference;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaRegistrationResponse;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
public class CachingRegistryClient implements SchemaRegistryClient {
|
||||
|
||||
private static final String CACHE_PREFIX = "org.springframework.cloud.schema.registry.client";
|
||||
|
||||
private static final String ID_CACHE = CACHE_PREFIX + ".schemaByIdCache";
|
||||
|
||||
private static final String REF_CACHE = CACHE_PREFIX + ".schemaByReferenceCache";
|
||||
|
||||
private SchemaRegistryClient delegate;
|
||||
|
||||
@Autowired
|
||||
private CacheManager cacheManager;
|
||||
|
||||
public CachingRegistryClient(SchemaRegistryClient delegate) {
|
||||
Assert.notNull(delegate, "The delegate cannot be null");
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SchemaRegistrationResponse register(String subject, String format, String schema) {
|
||||
SchemaRegistrationResponse response = this.delegate.register(subject, format, schema);
|
||||
this.cacheManager.getCache(ID_CACHE).put(response.getId(), schema);
|
||||
this.cacheManager.getCache(REF_CACHE).put(response.getSchemaReference(), schema);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Cacheable(cacheNames = REF_CACHE)
|
||||
public String fetch(SchemaReference schemaReference) {
|
||||
return this.delegate.fetch(schemaReference);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Cacheable(cacheNames = ID_CACHE)
|
||||
public String fetch(int id) {
|
||||
return this.delegate.fetch(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2016-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.client;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaNotFoundException;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaReference;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaRegistrationResponse;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
* @author Marius Bogoevici
|
||||
* @author Jon Archer
|
||||
* @author Tengzhou Dong
|
||||
*/
|
||||
public class ConfluentSchemaRegistryClient implements SchemaRegistryClient {
|
||||
|
||||
private static final List<String> ACCEPT_HEADERS = Arrays.asList(
|
||||
"application/vnd.schemaregistry.v1+json",
|
||||
"application/vnd.schemaregistry+json", "application/json");
|
||||
|
||||
private RestTemplate template;
|
||||
|
||||
private String endpoint = "http://localhost:8081";
|
||||
|
||||
private ObjectMapper mapper;
|
||||
|
||||
public ConfluentSchemaRegistryClient() {
|
||||
this(new RestTemplate());
|
||||
}
|
||||
|
||||
public ConfluentSchemaRegistryClient(RestTemplate template) {
|
||||
this(template, new ObjectMapper());
|
||||
}
|
||||
|
||||
public ConfluentSchemaRegistryClient(RestTemplate template, ObjectMapper mapper) {
|
||||
this.template = template;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public void setEndpoint(String endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SchemaRegistrationResponse register(String subject, String format, String schema) {
|
||||
Assert.isTrue("avro".equals(format), "Only Avro is supported");
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.put("Accept", ACCEPT_HEADERS);
|
||||
headers.add("Content-Type", "application/json");
|
||||
Integer version = null;
|
||||
Integer id = null;
|
||||
String payload = null;
|
||||
Map<String, String> maps = new HashMap<>();
|
||||
maps.put("schema", schema);
|
||||
try {
|
||||
payload = this.mapper.writeValueAsString(maps);
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Could not parse schema, invalid JSON format", e);
|
||||
}
|
||||
try {
|
||||
HttpEntity<String> request = new HttpEntity<>(payload, headers);
|
||||
ResponseEntity<Map> response = this.template.exchange(
|
||||
this.endpoint + "/subjects/" + subject + "/versions", HttpMethod.POST, request, Map.class);
|
||||
id = (Integer) response.getBody().get("id");
|
||||
}
|
||||
catch (HttpStatusCodeException httpException) {
|
||||
throw new RuntimeException(String.format("Failed to register subject %s, server replied with status %d",
|
||||
subject, httpException.getStatusCode().value()), httpException);
|
||||
}
|
||||
|
||||
try {
|
||||
ResponseEntity<List> response = this.template.getForEntity(
|
||||
this.endpoint + "/subjects/" + subject + "/versions", List.class);
|
||||
|
||||
final List body = response.getBody();
|
||||
if (!CollectionUtils.isEmpty(body)) {
|
||||
version = (Integer) body.get(body.size() - 1);
|
||||
}
|
||||
}
|
||||
catch (HttpStatusCodeException httpException) {
|
||||
throw new RuntimeException(String.format("Failed to register subject %s, server replied with status %d",
|
||||
subject, httpException.getStatusCode().value()), httpException);
|
||||
}
|
||||
|
||||
SchemaRegistrationResponse schemaRegistrationResponse = new SchemaRegistrationResponse();
|
||||
schemaRegistrationResponse.setId(id);
|
||||
schemaRegistrationResponse.setSchemaReference(new SchemaReference(subject, version, "avro"));
|
||||
return schemaRegistrationResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fetch(SchemaReference schemaReference) {
|
||||
String path = String.format("/subjects/%s/versions/%d", schemaReference.getSubject(), schemaReference.getVersion());
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.put("Accept", ACCEPT_HEADERS);
|
||||
headers.add("Content-Type", "application/vnd.schemaregistry.v1+json");
|
||||
HttpEntity<String> request = new HttpEntity<>("", headers);
|
||||
try {
|
||||
ResponseEntity<Map> response = this.template.exchange(this.endpoint + path,
|
||||
HttpMethod.GET, request, Map.class);
|
||||
return (String) response.getBody().get("schema");
|
||||
}
|
||||
catch (HttpStatusCodeException e) {
|
||||
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||
throw new SchemaNotFoundException(String.format("Could not find schema for reference: %s", schemaReference));
|
||||
}
|
||||
else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fetch(int id) {
|
||||
String path = String.format("/schemas/ids/%d", id);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.put("Accept", ACCEPT_HEADERS);
|
||||
headers.add("Content-Type", "application/vnd.schemaregistry.v1+json");
|
||||
HttpEntity<String> request = new HttpEntity<>("", headers);
|
||||
try {
|
||||
ResponseEntity<Map> response = this.template.exchange(this.endpoint + path,
|
||||
HttpMethod.GET, request, Map.class);
|
||||
return (String) response.getBody().get("schema");
|
||||
}
|
||||
catch (HttpStatusCodeException e) {
|
||||
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||
throw new SchemaNotFoundException(String.format("Could not find schema with id: %s", id));
|
||||
}
|
||||
else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2016-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.client;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaReference;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaRegistrationResponse;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
* @author Vinicius Carvalho
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class DefaultSchemaRegistryClient implements SchemaRegistryClient {
|
||||
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
private String endpoint = "http://localhost:8990";
|
||||
|
||||
public DefaultSchemaRegistryClient(RestTemplateBuilder builder) {
|
||||
this(builder.build());
|
||||
}
|
||||
|
||||
public DefaultSchemaRegistryClient(RestTemplate restTemplate) {
|
||||
Assert.notNull(restTemplate, "'restTemplate' must not be null.");
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
protected String getEndpoint() {
|
||||
return this.endpoint;
|
||||
}
|
||||
|
||||
public void setEndpoint(String endpoint) {
|
||||
Assert.hasText(endpoint, "cannot be empty");
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
protected RestTemplate getRestTemplate() {
|
||||
return this.restTemplate;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@Override
|
||||
public SchemaRegistrationResponse register(String subject, String format, String schema) {
|
||||
Map<String, String> requestBody = new HashMap<>();
|
||||
requestBody.put("subject", subject);
|
||||
requestBody.put("format", format);
|
||||
requestBody.put("definition", schema);
|
||||
ResponseEntity<Map> responseEntity = this.restTemplate.postForEntity(this.endpoint, requestBody, Map.class);
|
||||
if (responseEntity.getStatusCode().is2xxSuccessful()) {
|
||||
SchemaRegistrationResponse registrationResponse = new SchemaRegistrationResponse();
|
||||
Map<String, Object> responseBody = (Map<String, Object>) responseEntity.getBody();
|
||||
registrationResponse.setId((Integer) responseBody.get("id"));
|
||||
registrationResponse.setSchemaReference(new SchemaReference(subject, (Integer) responseBody.get("version"),
|
||||
responseBody.get("format").toString()));
|
||||
return registrationResponse;
|
||||
}
|
||||
throw new RuntimeException(
|
||||
"Failed to register schema: " + responseEntity.toString());
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
public String fetch(SchemaReference schemaReference) {
|
||||
ResponseEntity<Map> responseEntity = this.restTemplate.getForEntity(this.endpoint
|
||||
+ "/" + schemaReference.getSubject() + "/" + schemaReference.getFormat()
|
||||
+ "/v" + schemaReference.getVersion(), Map.class);
|
||||
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
|
||||
throw new RuntimeException("Failed to fetch schema: " + responseEntity.toString());
|
||||
}
|
||||
return (String) responseEntity.getBody().get("definition");
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
public String fetch(int id) {
|
||||
ResponseEntity<Map> responseEntity = this.restTemplate.getForEntity(this.endpoint + "/schemas/" + id, Map.class);
|
||||
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
|
||||
throw new RuntimeException("Failed to fetch schema: " + responseEntity.toString());
|
||||
}
|
||||
return (String) responseEntity.getBody().get("definition");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.client;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.cloud.stream.schema.registry.client.config.SchemaRegistryClientConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
@Configuration
|
||||
@Import(SchemaRegistryClientConfiguration.class)
|
||||
public @interface EnableSchemaRegistryClient {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.client;
|
||||
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaReference;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaRegistrationResponse;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
public interface SchemaRegistryClient {
|
||||
|
||||
/**
|
||||
* Registers a schema with the remote repository returning the unique identifier
|
||||
* associated with this schema.
|
||||
* @param subject the full name of the schema
|
||||
* @param format format of the schema
|
||||
* @param schema string representation of the schema
|
||||
* @return a {@link SchemaRegistrationResponse} representing the result of the
|
||||
* operation
|
||||
*/
|
||||
SchemaRegistrationResponse register(String subject, String format, String schema);
|
||||
|
||||
/**
|
||||
* Retrieves a schema by its reference (subject and version).
|
||||
* @param schemaReference a {@link SchemaReference} used to identify the target
|
||||
* schema.
|
||||
* @return schema
|
||||
*/
|
||||
String fetch(SchemaReference schemaReference);
|
||||
|
||||
/**
|
||||
* Retrieves a schema by its identifier.
|
||||
* @param id the id of the target schema.
|
||||
* @return schema
|
||||
*/
|
||||
String fetch(int id);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2016-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.client.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.cloud.stream.schema.registry.client.CachingRegistryClient;
|
||||
import org.springframework.cloud.stream.schema.registry.client.DefaultSchemaRegistryClient;
|
||||
import org.springframework.cloud.stream.schema.registry.client.SchemaRegistryClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
* @author Vinicius Carvalho
|
||||
* @author Soby Chacko
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(SchemaRegistryClientProperties.class)
|
||||
public class SchemaRegistryClientConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SchemaRegistryClient schemaRegistryClient(SchemaRegistryClientProperties schemaRegistryClientProperties,
|
||||
RestTemplateBuilder restTemplateBuilder) {
|
||||
DefaultSchemaRegistryClient defaultSchemaRegistryClient = new DefaultSchemaRegistryClient(restTemplateBuilder);
|
||||
|
||||
if (StringUtils.hasText(schemaRegistryClientProperties.getEndpoint())) {
|
||||
defaultSchemaRegistryClient.setEndpoint(schemaRegistryClientProperties.getEndpoint());
|
||||
}
|
||||
|
||||
SchemaRegistryClient client = (schemaRegistryClientProperties.isCached())
|
||||
? new CachingRegistryClient(defaultSchemaRegistryClient)
|
||||
: defaultSchemaRegistryClient;
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.client.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
* @author Vinicius Carvalho
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "spring.cloud.stream.schema-registry-client")
|
||||
public class SchemaRegistryClientProperties {
|
||||
|
||||
private String endpoint;
|
||||
|
||||
private boolean cached = false;
|
||||
|
||||
public String getEndpoint() {
|
||||
return this.endpoint;
|
||||
}
|
||||
|
||||
public void setEndpoint(String endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
public boolean isCached() {
|
||||
return this.cached;
|
||||
}
|
||||
|
||||
public void setCached(boolean cached) {
|
||||
this.cached = cached;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.stream.schema.registry.avro.AvroMessageConverterAutoConfiguration
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2020-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.avro;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.cloud.stream.function.StreamBridge;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaMessageConverter;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaServiceManager;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaServiceManagerImpl;
|
||||
import org.springframework.cloud.stream.schema.registry.client.SchemaRegistryClient;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class AvroSchemaLocationsTest {
|
||||
|
||||
@Test
|
||||
public void schemaLocationWithMultipleRecords() throws Exception {
|
||||
testMultipleRecordsSchemaLoading(
|
||||
"--spring.cloud.schema.avro.schema-locations=classpath:schemas/user1_multiple_records.schema");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void schemaImportWithMultipleRecords() throws Exception {
|
||||
testMultipleRecordsSchemaLoading(
|
||||
"--spring.cloud.schema.avro.schema-imports=classpath:schemas/user1_multiple_records.schema");
|
||||
}
|
||||
|
||||
private void testMultipleRecordsSchemaLoading(String schemaLoadingProperty) throws Exception {
|
||||
User1 user1 = new User1();
|
||||
user1.setFavoriteColor("foo" + UUID.randomUUID());
|
||||
user1.setName("foo" + UUID.randomUUID());
|
||||
|
||||
org.springframework.cloud.stream.schema.avro.v2.User1 user2 = new org.springframework.cloud.stream.schema.avro.v2.User1();
|
||||
user2.setFavoriteColor("foo" + UUID.randomUUID().toString());
|
||||
user2.setName("foo" + UUID.randomUUID().toString());
|
||||
user2.setFavoritePlace("Amsterdam");
|
||||
|
||||
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(AvroSourceApplication.class))
|
||||
.web(WebApplicationType.NONE).run("--server.port=0",
|
||||
"--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.bindings.myBinding-out-0.contentType=application/*+avro", schemaLoadingProperty)) {
|
||||
StreamBridge streamBridge = context.getBean(StreamBridge.class);
|
||||
|
||||
streamBridge.send("myBinding-out-0", MessageBuilder.withPayload(user1).build());
|
||||
streamBridge.send("myBinding-out-0", MessageBuilder.withPayload(user2).build());
|
||||
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> result = output.receive();
|
||||
|
||||
final MessageConverter avroSchemaMessageConverter = (MessageConverter) context.getBean("userMessageConverter");
|
||||
final User1 receivedUser1 = (User1) avroSchemaMessageConverter.fromMessage(result, User1.class);
|
||||
result = output.receive();
|
||||
final User1 receivedUser2 = (User1) avroSchemaMessageConverter.fromMessage(result, User1.class);
|
||||
|
||||
assertThat(receivedUser1.getFavoriteColor()).isEqualTo(user1.getFavoriteColor());
|
||||
assertThat(receivedUser1.getName()).isEqualTo(user1.getName());
|
||||
|
||||
assertThat(receivedUser2.getFavoriteColor()).isEqualTo(user2.getFavoriteColor());
|
||||
assertThat(receivedUser2.getName()).isEqualTo(user2.getName());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
static SchemaRegistryClient stubSchemaRegistryClient = new StubSchemaRegistryClient();
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class AvroSourceApplication {
|
||||
|
||||
private Resource schemaLocation;
|
||||
|
||||
@Bean
|
||||
public SchemaRegistryClient schemaRegistryClient() {
|
||||
return stubSchemaRegistryClient;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MessageConverter userMessageConverter() throws IOException {
|
||||
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
|
||||
AvroSchemaMessageConverter avroSchemaMessageConverter = new AvroSchemaMessageConverter(
|
||||
MimeType.valueOf("application/avro"), manager);
|
||||
if (this.schemaLocation != null) {
|
||||
avroSchemaMessageConverter.setSchemaLocation(this.schemaLocation);
|
||||
}
|
||||
return avroSchemaMessageConverter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.avro;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.cloud.stream.function.StreamBridge;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaMessageConverter;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaServiceManager;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaServiceManagerImpl;
|
||||
import org.springframework.cloud.stream.schema.registry.client.SchemaRegistryClient;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
public class AvroSchemaMessageConverterTests {
|
||||
|
||||
static StubSchemaRegistryClient stubSchemaRegistryClient = new StubSchemaRegistryClient();
|
||||
|
||||
@Test
|
||||
public void testSendMessageWithLocation() throws Exception {
|
||||
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(AvroSourceApplication.class))
|
||||
.web(WebApplicationType.NONE).run("--server.port=0",
|
||||
"--spring.jmx.enabled=false",
|
||||
"--schemaLocation=classpath:schemas/users_v1.schema",
|
||||
"--spring.cloud.stream.bindings.myBinding-out-0.contentType=avro/bytes")) {
|
||||
StreamBridge streamBridge = context.getBean(StreamBridge.class);
|
||||
|
||||
User1 user1 = new User1();
|
||||
user1.setName("foo" + UUID.randomUUID());
|
||||
user1.setFavoriteColor("foo" + UUID.randomUUID());
|
||||
|
||||
streamBridge.send("myBinding-out-0", user1);
|
||||
|
||||
User2 user2 = new User2();
|
||||
user2.setFavoriteColor("foo" + UUID.randomUUID().toString());
|
||||
user2.setFavoritePlace("foo" + UUID.randomUUID().toString());
|
||||
user2.setName("foo" + UUID.randomUUID().toString());
|
||||
|
||||
streamBridge.send("myBinding-out-0", user2);
|
||||
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> result = output.receive();
|
||||
|
||||
final MessageConverter userMessageConverter = (MessageConverter) context.getBean("userMessageConverter");
|
||||
final User1 receivedUser1 = (User1) userMessageConverter.fromMessage(result, User1.class);
|
||||
|
||||
assertThat(receivedUser1).isNotNull();
|
||||
assertThat(receivedUser1).isNotSameAs(user1);
|
||||
assertThat(receivedUser1.getFavoriteColor()).isEqualTo(user1.getFavoriteColor());
|
||||
assertThat(receivedUser1.getName()).isEqualTo(user1.getName());
|
||||
|
||||
result = output.receive();
|
||||
|
||||
final User2 receivedUser2 = (User2) userMessageConverter.fromMessage(result, User2.class);
|
||||
|
||||
assertThat(receivedUser2).isNotNull();
|
||||
assertThat(receivedUser2).isNotSameAs(user2);
|
||||
assertThat(receivedUser2.getFavoriteColor()).isEqualTo(user2.getFavoriteColor());
|
||||
assertThat(receivedUser2.getFavoritePlace()).isEqualTo(user2.getFavoritePlace());
|
||||
assertThat(receivedUser2.getName()).isEqualTo(user2.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendMessageWithoutLocation() throws Exception {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(AvroSourceApplication.class))
|
||||
.web(WebApplicationType.NONE).run("--server.port=0",
|
||||
"--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.bindings.myBinding-out-0.contentType=avro/bytes")) {
|
||||
StreamBridge streamBridge = context.getBean(StreamBridge.class);
|
||||
|
||||
User1 user1 = new User1();
|
||||
user1.setName("foo" + UUID.randomUUID());
|
||||
user1.setFavoriteColor("foo" + UUID.randomUUID());
|
||||
|
||||
streamBridge.send("myBinding-out-0", user1);
|
||||
|
||||
User2 user2 = new User2();
|
||||
user2.setFavoriteColor("foo" + UUID.randomUUID().toString());
|
||||
user2.setFavoritePlace("foo" + UUID.randomUUID().toString());
|
||||
user2.setName("foo" + UUID.randomUUID().toString());
|
||||
|
||||
streamBridge.send("myBinding-out-0", user2);
|
||||
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> result = output.receive();
|
||||
|
||||
final MessageConverter userMessageConverter = (MessageConverter) context.getBean("userMessageConverter");
|
||||
final User1 receivedUser1 = (User1) userMessageConverter.fromMessage(result, User1.class);
|
||||
|
||||
assertThat(receivedUser1).isNotNull();
|
||||
assertThat(receivedUser1).isNotSameAs(user1);
|
||||
assertThat(receivedUser1.getFavoriteColor()).isEqualTo(user1.getFavoriteColor());
|
||||
assertThat(receivedUser1.getName()).isEqualTo(user1.getName());
|
||||
|
||||
result = output.receive();
|
||||
|
||||
final User2 receivedUser2 = (User2) userMessageConverter.fromMessage(result, User2.class);
|
||||
|
||||
assertThat(receivedUser2).isNotNull();
|
||||
assertThat(receivedUser2).isNotSameAs(user2);
|
||||
assertThat(receivedUser2.getFavoriteColor()).isEqualTo(user2.getFavoriteColor());
|
||||
assertThat(receivedUser2.getFavoritePlace()).isEqualTo(user2.getFavoritePlace());
|
||||
assertThat(receivedUser2.getName()).isEqualTo(user2.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class AvroSourceApplication {
|
||||
|
||||
private Resource schemaLocation;
|
||||
|
||||
@Bean
|
||||
public SchemaRegistryClient schemaRegistryClient() {
|
||||
return stubSchemaRegistryClient;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MessageConverter userMessageConverter() throws IOException {
|
||||
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
|
||||
AvroSchemaMessageConverter avroSchemaMessageConverter = new AvroSchemaMessageConverter(
|
||||
MimeType.valueOf("avro/bytes"), manager);
|
||||
if (this.schemaLocation != null) {
|
||||
avroSchemaMessageConverter.setSchemaLocation(this.schemaLocation);
|
||||
}
|
||||
return avroSchemaMessageConverter;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.avro;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.avro.AvroFactory;
|
||||
import com.fasterxml.jackson.dataformat.avro.AvroMapper;
|
||||
import com.fasterxml.jackson.dataformat.avro.AvroSchema;
|
||||
import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaGenerator;
|
||||
import org.apache.avro.Schema;
|
||||
import org.apache.avro.SchemaParseException;
|
||||
import org.apache.avro.file.DataFileReader;
|
||||
import org.apache.avro.file.DataFileWriter;
|
||||
import org.apache.avro.io.DatumReader;
|
||||
import org.apache.avro.io.DatumWriter;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.cloud.stream.schema.avro.domain.FoodOrder;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaMessageConverter;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaServiceManager;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaServiceManagerImpl;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* @author Ish Mahajan
|
||||
*/
|
||||
public class AvroSchemaServiceManagerTests {
|
||||
|
||||
private final Log logger = LogFactory.getLog(AvroSchemaServiceManagerTests.class);
|
||||
|
||||
@Test
|
||||
public void testWithDefaultImplementation() throws IOException {
|
||||
assertThatThrownBy(() -> {
|
||||
|
||||
AvroSchemaServiceManager defaultServiceManager = new AvroSchemaServiceManagerImpl();
|
||||
|
||||
Schema schema = defaultServiceManager.getSchema(FoodOrder.class);
|
||||
FoodOrder foodOrder = new FoodOrder();
|
||||
foodOrder.setRestaurant("Spring Kitchen");
|
||||
foodOrder.setOrderDescription("avro makhani");
|
||||
foodOrder.setCustomerAddress("world wide web");
|
||||
File file = new File("foodorder.avro");
|
||||
|
||||
DatumWriter datumWriter = defaultServiceManager.getDatumWriter(foodOrder.getClass(), schema);
|
||||
DataFileWriter<FoodOrder> dataFileWriter = new DataFileWriter<FoodOrder>(datumWriter);
|
||||
dataFileWriter.create(schema, file);
|
||||
dataFileWriter.append(foodOrder);
|
||||
|
||||
FoodOrder foodOrder2 = new FoodOrder();
|
||||
dataFileWriter.append(foodOrder2);
|
||||
dataFileWriter.close();
|
||||
|
||||
DatumReader userDatumReader = defaultServiceManager.getDatumReader(foodOrder.getClass(), schema, schema);
|
||||
DataFileReader<FoodOrder> dataFileReader = new DataFileReader<FoodOrder>(file, userDatumReader);
|
||||
FoodOrder foodOrderDeserialized = null;
|
||||
while (dataFileReader.hasNext()) {
|
||||
// Reuse user object by passing it to next(). This saves us from
|
||||
// allocating and garbage collecting many objects for files with
|
||||
// many items.
|
||||
foodOrderDeserialized = dataFileReader.next(foodOrderDeserialized);
|
||||
logger.info("De-serialised Successfully : " + foodOrderDeserialized);
|
||||
}
|
||||
}).isInstanceOf(DataFileWriter.AppendWriteException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithCustomImplementation() throws IOException {
|
||||
AvroSchemaServiceManager manager = new AvroSchemaServiceManager() {
|
||||
@Override
|
||||
public Schema getSchema(Class<?> clazz) {
|
||||
ObjectMapper mapper = new ObjectMapper(new AvroFactory());
|
||||
AvroSchemaGenerator gen = new AvroSchemaGenerator();
|
||||
try {
|
||||
mapper.acceptJsonFormatVisitor(FoodOrder.class, gen);
|
||||
}
|
||||
catch (JsonMappingException e) {
|
||||
fail("Error while setting acceptJsonFormatVisitor {}", e);
|
||||
}
|
||||
AvroSchema schemaWrapper = gen.getGeneratedSchema();
|
||||
return schemaWrapper.getAvroSchema();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatumWriter<Object> getDatumWriter(Class<?> type, Schema schema) {
|
||||
return new AvroSchemaServiceManagerImpl().getDatumWriter(type, schema);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatumReader<Object> getDatumReader(Class<?> type, Schema schema, Schema writerSchema) {
|
||||
return new AvroSchemaServiceManagerImpl().getDatumReader(type, schema, schema);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object readData(Class<? extends Object> targetClass, byte[] payload, Schema readerSchema,
|
||||
Schema writerSchema) throws IOException {
|
||||
ObjectMapper mapper = new ObjectMapper(new AvroFactory());
|
||||
AvroSchemaGenerator gen = new AvroSchemaGenerator();
|
||||
try {
|
||||
mapper.acceptJsonFormatVisitor(targetClass, gen);
|
||||
}
|
||||
catch (JsonMappingException e) {
|
||||
fail("Error while setting acceptJsonFormatVisitor {}", e);
|
||||
}
|
||||
return mapper.readerFor(targetClass)
|
||||
.with(new AvroSchema(readerSchema))
|
||||
.readValue(payload);
|
||||
}
|
||||
};
|
||||
|
||||
FoodOrder foodOrder1 = new FoodOrder();
|
||||
foodOrder1.setRestaurant("Spring Kitchen");
|
||||
foodOrder1.setOrderDescription("avro makhani");
|
||||
foodOrder1.setCustomerAddress("world wide web");
|
||||
FoodOrder foodOrder2 = new FoodOrder();
|
||||
foodOrder2.setRestaurant("Spring Kitchen");
|
||||
|
||||
Schema schema = manager.getSchema(FoodOrder.class);
|
||||
AvroMapper mapper = new AvroMapper();
|
||||
byte[] payload1 = mapper.writer(new AvroSchema(schema)).writeValueAsBytes(foodOrder1);
|
||||
byte[] payload2 = mapper.writer(new AvroSchema(schema)).writeValueAsBytes(foodOrder2);
|
||||
foodOrder1 = (FoodOrder) manager.readData(foodOrder1.getClass(), payload1, schema, schema);
|
||||
foodOrder2 = (FoodOrder) manager.readData(foodOrder1.getClass(), payload2, schema, schema);
|
||||
assertThat(foodOrder2.getOrderDescription()).isNull();
|
||||
assertThat(foodOrder2.getCustomerAddress()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvroSchemaMessageConverter() {
|
||||
AvroSchemaMessageConverter converter = new AvroSchemaMessageConverter();
|
||||
MimeType mimeType = new MimeType("application", "avro");
|
||||
assertThat(mimeType).isEqualTo(converter.getSupportedMimeTypes().get(0));
|
||||
|
||||
AvroSchemaMessageConverter converter2 = new AvroSchemaMessageConverter(mimeType);
|
||||
assertThat(mimeType).isEqualTo(converter2.getSupportedMimeTypes().get(0));
|
||||
|
||||
AvroSchemaMessageConverter converter3 =
|
||||
new AvroSchemaMessageConverter(Lists.newArrayList(mimeType));
|
||||
assertThat(mimeType).isEqualTo(converter3.getSupportedMimeTypes().get(0));
|
||||
|
||||
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
|
||||
AvroSchemaMessageConverter converter4 = new AvroSchemaMessageConverter(manager);
|
||||
assertThat(mimeType).isEqualTo(converter4.getSupportedMimeTypes().get(0));
|
||||
|
||||
AvroSchemaMessageConverter converter5 =
|
||||
new AvroSchemaMessageConverter(Lists.newArrayList(mimeType), manager);
|
||||
Schema schema = manager.getSchema(FoodOrder.class);
|
||||
converter5.setSchema(schema);
|
||||
assertThat(mimeType).isEqualTo(converter5.getSupportedMimeTypes().get(0));
|
||||
assertThat(schema).isEqualTo(converter5.getSchema());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvroSchemaMessageConverterException() {
|
||||
assertThatThrownBy(() -> {
|
||||
MimeType mimeType = new MimeType("application", "avro");
|
||||
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
|
||||
AvroSchemaMessageConverter converter =
|
||||
new AvroSchemaMessageConverter(Lists.newArrayList(mimeType), manager);
|
||||
converter.setSchemaLocation(new ByteArrayResource(new byte[2]) {
|
||||
});
|
||||
}).isInstanceOf(SchemaParseException.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.avro;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaNotFoundException;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaReference;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaRegistrationResponse;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaRegistryClientMessageConverter;
|
||||
import org.springframework.cloud.stream.schema.registry.client.SchemaRegistryClient;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
public class StubSchemaRegistryClient implements SchemaRegistryClient {
|
||||
|
||||
private final AtomicInteger index = new AtomicInteger(0);
|
||||
|
||||
private final Map<Integer, String> schemasById = new HashMap<>();
|
||||
|
||||
private final Map<String, Map<Integer, SchemaWithId>> storedSchemas = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public SchemaRegistrationResponse register(String subject, String format,
|
||||
String schema) {
|
||||
if (!this.storedSchemas.containsKey(subject)) {
|
||||
this.storedSchemas.put(subject, new TreeMap<Integer, SchemaWithId>());
|
||||
}
|
||||
Map<Integer, SchemaWithId> schemaVersions = this.storedSchemas.get(subject);
|
||||
for (Map.Entry<Integer, SchemaWithId> integerSchemaEntry : schemaVersions
|
||||
.entrySet()) {
|
||||
|
||||
if (integerSchemaEntry.getValue().getSchema().equals(schema)) {
|
||||
SchemaRegistrationResponse schemaRegistrationResponse = new SchemaRegistrationResponse();
|
||||
schemaRegistrationResponse.setId(integerSchemaEntry.getValue().getId());
|
||||
schemaRegistrationResponse.setSchemaReference(
|
||||
new SchemaReference(subject, integerSchemaEntry.getKey(),
|
||||
AvroSchemaRegistryClientMessageConverter.AVRO_FORMAT));
|
||||
return schemaRegistrationResponse;
|
||||
}
|
||||
}
|
||||
int nextVersion = schemaVersions.size() + 1;
|
||||
int id = this.index.incrementAndGet();
|
||||
schemaVersions.put(nextVersion, new SchemaWithId(id, schema));
|
||||
SchemaRegistrationResponse schemaRegistrationResponse = new SchemaRegistrationResponse();
|
||||
schemaRegistrationResponse.setId(this.index.getAndIncrement());
|
||||
schemaRegistrationResponse.setSchemaReference(new SchemaReference(subject,
|
||||
nextVersion, AvroSchemaRegistryClientMessageConverter.AVRO_FORMAT));
|
||||
this.schemasById.put(id, schema);
|
||||
return schemaRegistrationResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fetch(SchemaReference schemaReference) {
|
||||
if (!AvroSchemaRegistryClientMessageConverter.AVRO_FORMAT
|
||||
.equals(schemaReference.getFormat())) {
|
||||
throw new IllegalArgumentException("Only 'avro' is supported by this client");
|
||||
}
|
||||
if (!this.storedSchemas.containsKey(schemaReference.getSubject())) {
|
||||
throw new SchemaNotFoundException("Not found: " + schemaReference);
|
||||
}
|
||||
if (!this.storedSchemas.get(schemaReference.getSubject())
|
||||
.containsKey(schemaReference.getVersion())) {
|
||||
throw new SchemaNotFoundException("Not found: " + schemaReference);
|
||||
}
|
||||
return this.storedSchemas.get(schemaReference.getSubject())
|
||||
.get(schemaReference.getVersion()).getSchema();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fetch(int id) {
|
||||
return this.schemasById.get(id);
|
||||
}
|
||||
|
||||
static class SchemaWithId {
|
||||
|
||||
int id;
|
||||
|
||||
String schema;
|
||||
|
||||
SchemaWithId(int id, String schema) {
|
||||
this.id = id;
|
||||
this.schema = schema;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getSchema() {
|
||||
return this.schema;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.avro;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.cloud.stream.function.StreamBridge;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaMessageConverter;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaServiceManager;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaServiceManagerImpl;
|
||||
import org.springframework.cloud.stream.schema.registry.client.SchemaRegistryClient;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author David Kalosi
|
||||
* @author José A. Íñigo
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class SubjectNamingStrategyTest {
|
||||
|
||||
private static StubSchemaRegistryClient stubSchemaRegistryClient = new StubSchemaRegistryClient();
|
||||
|
||||
@Test
|
||||
public void testQualifiedSubjectNamingStrategy() throws Exception {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(AvroSourceApplication.class))
|
||||
.web(WebApplicationType.NONE).run("--server.port=0",
|
||||
"--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.bindings.myBinding-out-0.contentType=application/*+avro",
|
||||
"--spring.cloud.stream.schema.avro.subjectNamingStrategy="
|
||||
+ "org.springframework.cloud.schema.registry.avro.QualifiedSubjectNamingStrategy",
|
||||
"--spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled=true")) {
|
||||
StreamBridge streamBridge = context.getBean(StreamBridge.class);
|
||||
|
||||
User1 user1 = new User1();
|
||||
user1.setFavoriteColor("foo" + UUID.randomUUID());
|
||||
user1.setName("foo" + UUID.randomUUID());
|
||||
|
||||
streamBridge.send("myBinding-out-0", user1);
|
||||
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> result = output.receive();
|
||||
|
||||
final MessageConverter userMessageConverter = (MessageConverter) context.getBean("userMessageConverter");
|
||||
final User1 receivedUser1 = (User1) userMessageConverter.fromMessage(result, User1.class);
|
||||
|
||||
assertThat(receivedUser1).isNotNull();
|
||||
assertThat(receivedUser1).isNotSameAs(user1);
|
||||
assertThat(receivedUser1.getFavoriteColor()).isEqualTo(user1.getFavoriteColor());
|
||||
assertThat(receivedUser1.getName()).isEqualTo(user1.getName());
|
||||
|
||||
// assertThat(result.getHeaders().get("contentType")).isEqualTo(MimeType.valueOf(
|
||||
// "application/vnd.org.springframework.cloud.schema.avro.User1.v1+avro"));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
public static class AvroSourceApplication {
|
||||
|
||||
private Resource schemaLocation;
|
||||
|
||||
@Bean
|
||||
public SchemaRegistryClient schemaRegistryClient() {
|
||||
return stubSchemaRegistryClient;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MessageConverter userMessageConverter() throws IOException {
|
||||
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
|
||||
AvroSchemaMessageConverter avroSchemaMessageConverter = new AvroSchemaMessageConverter(
|
||||
MimeType.valueOf("application/avro"), manager);
|
||||
if (this.schemaLocation != null) {
|
||||
avroSchemaMessageConverter.setSchemaLocation(this.schemaLocation);
|
||||
}
|
||||
return avroSchemaMessageConverter;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.avro;
|
||||
|
||||
import org.apache.avro.reflect.Nullable;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
public class User1 {
|
||||
|
||||
@Nullable
|
||||
private String name;
|
||||
|
||||
private int favoriteNumber;
|
||||
|
||||
@Nullable
|
||||
private String favoriteColor;
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getFavoriteNumber() {
|
||||
return this.favoriteNumber;
|
||||
}
|
||||
|
||||
public void setFavoriteNumber(int favoriteNumber) {
|
||||
this.favoriteNumber = favoriteNumber;
|
||||
}
|
||||
|
||||
public String getFavoriteColor() {
|
||||
return this.favoriteColor;
|
||||
}
|
||||
|
||||
public void setFavoriteColor(String favoriteColor) {
|
||||
this.favoriteColor = favoriteColor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.avro;
|
||||
|
||||
import org.apache.avro.reflect.AvroDefault;
|
||||
import org.apache.avro.reflect.Nullable;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
public class User2 {
|
||||
|
||||
@Nullable
|
||||
private String name;
|
||||
|
||||
private int favoriteNumber;
|
||||
|
||||
@Nullable
|
||||
private String favoriteColor;
|
||||
|
||||
@AvroDefault("\"NYC\"")
|
||||
private String favoritePlace = "Boston";
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getFavoriteNumber() {
|
||||
return this.favoriteNumber;
|
||||
}
|
||||
|
||||
public void setFavoriteNumber(int favoriteNumber) {
|
||||
this.favoriteNumber = favoriteNumber;
|
||||
}
|
||||
|
||||
public String getFavoriteColor() {
|
||||
return this.favoriteColor;
|
||||
}
|
||||
|
||||
public void setFavoriteColor(String favoriteColor) {
|
||||
this.favoriteColor = favoriteColor;
|
||||
}
|
||||
|
||||
public String getFavoritePlace() {
|
||||
return this.favoritePlace;
|
||||
}
|
||||
|
||||
public void setFavoritePlace(String favoritePlace) {
|
||||
this.favoritePlace = favoritePlace;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.avro.client;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaNotFoundException;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaReference;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaRegistrationResponse;
|
||||
import org.springframework.cloud.stream.schema.registry.client.ConfluentSchemaRegistryClient;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withBadRequest;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
* @author TengZhou Dong
|
||||
*/
|
||||
public class ConfluentSchemaRegistryClientTests {
|
||||
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
private MockRestServiceServer mockRestServiceServer;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.restTemplate = new RestTemplate();
|
||||
this.mockRestServiceServer = MockRestServiceServer
|
||||
.createServer(this.restTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerSchema() throws Exception {
|
||||
this.mockRestServiceServer
|
||||
.expect(requestTo("http://localhost:8081/subjects/user/versions"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(header("Content-Type", "application/json"))
|
||||
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
|
||||
.andRespond(withSuccess("{\"id\":101,\"version\":1}", MediaType.APPLICATION_JSON));
|
||||
|
||||
this.mockRestServiceServer
|
||||
.expect(requestTo("http://localhost:8081/subjects/user/versions"))
|
||||
.andExpect(method(HttpMethod.GET))
|
||||
.andRespond((withSuccess("[1]", MediaType.APPLICATION_JSON)));
|
||||
|
||||
ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(
|
||||
this.restTemplate);
|
||||
SchemaRegistrationResponse response = client.register("user", "avro", "{}");
|
||||
assertThat(response.getSchemaReference().getVersion()).isEqualTo(1);
|
||||
assertThat(response.getId()).isEqualTo(101);
|
||||
this.mockRestServiceServer.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerWithInvalidJson() {
|
||||
assertThatThrownBy(() -> {
|
||||
this.mockRestServiceServer
|
||||
.expect(requestTo("http://localhost:8081/subjects/user/versions"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(header("Content-Type", "application/json"))
|
||||
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
|
||||
.andRespond(withBadRequest());
|
||||
ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(
|
||||
this.restTemplate);
|
||||
SchemaRegistrationResponse response = client.register("user", "avro", "<>");
|
||||
}).isInstanceOf(RuntimeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerIncompatibleSchema() {
|
||||
this.mockRestServiceServer
|
||||
.expect(requestTo("http://localhost:8081/subjects/user/versions"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(header("Content-Type", "application/json"))
|
||||
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
|
||||
.andRespond(withStatus(HttpStatus.CONFLICT));
|
||||
ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(
|
||||
this.restTemplate);
|
||||
Exception expected = null;
|
||||
try {
|
||||
SchemaRegistrationResponse response = client.register("user", "avro", "{}");
|
||||
}
|
||||
catch (Exception e) {
|
||||
expected = e;
|
||||
}
|
||||
assertThat(expected instanceof RuntimeException).isTrue();
|
||||
assertThat(expected.getCause() instanceof HttpStatusCodeException).isTrue();
|
||||
this.mockRestServiceServer.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByReference() {
|
||||
this.mockRestServiceServer
|
||||
.expect(requestTo("http://localhost:8081/subjects/user/versions/1"))
|
||||
.andExpect(method(HttpMethod.GET))
|
||||
.andExpect(
|
||||
header("Content-Type", "application/vnd.schemaregistry.v1+json"))
|
||||
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
|
||||
.andRespond(withSuccess("{\"schema\":\"\"}", MediaType.APPLICATION_JSON));
|
||||
ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(
|
||||
this.restTemplate);
|
||||
SchemaReference reference = new SchemaReference("user", 1, "avro");
|
||||
String schema = client.fetch(reference);
|
||||
assertThat(schema).isEqualTo("");
|
||||
this.mockRestServiceServer.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void schemaNotFound() {
|
||||
assertThatThrownBy(() -> {
|
||||
this.mockRestServiceServer
|
||||
.expect(requestTo("http://localhost:8081/subjects/user/versions/1"))
|
||||
.andExpect(method(HttpMethod.GET))
|
||||
.andExpect(
|
||||
header("Content-Type", "application/vnd.schemaregistry.v1+json"))
|
||||
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
|
||||
.andRespond(withStatus(HttpStatus.NOT_FOUND));
|
||||
ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(
|
||||
this.restTemplate);
|
||||
SchemaReference reference = new SchemaReference("user", 1, "avro");
|
||||
client.fetch(reference);
|
||||
}).isInstanceOf(SchemaNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void responseErrorFetch() {
|
||||
this.mockRestServiceServer
|
||||
.expect(requestTo("http://localhost:8081/subjects/user/versions"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(header("Content-Type", "application/json"))
|
||||
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
|
||||
.andRespond(withBadRequest());
|
||||
|
||||
ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(
|
||||
this.restTemplate);
|
||||
Exception expected = null;
|
||||
try {
|
||||
SchemaRegistrationResponse response = client.register("user", "avro", "{}");
|
||||
}
|
||||
catch (Exception e) {
|
||||
expected = e;
|
||||
}
|
||||
assertThat(expected != null).isTrue();
|
||||
assertThat(expected.getCause() instanceof HttpStatusCodeException).isTrue();
|
||||
this.mockRestServiceServer.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fetchById() {
|
||||
this.mockRestServiceServer
|
||||
.expect(requestTo("http://localhost:8081/schemas/ids/1"))
|
||||
.andExpect(method(HttpMethod.GET))
|
||||
.andExpect(
|
||||
header("Content-Type", "application/vnd.schemaregistry.v1+json"))
|
||||
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
|
||||
.andRespond(withSuccess("{\"schema\":\"\"}", MediaType.APPLICATION_JSON));
|
||||
ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(
|
||||
this.restTemplate);
|
||||
String schema = client.fetch(1);
|
||||
assertThat(schema).isEqualTo("");
|
||||
this.mockRestServiceServer.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fetchByIdSchemaNotFound() {
|
||||
assertThatThrownBy(() -> {
|
||||
this.mockRestServiceServer
|
||||
.expect(requestTo("http://localhost:8081/schemas/ids/1"))
|
||||
.andExpect(method(HttpMethod.GET))
|
||||
.andExpect(
|
||||
header("Content-Type", "application/vnd.schemaregistry.v1+json"))
|
||||
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
|
||||
.andRespond(withStatus(HttpStatus.NOT_FOUND));
|
||||
ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(
|
||||
this.restTemplate);
|
||||
client.fetch(1);
|
||||
}).isInstanceOf(SchemaNotFoundException.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.avro.domain;
|
||||
|
||||
/**
|
||||
* @author Ish Mahajan
|
||||
*/
|
||||
public class FoodOrder {
|
||||
private String restaurant;
|
||||
private String customerAddress;
|
||||
private String orderDescription;
|
||||
public String getRestaurant() {
|
||||
return restaurant;
|
||||
}
|
||||
public void setRestaurant(String restaurant) {
|
||||
this.restaurant = restaurant;
|
||||
}
|
||||
public String getCustomerAddress() {
|
||||
return customerAddress;
|
||||
}
|
||||
public void setCustomerAddress(String customerAddress) {
|
||||
this.customerAddress = customerAddress;
|
||||
}
|
||||
public String getOrderDescription() {
|
||||
return orderDescription;
|
||||
}
|
||||
public void setOrderDescription(String orderDescription) {
|
||||
this.orderDescription = orderDescription;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.avro.v2;
|
||||
|
||||
import org.apache.avro.reflect.AvroDefault;
|
||||
import org.apache.avro.reflect.Nullable;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
public class User1 {
|
||||
|
||||
@Nullable
|
||||
private String name;
|
||||
|
||||
private int favoriteNumber;
|
||||
|
||||
@Nullable
|
||||
private String favoriteColor;
|
||||
|
||||
@AvroDefault("\"NYC\"")
|
||||
private String favoritePlace = "Boston";
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getFavoriteNumber() {
|
||||
return this.favoriteNumber;
|
||||
}
|
||||
|
||||
public void setFavoriteNumber(int favoriteNumber) {
|
||||
this.favoriteNumber = favoriteNumber;
|
||||
}
|
||||
|
||||
public String getFavoriteColor() {
|
||||
return this.favoriteColor;
|
||||
}
|
||||
|
||||
public void setFavoriteColor(String favoriteColor) {
|
||||
this.favoriteColor = favoriteColor;
|
||||
}
|
||||
|
||||
public String getFavoritePlace() {
|
||||
return this.favoritePlace;
|
||||
}
|
||||
|
||||
public void setFavoritePlace(String favoritePlace) {
|
||||
this.favoritePlace = favoritePlace;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright 2017-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.serialization;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import example.avro.Command;
|
||||
import example.avro.Email;
|
||||
import example.avro.PushNotification;
|
||||
import example.avro.Sms;
|
||||
import example.avro.User;
|
||||
import org.apache.avro.Schema;
|
||||
import org.apache.avro.generic.GenericData;
|
||||
import org.apache.avro.generic.GenericRecord;
|
||||
import org.apache.avro.io.DatumWriter;
|
||||
import org.apache.avro.io.Encoder;
|
||||
import org.apache.avro.io.EncoderFactory;
|
||||
import org.apache.avro.specific.SpecificDatumWriter;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.cache.support.NoOpCacheManager;
|
||||
import org.springframework.cloud.stream.schema.registry.EnableSchemaRegistryServer;
|
||||
import org.springframework.cloud.stream.schema.registry.SchemaReference;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaRegistryClientMessageConverter;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaServiceManager;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaServiceManagerImpl;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.DefaultSubjectNamingStrategy;
|
||||
import org.springframework.cloud.stream.schema.registry.client.DefaultSchemaRegistryClient;
|
||||
import org.springframework.cloud.stream.schema.registry.client.SchemaRegistryClient;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.integration.support.MutableMessageHeaders;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
* @author Sercan Karaoglu
|
||||
*/
|
||||
public class AvroMessageConverterSerializationTests {
|
||||
|
||||
Pattern versionedSchema = Pattern.compile(
|
||||
"application/" + "vnd" + "\\.([\\p{Alnum}\\$\\.]+)\\.v(\\p{Digit}+)\\+avro");
|
||||
|
||||
Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private ConfigurableApplicationContext schemaRegistryServerContext;
|
||||
|
||||
private RestTemplateBuilder restTemplateBuilder;
|
||||
|
||||
public static Command notification() {
|
||||
Command messageToSend = getCommandToSend();
|
||||
messageToSend.setType("notification");
|
||||
PushNotification pushNotification = new PushNotification();
|
||||
pushNotification.setArn("google");
|
||||
pushNotification.setText("hello");
|
||||
messageToSend.setPayload(pushNotification);
|
||||
return messageToSend;
|
||||
}
|
||||
|
||||
public static Command sms() {
|
||||
Command messageToSend = getCommandToSend();
|
||||
messageToSend.setType("sms");
|
||||
Sms sms = new Sms();
|
||||
sms.setPhoneNumber("6141231212");
|
||||
sms.setText("hello");
|
||||
messageToSend.setPayload(sms);
|
||||
return messageToSend;
|
||||
}
|
||||
|
||||
public static Command email() {
|
||||
Command messageToSend = getCommandToSend();
|
||||
messageToSend.setType("email");
|
||||
Email email = new Email();
|
||||
email.setAddressTo("sercan");
|
||||
email.setText("hello");
|
||||
email.setTitle("hi");
|
||||
messageToSend.setPayload(email);
|
||||
return messageToSend;
|
||||
}
|
||||
|
||||
public static Command getCommandToSend() {
|
||||
Command messageToSend = new Command();
|
||||
messageToSend.setCorrelationId("abc");
|
||||
return messageToSend;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.schemaRegistryServerContext = SpringApplication.run(
|
||||
ServerApplication.class, "--spring.main.allow-bean-definition-overriding=true");
|
||||
restTemplateBuilder = this.schemaRegistryServerContext.getBean(RestTemplateBuilder.class);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
this.schemaRegistryServerContext.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaImport() throws Exception {
|
||||
SchemaRegistryClient client = new DefaultSchemaRegistryClient(restTemplateBuilder);
|
||||
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
|
||||
AvroSchemaRegistryClientMessageConverter converter = new AvroSchemaRegistryClientMessageConverter(
|
||||
client, new NoOpCacheManager(), manager);
|
||||
converter.setSubjectNamingStrategy(new DefaultSubjectNamingStrategy());
|
||||
converter.setDynamicSchemaGenerationEnabled(false);
|
||||
converter.setSchemaLocations(this.schemaRegistryServerContext
|
||||
.getResources("classpath:schemas/Command.avsc"));
|
||||
converter.setSchemaImports(this.schemaRegistryServerContext
|
||||
.getResources("classpath:schemas/imports/*.avsc"));
|
||||
converter.afterPropertiesSet();
|
||||
Command notification = notification();
|
||||
Message specificMessage = converter.toMessage(notification,
|
||||
new MutableMessageHeaders(Collections.<String, Object>emptyMap()));
|
||||
Object o = converter.fromMessage(specificMessage, Command.class);
|
||||
|
||||
assertThat(o).isEqualTo(notification)
|
||||
.as("Serialization issue when use schema-imports");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceWriteSameVersion() throws Exception {
|
||||
User specificRecord = new User();
|
||||
specificRecord.setName("joe");
|
||||
Schema v1 = new Schema.Parser().parse(AvroMessageConverterSerializationTests.class
|
||||
.getClassLoader().getResourceAsStream("schemas/user.avsc"));
|
||||
GenericRecord genericRecord = new GenericData.Record(v1);
|
||||
genericRecord.put("name", "joe");
|
||||
SchemaRegistryClient client = new DefaultSchemaRegistryClient(restTemplateBuilder);
|
||||
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
|
||||
AvroSchemaRegistryClientMessageConverter converter = new AvroSchemaRegistryClientMessageConverter(
|
||||
client, new NoOpCacheManager(), manager);
|
||||
|
||||
converter.setSubjectNamingStrategy(new DefaultSubjectNamingStrategy());
|
||||
converter.setDynamicSchemaGenerationEnabled(false);
|
||||
converter.afterPropertiesSet();
|
||||
|
||||
Message specificMessage = converter.toMessage(specificRecord,
|
||||
new MutableMessageHeaders(Collections.<String, Object>emptyMap()),
|
||||
MimeTypeUtils.parseMimeType("application/*+avro"));
|
||||
SchemaReference specificRef = extractSchemaReference(MimeTypeUtils.parseMimeType(
|
||||
specificMessage.getHeaders().get("contentType").toString()));
|
||||
|
||||
Message genericMessage = converter.toMessage(genericRecord,
|
||||
new MutableMessageHeaders(Collections.<String, Object>emptyMap()),
|
||||
MimeTypeUtils.parseMimeType("application/*+avro"));
|
||||
SchemaReference genericRef = extractSchemaReference(MimeTypeUtils.parseMimeType(
|
||||
genericMessage.getHeaders().get("contentType").toString()));
|
||||
|
||||
assertThat(specificRef).isEqualTo(genericRef);
|
||||
assertThat(genericRef.getVersion()).isEqualTo(1);
|
||||
}
|
||||
|
||||
public void testOriginalContentTypeHeaderOnly() throws Exception {
|
||||
User specificRecord = new User();
|
||||
specificRecord.setName("joe");
|
||||
Schema v1 = new Schema.Parser().parse(AvroMessageConverterSerializationTests.class
|
||||
.getClassLoader().getResourceAsStream("schemas/user.avsc"));
|
||||
GenericRecord genericRecord = new GenericData.Record(v1);
|
||||
genericRecord.put("name", "joe");
|
||||
SchemaRegistryClient client = new DefaultSchemaRegistryClient(restTemplateBuilder);
|
||||
client.register("user", "avro", v1.toString());
|
||||
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
|
||||
AvroSchemaRegistryClientMessageConverter converter = new AvroSchemaRegistryClientMessageConverter(
|
||||
client, new NoOpCacheManager(), manager);
|
||||
converter.setDynamicSchemaGenerationEnabled(false);
|
||||
converter.afterPropertiesSet();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
DatumWriter<User> writer = new SpecificDatumWriter<>(User.class);
|
||||
Encoder encoder = EncoderFactory.get().binaryEncoder(baos, null);
|
||||
writer.write(specificRecord, encoder);
|
||||
encoder.flush();
|
||||
Message source = MessageBuilder.withPayload(baos.toByteArray())
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE,
|
||||
MimeTypeUtils.APPLICATION_OCTET_STREAM)
|
||||
.build();
|
||||
Object converted = converter.fromMessage(source, User.class);
|
||||
assertThat(converted).isNotNull();
|
||||
assertThat(specificRecord.getName().toString())
|
||||
.isEqualTo(((User) converted).getName().toString());
|
||||
}
|
||||
|
||||
|
||||
private SchemaReference extractSchemaReference(MimeType mimeType) {
|
||||
SchemaReference schemaReference = null;
|
||||
Matcher schemaMatcher = this.versionedSchema.matcher(mimeType.toString());
|
||||
if (schemaMatcher.find()) {
|
||||
String subject = schemaMatcher.group(1);
|
||||
Integer version = Integer.parseInt(schemaMatcher.group(2));
|
||||
schemaReference = new SchemaReference(subject, version,
|
||||
AvroSchemaRegistryClientMessageConverter.AVRO_FORMAT);
|
||||
}
|
||||
return schemaReference;
|
||||
}
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableSchemaRegistryServer
|
||||
public static class ServerApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ServerApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright 2016-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.serialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import example.avro.Command;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.support.NoOpCache;
|
||||
import org.springframework.cache.support.NoOpCacheManager;
|
||||
import org.springframework.cloud.stream.binder.test.OutputDestination;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.cloud.stream.function.StreamBridge;
|
||||
import org.springframework.cloud.stream.schema.avro.User1;
|
||||
import org.springframework.cloud.stream.schema.registry.EnableSchemaRegistryServer;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaMessageConverter;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaRegistryClientMessageConverter;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaServiceManager;
|
||||
import org.springframework.cloud.stream.schema.registry.avro.AvroSchemaServiceManagerImpl;
|
||||
import org.springframework.cloud.stream.schema.registry.client.DefaultSchemaRegistryClient;
|
||||
import org.springframework.cloud.stream.schema.registry.client.EnableSchemaRegistryClient;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.cloud.stream.schema.serialization.AvroMessageConverterSerializationTests.notification;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Sercan Karaoglu
|
||||
* @author James Gee
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class AvroSchemaRegistryClientMessageConverterTests {
|
||||
|
||||
|
||||
private ConfigurableApplicationContext schemaRegistryServerContext;
|
||||
private RestTemplateBuilder restTemplateBuilder;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.schemaRegistryServerContext = SpringApplication.run(
|
||||
ServerApplication.class, "--spring.main.allow-bean-definition-overriding=true");
|
||||
this.restTemplateBuilder = this.schemaRegistryServerContext.getBean(RestTemplateBuilder.class);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
this.schemaRegistryServerContext.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendMessage() throws Exception {
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(AvroSourceApplication.class))
|
||||
.web(WebApplicationType.NONE).run("--server.port=0",
|
||||
"--spring.jmx.enabled=false",
|
||||
"--spring.cloud.stream.bindings.myBinding-out-0.contentType=application/*+avro",
|
||||
"--spring.cloud.schema.avro.dynamicSchemaGenerationEnabled=true")) {
|
||||
StreamBridge streamBridge = context.getBean(StreamBridge.class);
|
||||
|
||||
User1 user1 = new User1();
|
||||
user1.setFavoriteColor("foo" + UUID.randomUUID());
|
||||
user1.setName("foo" + UUID.randomUUID());
|
||||
|
||||
streamBridge.send("myBinding-out-0", user1);
|
||||
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> result = output.receive();
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
|
||||
final MessageConverter userMessageConverter = (MessageConverter) context.getBean("userMessageConverter");
|
||||
final User1 receivedUser1 = (User1) userMessageConverter.fromMessage(result, User1.class);
|
||||
|
||||
assertThat(receivedUser1).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaImportConfiguration() throws Exception {
|
||||
|
||||
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
TestChannelBinderConfiguration.getCompleteConfiguration(AvroSourceApplication.class))
|
||||
.web(WebApplicationType.NONE).run("--server.port=0", "--spring.jmx.enabled=false",
|
||||
"--spring.cloud.schema.avro.dynamicSchemaGenerationEnabled=true",
|
||||
"--spring.cloud.stream.bindings.foo-out-0.contentType=application/*+avro",
|
||||
"--spring.cloud.stream.bindings.output.destination=test",
|
||||
"--spring.cloud.stream.bindings.schema-registry-client.endpoint=http://localhost:8990",
|
||||
"--spring.cloud.schema.avro.schema-locations=classpath:schemas/Command.avsc",
|
||||
"--spring.cloud.schema.avro.schema-imports=classpath:schemas/imports/Sms.avsc,"
|
||||
+ " classpath:schemas/imports/Email.avsc, classpath:schemas/imports/PushNotification.avsc")) {
|
||||
StreamBridge streamBridge = context.getBean(StreamBridge.class);
|
||||
|
||||
final Command notification = notification();
|
||||
streamBridge.send("foo-out-0", notification);
|
||||
|
||||
OutputDestination output = context.getBean(OutputDestination.class);
|
||||
Message<byte[]> result = output.receive();
|
||||
|
||||
final MessageConverter cmdConverter = (MessageConverter) context.getBean("userMessageConverter");
|
||||
final Command command = (Command) cmdConverter.fromMessage(result, Command.class);
|
||||
|
||||
assertThat(command).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoCacheConfiguration() {
|
||||
ConfigurableApplicationContext sourceContext = SpringApplication
|
||||
.run(NoCacheConfiguration.class, "--spring.main.web-environment=false");
|
||||
AvroSchemaRegistryClientMessageConverter converter = sourceContext
|
||||
.getBean(AvroSchemaRegistryClientMessageConverter.class);
|
||||
DirectFieldAccessor accessor = new DirectFieldAccessor(converter);
|
||||
assertThat(accessor.getPropertyValue("cacheManager")).isInstanceOf(NoOpCacheManager.class);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNamedCacheIsRequested() {
|
||||
CacheManager mockCache = Mockito.mock(CacheManager.class);
|
||||
when(mockCache.getCache(any())).thenReturn(new NoOpCache(""));
|
||||
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
|
||||
AvroSchemaRegistryClientMessageConverter converter = new AvroSchemaRegistryClientMessageConverter(
|
||||
new DefaultSchemaRegistryClient(restTemplateBuilder), mockCache, manager);
|
||||
ReflectionTestUtils.invokeMethod(converter, "getCache", "TEST_CACHE");
|
||||
verify(mockCache).getCache("TEST_CACHE");
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@EnableSchemaRegistryClient
|
||||
public static class AvroSourceApplication {
|
||||
|
||||
private Resource schemaLocation;
|
||||
|
||||
@Bean
|
||||
public MessageConverter userMessageConverter() throws IOException {
|
||||
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
|
||||
AvroSchemaMessageConverter avroSchemaMessageConverter = new AvroSchemaMessageConverter(
|
||||
MimeType.valueOf("application/avro"), manager);
|
||||
if (this.schemaLocation != null) {
|
||||
avroSchemaMessageConverter.setSchemaLocation(this.schemaLocation);
|
||||
}
|
||||
return avroSchemaMessageConverter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
public static class NoCacheConfiguration {
|
||||
|
||||
@Bean
|
||||
AvroSchemaRegistryClientMessageConverter avroSchemaRegistryClientMessageConverter() {
|
||||
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
|
||||
return new AvroSchemaRegistryClientMessageConverter(
|
||||
new DefaultSchemaRegistryClient(new RestTemplate()), new NoOpCacheManager(), manager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ServletWebServerFactory servletWebServerFactory() {
|
||||
return new TomcatServletWebServerFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableSchemaRegistryServer
|
||||
public static class ServerApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AvroMessageConverterSerializationTests.ServerApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"namespace":"example.avro",
|
||||
"name":"Command",
|
||||
"type":"record",
|
||||
"fields":[
|
||||
{
|
||||
"name":"type",
|
||||
"type":"string"
|
||||
},
|
||||
{
|
||||
"name":"correlationId",
|
||||
"type":"string"
|
||||
},
|
||||
{
|
||||
"name":"payload",
|
||||
"type":["Sms", "Email", "PushNotification"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"namespace":"example.avro",
|
||||
"name": "Email",
|
||||
"type": "record",
|
||||
"fields":[
|
||||
{
|
||||
"name":"addressTo",
|
||||
"type":"string"
|
||||
},
|
||||
{
|
||||
"name":"title",
|
||||
"type":"string"
|
||||
},
|
||||
{
|
||||
"name":"text",
|
||||
"type":"string"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"namespace":"example.avro",
|
||||
"name": "PushNotification",
|
||||
"type": "record",
|
||||
"fields":[
|
||||
{
|
||||
"name":"arn",
|
||||
"type":"string"
|
||||
},
|
||||
{
|
||||
"name":"text",
|
||||
"type":"string"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"namespace":"example.avro",
|
||||
"name": "Sms",
|
||||
"type": "record",
|
||||
"fields":[
|
||||
{
|
||||
"name":"phoneNumber",
|
||||
"type":"string"
|
||||
},{
|
||||
"name":"text",
|
||||
"type":"string"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"namespace":"org.springframework.cloud.stream.samples",
|
||||
"name": "Status",
|
||||
"type" : "record",
|
||||
"fields": [
|
||||
{"name": "id", "type": "string"},
|
||||
{"name": "text", "type": "string"},
|
||||
{"name": "timestamp", "type": "long"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{"namespace": "example.avro",
|
||||
"type": "record",
|
||||
"name": "User",
|
||||
"fields": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "favoriteNumber", "type": ["int", "null"]},
|
||||
{"name": "favoriteColor", "type": ["string", "null"]}
|
||||
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{"namespace": "org.springframework.cloud.schema.avro",
|
||||
"type": "record",
|
||||
"name": "User1",
|
||||
"fields": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "favoriteNumber", "type": ["int", "null"]},
|
||||
{"name": "favoriteColor", "type": ["string", "null"]}
|
||||
|
||||
]
|
||||
},
|
||||
{"namespace": "org.springframework.cloud.schema.avro.v2",
|
||||
"type": "record",
|
||||
"name": "User1",
|
||||
"fields": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "favoriteNumber", "type": ["int", "null"]},
|
||||
{"name": "favoriteColor", "type": ["string", "null"]},
|
||||
{"name": "favoritePlace", "type": ["string","null"], "default" : "NYC"}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
{"namespace": "org.springframework.cloud.schema.avro",
|
||||
"type": "record",
|
||||
"name": "User1",
|
||||
"fields": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "favoriteNumber", "type": ["int", "null"]},
|
||||
{"name": "favoriteColor", "type": ["string", "null"]}
|
||||
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{"namespace": "org.springframework.cloud.schema.avro.v2",
|
||||
"type": "record",
|
||||
"name": "User1",
|
||||
"fields": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "favoriteNumber", "type": ["int", "null"]},
|
||||
{"name": "favoriteColor", "type": ["string", "null"]},
|
||||
{"name": "favoritePlace", "type": ["string","null"], "default" : "NYC"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{"namespace": "example.avro.v2",
|
||||
"type": "record",
|
||||
"name": "User",
|
||||
"fields": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "favoriteNumber", "type": ["int", "null"]},
|
||||
{"name": "favoriteColor", "type": ["string", "null"]},
|
||||
{"name": "favoritePlace", "type": ["string","null"], "default" : "NYC"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{"namespace": "example.avro",
|
||||
"type": "record",
|
||||
"name": "User",
|
||||
"fields": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "favoriteNumber", "type": ["int", "null"]},
|
||||
{"name": "favoriteColor", "type": ["string", "null"]}
|
||||
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{"namespace": "example.avro",
|
||||
"type": "record",
|
||||
"name": "User",
|
||||
"fields": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "favoriteNumber", "type": ["int", "null"]},
|
||||
{"name": "favoriteColor", "type": ["string", "null"]},
|
||||
{"name": "favoritePlace", "type": ["string","null"], "default" : "NYC"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>spring-cloud-stream-schema-registry-core</artifactId>
|
||||
|
||||
<parent>
|
||||
<artifactId>spring-cloud-stream-schema-registry</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.avro</groupId>
|
||||
<artifactId>avro</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.cloud.stream.schema.registry.config.SchemaServerConfiguration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* Enables the schema registry server enpoints.
|
||||
*
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Import(SchemaServerConfiguration.class)
|
||||
public @interface EnableSchemaRegistryServer {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.config;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScanPackages;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.stream.schema.registry.controllers.ServerController;
|
||||
import org.springframework.cloud.stream.schema.registry.model.Schema;
|
||||
import org.springframework.cloud.stream.schema.registry.repository.SchemaRepository;
|
||||
import org.springframework.cloud.stream.schema.registry.support.AvroSchemaValidator;
|
||||
import org.springframework.cloud.stream.schema.registry.support.SchemaValidator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
* @author Soby Chacko
|
||||
*/
|
||||
@Configuration
|
||||
@EnableJpaRepositories(basePackageClasses = SchemaRepository.class)
|
||||
@EnableConfigurationProperties(SchemaServerProperties.class)
|
||||
@Import(ServerController.class)
|
||||
public class SchemaServerConfiguration {
|
||||
|
||||
@Bean
|
||||
public static BeanFactoryPostProcessor entityScanPackagesPostProcessor() {
|
||||
return beanFactory -> {
|
||||
if (beanFactory instanceof BeanDefinitionRegistry) {
|
||||
EntityScanPackages.register((BeanDefinitionRegistry) beanFactory,
|
||||
Collections.singletonList(Schema.class.getPackage().getName()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Map<String, SchemaValidator> schemaValidators() {
|
||||
Map<String, SchemaValidator> validatorMap = new HashMap<>();
|
||||
validatorMap.put("avro", new AvroSchemaValidator());
|
||||
return validatorMap;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@ConfigurationProperties("spring.cloud.stream.schema.server")
|
||||
public class SchemaServerProperties {
|
||||
|
||||
/**
|
||||
* Prefix for configuration resource paths (default is empty). Useful when embedding
|
||||
* in another application when you don't want to change the context path or servlet
|
||||
* path.
|
||||
*/
|
||||
private String path;
|
||||
|
||||
/**
|
||||
* Boolean flag to enable/disable schema deletion.
|
||||
*/
|
||||
private boolean allowSchemaDeletion;
|
||||
|
||||
public String getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public boolean isAllowSchemaDeletion() {
|
||||
return this.allowSchemaDeletion;
|
||||
}
|
||||
|
||||
public void setAllowSchemaDeletion(boolean allowSchemaDeletion) {
|
||||
this.allowSchemaDeletion = allowSchemaDeletion;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.controllers;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.cloud.stream.schema.registry.config.SchemaServerProperties;
|
||||
import org.springframework.cloud.stream.schema.registry.model.Schema;
|
||||
import org.springframework.cloud.stream.schema.registry.repository.SchemaRepository;
|
||||
import org.springframework.cloud.stream.schema.registry.support.InvalidSchemaException;
|
||||
import org.springframework.cloud.stream.schema.registry.support.SchemaDeletionNotAllowedException;
|
||||
import org.springframework.cloud.stream.schema.registry.support.SchemaNotFoundException;
|
||||
import org.springframework.cloud.stream.schema.registry.support.SchemaValidator;
|
||||
import org.springframework.cloud.stream.schema.registry.support.UnsupportedFormatException;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @author Jeff Maxwell
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(path = "${spring.cloud.stream.schema.server.path:}")
|
||||
public class ServerController {
|
||||
|
||||
private final SchemaRepository repository;
|
||||
|
||||
private final Map<String, SchemaValidator> validators;
|
||||
|
||||
private final SchemaServerProperties schemaServerProperties;
|
||||
|
||||
public ServerController(SchemaRepository repository, Map<String, SchemaValidator> validators,
|
||||
SchemaServerProperties schemaServerProperties) {
|
||||
Assert.notNull(repository, "cannot be null");
|
||||
Assert.notEmpty(validators, "cannot be empty");
|
||||
this.repository = repository;
|
||||
this.validators = validators;
|
||||
this.schemaServerProperties = schemaServerProperties;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/", consumes = "application/json", produces = "application/json")
|
||||
public synchronized ResponseEntity<Schema> register(@RequestBody Schema schema, UriComponentsBuilder builder) {
|
||||
|
||||
SchemaValidator validator = this.validators.get(schema.getFormat());
|
||||
|
||||
if (validator == null) {
|
||||
throw new UnsupportedFormatException(String.format("Invalid format, supported types are: %s",
|
||||
StringUtils.collectionToCommaDelimitedString(this.validators.keySet())));
|
||||
}
|
||||
|
||||
validator.validate(schema.getDefinition());
|
||||
|
||||
Schema result;
|
||||
List<Schema> registeredEntities =
|
||||
this.repository.findBySubjectAndFormatOrderByVersion(schema.getSubject(), schema.getFormat());
|
||||
if (registeredEntities.isEmpty()) {
|
||||
schema.setVersion(1);
|
||||
result = this.repository.save(schema);
|
||||
}
|
||||
else {
|
||||
result = validator.match(registeredEntities, schema.getDefinition());
|
||||
if (result == null) {
|
||||
schema.setVersion(registeredEntities.get(registeredEntities.size() - 1).getVersion() + 1);
|
||||
result = this.repository.save(schema);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add(HttpHeaders.LOCATION, builder.path("/{subject}/{format}/v{version}")
|
||||
.buildAndExpand(result.getSubject(), result.getFormat(), result.getVersion())
|
||||
.toString());
|
||||
ResponseEntity<Schema> response = new ResponseEntity<>(result, headers, HttpStatus.CREATED);
|
||||
|
||||
return response;
|
||||
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, produces = "application/json", path = "/{subject}/{format}/v{version}")
|
||||
public ResponseEntity<Schema> findOne(@PathVariable("subject") String subject,
|
||||
@PathVariable("format") String format,
|
||||
@PathVariable("version") Integer version) {
|
||||
Schema schema = this.repository.findOneBySubjectAndFormatAndVersion(subject, format, version);
|
||||
if (schema == null) {
|
||||
throw new SchemaNotFoundException(
|
||||
String.format("Could not find Schema by subject: %s, format: %s, version %s",
|
||||
subject, format, version));
|
||||
}
|
||||
return new ResponseEntity<>(schema, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, produces = "application/json", path = "/schemas/{id}")
|
||||
public ResponseEntity<Schema> findOne(@PathVariable("id") Integer id) {
|
||||
Optional<Schema> schema = this.repository.findById(id);
|
||||
if (!schema.isPresent()) {
|
||||
throw new SchemaNotFoundException(String.format("Could not find Schema by id: %s", id));
|
||||
}
|
||||
return new ResponseEntity<>(schema.get(), HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Find by {@link Schema#getSubject() subject} and {@link Schema#getFormat() format}.
|
||||
*
|
||||
* @param subject the {@link Schema#getSubject() subject}, must not be
|
||||
* {@literal null}.
|
||||
* @param format the {@link Schema#getFormat() format}, must not be {@literal null}.
|
||||
* @return An {@link HttpStatus#OK} response populated with the list of {@link Schema
|
||||
* Schemas}, in ascending order by {@link Schema#getVersion() version}, that matched
|
||||
* the supplied {@link Schema#getSubject() subject} and {@link Schema#getFormat()
|
||||
* format}.
|
||||
* @deprecated use {@link #findBySubjectAndFormat(String, String)}
|
||||
* @see <a href=
|
||||
* "https://github.com/spring-cloud/spring-cloud-stream/issues/1760">GH-1760</a>
|
||||
*/
|
||||
@Deprecated
|
||||
public ResponseEntity<List<Schema>> findBySubjectAndVersion(@PathVariable("subject") String subject,
|
||||
@PathVariable("format") String format) {
|
||||
return findBySubjectAndFormatOrderByVersionAsc(subject, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find by {@link Schema#getSubject() subject} and {@link Schema#getFormat() format}.
|
||||
*
|
||||
* @param subject the {@link Schema#getSubject() subject}, must not be
|
||||
* {@literal null}.
|
||||
* @param format the {@link Schema#getFormat() format}, must not be {@literal null}.
|
||||
* @return An {@link HttpStatus#OK} response populated with the list of {@link Schema
|
||||
* Schemas}, in ascending order by {@link Schema#getVersion() version}, that matched
|
||||
* the supplied {@link Schema#getSubject() subject} and {@link Schema#getFormat()
|
||||
* format}.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@GetMapping(produces = APPLICATION_JSON_VALUE, path = "/{subject}/{format}")
|
||||
@NonNull
|
||||
public ResponseEntity<List<Schema>> findBySubjectAndFormat(@NonNull @PathVariable("subject") final String subject,
|
||||
@NonNull @PathVariable("format") final String format) {
|
||||
return findBySubjectAndFormatOrderByVersionAsc(subject, format);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/{subject}/{format}/v{version}", method = RequestMethod.DELETE)
|
||||
public void delete(@PathVariable("subject") String subject,
|
||||
@PathVariable("format") String format,
|
||||
@PathVariable("version") Integer version) {
|
||||
if (this.schemaServerProperties.isAllowSchemaDeletion()) {
|
||||
Schema schema = this.repository.findOneBySubjectAndFormatAndVersion(subject, format, version);
|
||||
if (schema == null) {
|
||||
throw new SchemaNotFoundException(
|
||||
String.format("Could not find Schema by subject: %s, format: %s, version %s",
|
||||
subject, format, version));
|
||||
}
|
||||
deleteSchema(schema);
|
||||
}
|
||||
else {
|
||||
throw new SchemaDeletionNotAllowedException(String.format("Not permitted deletion of Schema by " +
|
||||
"subject: %s, format: %s, version %s", subject, format, version));
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/schemas/{id}", method = RequestMethod.DELETE)
|
||||
public void delete(@PathVariable("id") Integer id) {
|
||||
if (this.schemaServerProperties.isAllowSchemaDeletion()) {
|
||||
Optional<Schema> schema = this.repository.findById(id);
|
||||
if (!schema.isPresent()) {
|
||||
throw new SchemaNotFoundException(String.format("Could not find Schema by id: %s", id));
|
||||
}
|
||||
deleteSchema(schema.get());
|
||||
}
|
||||
else {
|
||||
throw new SchemaDeletionNotAllowedException(String.format("Not permitted deletion of Schema by id: %s", id));
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/{subject}", method = RequestMethod.DELETE)
|
||||
public void delete(@PathVariable("subject") String subject) {
|
||||
if (this.schemaServerProperties.isAllowSchemaDeletion()) {
|
||||
for (Schema schema : this.repository.findAll()) {
|
||||
if (schema.getSubject().equals(subject)) {
|
||||
deleteSchema(schema);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new SchemaDeletionNotAllowedException(String.format("Not permitted deletion of Schema by " +
|
||||
"subject: %s", subject));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public final ResponseEntity<List<Schema>> findBySubjectAndFormatOrderByVersionAsc(@NonNull final String subject,
|
||||
@NonNull final String format) {
|
||||
List<Schema> schemas = this.repository.findBySubjectAndFormatOrderByVersion(subject, format);
|
||||
if (schemas.isEmpty()) {
|
||||
throw new SchemaNotFoundException(
|
||||
String.format("No schemas found for subject %s and format %s", subject, format));
|
||||
}
|
||||
return new ResponseEntity<>(schemas, HttpStatus.OK);
|
||||
}
|
||||
|
||||
private void deleteSchema(Schema schema) {
|
||||
if (schema == null) {
|
||||
throw new SchemaNotFoundException("Could not find Schema");
|
||||
}
|
||||
this.repository.delete(schema);
|
||||
}
|
||||
|
||||
@ExceptionHandler(UnsupportedFormatException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
@ResponseBody
|
||||
public String onUnsupportedFormat(UnsupportedFormatException e) {
|
||||
return errorMessage("Format not supported", e);
|
||||
}
|
||||
|
||||
@ExceptionHandler(InvalidSchemaException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
@ResponseBody
|
||||
public String onInvalidSchema(InvalidSchemaException e) {
|
||||
return errorMessage("Invalid Schema", e);
|
||||
}
|
||||
|
||||
@ExceptionHandler(SchemaNotFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
@ResponseBody
|
||||
public String schemaNotFound(SchemaNotFoundException ex) {
|
||||
return errorMessage("Schema not found", ex);
|
||||
}
|
||||
|
||||
@ExceptionHandler(SchemaDeletionNotAllowedException.class)
|
||||
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
@ResponseBody
|
||||
public String schemaDeletionNotPermitted(SchemaDeletionNotAllowedException ex) {
|
||||
return errorMessage("Schema deletion is not permitted", ex);
|
||||
}
|
||||
|
||||
private String errorMessage(String prefix, Throwable e) {
|
||||
return prefix + (StringUtils.hasText(e.getMessage()) ? ": " + e.getMessage() : "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.model;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
public enum Compatibility {
|
||||
|
||||
/**
|
||||
* Backward compatibiltity.
|
||||
*/
|
||||
BACKWARD,
|
||||
|
||||
/**
|
||||
* Forward compatibility.
|
||||
*/
|
||||
FORWARD,
|
||||
|
||||
/**
|
||||
* Full compatibility.
|
||||
*/
|
||||
FULL,
|
||||
|
||||
/**
|
||||
* Lack of compatibility.
|
||||
*/
|
||||
INCOMPATIBLE;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*
|
||||
* Represents a persisted schema entity.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "SCHEMA_REPOSITORY")
|
||||
public class Schema {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@Column(name = "ID")
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "VERSION", nullable = false)
|
||||
private Integer version;
|
||||
|
||||
@Column(name = "SUBJECT", nullable = false)
|
||||
private String subject;
|
||||
|
||||
@Column(name = "FORMAT", nullable = false)
|
||||
private String format;
|
||||
|
||||
@Lob
|
||||
@Column(name = "DEFINITION", nullable = false, length = 8192)
|
||||
private String definition;
|
||||
|
||||
public Integer getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Integer getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public void setVersion(Integer version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return this.subject;
|
||||
}
|
||||
|
||||
public void setSubject(String subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
public String getFormat() {
|
||||
return this.format;
|
||||
}
|
||||
|
||||
public void setFormat(String format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public String getDefinition() {
|
||||
return this.definition;
|
||||
}
|
||||
|
||||
public void setDefinition(String definition) {
|
||||
this.definition = definition;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.cloud.stream.schema.registry.model.Schema;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
public interface SchemaRepository extends CrudRepository<Schema, Integer> {
|
||||
|
||||
@Transactional
|
||||
List<Schema> findBySubjectAndFormatOrderByVersion(String subject, String format);
|
||||
|
||||
@Transactional
|
||||
Schema findOneBySubjectAndFormatAndVersion(String subject, String format, Integer version);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2016-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.support;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.avro.SchemaParseException;
|
||||
|
||||
import org.springframework.cloud.stream.schema.registry.model.Compatibility;
|
||||
import org.springframework.cloud.stream.schema.registry.model.Schema;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class AvroSchemaValidator implements SchemaValidator {
|
||||
|
||||
/**
|
||||
* Unique Avro schema format identifier.
|
||||
*/
|
||||
public static final String AVRO_FORMAT = "avro";
|
||||
|
||||
@Override
|
||||
public boolean isValid(String definition) {
|
||||
boolean result = true;
|
||||
try {
|
||||
new org.apache.avro.Schema.Parser().parse(definition);
|
||||
}
|
||||
catch (SchemaParseException ex) {
|
||||
result = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(String definition) {
|
||||
try {
|
||||
new org.apache.avro.Schema.Parser().parse(definition);
|
||||
}
|
||||
catch (SchemaParseException ex) {
|
||||
throw new InvalidSchemaException((ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Compatibility compatibilityCheck(String source, String other) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Schema match(List<Schema> schemas, String definition) {
|
||||
Schema result = null;
|
||||
org.apache.avro.Schema source = new org.apache.avro.Schema.Parser().parse(definition);
|
||||
for (Schema s : schemas) {
|
||||
org.apache.avro.Schema target = new org.apache.avro.Schema.Parser().parse(s.getDefinition());
|
||||
if (target.equals(source)) {
|
||||
result = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormat() {
|
||||
return AVRO_FORMAT;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.support;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
public class InvalidSchemaException extends RuntimeException {
|
||||
|
||||
public InvalidSchemaException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.support;
|
||||
|
||||
/**
|
||||
* @author Ilayaperumal Gopinathan
|
||||
*/
|
||||
public class SchemaDeletionNotAllowedException extends RuntimeException {
|
||||
|
||||
public SchemaDeletionNotAllowedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SchemaDeletionNotAllowedException() {
|
||||
super("Schema Deletion Not Allowed");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.support;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
public class SchemaNotFoundException extends RuntimeException {
|
||||
|
||||
public SchemaNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2016-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.support;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.cloud.stream.schema.registry.model.Compatibility;
|
||||
import org.springframework.cloud.stream.schema.registry.model.Schema;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
* @author Christian Tzolov
|
||||
*
|
||||
* Provides utility methods to validate, check compatibility and match schemas of
|
||||
* different implementations
|
||||
*/
|
||||
public interface SchemaValidator {
|
||||
|
||||
/**
|
||||
* Verifies if a definition is a valid schema.
|
||||
* @param definition - The textual representation of the schema file
|
||||
* @return true if valid, false otherwise
|
||||
*/
|
||||
boolean isValid(String definition);
|
||||
|
||||
/**
|
||||
* Validates a schema definition and throws an {@link InvalidSchemaException} when the schema is invalid.
|
||||
* The exception is expected to have the violation description.
|
||||
* @param definition - The textual representation of the schema file
|
||||
*/
|
||||
default void validate(String definition) {
|
||||
if (!this.isValid(definition)) {
|
||||
throw new InvalidSchemaException("Invalid Schema");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for compatibility between two schemas @see Compatibility class for types
|
||||
* This method may not be supported for certain formats.
|
||||
* @param source - The textual representation of the schema to tested
|
||||
* @param other - The textual representation of the other schema to tested
|
||||
* @return {@link Compatibility}
|
||||
*/
|
||||
Compatibility compatibilityCheck(String source, String other);
|
||||
|
||||
/**
|
||||
* Return the Schema that is represented by the definition.
|
||||
* @param schemas List of schemas to be tested
|
||||
* @param definition Textual representation of the schema
|
||||
* @return A full Schema object with identifier and subject properties
|
||||
*/
|
||||
Schema match(List<Schema> schemas, String definition);
|
||||
|
||||
String getFormat();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.support;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
public class UnsupportedFormatException extends RuntimeException {
|
||||
|
||||
public UnsupportedFormatException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
spring:
|
||||
application:
|
||||
name: SchemaRegistryServer
|
||||
server:
|
||||
port: 8990
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2019-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.entityScanning;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.stream.schema.registry.config.SchemaServerProperties;
|
||||
import org.springframework.cloud.stream.schema.registry.model.Schema;
|
||||
import org.springframework.cloud.stream.schema.registry.repository.SchemaRepository;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public abstract class AbstractServerControllerTest {
|
||||
|
||||
protected MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
protected SchemaRepository schemaRepository;
|
||||
|
||||
@Autowired
|
||||
protected SchemaServerProperties schemaServerProperties;
|
||||
|
||||
@Autowired
|
||||
private WebApplicationContext wac;
|
||||
|
||||
@BeforeEach
|
||||
public void setupMocks() {
|
||||
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac)
|
||||
.defaultRequest(get("/").accept(MediaType.APPLICATION_JSON)).build();
|
||||
Schema schema = new Schema();
|
||||
schema.setSubject("test667");
|
||||
schema.setVersion(667);
|
||||
schema.setFormat("format");
|
||||
schema.setDefinition("Test Schema Definition");
|
||||
schemaRepository.save(schema);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.entityScanning;
|
||||
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.cloud.stream.schema.registry.EnableSchemaRegistryServer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
public class EntityScanningTests {
|
||||
|
||||
@Test
|
||||
public void testApplicationWithEmbeddedSchemaRegistryServerOutsideOfRootPackage()
|
||||
throws Exception {
|
||||
final ConfigurableApplicationContext context = SpringApplication
|
||||
.run(CustomApplicationEmbeddingSchemaServer.class, "--server.port=0");
|
||||
context.close();
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@EnableSchemaRegistryServer
|
||||
public static class CustomApplicationEmbeddingSchemaServer {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.entityScanning;
|
||||
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.cloud.stream.schema.registry.EnableSchemaRegistryServer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
public class EntityScanningTestsWithEntityScan {
|
||||
|
||||
@Test
|
||||
public void testApplicationWithEmbeddedSchemaRegistryServerOutsideOfRootPackage() {
|
||||
final ConfigurableApplicationContext context = SpringApplication
|
||||
.run(CustomApplicationEmbeddingSchemaServer.class, "--server.port=0");
|
||||
context.close();
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@EnableSchemaRegistryServer
|
||||
@EntityScan(basePackages = "org.springframework.cloud.stream.schema.registry.entityScanning.domain")
|
||||
public static class CustomApplicationEmbeddingSchemaServer {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2019-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.entityScanning;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.stream.schema.registry.config.SchemaServerConfiguration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest(classes = { SchemaServerConfiguration.class })
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
|
||||
@EnableAutoConfiguration
|
||||
@TestPropertySource(properties = {
|
||||
"spring.cloud.stream.schema.server.path=/testpath",
|
||||
"spring.cloud.stream.schema.server.allowSchemaDeletion=false"
|
||||
})
|
||||
public class ServerControllerTest extends AbstractServerControllerTest {
|
||||
|
||||
@Test
|
||||
public void propertiesTest() {
|
||||
assertThat(schemaServerProperties.getPath()).isEqualTo("/testpath");
|
||||
assertThat(schemaServerProperties.isAllowSchemaDeletion()).isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findSchema() throws Exception {
|
||||
mockMvc.perform(get(schemaServerProperties.getPath() + "/test667/format/v667")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("Test Schema Definition")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.entityScanning.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
@Entity
|
||||
public class TestEntity {
|
||||
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
|
||||
public long getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>spring-cloud-stream-schema-registry-server</artifactId>
|
||||
|
||||
<parent>
|
||||
<artifactId>spring-cloud-stream-schema-registry</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<docker.image.prefix>springcloud</docker.image.prefix>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-schema-registry-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.server;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.stream.schema.registry.EnableSchemaRegistryServer;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
// @checkstyle:off
|
||||
@SpringBootApplication
|
||||
@EnableSchemaRegistryServer
|
||||
public class SchemaRegistryServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SchemaRegistryServerApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
// @checkstyle:on
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
spring:
|
||||
application:
|
||||
name: SchemaRegistryServer
|
||||
server:
|
||||
port: 8990
|
||||
|
||||
@@ -0,0 +1,623 @@
|
||||
/*
|
||||
* Copyright 2016-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.schema.registry.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.avro.Schema.Parser;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.cloud.stream.schema.registry.config.SchemaServerProperties;
|
||||
import org.springframework.cloud.stream.schema.registry.controllers.ServerController;
|
||||
import org.springframework.cloud.stream.schema.registry.model.Schema;
|
||||
import org.springframework.cloud.stream.schema.registry.support.SchemaNotFoundException;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.client.DefaultResponseErrorHandler;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.springframework.test.annotation.DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
// @checkstyle:off
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, properties = "spring.main.allow-bean-definition-overriding=true")
|
||||
// @checkstyle:on
|
||||
@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD)
|
||||
public class SchemaRegistryServerAvroTests {
|
||||
|
||||
private static final String AVRO_FORMAT_NAME = "avro";
|
||||
|
||||
private static final org.apache.avro.Schema AVRO_USER_AVRO_SCHEMA_V1 = new Parser()
|
||||
.parse(resourceToString("classpath:/avro_user_definition_schema_v1.json"));
|
||||
|
||||
private static final org.apache.avro.Schema AVRO_USER_AVRO_SCHEMA_V2 = new Parser()
|
||||
.parse(resourceToString("classpath:/avro_user_definition_schema_v2.json"));
|
||||
|
||||
private static final String AVRO_USER_SCHEMA_DEFAULT_NAME_STRATEGY_SUBJECT = AVRO_USER_AVRO_SCHEMA_V1.getName()
|
||||
.toLowerCase();
|
||||
|
||||
|
||||
private static final String AVRO_USER_SCHEMA_QUALIFED_NAME_STRATEGY_SUBJECT = AVRO_USER_AVRO_SCHEMA_V1
|
||||
.getFullName()
|
||||
.toLowerCase();
|
||||
|
||||
private static final Schema AVRO_USER_REGISTRY_SCHEMA_V1 = toSchema(
|
||||
AVRO_USER_SCHEMA_DEFAULT_NAME_STRATEGY_SUBJECT,
|
||||
AVRO_FORMAT_NAME, AVRO_USER_AVRO_SCHEMA_V1.toString());
|
||||
|
||||
private static final Schema AVRO_USER_REGISTRY_SCHEMA_V2 = toSchema(
|
||||
AVRO_USER_SCHEMA_DEFAULT_NAME_STRATEGY_SUBJECT,
|
||||
AVRO_FORMAT_NAME, AVRO_USER_AVRO_SCHEMA_V2.toString());
|
||||
|
||||
private static final Schema AAVRO_USER_REGISTRY_SCHEMA_V1_WITH_QUAL_SUBJECT = toSchema(
|
||||
AVRO_USER_SCHEMA_QUALIFED_NAME_STRATEGY_SUBJECT,
|
||||
AVRO_FORMAT_NAME, AVRO_USER_AVRO_SCHEMA_V1.toString());
|
||||
@Autowired
|
||||
private TestRestTemplate client;
|
||||
|
||||
@Autowired
|
||||
private SchemaServerProperties schemaServerProperties;
|
||||
|
||||
@Autowired
|
||||
private ServerController serverController;
|
||||
|
||||
@Autowired
|
||||
private ServerProperties serverProperties;
|
||||
|
||||
private URI serverControllerUri;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
|
||||
String scheme = Optional.ofNullable(this.serverProperties.getSsl())
|
||||
.filter(Ssl::isEnabled)
|
||||
.map(ssl -> "https").orElse("http");
|
||||
|
||||
Integer port = this.serverProperties.getPort();
|
||||
String contextPath = this.serverProperties.getServlet().getContextPath();
|
||||
|
||||
this.serverControllerUri = UriComponentsBuilder.newInstance().scheme(scheme)
|
||||
.host("localhost")
|
||||
.port(port)
|
||||
.path(contextPath).build().toUri();
|
||||
|
||||
this.client.getRestTemplate().setErrorHandler(new DefaultResponseErrorHandler());
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
static Schema toSchema(String subject, String format, String definition) {
|
||||
Schema schema = new Schema();
|
||||
schema.setSubject(subject);
|
||||
schema.setFormat(format);
|
||||
schema.setDefinition(definition);
|
||||
return schema;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnsupportedFormat() {
|
||||
Schema schema = toSchema("spring", "boot", null);
|
||||
try {
|
||||
this.client.postForEntity(this.serverControllerUri, schema, Schema.class);
|
||||
fail("Expects: " + HttpStatus.BAD_REQUEST + " error");
|
||||
}
|
||||
catch (HttpClientErrorException.BadRequest badRequest) {
|
||||
assertThat(badRequest.getMessage()).isEqualTo("400 : \"Format not supported: Invalid format, supported types are: avro\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidSchema() {
|
||||
Schema schema = toSchema("boot", AVRO_FORMAT_NAME, "{}");
|
||||
try {
|
||||
this.client.postForEntity(this.serverControllerUri, schema, Schema.class);
|
||||
fail("Expects: " + HttpStatus.BAD_REQUEST + " error");
|
||||
}
|
||||
catch (HttpClientErrorException.BadRequest badRequest) {
|
||||
assertThat(badRequest.getMessage()).isEqualTo("400 : \"Invalid Schema: No type: {}\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidSchemaGh22() {
|
||||
Schema schema = toSchema("boot", AVRO_FORMAT_NAME,
|
||||
resourceToString("classpath:/invalid_schema.json"));
|
||||
try {
|
||||
this.client.postForEntity(this.serverControllerUri, schema, Schema.class);
|
||||
fail("Expects: " + HttpStatus.BAD_REQUEST + " error");
|
||||
}
|
||||
catch (HttpClientErrorException.BadRequest badRequest) {
|
||||
assertThat(badRequest.getMessage()).isEqualTo("400 : \"Invalid Schema: \"SomeType\" is not a defined name. " +
|
||||
"The type of the \"field\" field must be a defined name or a {\"type\": ...} expression.\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegister1AvroSchema() {
|
||||
Schema schema = toSchema("org.springframework.cloud.stream.schema.User", AVRO_FORMAT_NAME,
|
||||
resourceToString("classpath:/avro_user_definition_schema_v1.json"));
|
||||
registerSchemaAndAssertSuccess(schema, 1, 1);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindByIdFound() {
|
||||
|
||||
ResponseEntity<Schema> registerSchemaReponse = registerSchemaAndAssertSuccess(
|
||||
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
|
||||
|
||||
Schema registeredSchema = registerSchemaReponse.getBody();
|
||||
|
||||
URI findByIdUriId1 = this.serverControllerUri.resolve("/schemas/" + registeredSchema.getId());
|
||||
|
||||
ResponseEntity<Schema> findByIdResponse = this.client
|
||||
.getForEntity(findByIdUriId1, Schema.class);
|
||||
|
||||
assertThat(findByIdResponse.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
|
||||
Schema actual = findByIdResponse.getBody();
|
||||
assertSchema(registeredSchema, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindByIdNotFound() {
|
||||
|
||||
registerSchemaAndAssertSuccess(AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
|
||||
|
||||
URI findByIdUriId1 = this.serverControllerUri.resolve("/schemas/" + 2);
|
||||
|
||||
try {
|
||||
this.client.getForEntity(findByIdUriId1, Schema.class);
|
||||
fail("Expects: " + HttpStatus.NOT_FOUND + " error");
|
||||
}
|
||||
catch (HttpClientErrorException.NotFound notFound) {
|
||||
assertThat(notFound.getMessage()).isEqualTo("404 : \"Schema not found: Could not find Schema by id: 2\"");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserSchemaV2() {
|
||||
registerSchemasAndAssertSuccess(AVRO_USER_REGISTRY_SCHEMA_V1, AVRO_USER_REGISTRY_SCHEMA_V2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIdempotentRegistration() {
|
||||
|
||||
registerSchemaAndAssertSuccess(AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
|
||||
|
||||
registerSchemaAndAssertSuccess(AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
|
||||
|
||||
}
|
||||
|
||||
//@Test(expected = HttpClientErrorException.NotFound.class)
|
||||
public void testSchemaNotfound() {
|
||||
this.client.getForEntity("http://localhost:8990/foo/avro/v42", Schema.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaDeletionBySubjectFormatVersion() {
|
||||
|
||||
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
|
||||
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
|
||||
|
||||
this.schemaServerProperties.setAllowSchemaDeletion(true);
|
||||
|
||||
URI subjectFormatVersionUri = this.serverControllerUri
|
||||
.resolve(registerSchemaAndAssertSuccess.getHeaders().getLocation());
|
||||
|
||||
|
||||
ResponseEntity<Void> deleteResponse = this.client.exchange(
|
||||
new RequestEntity<>(HttpMethod.DELETE, subjectFormatVersionUri),
|
||||
Void.class);
|
||||
|
||||
assertThat(deleteResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
try {
|
||||
this.client.getForEntity(subjectFormatVersionUri, Schema.class);
|
||||
}
|
||||
catch (HttpClientErrorException.NotFound notFound) {
|
||||
assertThat(notFound.getMessage()).isEqualTo("404 : \"Schema not found: Could not find Schema by " +
|
||||
"subject: user, format: avro, version 1\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaDeletionBySubjectFormatVersionNotFound() {
|
||||
|
||||
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
|
||||
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
|
||||
|
||||
this.schemaServerProperties.setAllowSchemaDeletion(true);
|
||||
|
||||
URI subjectFormatVersionUri = this.serverControllerUri
|
||||
.resolve(registerSchemaAndAssertSuccess.getHeaders()
|
||||
.getLocation().toString().replace("v1", "v100"));
|
||||
|
||||
try {
|
||||
this.client.exchange(new RequestEntity<>(HttpMethod.DELETE, subjectFormatVersionUri), Void.class);
|
||||
}
|
||||
catch (HttpClientErrorException.NotFound notFound) {
|
||||
assertThat(notFound.getMessage()).isEqualTo("404 : \"Schema not found: Could not find Schema by " +
|
||||
"subject: user, format: avro, version 100\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaDeletionBySubjectFormatVersionNotAllowed() {
|
||||
|
||||
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
|
||||
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
|
||||
|
||||
URI versionUri = this.serverControllerUri.resolve(registerSchemaAndAssertSuccess.getHeaders().getLocation());
|
||||
|
||||
try {
|
||||
this.client.exchange(new RequestEntity<>(HttpMethod.DELETE, versionUri), Void.class);
|
||||
}
|
||||
catch (HttpClientErrorException.MethodNotAllowed methodNotAllowed) {
|
||||
assertThat(methodNotAllowed.getMessage()).isEqualTo("405 : \"Schema deletion is not permitted: Not permitted " +
|
||||
"deletion of Schema by subject: user, format: avro, version 1\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaDeletionById() {
|
||||
|
||||
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
|
||||
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
|
||||
|
||||
this.schemaServerProperties.setAllowSchemaDeletion(true);
|
||||
this.client.delete(this.serverControllerUri
|
||||
.resolve("/schemas/" + registerSchemaAndAssertSuccess.getBody().getVersion()));
|
||||
|
||||
try {
|
||||
this.client.getForEntity(registerSchemaAndAssertSuccess.getHeaders().getLocation(), Schema.class);
|
||||
fail("Expects: " + HttpStatus.NOT_FOUND + " error");
|
||||
}
|
||||
catch (HttpClientErrorException.NotFound notFound) {
|
||||
assertThat(notFound.getMessage()).isEqualTo("404 : \"Schema not found: Could not find Schema by subject: " +
|
||||
"user, format: avro, version 1\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaDeletionByIdNotFound() {
|
||||
|
||||
registerSchemaAndAssertSuccess(AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
|
||||
|
||||
this.schemaServerProperties.setAllowSchemaDeletion(true);
|
||||
|
||||
try {
|
||||
this.client.exchange(new RequestEntity<>(HttpMethod.DELETE,
|
||||
this.serverControllerUri.resolve("/schemas/" + 2)), Void.class);
|
||||
fail("Expects: " + HttpStatus.NOT_FOUND + " error");
|
||||
}
|
||||
catch (HttpClientErrorException.NotFound notFound) {
|
||||
assertThat(notFound.getMessage()).isEqualTo("404 : \"Schema not found: Could not find Schema by id: 2\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaDeletionByIdNotAllowed() {
|
||||
|
||||
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
|
||||
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
|
||||
|
||||
URI schemaIdUri = this.serverControllerUri.resolve(this.serverControllerUri
|
||||
.resolve("/schemas/" + registerSchemaAndAssertSuccess.getBody().getVersion()));
|
||||
|
||||
try {
|
||||
this.client.exchange(new RequestEntity<>(HttpMethod.DELETE, schemaIdUri), Void.class);
|
||||
fail("Expects: " + HttpStatus.METHOD_NOT_ALLOWED + " error");
|
||||
}
|
||||
catch (HttpClientErrorException.MethodNotAllowed methodNotAllowed) {
|
||||
assertThat(methodNotAllowed.getMessage()).isEqualTo("405 : \"Schema deletion is not permitted: Not " +
|
||||
"permitted deletion of Schema by id: 1\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaDeletionBySubject() {
|
||||
Map<String, Map<String, List<ResponseEntity<Schema>>>> registerSchemaResponsesByFormatBySubject = registerSchemasAndAssertSuccess(
|
||||
AVRO_USER_REGISTRY_SCHEMA_V1,
|
||||
AVRO_USER_REGISTRY_SCHEMA_V2, AAVRO_USER_REGISTRY_SCHEMA_V1_WITH_QUAL_SUBJECT);
|
||||
|
||||
this.schemaServerProperties.setAllowSchemaDeletion(true);
|
||||
|
||||
registerSchemaResponsesByFormatBySubject.forEach((subject, registerSchemaResponsesByFormat) -> {
|
||||
|
||||
assertThat(registerSchemaResponsesByFormat).isNotEmpty();
|
||||
ResponseEntity<Void> deleteBySubject = this.client.exchange(
|
||||
new RequestEntity<>(HttpMethod.DELETE, this.serverControllerUri
|
||||
.resolve("/" + subject)),
|
||||
Void.class);
|
||||
|
||||
assertThat(deleteBySubject.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
registerSchemaResponsesByFormat.forEach((format, registerSchemaResponses) -> {
|
||||
|
||||
assertThat(registerSchemaResponses).isNotEmpty();
|
||||
|
||||
registerSchemaResponses.forEach(registerSchemaResponse -> {
|
||||
|
||||
try {
|
||||
this.client.getForEntity(registerSchemaResponse.getHeaders().getLocation(), Schema.class);
|
||||
fail("Expects: " + HttpStatus.NOT_FOUND + " error");
|
||||
}
|
||||
catch (HttpClientErrorException.NotFound notFound) {
|
||||
//do nothing
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaDeletionBySubjectNotFound() {
|
||||
|
||||
registerSchemaAndAssertSuccess(AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
|
||||
|
||||
this.schemaServerProperties.setAllowSchemaDeletion(true);
|
||||
|
||||
ResponseEntity<Void> deleteBySubject = this.client.exchange(
|
||||
new RequestEntity<>(HttpMethod.DELETE, this.serverControllerUri.resolve("/foo")), Void.class);
|
||||
|
||||
assertThat(deleteBySubject.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaDeletionBySubjectNotAllowed() {
|
||||
|
||||
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
|
||||
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
|
||||
|
||||
Schema schema = registerSchemaAndAssertSuccess.getBody();
|
||||
|
||||
try {
|
||||
this.client.exchange(new RequestEntity<>(HttpMethod.DELETE,
|
||||
this.serverControllerUri.resolve("/" + schema.getSubject())), Void.class);
|
||||
fail("Expects: " + HttpStatus.METHOD_NOT_ALLOWED + " error");
|
||||
}
|
||||
catch (HttpClientErrorException.MethodNotAllowed methodNotAllowed) {
|
||||
assertThat(methodNotAllowed.getMessage()).isEqualTo("405 : \"Schema deletion is not permitted: " +
|
||||
"Not permitted deletion of Schema by subject: user\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindSchemasBySubjectAndVersion() {
|
||||
|
||||
Map<String, Map<String, List<ResponseEntity<Schema>>>> registerSchemaResponsesByFormatBySubject = registerSchemasAndAssertSuccess(
|
||||
AVRO_USER_REGISTRY_SCHEMA_V1,
|
||||
AVRO_USER_REGISTRY_SCHEMA_V2);
|
||||
|
||||
registerSchemaResponsesByFormatBySubject.forEach((subject, schemasByFormat) -> {
|
||||
|
||||
assertThat(schemasByFormat).hasSize(1);
|
||||
|
||||
schemasByFormat.forEach((format, schemas) -> {
|
||||
assertThat(schemas).hasSize(2);
|
||||
|
||||
final ResponseEntity<List<Schema>> findBySubjectAndVersionResponseEntity = this.serverController
|
||||
.findBySubjectAndVersion(subject, format);
|
||||
|
||||
assertThat(findBySubjectAndVersionResponseEntity.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
|
||||
final List<Schema> schemaResponseBody = findBySubjectAndVersionResponseEntity.getBody();
|
||||
|
||||
assertThat(schemaResponseBody).zipSatisfy(schemas.stream().map(ResponseEntity::getBody)
|
||||
.collect(toList()), this::assertSchema);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindBySubjectAndFormatOrderByVersionAscNoMatch() {
|
||||
String subject = "test";
|
||||
|
||||
String format = AVRO_FORMAT_NAME;
|
||||
|
||||
assertThatExceptionOfType(SchemaNotFoundException.class).isThrownBy(() -> this.serverController
|
||||
.findBySubjectAndFormatOrderByVersionAsc(subject, format))
|
||||
.withMessage("No schemas found for subject %s and format %s", subject, format)
|
||||
.withNoCause();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindSchemasBySubjectAndFormat() {
|
||||
|
||||
Map<String, Map<String, List<ResponseEntity<Schema>>>> registerSchemaResponsesByFormatBySubject = registerSchemasAndAssertSuccess(
|
||||
AVRO_USER_REGISTRY_SCHEMA_V1,
|
||||
AVRO_USER_REGISTRY_SCHEMA_V2);
|
||||
|
||||
registerSchemaResponsesByFormatBySubject.forEach((subject, schemasByFormat) -> {
|
||||
|
||||
assertThat(schemasByFormat).hasSize(1);
|
||||
|
||||
schemasByFormat.forEach((format, schemas) -> {
|
||||
assertThat(schemas).hasSize(2);
|
||||
|
||||
ResponseEntity<List<Schema>> findBySubjectFormatResponse = this.client.exchange(
|
||||
this.serverControllerUri.resolve("/" + subject + "/" + format), HttpMethod.GET, null,
|
||||
new ParameterizedTypeReference<List<Schema>>() {
|
||||
});
|
||||
|
||||
assertThat(findBySubjectFormatResponse.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
|
||||
final List<Schema> schemaResponseBody = findBySubjectFormatResponse.getBody();
|
||||
|
||||
assertThat(schemaResponseBody).zipSatisfy(schemas.stream().map(ResponseEntity::getBody)
|
||||
.collect(toList()), this::assertSchema);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private Map<String, Map<String, List<ResponseEntity<Schema>>>> registerSchemasAndAssertSuccess(
|
||||
@NonNull Schema... schemas) {
|
||||
Map<String, Map<String, Integer>> versionsByFormatAndSubject = new HashMap<>();
|
||||
Map<String, Map<String, List<ResponseEntity<Schema>>>> result = new HashMap<>();
|
||||
int numOfSchemas = schemas.length;
|
||||
int id = 0;
|
||||
for (int i = 0; i < numOfSchemas; i++) {
|
||||
Schema schema = schemas[i];
|
||||
id++;
|
||||
String format = schema.getFormat();
|
||||
String subject = schema.getSubject();
|
||||
Integer version = versionsByFormatAndSubject
|
||||
.compute(subject,
|
||||
(_subject, currentValue) -> currentValue == null ? new HashMap<>() : currentValue)
|
||||
.merge(format, 1, Integer::sum);
|
||||
ResponseEntity<Schema> registerSchemaResponse = registerSchemaAndAssertSuccess(schema, version, id);
|
||||
result.compute(subject,
|
||||
(_subject, currentValue) -> currentValue == null ? new HashMap<>() : currentValue)
|
||||
|
||||
.compute(format, (_format, currentValue) -> {
|
||||
List<ResponseEntity<Schema>> value = currentValue == null ? new ArrayList<>() : currentValue;
|
||||
value.add(registerSchemaResponse);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
Stream<ResponseEntity<Schema>> asStream = result.entrySet().stream()
|
||||
.map(Entry::getValue)
|
||||
.map(Map::entrySet)
|
||||
.flatMap(Collection::stream)
|
||||
.map(Entry::getValue)
|
||||
.flatMap(Collection::stream);
|
||||
assertThat(asStream).hasSize(numOfSchemas);
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private ResponseEntity<Schema> registerSchemaAndAssertSuccess(@NonNull Schema schema,
|
||||
@Nullable Integer expectedVersion,
|
||||
@Nullable Integer expectedId) {
|
||||
|
||||
ResponseEntity<Schema> registerReponse = this.client
|
||||
.postForEntity(this.serverControllerUri, schema, Schema.class);
|
||||
|
||||
HttpStatusCode statusCode = registerReponse.getStatusCode();
|
||||
assertThat(statusCode.is2xxSuccessful()).isTrue();
|
||||
|
||||
Schema registeredSchema = registerReponse.getBody();
|
||||
assertSchema(schema, expectedVersion, expectedId, registeredSchema);
|
||||
|
||||
HttpHeaders headers = registerReponse.getHeaders();
|
||||
assertLocation(headers, registeredSchema);
|
||||
|
||||
return registerReponse;
|
||||
}
|
||||
|
||||
private void assertLocation(HttpHeaders headers, Schema registeredSchema) {
|
||||
URI location = headers.getLocation();
|
||||
|
||||
assertThat(location).isNotNull();
|
||||
assertPersisted(location, registeredSchema);
|
||||
}
|
||||
|
||||
private void assertPersisted(URI location, Schema registeredSchema) {
|
||||
|
||||
ResponseEntity<Schema> findOneResponse = this.client.getForEntity(location,
|
||||
Schema.class);
|
||||
|
||||
HttpStatusCode statusCode = findOneResponse.getStatusCode();
|
||||
assertThat(statusCode.is2xxSuccessful()).isTrue();
|
||||
|
||||
Schema actual = findOneResponse.getBody();
|
||||
assertSchema(registeredSchema, registeredSchema.getVersion(), registeredSchema.getId(), actual);
|
||||
|
||||
}
|
||||
|
||||
private void assertSchema(@NonNull Schema expected, @NonNull Schema actual) {
|
||||
|
||||
assertSchema(expected, expected.getVersion(), expected.getId(), actual);
|
||||
}
|
||||
|
||||
private void assertSchema(@NonNull Schema expected, Integer expectedVersion, Integer expectedId,
|
||||
@NonNull Schema actual) {
|
||||
|
||||
assertThat(actual).isEqualToIgnoringGivenFields(expected, "version", "id");
|
||||
if (expectedVersion != null) {
|
||||
assertThat(actual.getVersion()).isEqualTo(expectedVersion);
|
||||
}
|
||||
if (expectedId != null) {
|
||||
assertThat(actual.getId()).isEqualTo(expectedId);
|
||||
}
|
||||
}
|
||||
|
||||
private static String resourceToString(String resourceUri) {
|
||||
try {
|
||||
return StreamUtils.copyToString(new DefaultResourceLoader().getResource(resourceUri)
|
||||
.getInputStream(), StandardCharsets.UTF_8);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException("Could not extract resource: " + resourceUri, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"namespace": "example.avro",
|
||||
"type": "record",
|
||||
"name": "User",
|
||||
"fields": [
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "favorite_number",
|
||||
"type": [
|
||||
"int",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"namespace": "example.avro",
|
||||
"type": "record",
|
||||
"name": "User",
|
||||
"fields": [
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "favorite_number",
|
||||
"type": [
|
||||
"int",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "favorite_color",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "record",
|
||||
"name": "SuperType",
|
||||
"namespace": "some.namespace",
|
||||
"fields": [
|
||||
{
|
||||
"name": "field",
|
||||
"type": "SomeType"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user