Move Schema Registry out of Spring Cloud Stream

Migrated to https://github.com/spring-cloud/spring-cloud-schema-registry

Resolves #1786
This commit is contained in:
Soby Chacko
2019-08-19 19:16:36 -04:00
committed by Oleg Zhurakousky
parent ea924ffb95
commit e870c6505a
67 changed files with 0 additions and 5660 deletions

View File

@@ -48,11 +48,6 @@
<artifactId>spring-cloud-stream-tools</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-schema-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-test</artifactId>
@@ -109,9 +104,6 @@
<!--
<module>spring-cloud-stream-reactive</module>
-->
<module>spring-cloud-stream-schema</module>
<module>spring-cloud-stream-schema-server</module>
<module>docs</module>
</modules>
<build>

View File

@@ -1,59 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
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-server</artifactId>
<parent>
<artifactId>spring-cloud-stream-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.0.0.BUILD-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<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>
<version>1.4.192</version>
</dependency>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support-internal</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,39 +0,0 @@
/*
* 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.server;
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.server.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

@@ -1,35 +0,0 @@
/*
* 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.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @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

@@ -1,72 +0,0 @@
/*
* 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.server.config;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
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.server.controllers.ServerController;
import org.springframework.cloud.stream.schema.server.model.Schema;
import org.springframework.cloud.stream.schema.server.repository.SchemaRepository;
import org.springframework.cloud.stream.schema.server.support.AvroSchemaValidator;
import org.springframework.cloud.stream.schema.server.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 new BeanFactoryPostProcessor() {
@Override
public void postProcessBeanFactory(
ConfigurableListableBeanFactory beanFactory) throws BeansException {
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

@@ -1,56 +0,0 @@
/*
* 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.server.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Vinicius Carvalho
* @author Ilayaperumal Gopinathan
*/
@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

@@ -1,263 +0,0 @@
/*
* 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.server.controllers;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.cloud.stream.schema.server.config.SchemaServerProperties;
import org.springframework.cloud.stream.schema.server.model.Schema;
import org.springframework.cloud.stream.schema.server.repository.SchemaRepository;
import org.springframework.cloud.stream.schema.server.support.InvalidSchemaException;
import org.springframework.cloud.stream.schema.server.support.SchemaDeletionNotAllowedException;
import org.springframework.cloud.stream.schema.server.support.SchemaNotFoundException;
import org.springframework.cloud.stream.schema.server.support.SchemaValidator;
import org.springframework.cloud.stream.schema.server.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.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
*/
@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())));
}
if (!validator.isValid(schema.getDefinition())) {
throw new InvalidSchemaException("Invalid schema");
}
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("Could not find Schema");
}
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("Could not find Schema");
}
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);
deleteSchema(schema);
}
else {
throw new SchemaDeletionNotAllowedException();
}
}
@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("Could not find Schema");
}
deleteSchema(schema.get());
}
else {
throw new SchemaDeletionNotAllowedException();
}
}
@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();
}
}
@NonNull
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(value = HttpStatus.BAD_REQUEST, reason = "Format not supported")
public void unsupportedFormat(UnsupportedFormatException ex) {
}
@ExceptionHandler(InvalidSchemaException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Invalid schema")
public void invalidSchema(InvalidSchemaException ex) {
}
@ExceptionHandler(SchemaNotFoundException.class)
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Schema not found")
public void schemaNotFound(SchemaNotFoundException ex) {
}
@ExceptionHandler(SchemaDeletionNotAllowedException.class)
@ResponseStatus(value = HttpStatus.METHOD_NOT_ALLOWED, reason = "Schema deletion is not permitted")
public void schemaDeletionNotPermitted(SchemaDeletionNotAllowedException ex) {
}
}

View File

@@ -1,44 +0,0 @@
/*
* 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.server.model;
/**
* @author Vinicius Carvalho
*/
public enum Compatibility {
/**
* Backward compatibiltity.
*/
BACKWARD,
/**
* Forward compatibility.
*/
FORWARD,
/**
* Full compatibility.
*/
FULL,
/**
* Lack of compatibility.
*/
INCOMPATIBLE;
}

View File

@@ -1,93 +0,0 @@
/*
* 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.server.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.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

@@ -1,37 +0,0 @@
/*
* 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.server.repository;
import java.util.List;
import org.springframework.cloud.stream.schema.server.model.Schema;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Vinicius Carvalho
*/
public interface SchemaRepository extends PagingAndSortingRepository<Schema, Integer> {
@Transactional
List<Schema> findBySubjectAndFormatOrderByVersion(String subject, String format);
@Transactional
Schema findOneBySubjectAndFormatAndVersion(String subject, String format,
Integer version);
}

View File

@@ -1,69 +0,0 @@
/*
* 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.server.support;
import java.util.List;
import org.apache.avro.SchemaParseException;
import org.springframework.cloud.stream.schema.server.model.Compatibility;
import org.springframework.cloud.stream.schema.server.model.Schema;
/**
* @author Vinicius Carvalho
*/
public class AvroSchemaValidator implements SchemaValidator {
@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 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";
}
}

View File

@@ -1,28 +0,0 @@
/*
* 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.server.support;
/**
* @author Vinicius Carvalho
*/
public class InvalidSchemaException extends RuntimeException {
public InvalidSchemaException(String message) {
super(message);
}
}

View File

@@ -1,32 +0,0 @@
/*
* 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.server.support;
/**
* @author Ilayaperumal Gopinathan
*/
public class SchemaDeletionNotAllowedException extends RuntimeException {
public SchemaDeletionNotAllowedException(String message) {
super(message);
}
public SchemaDeletionNotAllowedException() {
super("Schema Deletion Not Allowed");
}
}

View File

@@ -1,28 +0,0 @@
/*
* 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.server.support;
/**
* @author Vinicius Carvalho
*/
public class SchemaNotFoundException extends RuntimeException {
public SchemaNotFoundException(String message) {
super(message);
}
}

View File

@@ -1,58 +0,0 @@
/*
* 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.server.support;
import java.util.List;
import org.springframework.cloud.stream.schema.server.model.Compatibility;
import org.springframework.cloud.stream.schema.server.model.Schema;
/**
* @author Vinicius Carvalho
*
* 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);
/**
* 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

@@ -1,28 +0,0 @@
/*
* 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.server.support;
/**
* @author Vinicius Carvalho
*/
public class UnsupportedFormatException extends RuntimeException {
public UnsupportedFormatException(String message) {
super(message);
}
}

View File

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

View File

@@ -1,594 +0,0 @@
/*
* 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.server.controllers;
import java.net.URI;
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.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
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.server.config.SchemaServerProperties;
import org.springframework.cloud.stream.schema.server.model.Schema;
import org.springframework.cloud.stream.schema.server.support.SchemaNotFoundException;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
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.junit4.SpringRunner;
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.springframework.test.annotation.DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD;
/**
* @author Vinicius Carvalho
* @author Ilayaperumal Gopinathan
*/
@RunWith(SpringRunner.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 String AVRO_USER_DEFINITION_SCHEMA_V1 = "{\"namespace\": \"example.avro\",\n"
+ " \"type\": \"record\",\n" + " \"name\": \"User\",\n" + " \"fields\": [\n"
+ " {\"name\": \"name\", \"type\": \"string\"},\n"
+ " {\"name\": \"favorite_number\", \"type\": [\"int\", \"null\"]}\n"
+ " ]\n" + "}";
private static final String AVRO_USER_DEFINTITION_SCHEMA_V2 = "{\"namespace\": \"example.avro\",\n"
+ " \"type\": \"record\",\n" + " \"name\": \"User\",\n" + " \"fields\": [\n"
+ " {\"name\": \"name\", \"type\": \"string\"},\n"
+ " {\"name\": \"favorite_number\", \"type\": [\"int\", \"null\"]},\n"
+ " {\"name\": \"favorite_color\", \"type\": [\"string\", \"null\"]}\n"
+ " ]\n" + "}";
private static final org.apache.avro.Schema AVRO_USER_AVRO_SCHEMA_V1 = new Parser()
.parse(AVRO_USER_DEFINITION_SCHEMA_V1);
private static final org.apache.avro.Schema AVRO_USER_AVRO_SCHEMA_V2 = new Parser()
.parse(AVRO_USER_DEFINTITION_SCHEMA_V2);
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;
@Before
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();
}
@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() throws Exception {
Schema schema = new Schema();
schema.setFormat("spring");
schema.setSubject("boot");
ResponseEntity<Schema> response = this.client
.postForEntity(this.serverControllerUri, schema, Schema.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
@Test
public void testInvalidSchema() throws Exception {
Schema schema = new Schema();
schema.setFormat(AVRO_FORMAT_NAME);
schema.setSubject("boot");
schema.setDefinition("{}");
ResponseEntity<Schema> response = this.client
.postForEntity(this.serverControllerUri, schema, Schema.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
@Test
public void testRegister1AvroSchema() {
Schema schema = new Schema();
schema.setFormat(AVRO_FORMAT_NAME);
schema.setSubject("org.springframework.cloud.stream.schema.User");
schema.setDefinition(SchemaRegistryServerAvroTests.AVRO_USER_DEFINITION_SCHEMA_V1);
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);
ResponseEntity<Schema> response = this.client
.getForEntity(findByIdUriId1, Schema.class);
final HttpStatus statusCode = response.getStatusCode();
assertThat(statusCode).isEqualTo(HttpStatus.NOT_FOUND);
}
@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
public void testSchemaNotfound() throws Exception {
ResponseEntity<Schema> response = this.client
.getForEntity("http://localhost:8990/foo/avro/v42", Schema.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void testSchemaDeletionBySubjectFormatVersion() throws Exception {
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);
ResponseEntity<Schema> findBySubjectFormatVersionUriResponse = this.client
.getForEntity(subjectFormatVersionUri, Schema.class);
assertThat(findBySubjectFormatVersionUriResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void testSchemaDeletionBySubjectFormatVersionNotFound() throws Exception {
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"));
ResponseEntity<Void> deleteResponse = this.client.exchange(
new RequestEntity<>(HttpMethod.DELETE, subjectFormatVersionUri),
Void.class);
assertThat(deleteResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void testSchemaDeletionBySubjectFormatVersionNotAllowed() throws Exception {
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
URI versionUri = this.serverControllerUri
.resolve(registerSchemaAndAssertSuccess.getHeaders().getLocation());
ResponseEntity<Void> deleteResponse = this.client.exchange(new RequestEntity<>(HttpMethod.DELETE, versionUri),
Void.class);
assertThat(deleteResponse.getStatusCode()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
}
@Test
public void testSchemaDeletionById() throws Exception {
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()));
ResponseEntity<Schema> response3 = this.client
.getForEntity(registerSchemaAndAssertSuccess.getHeaders().getLocation(), Schema.class);
assertThat(response3.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void testSchemaDeletionByIdNotFound() throws Exception {
registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
this.schemaServerProperties.setAllowSchemaDeletion(true);
ResponseEntity<Void> deleteByIdResponse = this.client.exchange(
new RequestEntity<>(HttpMethod.DELETE, this.serverControllerUri
.resolve("/schemas/" + 2)),
Void.class);
assertThat(deleteByIdResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void testSchemaDeletionByIdNotAllowed() throws Exception {
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
URI schemaIdUri = this.serverControllerUri
.resolve(this.serverControllerUri
.resolve("/schemas/" + registerSchemaAndAssertSuccess.getBody().getVersion()));
ResponseEntity<Void> exchange = this.client.exchange(new RequestEntity<>(HttpMethod.DELETE, schemaIdUri),
Void.class);
assertThat(exchange.getStatusCode()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
}
@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 -> {
ResponseEntity<Schema> shouldBe404Response = this.client.getForEntity(
registerSchemaResponse.getHeaders().getLocation(),
Schema.class);
assertThat(shouldBe404Response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
});
});
});
}
@Test
public void testSchemaDeletionBySubjectNotFound() throws Exception {
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() throws Exception {
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
Schema schema = registerSchemaAndAssertSuccess.getBody();
ResponseEntity<Void> deleteBySubject = this.client.exchange(
new RequestEntity<>(HttpMethod.DELETE, this.serverControllerUri
.resolve("/" + schema.getSubject())),
Void.class);
assertThat(deleteBySubject.getStatusCode())
.isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
}
@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);
@SuppressWarnings("deprecation")
final ResponseEntity<List<Schema>> findBySubjectAndVersionResponseEntity = this.serverController
.findBySubjectAndVersion(subject, format);
assertThat(findBySubjectAndVersionResponseEntity.getStatusCode().is2xxSuccessful()).isTrue();
final List<Schema> schemaResponseBody = findBySubjectAndVersionResponseEntity.getBody();
assertThat(schemaResponseBody)
.<Schema>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)
.<Schema>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);
HttpStatus 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);
HttpStatus 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);
}
}
}

View File

@@ -1,45 +0,0 @@
/*
* 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.server.entityScanning;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.schema.server.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

@@ -1,47 +0,0 @@
/*
* 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.server.entityScanning;
import org.junit.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.server.EnableSchemaRegistryServer;
import org.springframework.context.ConfigurableApplicationContext;
/**
* @author Marius Bogoevici
*/
public class EntityScanningTestsWithEntityScan {
@Test
public void testApplicationWithEmbeddedSchemaRegistryServerOutsideOfRootPackage()
throws Exception {
final ConfigurableApplicationContext context = SpringApplication
.run(CustomApplicationEmbeddingSchemaServer.class, "--server.port=0");
context.close();
}
@EnableAutoConfiguration
@EnableSchemaRegistryServer
@EntityScan(basePackages = "org.springframework.cloud.stream.schema.server.entityScanning.domain")
public static class CustomApplicationEmbeddingSchemaServer {
}
}

View File

@@ -1,51 +0,0 @@
/*
* 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.server.entityScanning.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.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

@@ -1,103 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
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-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.0.0.BUILD-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-stream-schema</artifactId>
<properties>
<avro.version>1.8.1</avro.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>${avro.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-schema-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-avro</artifactId>
<scope>test</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

@@ -1,65 +0,0 @@
/*
* 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;
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

@@ -1,28 +0,0 @@
/*
* 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;
/**
* @author Vinicius Carvalho
*/
public class SchemaNotFoundException extends RuntimeException {
public SchemaNotFoundException(String message) {
super(message);
}
}

View File

@@ -1,105 +0,0 @@
/*
* 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;
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

@@ -1,44 +0,0 @@
/*
* 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;
/**
* @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

@@ -1,146 +0,0 @@
/*
* 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.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

@@ -1,107 +0,0 @@
/*
* 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.avro;
import java.lang.reflect.Constructor;
import org.springframework.beans.factory.annotation.Autowired;
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.annotation.StreamMessageConverter;
import org.springframework.cloud.stream.schema.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
*/
@Configuration
@ConditionalOnClass(name = "org.apache.avro.Schema")
@ConditionalOnProperty(value = "spring.cloud.stream.schemaRegistryClient.enabled", matchIfMissing = true)
@ConditionalOnBean(type = "org.springframework.cloud.stream.schema.client.SchemaRegistryClient")
@EnableConfigurationProperties({ AvroMessageConverterProperties.class })
@Import(AvroSchemaServiceManagerImpl.class)
public class AvroMessageConverterAutoConfiguration {
@Autowired
private AvroMessageConverterProperties avroMessageConverterProperties;
@Autowired
private AvroSchemaServiceManager avroSchemaServiceManager;
@Bean
@ConditionalOnMissingBean(AvroSchemaRegistryClientMessageConverter.class)
@StreamMessageConverter
public AvroSchemaRegistryClientMessageConverter avroSchemaMessageConverter(
SchemaRegistryClient schemaRegistryClient) {
AvroSchemaRegistryClientMessageConverter avroSchemaRegistryClientMessageConverter;
avroSchemaRegistryClientMessageConverter = new AvroSchemaRegistryClientMessageConverter(
schemaRegistryClient, cacheManager(), avroSchemaServiceManager);
avroSchemaRegistryClientMessageConverter.setDynamicSchemaGenerationEnabled(
this.avroMessageConverterProperties.isDynamicSchemaGenerationEnabled());
if (this.avroMessageConverterProperties.getReaderSchema() != null) {
avroSchemaRegistryClientMessageConverter.setReaderSchema(
this.avroMessageConverterProperties.getReaderSchema());
}
if (!ObjectUtils
.isEmpty(this.avroMessageConverterProperties.getSchemaLocations())) {
avroSchemaRegistryClientMessageConverter.setSchemaLocations(
this.avroMessageConverterProperties.getSchemaLocations());
}
if (!ObjectUtils
.isEmpty(this.avroMessageConverterProperties.getSchemaImports())) {
avroSchemaRegistryClientMessageConverter.setSchemaImports(
this.avroMessageConverterProperties.getSchemaImports());
}
avroSchemaRegistryClientMessageConverter
.setPrefix(this.avroMessageConverterProperties.getPrefix());
try {
Class<?> clazz = this.avroMessageConverterProperties
.getSubjectNamingStrategy();
Constructor constructor = ReflectionUtils.accessibleConstructor(clazz);
avroSchemaRegistryClientMessageConverter.setSubjectNamingStrategy(
(SubjectNamingStrategy) constructor.newInstance());
}
catch (Exception ex) {
throw new IllegalStateException("Unable to create SubjectNamingStrategy "
+ this.avroMessageConverterProperties.getSubjectNamingStrategy()
.toString(),
ex);
}
return avroSchemaRegistryClientMessageConverter;
}
@Bean
@ConditionalOnMissingBean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
}

View File

@@ -1,107 +0,0 @@
/*
* 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.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
*/
@ConfigurationProperties(prefix = "spring.cloud.stream.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 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;
}
}

View File

@@ -1,152 +0,0 @@
/*
* 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.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

@@ -1,424 +0,0 @@
/*
* 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.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.ParsedSchema;
import org.springframework.cloud.stream.schema.SchemaNotFoundException;
import org.springframework.cloud.stream.schema.SchemaReference;
import org.springframework.cloud.stream.schema.SchemaRegistrationResponse;
import org.springframework.cloud.stream.schema.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 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;
}
@Override
public void afterPropertiesSet() throws Exception {
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 -> {
this.logger.info("Scanning avro schema resources on classpath");
if (this.logger.isInfoEnabled()) {
this.logger.info("Parsing" + this.schemaImports.length);
}
}).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(Schema schema) {
return this.subjectNamingStrategy.toSubject(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(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) {
if (this.readerSchema == null) {
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(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

@@ -1,75 +0,0 @@
/*
* 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 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

@@ -1,174 +0,0 @@
/*
* 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 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 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"})
@Override
public DatumReader<Object> getDatumReader(Class<?> type, Schema schema, Schema writerSchema) {
DatumReader<Object> reader = null;
if (SpecificRecord.class.isAssignableFrom(type)) {
if (schema != null) {
if (writerSchema != null) {
reader = new SpecificDatumReader<>(writerSchema, schema);
}
else {
reader = new SpecificDatumReader<>(schema);
}
}
else {
reader = new SpecificDatumReader(type);
if (writerSchema != null) {
reader.setSchema(writerSchema);
}
}
}
else if (GenericRecord.class.isAssignableFrom(type)) {
if (schema != null) {
if (writerSchema != null) {
reader = new GenericDatumReader<>(writerSchema, schema);
}
else {
reader = new GenericDatumReader<>(schema);
}
}
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

@@ -1,31 +0,0 @@
/*
* 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.avro;
import org.apache.avro.Schema;
/**
* @author David Kalosi
*/
public class DefaultSubjectNamingStrategy implements SubjectNamingStrategy {
@Override
public String toSubject(Schema schema) {
return schema.getName().toLowerCase();
}
}

View File

@@ -1,59 +0,0 @@
/*
* 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.avro;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.springframework.cloud.stream.binder.BinderHeaders;
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 ConcurrentMap<String, MimeType> mimeTypeCache = new ConcurrentHashMap<>();
@Override
public MimeType resolve(MessageHeaders headers) {
Object contentType = headers
.get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE) != null
? headers.get(BinderHeaders.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

@@ -1,32 +0,0 @@
/*
* 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.Schema;
/**
* @author José A. Íñigo
* @since 2.2.0
*/
public class QualifiedSubjectNamingStrategy implements SubjectNamingStrategy {
@Override
public String toSubject(Schema schema) {
return schema.getFullName().toLowerCase();
}
}

View File

@@ -1,36 +0,0 @@
/*
* 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.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 schema schema to register
* @return subject name
*/
String toSubject(Schema schema);
}

View File

@@ -1,69 +0,0 @@
/*
* 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.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.SchemaReference;
import org.springframework.cloud.stream.schema.SchemaRegistrationResponse;
import org.springframework.util.Assert;
/**
* @author Vinicius Carvalho
*/
public class CachingRegistryClient implements SchemaRegistryClient {
private static final String CACHE_PREFIX = "org.springframework.cloud.stream.schema.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

@@ -1,160 +0,0 @@
/*
* 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.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.SchemaNotFoundException;
import org.springframework.cloud.stream.schema.SchemaReference;
import org.springframework.cloud.stream.schema.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.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("subject", subject);
maps.put("format", format);
maps.put("definition", 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,
HttpMethod.POST, request, Map.class);
id = (Integer) response.getBody().get("id");
version = (Integer) ((Map) response.getBody()).get("version");
}
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("/%s/%s/v%d",
schemaReference.getSubject(), schemaReference.getFormat(), 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/%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

@@ -1,109 +0,0 @@
/*
* 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.client;
import java.util.HashMap;
import java.util.Map;
import org.springframework.cloud.stream.schema.SchemaReference;
import org.springframework.cloud.stream.schema.SchemaRegistrationResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;
/**
* @author Marius Bogoevici
* @author Vinicius Carvalho
*/
public class DefaultSchemaRegistryClient implements SchemaRegistryClient {
private RestTemplate restTemplate;
private String endpoint = "http://localhost:8990";
public DefaultSchemaRegistryClient() {
this(new RestTemplate());
}
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

@@ -1,41 +0,0 @@
/*
* 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.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.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

@@ -1,54 +0,0 @@
/*
* 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.client;
import org.springframework.cloud.stream.schema.SchemaReference;
import org.springframework.cloud.stream.schema.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

@@ -1,58 +0,0 @@
/*
* 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.client.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.stream.schema.client.CachingRegistryClient;
import org.springframework.cloud.stream.schema.client.DefaultSchemaRegistryClient;
import org.springframework.cloud.stream.schema.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 {
@Autowired
private SchemaRegistryClientProperties schemaRegistryClientProperties;
@Bean
@ConditionalOnMissingBean
public SchemaRegistryClient schemaRegistryClient() {
DefaultSchemaRegistryClient defaultSchemaRegistryClient = new DefaultSchemaRegistryClient();
if (StringUtils.hasText(this.schemaRegistryClientProperties.getEndpoint())) {
defaultSchemaRegistryClient
.setEndpoint(this.schemaRegistryClientProperties.getEndpoint());
}
SchemaRegistryClient client = (this.schemaRegistryClientProperties.isCached())
? new CachingRegistryClient(defaultSchemaRegistryClient)
: defaultSchemaRegistryClient;
return client;
}
}

View File

@@ -1,48 +0,0 @@
/*
* 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.client.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Marius Bogoevici
* @author Vinicius Carvalho
*/
@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

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

View File

@@ -1,224 +0,0 @@
/*
* 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.schema.avro;
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.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.cache.support.NoOpCacheManager;
import org.springframework.cloud.stream.binder.BinderHeaders;
import org.springframework.cloud.stream.schema.SchemaReference;
import org.springframework.cloud.stream.schema.avro.AvroSchemaRegistryClientMessageConverter;
import org.springframework.cloud.stream.schema.avro.AvroSchemaServiceManager;
import org.springframework.cloud.stream.schema.avro.AvroSchemaServiceManagerImpl;
import org.springframework.cloud.stream.schema.avro.DefaultSubjectNamingStrategy;
import org.springframework.cloud.stream.schema.client.DefaultSchemaRegistryClient;
import org.springframework.cloud.stream.schema.client.SchemaRegistryClient;
import org.springframework.cloud.stream.schema.server.SchemaRegistryServerApplication;
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;
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;
}
@Before
public void setup() {
this.schemaRegistryServerContext = SpringApplication.run(
SchemaRegistryServerApplication.class,
"--spring.main.allow-bean-definition-overriding=true");
}
@After
public void tearDown() {
this.schemaRegistryServerContext.close();
}
@Test
public void testSchemaImport() throws Exception {
SchemaRegistryClient client = new DefaultSchemaRegistryClient();
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();
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);
}
@Test
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();
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)
.setHeader(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE,
"application/vnd.user.v1+avro")
.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;
}
}

View File

@@ -1,270 +0,0 @@
/*
* 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.schema.avro;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.annotation.StreamMessageConverter;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.cloud.stream.schema.avro.AvroSchemaMessageConverter;
import org.springframework.cloud.stream.schema.avro.AvroSchemaServiceManager;
import org.springframework.cloud.stream.schema.avro.AvroSchemaServiceManagerImpl;
import org.springframework.cloud.stream.schema.client.SchemaRegistryClient;
import org.springframework.cloud.stream.test.binder.MessageCollector;
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 Marius Bogoevici
*/
public class AvroSchemaMessageConverterTests {
static StubSchemaRegistryClient stubSchemaRegistryClient = new StubSchemaRegistryClient();
@Test
public void testSendMessageWithLocation() throws Exception {
ConfigurableApplicationContext sourceContext = SpringApplication.run(
AvroSourceApplication.class, "--server.port=0",
"--spring.jmx.enabled=false",
"--schemaLocation=classpath:schemas/users_v1.schema",
"--spring.cloud.stream.schemaRegistryClient.enabled=false",
"--spring.cloud.stream.bindings.output.contentType=avro/bytes");
Source source = sourceContext.getBean(Source.class);
User1 firstOutboundFoo = new User1();
firstOutboundFoo.setName("foo" + UUID.randomUUID().toString());
firstOutboundFoo.setFavoriteColor("foo" + UUID.randomUUID().toString());
source.output().send(MessageBuilder.withPayload(firstOutboundFoo).build());
MessageCollector sourceMessageCollector = sourceContext
.getBean(MessageCollector.class);
Message<?> outboundMessage = sourceMessageCollector.forChannel(source.output())
.poll(1000, TimeUnit.MILLISECONDS);
ConfigurableApplicationContext barSourceContext = SpringApplication.run(
AvroSourceApplication.class, "--server.port=0",
"--spring.jmx.enabled=false",
"--schemaLocation=classpath:schemas/users_v1.schema",
"--spring.cloud.stream.schemaRegistryClient.enabled=false",
"--spring.cloud.stream.bindings.output.contentType=avro/bytes");
Source barSource = barSourceContext.getBean(Source.class);
User2 firstOutboundUser2 = new User2();
firstOutboundUser2.setFavoriteColor("foo" + UUID.randomUUID().toString());
firstOutboundUser2.setFavoritePlace("foo" + UUID.randomUUID().toString());
firstOutboundUser2.setName("foo" + UUID.randomUUID().toString());
barSource.output().send(MessageBuilder.withPayload(firstOutboundUser2).build());
MessageCollector barSourceMessageCollector = barSourceContext
.getBean(MessageCollector.class);
Message<?> barOutboundMessage = barSourceMessageCollector
.forChannel(barSource.output()).poll(1000, TimeUnit.MILLISECONDS);
assertThat(barOutboundMessage).isNotNull();
User2 secondUser2OutboundPojo = new User2();
secondUser2OutboundPojo.setFavoriteColor("foo" + UUID.randomUUID().toString());
secondUser2OutboundPojo.setFavoritePlace("foo" + UUID.randomUUID().toString());
secondUser2OutboundPojo.setName("foo" + UUID.randomUUID().toString());
source.output().send(MessageBuilder.withPayload(secondUser2OutboundPojo).build());
Message<?> secondBarOutboundMessage = sourceMessageCollector
.forChannel(source.output()).poll(1000, TimeUnit.MILLISECONDS);
ConfigurableApplicationContext sinkContext = SpringApplication.run(
AvroSinkApplication.class, "--server.port=0",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.schemaRegistryClient.enabled=false",
"--schemaLocation=classpath:schemas/users_v1.schema");
Sink sink = sinkContext.getBean(Sink.class);
sink.input().send(outboundMessage);
sink.input().send(barOutboundMessage);
sink.input().send(secondBarOutboundMessage);
List<User1> receivedUsers = sinkContext
.getBean(AvroSinkApplication.class).receivedUsers;
assertThat(receivedUsers).hasSize(3);
assertThat(receivedUsers.get(0)).isNotSameAs(firstOutboundFoo);
assertThat(receivedUsers.get(0).getFavoriteColor())
.isEqualTo(firstOutboundFoo.getFavoriteColor());
assertThat(receivedUsers.get(0).getName()).isEqualTo(firstOutboundFoo.getName());
assertThat(receivedUsers.get(1)).isNotSameAs(firstOutboundUser2);
assertThat(receivedUsers.get(1).getFavoriteColor())
.isEqualTo(firstOutboundUser2.getFavoriteColor());
assertThat(receivedUsers.get(1).getName())
.isEqualTo(firstOutboundUser2.getName());
assertThat(receivedUsers.get(2)).isNotSameAs(secondUser2OutboundPojo);
assertThat(receivedUsers.get(2).getFavoriteColor())
.isEqualTo(secondUser2OutboundPojo.getFavoriteColor());
assertThat(receivedUsers.get(2).getName())
.isEqualTo(secondUser2OutboundPojo.getName());
sourceContext.close();
}
@Test
public void testSendMessageWithoutLocation() throws Exception {
ConfigurableApplicationContext sourceContext = SpringApplication.run(
AvroSourceApplication.class, "--server.port=0",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.schemaRegistryClient.enabled=false",
"--spring.cloud.stream.bindings.output.contentType=avro/bytes");
Source source = sourceContext.getBean(Source.class);
User1 firstOutboundFoo = new User1();
firstOutboundFoo.setName("foo" + UUID.randomUUID().toString());
firstOutboundFoo.setFavoriteColor("foo" + UUID.randomUUID().toString());
source.output().send(MessageBuilder.withPayload(firstOutboundFoo).build());
MessageCollector sourceMessageCollector = sourceContext
.getBean(MessageCollector.class);
Message<?> outboundMessage = sourceMessageCollector.forChannel(source.output())
.poll(1000, TimeUnit.MILLISECONDS);
ConfigurableApplicationContext barSourceContext = SpringApplication.run(
AvroSourceApplication.class, "--server.port=0",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.schemaRegistryClient.enabled=false",
"--spring.cloud.stream.bindings.output.contentType=avro/bytes");
Source barSource = barSourceContext.getBean(Source.class);
User2 firstOutboundUser2 = new User2();
firstOutboundUser2.setFavoriteColor("foo" + UUID.randomUUID().toString());
firstOutboundUser2.setFavoritePlace("foo" + UUID.randomUUID().toString());
firstOutboundUser2.setName("foo" + UUID.randomUUID().toString());
barSource.output().send(MessageBuilder.withPayload(firstOutboundUser2).build());
MessageCollector barSourceMessageCollector = barSourceContext
.getBean(MessageCollector.class);
Message<?> barOutboundMessage = barSourceMessageCollector
.forChannel(barSource.output()).poll(1000, TimeUnit.MILLISECONDS);
assertThat(barOutboundMessage).isNotNull();
User2 secondUser2OutboundPojo = new User2();
secondUser2OutboundPojo.setFavoriteColor("foo" + UUID.randomUUID().toString());
secondUser2OutboundPojo.setFavoritePlace("foo" + UUID.randomUUID().toString());
secondUser2OutboundPojo.setName("foo" + UUID.randomUUID().toString());
source.output().send(MessageBuilder.withPayload(secondUser2OutboundPojo).build());
Message<?> secondBarOutboundMessage = sourceMessageCollector
.forChannel(source.output()).poll(1000, TimeUnit.MILLISECONDS);
ConfigurableApplicationContext sinkContext = SpringApplication.run(
AvroSinkApplication.class, "--server.port=0",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.schemaRegistryClient.enabled=false");
Sink sink = sinkContext.getBean(Sink.class);
sink.input().send(outboundMessage);
sink.input().send(barOutboundMessage);
sink.input().send(secondBarOutboundMessage);
List<User1> receivedUsers = sinkContext
.getBean(AvroSinkApplication.class).receivedUsers;
assertThat(receivedUsers).hasSize(3);
assertThat(receivedUsers.get(0)).isNotSameAs(firstOutboundFoo);
assertThat(receivedUsers.get(0).getFavoriteColor())
.isEqualTo(firstOutboundFoo.getFavoriteColor());
assertThat(receivedUsers.get(0).getName()).isEqualTo(firstOutboundFoo.getName());
assertThat(receivedUsers.get(1)).isNotSameAs(firstOutboundUser2);
assertThat(receivedUsers.get(1).getFavoriteColor())
.isEqualTo(firstOutboundUser2.getFavoriteColor());
assertThat(receivedUsers.get(1).getName())
.isEqualTo(firstOutboundUser2.getName());
assertThat(receivedUsers.get(2)).isNotSameAs(secondUser2OutboundPojo);
assertThat(receivedUsers.get(2).getFavoriteColor())
.isEqualTo(secondUser2OutboundPojo.getFavoriteColor());
assertThat(receivedUsers.get(2).getName())
.isEqualTo(secondUser2OutboundPojo.getName());
sourceContext.close();
}
@EnableBinding(Source.class)
@EnableAutoConfiguration
@ConfigurationProperties
public static class AvroSourceApplication {
private Resource schemaLocation;
@Bean
public SchemaRegistryClient schemaRegistryClient() {
return stubSchemaRegistryClient;
}
public void setSchemaLocation(Resource schemaLocation) {
this.schemaLocation = schemaLocation;
}
@Bean
@StreamMessageConverter
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;
}
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
@ConfigurationProperties
public static class AvroSinkApplication {
public List<User1> receivedUsers = new ArrayList<>();
private Resource schemaLocation;
@StreamListener(Sink.INPUT)
public void listen(User1 user) {
this.receivedUsers.add(user);
}
public void setSchemaLocation(Resource schemaLocation) {
this.schemaLocation = schemaLocation;
}
@Bean
@StreamMessageConverter
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

@@ -1,274 +0,0 @@
/*
* 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.schema.avro;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import example.avro.Command;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
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.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.annotation.StreamMessageConverter;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.cloud.stream.schema.avro.AvroSchemaRegistryClientMessageConverter;
import org.springframework.cloud.stream.schema.avro.AvroSchemaServiceManager;
import org.springframework.cloud.stream.schema.avro.AvroSchemaServiceManagerImpl;
import org.springframework.cloud.stream.schema.client.DefaultSchemaRegistryClient;
import org.springframework.cloud.stream.schema.client.EnableSchemaRegistryClient;
import org.springframework.cloud.stream.schema.client.SchemaRegistryClient;
import org.springframework.cloud.stream.schema.server.SchemaRegistryServerApplication;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.util.ReflectionTestUtils;
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.schema.avro.AvroMessageConverterSerializationTests.notification;
/**
* @author Marius Bogoevici
* @author Oleg Zhurakousky
* @author Sercan Karaoglu
* @author James Gee
*/
public class AvroSchemaRegistryClientMessageConverterTests {
static SchemaRegistryClient stubSchemaRegistryClient = new StubSchemaRegistryClient();
private ConfigurableApplicationContext schemaRegistryServerContext;
@Before
public void setup() {
this.schemaRegistryServerContext = SpringApplication.run(
SchemaRegistryServerApplication.class,
"--spring.main.allow-bean-definition-overriding=true");
}
@After
public void tearDown() {
this.schemaRegistryServerContext.close();
}
@Test
public void testSendMessage() throws Exception {
ConfigurableApplicationContext sourceContext = SpringApplication.run(
AvroSourceApplication.class, "--server.port=0",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.output.contentType=application/*+avro",
"--spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled=true");
Source source = sourceContext.getBean(Source.class);
User1 firstOutboundFoo = new User1();
firstOutboundFoo.setFavoriteColor("foo" + UUID.randomUUID().toString());
firstOutboundFoo.setName("foo" + UUID.randomUUID().toString());
source.output().send(MessageBuilder.withPayload(firstOutboundFoo).build());
MessageCollector sourceMessageCollector = sourceContext
.getBean(MessageCollector.class);
Message<?> outboundMessage = sourceMessageCollector.forChannel(source.output())
.poll(1000, TimeUnit.MILLISECONDS);
ConfigurableApplicationContext barSourceContext = SpringApplication.run(
AvroSourceApplication.class, "--server.port=0",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.output.contentType=application/vnd.user1.v1+avro",
"--spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled=true");
Source barSource = barSourceContext.getBean(Source.class);
User2 firstOutboundUser2 = new User2();
firstOutboundUser2.setFavoriteColor("foo" + UUID.randomUUID().toString());
firstOutboundUser2.setName("foo" + UUID.randomUUID().toString());
barSource.output().send(MessageBuilder.withPayload(firstOutboundUser2).build());
MessageCollector barSourceMessageCollector = barSourceContext
.getBean(MessageCollector.class);
Message<?> barOutboundMessage = barSourceMessageCollector
.forChannel(barSource.output()).poll(1000, TimeUnit.MILLISECONDS);
assertThat(barOutboundMessage).isNotNull();
User2 secondBarOutboundPojo = new User2();
secondBarOutboundPojo.setFavoriteColor("foo" + UUID.randomUUID().toString());
secondBarOutboundPojo.setName("foo" + UUID.randomUUID().toString());
source.output().send(MessageBuilder.withPayload(secondBarOutboundPojo).build());
Message<?> secondBarOutboundMessage = sourceMessageCollector
.forChannel(source.output()).poll(1000, TimeUnit.MILLISECONDS);
ConfigurableApplicationContext sinkContext = SpringApplication.run(
AvroSinkApplication.class, "--server.port=0",
"--spring.jmx.enabled=false");
Sink sink = sinkContext.getBean(Sink.class);
sink.input().send(outboundMessage);
sink.input().send(barOutboundMessage);
sink.input().send(secondBarOutboundMessage);
List<User2> receivedPojos = sinkContext
.getBean(AvroSinkApplication.class).receivedPojos;
assertThat(receivedPojos).hasSize(3);
assertThat(receivedPojos.get(0)).isNotSameAs(firstOutboundFoo);
assertThat(receivedPojos.get(0).getFavoriteColor())
.isEqualTo(firstOutboundFoo.getFavoriteColor());
assertThat(receivedPojos.get(0).getName()).isEqualTo(firstOutboundFoo.getName());
assertThat(receivedPojos.get(0).getFavoritePlace()).isEqualTo("NYC");
assertThat(receivedPojos.get(1)).isNotSameAs(firstOutboundUser2);
assertThat(receivedPojos.get(1).getFavoriteColor())
.isEqualTo(firstOutboundUser2.getFavoriteColor());
assertThat(receivedPojos.get(1).getName())
.isEqualTo(firstOutboundUser2.getName());
assertThat(receivedPojos.get(1).getFavoritePlace()).isEqualTo("Boston");
assertThat(receivedPojos.get(2)).isNotSameAs(secondBarOutboundPojo);
assertThat(receivedPojos.get(2).getFavoriteColor())
.isEqualTo(secondBarOutboundPojo.getFavoriteColor());
assertThat(receivedPojos.get(2).getName())
.isEqualTo(secondBarOutboundPojo.getName());
assertThat(receivedPojos.get(2).getFavoritePlace())
.isEqualTo(secondBarOutboundPojo.getFavoritePlace());
sinkContext.close();
barSourceContext.close();
sourceContext.close();
this.schemaRegistryServerContext.close();
}
@Test
public void testSchemaImportConfiguration() throws Exception {
final String[] args = { "--server.port=0", "--spring.jmx.enabled=false",
"--spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled=true",
"--spring.cloud.stream.bindings.output.contentType=application/*+avro",
"--spring.cloud.stream.bindings.output.destination=test",
"--spring.cloud.stream.bindings.schema-registry-client.endpoint=http://localhost:8990",
"--spring.cloud.stream.schema.avro.schema-locations=classpath:schemas/Command.avsc",
"--spring.cloud.stream.schema.avro.schema-imports=classpath:schemas/imports/Sms.avsc,"
+ " classpath:schemas/imports/Email.avsc, classpath:schemas/imports/PushNotification.avsc" };
final ConfigurableApplicationContext sourceContext = SpringApplication
.run(AvroSourceApplication.class, args);
final ConfigurableApplicationContext sinkContext = SpringApplication
.run(CommandSinkApplication.class, args);
final Source barSource = sourceContext.getBean(Source.class);
final Command notification = notification();
barSource.output().send(MessageBuilder.withPayload(notification).build());
final MessageCollector barSourceMessageCollector = sourceContext
.getBean(MessageCollector.class);
final Message<?> outboundMessage = barSourceMessageCollector
.forChannel(barSource.output()).poll(1000, TimeUnit.MILLISECONDS);
assertThat(outboundMessage).isNotNull();
Sink sink = sinkContext.getBean(Sink.class);
sink.input().send(outboundMessage);
List<Command> receivedPojos = sinkContext
.getBean(CommandSinkApplication.class).receivedPojos;
assertThat(receivedPojos).hasSize(1);
assertThat(receivedPojos.get(0)).isEqualTo(notification);
}
@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(), mockCache, manager);
ReflectionTestUtils.invokeMethod(converter, "getCache", "TEST_CACHE");
verify(mockCache).getCache("TEST_CACHE");
}
@EnableBinding(Source.class)
@EnableAutoConfiguration
@EnableSchemaRegistryClient
public static class AvroSourceApplication {
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
@EnableSchemaRegistryClient
public static class AvroSinkApplication {
public List<User2> receivedPojos = new ArrayList<>();
@StreamListener(Sink.INPUT)
public void listen(User2 fooPojo) {
this.receivedPojos.add(fooPojo);
}
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
@EnableSchemaRegistryClient
public static class CommandSinkApplication {
public List<Command> receivedPojos = new ArrayList<>();
@StreamListener(Sink.INPUT)
public void listen(Command fooPojo) {
this.receivedPojos.add(fooPojo);
}
}
@Configuration
public static class NoCacheConfiguration {
@Bean
@StreamMessageConverter
AvroSchemaRegistryClientMessageConverter avroSchemaRegistryClientMessageConverter() {
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
return new AvroSchemaRegistryClientMessageConverter(
new DefaultSchemaRegistryClient(), new NoOpCacheManager(), manager);
}
@Bean
ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
}

View File

@@ -1,179 +0,0 @@
/*
* 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.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.assertj.core.util.Lists;
import org.junit.Test;
import org.springframework.cloud.schema.avro.domain.FoodOrder;
import org.springframework.cloud.stream.schema.avro.AvroSchemaMessageConverter;
import org.springframework.cloud.stream.schema.avro.AvroSchemaServiceManager;
import org.springframework.cloud.stream.schema.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.fail;
/**
* @author Ish Mahajan
*/
public class AvroSchemaServiceManagerTests {
@SuppressWarnings({ "rawtypes", "unchecked", "resource" })
@Test(expected = DataFileWriter.AppendWriteException.class)
public void testWithDefaultImplementation() throws IOException {
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);
System.out.println("De-serialised Successfully : " + foodOrderDeserialized);
}
}
@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(expected = SchemaParseException.class)
public void testAvroSchemaMessageConverterException() {
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]) {
});
}
}

View File

@@ -1,152 +0,0 @@
/*
* 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.schema.avro;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.cloud.stream.schema.client.SchemaRegistryClient;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Marius Bogoevici
*/
public class AvroStubSchemaRegistryClientMessageConverterTests {
static SchemaRegistryClient stubSchemaRegistryClient = new StubSchemaRegistryClient();
@Test
public void testSendMessage() throws Exception {
ConfigurableApplicationContext sourceContext = SpringApplication.run(
AvroSourceApplication.class, "--server.port=0", "--debug",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.output.contentType=application/*+avro",
"--spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled=true");
Source source = sourceContext.getBean(Source.class);
User1 firstOutboundFoo = new User1();
firstOutboundFoo.setFavoriteColor("foo" + UUID.randomUUID().toString());
firstOutboundFoo.setName("foo" + UUID.randomUUID().toString());
source.output().send(MessageBuilder.withPayload(firstOutboundFoo).build());
MessageCollector sourceMessageCollector = sourceContext
.getBean(MessageCollector.class);
Message<?> outboundMessage = sourceMessageCollector.forChannel(source.output())
.poll(1000, TimeUnit.MILLISECONDS);
ConfigurableApplicationContext barSourceContext = SpringApplication.run(
AvroSourceApplication.class, "--server.port=0",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.output.contentType=application/vnd.user1.v1+avro",
"--spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled=true");
Source barSource = barSourceContext.getBean(Source.class);
User2 firstOutboundUser2 = new User2();
firstOutboundUser2.setFavoriteColor("foo" + UUID.randomUUID().toString());
firstOutboundUser2.setName("foo" + UUID.randomUUID().toString());
barSource.output().send(MessageBuilder.withPayload(firstOutboundUser2).build());
MessageCollector barSourceMessageCollector = barSourceContext
.getBean(MessageCollector.class);
Message<?> barOutboundMessage = barSourceMessageCollector
.forChannel(barSource.output()).poll(1000, TimeUnit.MILLISECONDS);
assertThat(barOutboundMessage).isNotNull();
User2 secondBarOutboundPojo = new User2();
secondBarOutboundPojo.setFavoriteColor("foo" + UUID.randomUUID().toString());
secondBarOutboundPojo.setName("foo" + UUID.randomUUID().toString());
source.output().send(MessageBuilder.withPayload(secondBarOutboundPojo).build());
Message<?> secondBarOutboundMessage = sourceMessageCollector
.forChannel(source.output()).poll(1000, TimeUnit.MILLISECONDS);
ConfigurableApplicationContext sinkContext = SpringApplication.run(
AvroSinkApplication.class, "--server.port=0",
"--spring.jmx.enabled=false");
Sink sink = sinkContext.getBean(Sink.class);
sink.input().send(outboundMessage);
sink.input().send(barOutboundMessage);
sink.input().send(secondBarOutboundMessage);
List<User2> receivedPojos = sinkContext
.getBean(AvroSinkApplication.class).receivedPojos;
assertThat(receivedPojos).hasSize(3);
assertThat(receivedPojos.get(0)).isNotSameAs(firstOutboundFoo);
assertThat(receivedPojos.get(0).getFavoriteColor())
.isEqualTo(firstOutboundFoo.getFavoriteColor());
assertThat(receivedPojos.get(0).getName()).isEqualTo(firstOutboundFoo.getName());
assertThat(receivedPojos.get(0).getFavoritePlace()).isEqualTo("NYC");
assertThat(receivedPojos.get(1)).isNotSameAs(firstOutboundUser2);
assertThat(receivedPojos.get(1).getFavoriteColor())
.isEqualTo(firstOutboundUser2.getFavoriteColor());
assertThat(receivedPojos.get(1).getName())
.isEqualTo(firstOutboundUser2.getName());
assertThat(receivedPojos.get(1).getFavoritePlace()).isEqualTo("Boston");
assertThat(receivedPojos.get(2)).isNotSameAs(secondBarOutboundPojo);
assertThat(receivedPojos.get(2).getFavoriteColor())
.isEqualTo(secondBarOutboundPojo.getFavoriteColor());
assertThat(receivedPojos.get(2).getName())
.isEqualTo(secondBarOutboundPojo.getName());
assertThat(receivedPojos.get(2).getFavoritePlace())
.isEqualTo(secondBarOutboundPojo.getFavoritePlace());
sourceContext.close();
}
@EnableBinding(Source.class)
@EnableAutoConfiguration
public static class AvroSourceApplication {
@Bean
public SchemaRegistryClient schemaRegistryClient() {
return stubSchemaRegistryClient;
}
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class AvroSinkApplication {
public List<User2> receivedPojos = new ArrayList<>();
@StreamListener(Sink.INPUT)
public void listen(User2 fooPojo) {
this.receivedPojos.add(fooPojo);
}
@Bean
public SchemaRegistryClient schemaRegistryClient() {
return stubSchemaRegistryClient;
}
}
}

View File

@@ -1,114 +0,0 @@
/*
* 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.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.SchemaNotFoundException;
import org.springframework.cloud.stream.schema.SchemaReference;
import org.springframework.cloud.stream.schema.SchemaRegistrationResponse;
import org.springframework.cloud.stream.schema.avro.AvroSchemaRegistryClientMessageConverter;
import org.springframework.cloud.stream.schema.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

@@ -1,82 +0,0 @@
/*
* 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.schema.avro;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.cloud.stream.schema.client.SchemaRegistryClient;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.MimeType;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author David Kalosi
* @author José A. Íñigo
*/
public class SubjectNamingStrategyTest {
private static StubSchemaRegistryClient stubSchemaRegistryClient = new StubSchemaRegistryClient();
@Test
public void testQualifiedSubjectNamingStrategy() throws Exception {
ConfigurableApplicationContext sourceContext = SpringApplication.run(
AvroSourceApplication.class, "--server.port=0", "--debug",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.output.contentType=application/*+avro",
"--spring.cloud.stream.schema.avro.subjectNamingStrategy="
+ "org.springframework.cloud.stream.schema.avro.QualifiedSubjectNamingStrategy",
"--spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled=true");
Source source = sourceContext.getBean(Source.class);
User1 user1 = new User1();
user1.setFavoriteColor("foo" + UUID.randomUUID().toString());
user1.setName("foo" + UUID.randomUUID().toString());
source.output().send(MessageBuilder.withPayload(user1).build());
MessageCollector barSourceMessageCollector = sourceContext
.getBean(MessageCollector.class);
Message<?> message = barSourceMessageCollector.forChannel(source.output())
.poll(1000, TimeUnit.MILLISECONDS);
assertThat(message.getHeaders().get("contentType")).isEqualTo(MimeType.valueOf(
"application/vnd.org.springframework.cloud.schema.avro.User1.v1+avro"));
}
@EnableBinding(Source.class)
@EnableAutoConfiguration
public static class AvroSourceApplication {
@Bean
public SchemaRegistryClient schemaRegistryClient() {
return stubSchemaRegistryClient;
}
}
}

View File

@@ -1,58 +0,0 @@
/*
* 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.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

@@ -1,70 +0,0 @@
/*
* 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.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

@@ -1,164 +0,0 @@
/*
* 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.schema.avro.client;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.stream.schema.SchemaNotFoundException;
import org.springframework.cloud.stream.schema.SchemaReference;
import org.springframework.cloud.stream.schema.SchemaRegistrationResponse;
import org.springframework.cloud.stream.schema.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.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;
@Before
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"))
.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));
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(expected = RuntimeException.class)
public void registerWithInvalidJson() {
this.mockRestServiceServer
.expect(requestTo("http://localhost:8081"))
.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", "<>");
}
@Test
public void registerIncompatibleSchema() {
this.mockRestServiceServer
.expect(requestTo("http://localhost:8081"))
.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/user/avro/v1"))
.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(expected = SchemaNotFoundException.class)
public void schemaNotFound() {
this.mockRestServiceServer
.expect(requestTo("http://localhost:8081/user/avro/v1"))
.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");
String schema = client.fetch(reference);
}
@Test
public void responseErrorFetch() {
this.mockRestServiceServer
.expect(requestTo("http://localhost:8081"))
.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();
}
}

View File

@@ -1,44 +0,0 @@
/*
* 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.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

@@ -1,19 +0,0 @@
{
"namespace":"example.avro",
"name":"Command",
"type":"record",
"fields":[
{
"name":"type",
"type":"string"
},
{
"name":"correlationId",
"type":"string"
},
{
"name":"payload",
"type":["Sms", "Email", "PushNotification"]
}
]
}

View File

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

View File

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

View File

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

View File

@@ -1,10 +0,0 @@
{
"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

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

View File

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

View File

@@ -1,10 +0,0 @@
{"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"}
]
}