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:
Soby Chacko
2022-08-23 20:29:37 -04:00
committed by Oleg Zhurakousky
parent 0daa58c48a
commit e1f173e139
83 changed files with 6041 additions and 0 deletions

View File

@@ -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
View 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>

View File

@@ -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>-->
<!-- &lt;!&ndash; used for testing https://github.com/spring-cloud/spring-cloud-schema-registry/issues/19 &ndash;&gt;-->
<!-- <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>

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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 + '\'' + '}';
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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");
}
}

View File

@@ -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 {
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.stream.schema.registry.avro.AvroMessageConverterAutoConfiguration

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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"]
}
]
}

View File

@@ -0,0 +1,19 @@
{
"namespace":"example.avro",
"name": "Email",
"type": "record",
"fields":[
{
"name":"addressTo",
"type":"string"
},
{
"name":"title",
"type":"string"
},
{
"name":"text",
"type":"string"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"namespace":"example.avro",
"name": "PushNotification",
"type": "record",
"fields":[
{
"name":"arn",
"type":"string"
},
{
"name":"text",
"type":"string"
}
]
}

View File

@@ -0,0 +1,14 @@
{
"namespace":"example.avro",
"name": "Sms",
"type": "record",
"fields":[
{
"name":"phoneNumber",
"type":"string"
},{
"name":"text",
"type":"string"
}
]
}

View File

@@ -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"}
]
}

View File

@@ -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"]}
]
}

View File

@@ -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"}
]
}
]

View File

@@ -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"]}
]
}

View File

@@ -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"}
]
}

View File

@@ -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"}
]
}

View File

@@ -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"]}
]
}

View File

@@ -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"}
]
}

View File

@@ -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>

View File

@@ -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 {
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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() : "");
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,6 @@
spring:
application:
name: SchemaRegistryServer
server:
port: 8990

View File

@@ -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);
}
}

View File

@@ -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 {
}
}

View File

@@ -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 {
}
}

View File

@@ -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")));
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -0,0 +1,6 @@
spring:
application:
name: SchemaRegistryServer
server:
port: 8990

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,18 @@
{
"namespace": "example.avro",
"type": "record",
"name": "User",
"fields": [
{
"name": "name",
"type": "string"
},
{
"name": "favorite_number",
"type": [
"int",
"null"
]
}
]
}

View File

@@ -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"
]
}
]
}

View File

@@ -0,0 +1,11 @@
{
"type": "record",
"name": "SuperType",
"namespace": "some.namespace",
"fields": [
{
"name": "field",
"type": "SomeType"
}
]
}