Add Avro serialization and schema management support
- Add schema server implementation - Add schema client abstraction - Add schema client implementation for own schema registry server - Add schema client supporting Confluent schema registry - Add Avro-based message converter supporting a static schema resource - Add Avro-based message converter with schema evolution support, via schema registry client. - On serialization, the converter register writer schemas with the schema registry server and augment the content type of outbound message with schema information. On deserialization, the reading converter will fetch the schema from the server if not available locally. Use class information if schema is not specified In the case of SpecificRecord and Reflective readers/writers, the class information can be used instead Make subtype prefix configurable and shorten the subject - Subtype prefix is now configurable and subject is the lowercase schema name - Enhance/correct javadoc Refine AbstractAvroMessageConverter - distinguish between writer and reader schema when reader is created Add schema registry and schema registry client docs
This commit is contained in:
committed by
Marius Bogoevici
parent
8dd22ebca0
commit
4422b21438
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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 {
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableSchemaRegistryServer
|
||||
public class SchemaRegistryServerApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SchemaRegistryServerApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.stream.schema.server.controllers.ServerController;
|
||||
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.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
@Configuration
|
||||
@EnableJpaRepositories(basePackageClasses = SchemaRepository.class)
|
||||
@EnableConfigurationProperties(SchemaServerProperties.class)
|
||||
public class SchemaServerConfiguration {
|
||||
|
||||
@Bean
|
||||
public ServerController serverController(SchemaRepository repository) {
|
||||
return new ServerController(repository, schemaValidators());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Map<String, SchemaValidator> schemaValidators() {
|
||||
Map<String, SchemaValidator> validatorMap = new HashMap<>();
|
||||
validatorMap.put("avro", new AvroSchemaValidator());
|
||||
return validatorMap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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
|
||||
*/
|
||||
@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;
|
||||
|
||||
public String getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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 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.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.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(path = "${spring.cloud.stream.schema.server.prefix:}")
|
||||
public class ServerController {
|
||||
|
||||
private final SchemaRepository repository;
|
||||
|
||||
private final Map<String, SchemaValidator> validators;
|
||||
|
||||
public ServerController(SchemaRepository repository,
|
||||
Map<String, SchemaValidator> validators) {
|
||||
Assert.notNull(repository, "cannot be null");
|
||||
Assert.notEmpty(validators, "cannot be empty");
|
||||
this.repository = repository;
|
||||
this.validators = validators;
|
||||
}
|
||||
|
||||
@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 == null || registeredEntities.size() == 0) {
|
||||
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) {
|
||||
Schema schema = this.repository.findOne(id);
|
||||
if (schema == null) {
|
||||
throw new SchemaNotFoundException("Could not find Schema");
|
||||
}
|
||||
return new ResponseEntity<>(schema, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@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) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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, FORWARD, FULL, INCOMPATIBLE;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*
|
||||
* Represents a persisted schema entity.
|
||||
*/
|
||||
@Entity
|
||||
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 id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Integer getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(Integer version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public void setSubject(String subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
public String getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public void setFormat(String format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public String getDefinition() {
|
||||
return definition;
|
||||
}
|
||||
|
||||
public void setDefinition(String definition) {
|
||||
this.definition = definition;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
public interface SchemaRepository extends PagingAndSortingRepository<Schema, Integer> {
|
||||
|
||||
List<Schema> findBySubjectAndFormatOrderByVersion(String subject,
|
||||
String format);
|
||||
|
||||
Schema findOneBySubjectAndFormatAndVersion(String subject, String format,
|
||||
Integer version);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
Compatibility compatibilityCheck(String source, String other);
|
||||
|
||||
/**
|
||||
* Return the Schema that is represented by the definition.
|
||||
* @param schemas List of schemas to be tested
|
||||
* @param definition Textual representation of the schema
|
||||
* @return A full Schema object with identifier and subject properties
|
||||
*/
|
||||
Schema match(List<Schema> schemas, String definition);
|
||||
|
||||
String getFormat();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
spring:
|
||||
application:
|
||||
name: SchemaRegistryServer
|
||||
server:
|
||||
port: 8990
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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.util.List;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.cloud.stream.schema.server.model.Schema;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
|
||||
public class SchemaRegistryServerAvroTests {
|
||||
|
||||
final String USER_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" + "}";
|
||||
|
||||
final String USER_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" + "}";
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate client;
|
||||
|
||||
@Test
|
||||
public void testUnsupportedFormat() throws Exception {
|
||||
Schema schema = new Schema();
|
||||
schema.setFormat("spring");
|
||||
schema.setSubject("boot");
|
||||
ResponseEntity<Schema> response = client.postForEntity("http://localhost:8990/",
|
||||
schema, Schema.class);
|
||||
Assert.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidSchema() throws Exception {
|
||||
Schema schema = new Schema();
|
||||
schema.setFormat("avro");
|
||||
schema.setSubject("boot");
|
||||
schema.setDefinition("{}");
|
||||
ResponseEntity<Schema> response = client.postForEntity("http://localhost:8990/",
|
||||
schema, Schema.class);
|
||||
Assert.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserSchemaV1() throws Exception {
|
||||
Schema schema = new Schema();
|
||||
schema.setFormat("avro");
|
||||
schema.setSubject("org.springframework.cloud.stream.schema.User");
|
||||
schema.setDefinition(USER_SCHEMA_V1);
|
||||
ResponseEntity<Schema> response = client.postForEntity("http://localhost:8990/",
|
||||
schema, Schema.class);
|
||||
Assert.assertTrue(response.getStatusCode().is2xxSuccessful());
|
||||
Assert.assertEquals(new Integer(1), response.getBody().getVersion());
|
||||
List<String> location = response.getHeaders().get(HttpHeaders.LOCATION);
|
||||
Assert.assertNotNull(location);
|
||||
ResponseEntity<Schema> persistedSchema = client.getForEntity(location.get(0),
|
||||
Schema.class);
|
||||
Assert.assertEquals(response.getBody().getId(),
|
||||
persistedSchema.getBody().getId());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserSchemaV2() throws Exception {
|
||||
Schema schema = new Schema();
|
||||
schema.setFormat("avro");
|
||||
schema.setSubject("org.springframework.cloud.stream.schema.User");
|
||||
schema.setDefinition(USER_SCHEMA_V1);
|
||||
|
||||
Schema schema2 = new Schema();
|
||||
schema2.setFormat("avro");
|
||||
schema2.setSubject("org.springframework.cloud.stream.schema.User");
|
||||
schema2.setDefinition(USER_SCHEMA_V2);
|
||||
|
||||
ResponseEntity<Schema> response = client.postForEntity("http://localhost:8990/",
|
||||
schema, Schema.class);
|
||||
Assert.assertTrue(response.getStatusCode().is2xxSuccessful());
|
||||
Assert.assertEquals(new Integer(1), response.getBody().getVersion());
|
||||
List<String> location = response.getHeaders().get(HttpHeaders.LOCATION);
|
||||
Assert.assertNotNull(location);
|
||||
|
||||
ResponseEntity<Schema> response2 = client.postForEntity("http://localhost:8990/",
|
||||
schema2, Schema.class);
|
||||
Assert.assertTrue(response.getStatusCode().is2xxSuccessful());
|
||||
Assert.assertEquals(new Integer(2), response2.getBody().getVersion());
|
||||
List<String> location2 = response2.getHeaders().get(HttpHeaders.LOCATION);
|
||||
Assert.assertNotNull(location2);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIdempotentRegistration() throws Exception {
|
||||
Schema schema = new Schema();
|
||||
schema.setFormat("avro");
|
||||
schema.setSubject("org.springframework.cloud.stream.schema.User");
|
||||
schema.setDefinition(USER_SCHEMA_V1);
|
||||
ResponseEntity<Schema> response = client.postForEntity("http://localhost:8990/",
|
||||
schema, Schema.class);
|
||||
Assert.assertTrue(response.getStatusCode().is2xxSuccessful());
|
||||
Assert.assertEquals(new Integer(1), response.getBody().getVersion());
|
||||
List<String> location = response.getHeaders().get(HttpHeaders.LOCATION);
|
||||
Assert.assertNotNull(location);
|
||||
ResponseEntity<Schema> response2 = client.postForEntity("http://localhost:8990/",
|
||||
schema, Schema.class);
|
||||
Assert.assertEquals(response.getBody().getId(), response2.getBody().getId());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaNotfound() throws Exception {
|
||||
ResponseEntity<Schema> response = client
|
||||
.getForEntity("http://localhost:8990/foo/avro/v42", Schema.class);
|
||||
Assert.assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class Config {
|
||||
@Bean
|
||||
public TestRestTemplate testRestTemplate() {
|
||||
return new TestRestTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user