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:
committed by
Oleg Zhurakousky
parent
ea924ffb95
commit
e870c6505a
8
pom.xml
8
pom.xml
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
spring:
|
||||
application:
|
||||
name: SchemaRegistryServer
|
||||
server:
|
||||
port: 8990
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + '\'' + '}';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.stream.schema.avro.AvroMessageConverterAutoConfiguration
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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]) {
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"namespace":"example.avro",
|
||||
"name": "Email",
|
||||
"type": "record",
|
||||
"fields":[
|
||||
{
|
||||
"name":"addressTo",
|
||||
"type":"string"
|
||||
},
|
||||
{
|
||||
"name":"title",
|
||||
"type":"string"
|
||||
},
|
||||
{
|
||||
"name":"text",
|
||||
"type":"string"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"namespace":"example.avro",
|
||||
"name": "PushNotification",
|
||||
"type": "record",
|
||||
"fields":[
|
||||
{
|
||||
"name":"arn",
|
||||
"type":"string"
|
||||
},
|
||||
{
|
||||
"name":"text",
|
||||
"type":"string"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"namespace":"example.avro",
|
||||
"name": "Sms",
|
||||
"type": "record",
|
||||
"fields":[
|
||||
{
|
||||
"name":"phoneNumber",
|
||||
"type":"string"
|
||||
},{
|
||||
"name":"text",
|
||||
"type":"string"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
@@ -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"]}
|
||||
|
||||
]
|
||||
}
|
||||
@@ -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"]}
|
||||
|
||||
]
|
||||
}
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user