Initial commit

Migrating schema registry code from Spring Cloud Stream core into its own code base
This commit is contained in:
Soby Chacko
2019-08-16 19:11:41 -04:00
commit fb3eabc669
89 changed files with 7070 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
/application.yml
/application.properties
asciidoctor.css
*~
.#*
*#
target/
build/
bin/
_site/
.classpath
.project
.settings
.springBeans
.sts4-cache/
.attach_pid*
.DS_Store
*.sw*
*.iml
*.ipr
*.iws
.idea/*
.factorypath
dump.rdb
.apt_generated
artifacts
**/dependency-reduced-pom.xml

1
.mvn/jvm.config Normal file
View File

@@ -0,0 +1 @@
-Xmx1024m -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom

1
.mvn/maven.config Normal file
View File

@@ -0,0 +1 @@
-DaltSnapshotDeploymentRepository=repo.spring.io::default::https://repo.spring.io/libs-snapshot-local -P spring

110
.mvn/wrapper/MavenWrapperDownloader.java vendored Executable file
View File

@@ -0,0 +1,110 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you 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.
*/
import java.net.*;
import java.io.*;
import java.nio.channels.*;
import java.util.Properties;
public class MavenWrapperDownloader {
/**
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
*/
private static final String DEFAULT_DOWNLOAD_URL =
"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar";
/**
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
* use instead of the default one.
*/
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
".mvn/wrapper/maven-wrapper.properties";
/**
* Path where the maven-wrapper.jar will be saved to.
*/
private static final String MAVEN_WRAPPER_JAR_PATH =
".mvn/wrapper/maven-wrapper.jar";
/**
* Name of the property which should be used to override the default download url for the wrapper.
*/
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
public static void main(String args[]) {
System.out.println("- Downloader started");
File baseDirectory = new File(args[0]);
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
// If the maven-wrapper.properties exists, read it and check if it contains a custom
// wrapperUrl parameter.
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
String url = DEFAULT_DOWNLOAD_URL;
if(mavenWrapperPropertyFile.exists()) {
FileInputStream mavenWrapperPropertyFileInputStream = null;
try {
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
Properties mavenWrapperProperties = new Properties();
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
} catch (IOException e) {
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
} finally {
try {
if(mavenWrapperPropertyFileInputStream != null) {
mavenWrapperPropertyFileInputStream.close();
}
} catch (IOException e) {
// Ignore ...
}
}
}
System.out.println("- Downloading from: : " + url);
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
if(!outputFile.getParentFile().exists()) {
if(!outputFile.getParentFile().mkdirs()) {
System.out.println(
"- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'");
}
}
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
try {
downloadFileFromURL(url, outputFile);
System.out.println("Done");
System.exit(0);
} catch (Throwable e) {
System.out.println("- Error downloading");
e.printStackTrace();
System.exit(1);
}
}
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
URL website = new URL(urlString);
ReadableByteChannel rbc;
rbc = Channels.newChannel(website.openStream());
FileOutputStream fos = new FileOutputStream(destination);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
fos.close();
rbc.close();
}
}

BIN
.mvn/wrapper/maven-wrapper.jar vendored Executable file

Binary file not shown.

1
.mvn/wrapper/maven-wrapper.properties vendored Executable file
View File

@@ -0,0 +1 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip

67
docs/pom.xml Normal file
View File

@@ -0,0 +1,67 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-schema-registry-docs</artifactId>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-scheam-registry-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<packaging>pom</packaging>
<name>Spring Cloud Schema Registry Docs</name>
<description>Spring Cloud Schema Registry Docs</description>
<properties>
<docs.main>spring-cloud-function</docs.main>
<main.basedir>${basedir}/..</main.basedir>
<maven.plugin.plugin.version>3.4</maven.plugin.plugin.version>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>docs</id>
<build>
<plugins>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>${asciidoctor-maven-plugin.version}</version>
<configuration>
<sourceDirectory>${project.build.directory}/refdocs/</sourceDirectory>
<attributes>
<spring-cloud-schema-registry-version>${project.version}</spring-cloud-schema-registry-version>
</attributes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,42 @@
== Spring Cloud Schema Registry
When organizations have a messaging based pub/sub architecture and multiple producer and consumer microservices communicate each other, it is often necessary for all those microservices to agree on a contract that is based on a schema.
When such a schema needs to evolve to accommodate new business requirements, the existing components are still required to continue to work.
This project provides support for a standalone schema registry server using which aforementioned schema can be registered and used by the applications.
It also contains support for avro based schema registry clients, which essentially provide message converters that communicates with the schema registry for reconciling schema during message conversion.
The schema evolution support provided by this project works both with the aforementioned standalone schema registry as well as the scheam registry provided by Confluent that specifically works with Apache Kafka.
==== Spring Cloud Schema Registry overview
Spring Cloud Schema Registry provides support for schema evolution so that the data can be evolved over time and still work with older or newer producers and consumers and vice versa. Most serialization models, especially the ones that aim for portability across different platforms and languages, rely on a schema that describes how the data is serialized in the binary payload. In order to serialize the data and then to interpret it, both the sending and receiving sides must have access to a schema that describes the binary format. In certain cases, the schema can be inferred from the payload type on serialization or from the target type on deserialization.
However, many applications benefit from having access to an explicit schema that describes the binary data format.
A schema registry lets you store schema information in a textual format (typically JSON) and makes that information accessible to various applications that need it to receive and send data in binary format.
A schema is referenceable as a tuple consisting of:
* A subject that is the logical name of the schema
* The schema version
* The schema format, which describes the binary format of the data
Spring Cloud Schema Registry provides the following compoents
* Standalone Schema Registry Server
By default, it is using an H2 database, but server can be used with other databases by providing appropriate datasource configuration.
* Schema registry clients capable of message marshalling by communicating with a Schema Registry.
Currently, the client can communicate to the standalone schema registry or the Confluent Schema Registry.
== Project page
You can read more about Spring Cloud Schema Registry by going to https://spring.io/projects/spring-cloud-schema-registry[the project page]
== Building
include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/building.adoc[]
== Contributing
include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/contributing.adoc[]

330
docs/src/main/asciidoc/ghpages.sh Executable file
View File

@@ -0,0 +1,330 @@
#!/bin/bash -x
set -e
# Set default props like MAVEN_PATH, ROOT_FOLDER etc.
function set_default_props() {
# The script should be executed from the root folder
ROOT_FOLDER=`pwd`
echo "Current folder is ${ROOT_FOLDER}"
if [[ ! -e "${ROOT_FOLDER}/.git" ]]; then
echo "You're not in the root folder of the project!"
exit 1
fi
# Prop that will let commit the changes
COMMIT_CHANGES="no"
MAVEN_PATH=${MAVEN_PATH:-}
echo "Path to Maven is [${MAVEN_PATH}]"
REPO_NAME=${PWD##*/}
echo "Repo name is [${REPO_NAME}]"
SPRING_CLOUD_STATIC_REPO=${SPRING_CLOUD_STATIC_REPO:-git@github.com:spring-cloud/spring-cloud-static.git}
echo "Spring Cloud Static repo is [${SPRING_CLOUD_STATIC_REPO}"
}
# Check if gh-pages exists and docs have been built
function check_if_anything_to_sync() {
git remote set-url --push origin `git config remote.origin.url | sed -e 's/^git:/https:/'`
if ! (git remote set-branches --add origin gh-pages && git fetch -q); then
echo "No gh-pages, so not syncing"
exit 0
fi
if ! [ -d docs/target/generated-docs ] && ! [ "${BUILD}" == "yes" ]; then
echo "No gh-pages sources in docs/target/generated-docs, so not syncing"
exit 0
fi
}
function retrieve_current_branch() {
# Code getting the name of the current branch. For master we want to publish as we did until now
# https://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch
# If there is a branch already passed will reuse it - otherwise will try to find it
CURRENT_BRANCH=${BRANCH}
if [[ -z "${CURRENT_BRANCH}" ]] ; then
CURRENT_BRANCH=$(git symbolic-ref -q HEAD)
CURRENT_BRANCH=${CURRENT_BRANCH##refs/heads/}
CURRENT_BRANCH=${CURRENT_BRANCH:-HEAD}
fi
echo "Current branch is [${CURRENT_BRANCH}]"
git checkout ${CURRENT_BRANCH} || echo "Failed to check the branch... continuing with the script"
}
# Switches to the provided value of the release version. We always prefix it with `v`
function switch_to_tag() {
git checkout v${VERSION}
}
# Build the docs if switch is on
function build_docs_if_applicable() {
if [[ "${BUILD}" == "yes" ]] ; then
./mvnw clean install -P docs -pl docs -DskipTests
fi
}
# Get the name of the `docs.main` property
# Get whitelisted branches - assumes that a `docs` module is available under `docs` profile
function retrieve_doc_properties() {
MAIN_ADOC_VALUE=$("${MAVEN_PATH}"mvn -q \
-Dexec.executable="echo" \
-Dexec.args='${docs.main}' \
--non-recursive \
org.codehaus.mojo:exec-maven-plugin:1.3.1:exec)
echo "Extracted 'main.adoc' from Maven build [${MAIN_ADOC_VALUE}]"
WHITELIST_PROPERTY=${WHITELIST_PROPERTY:-"docs.whitelisted.branches"}
WHITELISTED_BRANCHES_VALUE=$("${MAVEN_PATH}"mvn -q \
-Dexec.executable="echo" \
-Dexec.args="\${${WHITELIST_PROPERTY}}" \
org.codehaus.mojo:exec-maven-plugin:1.3.1:exec \
-P docs \
-pl docs)
echo "Extracted '${WHITELIST_PROPERTY}' from Maven build [${WHITELISTED_BRANCHES_VALUE}]"
}
# Stash any outstanding changes
function stash_changes() {
git diff-index --quiet HEAD && dirty=$? || (echo "Failed to check if the current repo is dirty. Assuming that it is." && dirty="1")
if [ "$dirty" != "0" ]; then git stash; fi
}
# Switch to gh-pages branch to sync it with current branch
function add_docs_from_target() {
local DESTINATION_REPO_FOLDER
if [[ -z "${DESTINATION}" && -z "${CLONE}" ]] ; then
DESTINATION_REPO_FOLDER=${ROOT_FOLDER}
elif [[ "${CLONE}" == "yes" ]]; then
mkdir -p ${ROOT_FOLDER}/target
local clonedStatic=${ROOT_FOLDER}/target/spring-cloud-static
if [[ ! -e "${clonedStatic}/.git" ]]; then
echo "Cloning Spring Cloud Static to target"
git clone ${SPRING_CLOUD_STATIC_REPO} ${clonedStatic} && git checkout gh-pages
else
echo "Spring Cloud Static already cloned - will pull changes"
cd ${clonedStatic} && git checkout gh-pages && git pull origin gh-pages
fi
DESTINATION_REPO_FOLDER=${clonedStatic}/${REPO_NAME}
mkdir -p ${DESTINATION_REPO_FOLDER}
else
if [[ ! -e "${DESTINATION}/.git" ]]; then
echo "[${DESTINATION}] is not a git repository"
exit 1
fi
DESTINATION_REPO_FOLDER=${DESTINATION}/${REPO_NAME}
mkdir -p ${DESTINATION_REPO_FOLDER}
echo "Destination was provided [${DESTINATION}]"
fi
cd ${DESTINATION_REPO_FOLDER}
git checkout gh-pages
git pull origin gh-pages
# Add git branches
###################################################################
if [[ -z "${VERSION}" ]] ; then
copy_docs_for_current_version
else
copy_docs_for_provided_version
fi
commit_changes_if_applicable
}
# Copies the docs by using the retrieved properties from Maven build
function copy_docs_for_current_version() {
if [[ "${CURRENT_BRANCH}" == "master" ]] ; then
echo -e "Current branch is master - will copy the current docs only to the root folder"
for f in docs/target/generated-docs/*; do
file=${f#docs/target/generated-docs/*}
if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then
# Not ignored...
cp -rf $f ${ROOT_FOLDER}/
git add -A ${ROOT_FOLDER}/$file
fi
done
COMMIT_CHANGES="yes"
else
echo -e "Current branch is [${CURRENT_BRANCH}]"
# https://stackoverflow.com/questions/29300806/a-bash-script-to-check-if-a-string-is-present-in-a-comma-separated-list-of-strin
if [[ ",${WHITELISTED_BRANCHES_VALUE}," = *",${CURRENT_BRANCH},"* ]] ; then
mkdir -p ${ROOT_FOLDER}/${CURRENT_BRANCH}
echo -e "Branch [${CURRENT_BRANCH}] is whitelisted! Will copy the current docs to the [${CURRENT_BRANCH}] folder"
for f in docs/target/generated-docs/*; do
file=${f#docs/target/generated-docs/*}
if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then
# Not ignored...
# We want users to access 2.0.0.BUILD-SNAPSHOT/ instead of 1.0.0.RELEASE/spring-cloud.sleuth.html
if [[ "${file}" == "${MAIN_ADOC_VALUE}.html" ]] ; then
# We don't want to copy the spring-cloud-sleuth.html
# we want it to be converted to index.html
cp -rf $f ${ROOT_FOLDER}/${CURRENT_BRANCH}/index.html
git add -A ${ROOT_FOLDER}/${CURRENT_BRANCH}/index.html
else
cp -rf $f ${ROOT_FOLDER}/${CURRENT_BRANCH}
git add -A ${ROOT_FOLDER}/${CURRENT_BRANCH}/$file
fi
fi
done
COMMIT_CHANGES="yes"
else
echo -e "Branch [${CURRENT_BRANCH}] is not on the white list! Check out the Maven [${WHITELIST_PROPERTY}] property in
[docs] module available under [docs] profile. Won't commit any changes to gh-pages for this branch."
fi
fi
}
# Copies the docs by using the explicitly provided version
function copy_docs_for_provided_version() {
local FOLDER=${DESTINATION_REPO_FOLDER}/${VERSION}
mkdir -p ${FOLDER}
echo -e "Current tag is [v${VERSION}] Will copy the current docs to the [${FOLDER}] folder"
for f in ${ROOT_FOLDER}/docs/target/generated-docs/*; do
file=${f#${ROOT_FOLDER}/docs/target/generated-docs/*}
copy_docs_for_branch ${file} ${FOLDER}
done
COMMIT_CHANGES="yes"
CURRENT_BRANCH="v${VERSION}"
}
# Copies the docs from target to the provided destination
# Params:
# $1 - file from target
# $2 - destination to which copy the files
function copy_docs_for_branch() {
local file=$1
local destination=$2
if ! git ls-files -i -o --exclude-standard --directory | grep -q ^${file}$; then
# Not ignored...
# We want users to access 2.0.0.BUILD-SNAPSHOT/ instead of 1.0.0.RELEASE/spring-cloud.sleuth.html
if [[ ("${file}" == "${MAIN_ADOC_VALUE}.html") || ("${file}" == "${REPO_NAME}.html") ]] ; then
# We don't want to copy the spring-cloud-sleuth.html
# we want it to be converted to index.html
cp -rf $f ${destination}/index.html
git add -A ${destination}/index.html
else
cp -rf $f ${destination}
git add -A ${destination}/$file
fi
fi
}
function commit_changes_if_applicable() {
if [[ "${COMMIT_CHANGES}" == "yes" ]] ; then
COMMIT_SUCCESSFUL="no"
git commit -a -m "Sync docs from ${CURRENT_BRANCH} to gh-pages" && COMMIT_SUCCESSFUL="yes" || echo "Failed to commit changes"
# Uncomment the following push if you want to auto push to
# the gh-pages branch whenever you commit to master locally.
# This is a little extreme. Use with care!
###################################################################
if [[ "${COMMIT_SUCCESSFUL}" == "yes" ]] ; then
git push origin gh-pages
fi
fi
}
# Switch back to the previous branch and exit block
function checkout_previous_branch() {
# If -version was provided we need to come back to root project
cd ${ROOT_FOLDER}
git checkout ${CURRENT_BRANCH} || echo "Failed to check the branch... continuing with the script"
if [ "$dirty" != "0" ]; then git stash pop; fi
exit 0
}
# Assert if properties have been properly passed
function assert_properties() {
echo "VERSION [${VERSION}], DESTINATION [${DESTINATION}], CLONE [${CLONE}]"
if [[ "${VERSION}" != "" && (-z "${DESTINATION}" && -z "${CLONE}") ]] ; then echo "Version was set but destination / clone was not!"; exit 1;fi
if [[ ("${DESTINATION}" != "" && "${CLONE}" != "") && -z "${VERSION}" ]] ; then echo "Destination / clone was set but version was not!"; exit 1;fi
if [[ "${DESTINATION}" != "" && "${CLONE}" == "yes" ]] ; then echo "Destination and clone was set. Pick one!"; exit 1;fi
}
# Prints the usage
function print_usage() {
cat <<EOF
The idea of this script is to update gh-pages branch with the generated docs. Without any options
the script will work in the following manner:
- if there's no gh-pages / target for docs module then the script ends
- for master branch the generated docs are copied to the root of gh-pages branch
- for any other branch (if that branch is whitelisted) a subfolder with branch name is created
and docs are copied there
- if the version switch is passed (-v) then a tag with (v) prefix will be retrieved and a folder
with that version number will be created in the gh-pages branch. WARNING! No whitelist verification will take place
- if the destination switch is passed (-d) then the script will check if the provided dir is a git repo and then will
switch to gh-pages of that repo and copy the generated docs to `docs/<project-name>/<version>`
- if the destination switch is passed (-d) then the script will check if the provided dir is a git repo and then will
switch to gh-pages of that repo and copy the generated docs to `docs/<project-name>/<version>`
USAGE:
You can use the following options:
-v|--version - the script will apply the whole procedure for a particular library version
-d|--destination - the root of destination folder where the docs should be copied. You have to use the full path.
E.g. point to spring-cloud-static folder. Can't be used with (-c)
-b|--build - will run the standard build process after checking out the branch
-c|--clone - will automatically clone the spring-cloud-static repo instead of providing the destination.
Obviously can't be used with (-d)
EOF
}
# ==========================================
# ____ ____ _____ _____ _____ _______
# / ____|/ ____| __ \|_ _| __ \__ __|
# | (___ | | | |__) | | | | |__) | | |
# \___ \| | | _ / | | | ___/ | |
# ____) | |____| | \ \ _| |_| | | |
# |_____/ \_____|_| \_\_____|_| |_|
#
# ==========================================
while [[ $# > 0 ]]
do
key="$1"
case ${key} in
-v|--version)
VERSION="$2"
shift # past argument
;;
-d|--destination)
DESTINATION="$2"
shift # past argument
;;
-b|--build)
BUILD="yes"
;;
-c|--clone)
CLONE="yes"
;;
-h|--help)
print_usage
exit 0
;;
*)
echo "Invalid option: [$1]"
print_usage
exit 1
;;
esac
shift # past argument or value
done
assert_properties
set_default_props
check_if_anything_to_sync
if [[ -z "${VERSION}" ]] ; then
retrieve_current_branch
else
switch_to_tag
fi
build_docs_if_applicable
retrieve_doc_properties
stash_changes
add_docs_from_target
checkout_previous_branch

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,16 @@
= Spring Cloud Schema Registry Reference Documentation
Unascribed
*{spring-cloud-schema-registry-version}*
:docinfo: shared
The reference documentation consists of the following sections:
[horizontal]
<<spring-cloud-schema-registry.adoc#,Reference Guide>> :: Spring Cloud Schema Registry Reference
Relevant Links:
[horizontal]
https://spring.io/projects/spring-cloud-stream[Spring Cloud Stream] :: Spring Cloud Stream

View File

@@ -0,0 +1,25 @@
Spring Cloud Schema Registry overview
Spring Cloud Schema Registry provides support for schema evolution so that the data can be evolved over time and still work with older or newer producers and consumers and vice versa. Most serialization models, especially the ones that aim for portability across different platforms and languages, rely on a schema that describes how the data is serialized in the binary payload. In order to serialize the data and then to interpret it, both the sending and receiving sides must have access to a schema that describes the binary format. In certain cases, the schema can be inferred from the payload type on serialization or from the target type on deserialization.
However, many applications benefit from having access to an explicit schema that describes the binary data format.
A schema registry lets you store schema information in a textual format (typically JSON) and makes that information accessible to various applications that need it to receive and send data in binary format.
A schema is referenceable as a tuple consisting of:
* A subject that is the logical name of the schema
* The schema version
* The schema format, which describes the binary format of the data
Spring Cloud Schema Registry provides the following compoents
* Standalone Schema Registry Server
By default, it is using an H2 database, but server can be used with other databases by providing appropriate datasource configuration.
* Schema registry clients capable of message marshalling by communicating with a Schema Registry.
Currently, the client can communicate to the standalone schema registry or the Confluent Schema Registry.

View File

@@ -0,0 +1,366 @@
= Spring Cloud Schema Registry
Unascribed
*{spring-cloud-schema-registry-version}*
---
:github: https://github.com/spring-cloud/spring-cloud-schema-registry
:githubmaster: {github}/tree/master
:docslink: {githubmaster}/docs/src/main/asciidoc
:nofooter:
== Introduction
When organizations have a messaging based pub/sub architecture and multiple producer and consumer microservices communicate each other, it is often necessary for all those microservices to agree on a contract that is based on a schema.
When such a schema needs to evolve to accommodate new business requirements, the existing components are still required to continue to work.
This project provides support for a standalone schema registry server using which aforementioned schema can be registered and used by the applications.
It also contains support for avro based schema registry clients, which essentially provide message converters that communicates with the schema registry for reconciling schema during message conversion.
The schema evolution support provided by this project works both with the aforementioned standalone schema registry as well as the scheam registry provided by Confluent that specifically works with Apache Kafka.
==== Spring Cloud Schema Registry overview
Spring Cloud Schema Registry provides support for schema evolution so that the data can be evolved over time and still work with older or newer producers and consumers and vice versa. Most serialization models, especially the ones that aim for portability across different platforms and languages, rely on a schema that describes how the data is serialized in the binary payload. In order to serialize the data and then to interpret it, both the sending and receiving sides must have access to a schema that describes the binary format. In certain cases, the schema can be inferred from the payload type on serialization or from the target type on deserialization.
However, many applications benefit from having access to an explicit schema that describes the binary data format.
A schema registry lets you store schema information in a textual format (typically JSON) and makes that information accessible to various applications that need it to receive and send data in binary format.
A schema is referenceable as a tuple consisting of:
* A subject that is the logical name of the schema
* The schema version
* The schema format, which describes the binary format of the data
Spring Cloud Schema Registry provides the following compoents
* Standalone Schema Registry Server
By default, it is using an H2 database, but server can be used with other databases by providing appropriate datasource configuration.
* Schema registry clients capable of message marshalling by communicating with a Schema Registry.
Currently, the client can communicate to the standalone schema registry or the Confluent Schema Registry.
=== Schema Registry Client
The client-side abstraction for interacting with schema registry servers is the `SchemaRegistryClient` interface, which has the following structure:
[source,java]
----
public interface SchemaRegistryClient {
SchemaRegistrationResponse register(String subject, String format, String schema);
String fetch(SchemaReference schemaReference);
String fetch(Integer id);
}
----
Spring Cloud Stream provides out-of-the-box implementations for interacting with its own schema server and for interacting with the Confluent Schema Registry.
A client for the Spring Cloud Stream schema registry can be configured by using the `@EnableSchemaRegistryClient`, as follows:
[source,java]
----
@EnableBinding(Sink.class)
@SpringBootApplication
@EnableSchemaRegistryClient
public static class AvroSinkApplication {
...
}
----
NOTE: The default converter is optimized to cache not only the schemas from the remote server but also the `parse()` and `toString()` methods, which are quite expensive.
Because of this, it uses a `DefaultSchemaRegistryClient` that does not cache responses.
If you intend to change the default behavior, you can use the client directly on your code and override it to the desired outcome.
To do so, you have to add the property `spring.cloud.stream.schemaRegistryClient.cached=true` to your application properties.
==== Schema Registry Client Properties
The Schema Registry Client supports the following properties:
`spring.cloud.stream.schemaRegistryClient.endpoint`:: The location of the schema-server.
When setting this, use a full URL, including protocol (`http` or `https`) , port, and context path.
+
Default:: `http://localhost:8990/`
`spring.cloud.stream.schemaRegistryClient.cached`:: Whether the client should cache schema server responses.
Normally set to `false`, as the caching happens in the message converter.
Clients using the schema registry client should set this to `true`.
+
Default:: `false`
=== Avro Schema Registry Client Message Converters
For applications that have a SchemaRegistryClient bean registered with the application context, Spring Cloud Stream auto configures an Apache Avro message converter for schema management.
This eases schema evolution, as applications that receive messages can get easy access to a writer schema that can be reconciled with their own reader schema.
For outbound messages, if the content type of the channel is set to `application/*+avro`, the `MessageConverter` is activated, as shown in the following example:
[source,properties]
----
spring.cloud.stream.bindings.output.contentType=application/*+avro
----
During the outbound conversion, the message converter tries to infer the schema of each outbound messages (based on its type) and register it to a subject (based on the payload type) by using the `SchemaRegistryClient`.
If an identical schema is already found, then a reference to it is retrieved.
If not, the schema is registered, and a new version number is provided.
The message is sent with a `contentType` header by using the following scheme: `application/[prefix].[subject].v[version]+avro`, where `prefix` is configurable and `subject` is deduced from the payload type.
For example, a message of the type `User` might be sent as a binary payload with a content type of `application/vnd.user.v2+avro`, where `user` is the subject and `2` is the version number.
When receiving messages, the converter infers the schema reference from the header of the incoming message and tries to retrieve it. The schema is used as the writer schema in the deserialization process.
==== Avro Schema Registry Message Converter Properties
If you have enabled Avro based schema registry client by setting `spring.cloud.stream.bindings.output.contentType=application/*+avro`, you can customize the behavior of the registration by setting the following properties.
spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled:: Enable if you want the converter to use reflection to infer a Schema from a POJO.
+
Default: `false`
+
spring.cloud.stream.schema.avro.readerSchema:: Avro compares schema versions by looking at a writer schema (origin payload) and a reader schema (your application payload). See the https://avro.apache.org/docs/1.7.6/spec.html[Avro documentation] for more information. If set, this overrides any lookups at the schema server and uses the local schema as the reader schema.
Default: `null`
+
spring.cloud.stream.schema.avro.schemaLocations:: Registers any `.avsc` files listed in this property with the Schema Server.
+
Default: `empty`
+
spring.cloud.stream.schema.avro.prefix:: The prefix to be used on the Content-Type header.
+
Default: `vnd`
spring.cloud.stream.schema.avro.subjectNamingStrategy:: Determines the subject name used to register the Avro schema in the schema registry. Two implementations are available, `org.springframework.cloud.stream.schema.avro.DefaultSubjectNamingStrategy`,
where the subject is the schema name, and `org.springframework.cloud.stream.schema.avro.QualifiedSubjectNamingStrategy`, which returns a fully qualified subject using the Avro schema namespace and name. Custom strategies can be created by implementing `org.springframework.cloud.stream.schema.avro.SubjectNamingStrategy`.
+
Default: `org.springframework.cloud.stream.schema.avro.DefaultSubjectNamingStrategy`
=== Apache Avro Message Converters
Spring Cloud Stream provides support for schema-based message converters through its `spring-cloud-stream-schema` module.
Currently, the only serialization format supported out of the box for schema-based message converters is Apache Avro, with more formats to be added in future versions.
The `spring-cloud-stream-schema` module contains two types of message converters that can be used for Apache Avro serialization:
* Converters that use the class information of the serialized or deserialized objects or a schema with a location known at startup.
* Converters that use a schema registry. They locate the schemas at runtime and dynamically register new schemas as domain objects evolve.
=== Converters with Schema Support
The `AvroSchemaMessageConverter` supports serializing and deserializing messages either by using a predefined schema or by using the schema information available in the class (either reflectively or contained in the `SpecificRecord`).
If you provide a custom converter, then the default AvroSchemaMessageConverter bean is not created. The following example shows a custom converter:
To use custom converters, you can simply add it to the application context, optionally specifying one or more `MimeTypes` with which to associate it.
The default `MimeType` is `application/avro`.
If the target type of the conversion is a `GenericRecord`, a schema must be set.
The following example shows how to configure a converter in a sink application by registering the Apache Avro `MessageConverter` without a predefined schema.
In this example, note that the mime type value is `avro/bytes`, not the default `application/avro`.
[source,java]
----
@EnableBinding(Sink.class)
@SpringBootApplication
public static class SinkApplication {
...
@Bean
public MessageConverter userMessageConverter() {
return new AvroSchemaMessageConverter(MimeType.valueOf("avro/bytes"));
}
}
----
Conversely, the following application registers a converter with a predefined schema (found on the classpath):
[source,java]
----
@EnableBinding(Sink.class)
@SpringBootApplication
public static class SinkApplication {
...
@Bean
public MessageConverter userMessageConverter() {
AvroSchemaMessageConverter converter = new AvroSchemaMessageConverter(MimeType.valueOf("avro/bytes"));
converter.setSchemaLocation(new ClassPathResource("schemas/User.avro"));
return converter;
}
}
----
=== Schema Registry Server
Spring Cloud Stream provides a schema registry server implementation.
To use it, you can add the `spring-cloud-stream-schema-server` artifact to your project and use the `@EnableSchemaRegistryServer` annotation, which adds the schema registry server REST controller to your application.
This annotation is intended to be used with Spring Boot web applications, and the listening port of the server is controlled by the `server.port` property.
The `spring.cloud.stream.schema.server.path` property can be used to control the root path of the schema server (especially when it is embedded in other applications).
The `spring.cloud.stream.schema.server.allowSchemaDeletion` boolean property enables the deletion of a schema. By default, this is disabled.
The schema registry server uses a relational database to store the schemas.
By default, it uses an embedded database.
You can customize the schema storage by using the http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-sql[Spring Boot SQL database and JDBC configuration options].
The following example shows a Spring Boot application that enables the schema registry:
[source,java]
----
@SpringBootApplication
@EnableSchemaRegistryServer
public class SchemaRegistryServerApplication {
public static void main(String[] args) {
SpringApplication.run(SchemaRegistryServerApplication.class, args);
}
}
----
==== Schema Registry Server API
The Schema Registry Server API consists of the following operations:
* `POST /` -- see `<<spring-cloud-stream-overview-registering-new-schema>>`
* 'GET /{subject}/{format}/{version}' -- see `<<spring-cloud-stream-overview-retrieve-schema-subject-format-version>>`
* `GET /{subject}/{format}` -- see `<<spring-cloud-stream-overview-retrieve-schema-subject-format>>`
* `GET /schemas/{id}` -- see `<<spring-cloud-stream-overview-retrieve-schema-id>>`
* `DELETE /{subject}/{format}/{version}` -- see `<<spring-cloud-stream-overview-deleting-schema-subject-format-version>>`
* `DELETE /schemas/{id}` -- see `<<spring-cloud-stream-overview-deleting-schema-id>>`
* `DELETE /{subject}` -- see `<<spring-cloud-stream-overview-deleting-schema-subject>>`
[[spring-cloud-stream-overview-registering-new-schema]]
===== Registering a New Schema
To register a new schema, send a `POST` request to the `/` endpoint.
The `/` accepts a JSON payload with the following fields:
* `subject`: The schema subject
* `format`: The schema format
* `definition`: The schema definition
Its response is a schema object in JSON, with the following fields:
* `id`: The schema ID
* `subject`: The schema subject
* `format`: The schema format
* `version`: The schema version
* `definition`: The schema definition
[[spring-cloud-stream-overview-retrieve-schema-subject-format-version]]
===== Retrieving an Existing Schema by Subject, Format, and Version
To retrieve an existing schema by subject, format, and version, send `GET` request to the `/{subject}/{format}/{version}` endpoint.
Its response is a schema object in JSON, with the following fields:
* `id`: The schema ID
* `subject`: The schema subject
* `format`: The schema format
* `version`: The schema version
* `definition`: The schema definition
[[spring-cloud-stream-overview-retrieve-schema-subject-format]]
===== Retrieving an Existing Schema by Subject and Format
To retrieve an existing schema by subject and format, send a `GET` request to the `/subject/format` endpoint.
Its response is a list of schemas with each schema object in JSON, with the following fields:
* `id`: The schema ID
* `subject`: The schema subject
* `format`: The schema format
* `version`: The schema version
* `definition`: The schema definition
[[spring-cloud-stream-overview-retrieve-schema-id]]
===== Retrieving an Existing Schema by ID
To retrieve a schema by its ID, send a `GET` request to the `/schemas/{id}` endpoint.
Its response is a schema object in JSON, with the following fields:
* `id`: The schema ID
* `subject`: The schema subject
* `format`: The schema format
* `version`: The schema version
* `definition`: The schema definition
[[spring-cloud-stream-overview-deleting-schema-subject-format-version]]
===== Deleting a Schema by Subject, Format, and Version
To delete a schema identified by its subject, format, and version, send a `DELETE` request to the `/{subject}/{format}/{version}` endpoint.
[[spring-cloud-stream-overview-deleting-schema-id]]
===== Deleting a Schema by ID
To delete a schema by its ID, send a `DELETE` request to the `/schemas/{id}` endpoint.
[[spring-cloud-stream-overview-deleting-schema-subject]]
===== Deleting a Schema by Subject
`DELETE /{subject}`
Delete existing schemas by their subject.
NOTE: This note applies to users of Spring Cloud Stream 1.1.0.RELEASE only.
Spring Cloud Stream 1.1.0.RELEASE used the table name, `schema`, for storing `Schema` objects. `Schema` is a keyword in a number of database implementations.
To avoid any conflicts in the future, starting with 1.1.1.RELEASE, we have opted for the name `SCHEMA_REPOSITORY` for the storage table.
Any Spring Cloud Stream 1.1.0.RELEASE users who upgrade should migrate their existing schemas to the new table before upgrading.
==== Using Confluent's Schema Registry
The default configuration creates a `DefaultSchemaRegistryClient` bean.
If you want to use the Confluent schema registry, you need to create a bean of type `ConfluentSchemaRegistryClient`, which supersedes the one configured by default by the framework. The following example shows how to create such a bean:
[source,java]
----
@Bean
public SchemaRegistryClient schemaRegistryClient(@Value("${spring.cloud.stream.schemaRegistryClient.endpoint}") String endpoint){
ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient();
client.setEndpoint(endpoint);
return client;
}
----
NOTE: The ConfluentSchemaRegistryClient is tested against Confluent platform version 4.0.0.
=== Schema Registration and Resolution
To better understand how Spring Cloud Stream registers and resolves new schemas and its use of Avro schema comparison features, we provide two separate subsections:
* `<<spring-cloud-stream-overview-schema-registration-process>>`
* `<<spring-cloud-stream-overview-schema-resolution-process>>`
[[spring-cloud-stream-overview-schema-registration-process]]
==== Schema Registration Process (Serialization)
The first part of the registration process is extracting a schema from the payload that is being sent over a channel.
Avro types such as `SpecificRecord` or `GenericRecord` already contain a schema, which can be retrieved immediately from the instance.
In the case of POJOs, a schema is inferred if the `spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled` property is set to `true` (the default).
.Schema Writer Resolution Process
image::{github-raw}/docs/src/main/asciidoc/images/schema_resolution.png[width=800,scaledwidth="75%",align="center"]
Ones a schema is obtained, the converter loads its metadata (version) from the remote server.
First, it queries a local cache. If no result is found, it submits the data to the server, which replies with versioning information.
The converter always caches the results to avoid the overhead of querying the Schema Server for every new message that needs to be serialized.
.Schema Registration Process
image::{github-raw}/docs/src/main/asciidoc/images/registration.png[width=800,scaledwidth="75%",align="center"]
With the schema version information, the converter sets the `contentType` header of the message to carry the version information -- for example: `application/vnd.user.v1+avro`.
[[spring-cloud-stream-overview-schema-resolution-process]]
==== Schema Resolution Process (Deserialization)
When reading messages that contain version information (that is, a `contentType` header with a scheme like the one described under `<<spring-cloud-stream-overview-schema-registration-process>>`, the converter queries the Schema server to fetch the writer schema of the message.
Once it has found the correct schema of the incoming message, it retrieves the reader schema and, by using Avro's schema resolution support, reads it into the reader definition (setting defaults and any missing properties).
.Schema Reading Resolution Process
image::{github-raw}/docs/src/main/asciidoc/images/schema_reading.png[width=800,scaledwidth="75%",align="center"]
NOTE: You should understand the difference between a writer schema (the application that wrote the message) and a reader schema (the receiving application).
We suggest taking a moment to read https://avro.apache.org/docs/1.7.6/spec.html[the Avro terminology] and understand the process.
Spring Cloud Stream always fetches the writer schema to determine how to read a message.
If you want to get Avro's schema evolution support working, you need to make sure that a `readerSchema` was properly set for your application.

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env ruby
base_dir = File.join(File.dirname(__FILE__),'../../..')
src_dir = File.join(base_dir, "/src/main/asciidoc")
require 'asciidoctor'
require 'optparse'
options = {}
file = "#{src_dir}/README.adoc"
OptionParser.new do |o|
o.on('-o OUTPUT_FILE', 'Output file (default is stdout)') { |file| options[:to_file] = file unless file=='-' }
o.on('-h', '--help') { puts o; exit }
o.parse!
end
file = ARGV[0] if ARGV.length>0
# Copied from https://github.com/asciidoctor/asciidoctor-extensions-lab/blob/master/scripts/asciidoc-coalescer.rb
doc = Asciidoctor.load_file file, safe: :unsafe, header_only: true, attributes: options[:attributes]
header_attr_names = (doc.instance_variable_get :@attributes_modified).to_a
header_attr_names.each {|k| doc.attributes[%(#{k}!)] = '' unless doc.attr? k }
attrs = doc.attributes
attrs['allow-uri-read'] = true
puts attrs
out = "// Do not edit this file (e.g. go instead to src/main/asciidoc)\n\n"
doc = Asciidoctor.load_file file, safe: :unsafe, parse: false, attributes: attrs
out << doc.reader.read
unless options[:to_file]
puts out
else
File.open(options[:to_file],'w+') do |file|
file.write(out)
end
end

286
mvnw vendored Executable file
View File

@@ -0,0 +1,286 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven2 Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
else
export JAVA_HOME="/Library/Java/Home"
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
# TODO classpath?
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`which java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found .mvn/wrapper/maven-wrapper.jar"
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
while IFS="=" read key value; do
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
esac
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
if [ "$MVNW_VERBOSE" = true ]; then
echo "Downloading from: $jarUrl"
fi
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
if command -v wget > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found wget ... using wget"
fi
wget "$jarUrl" -O "$wrapperJarPath"
elif command -v curl > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found curl ... using curl"
fi
curl -o "$wrapperJarPath" "$jarUrl"
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Falling back to using Java to download"
fi
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
if [ -e "$javaClass" ]; then
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Compiling MavenWrapperDownloader.java ..."
fi
# Compiling the Java class
("$JAVA_HOME/bin/javac" "$javaClass")
fi
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
# Running the downloader
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Running MavenWrapperDownloader.java ..."
fi
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
if [ "$MVNW_VERBOSE" = true ]; then
echo $MAVEN_PROJECTBASEDIR
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

161
mvnw.cmd vendored Executable file
View File

@@ -0,0 +1,161 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven2 Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
echo Found %WRAPPER_JAR%
) else (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %DOWNLOAD_URL%
powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"
echo Finished downloading %WRAPPER_JAR%
)
@REM End of extension
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%" == "on" pause
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
exit /B %ERROR_CODE%

208
pom.xml Normal file
View File

@@ -0,0 +1,208 @@
<?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-scheam-registry-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<relativePath/>
</parent>
<scm>
<url>https://github.com/spring-cloud/spring-cloud-stream</url>
<connection>scm:git:git://github.com/spring-cloud/spring-cloud-stream.git
</connection>
<developerConnection>
scm:git:ssh://git@github.com/spring-cloud/spring-cloud-stream.git
</developerConnection>
<tag>HEAD</tag>
</scm>
<properties>
<java.version>1.8</java.version>
<spring-cloud-stream.version>3.0.0.BUILD-SNAPSHOT</spring-cloud-stream.version>
<maven-checkstyle-plugin.failsOnError>true</maven-checkstyle-plugin.failsOnError>
<maven-checkstyle-plugin.failsOnViolation>true</maven-checkstyle-plugin.failsOnViolation>
<maven-checkstyle-plugin.includeTestSourceDirectory>true</maven-checkstyle-plugin.includeTestSourceDirectory>
</properties>
<modules>
<module>spring-cloud-schema-registry-core</module>
<module>spring-cloud-schema-registry-server</module>
<module>spring-cloud-schema-registry-client</module>
<module>docs</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
<version>${spring-cloud-stream.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<version>${spring-cloud-stream.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support-internal</artifactId>
<version>${spring-cloud-stream.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<quiet>true</quiet>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<compilerArgument>-parameters</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</plugin>
</plugins>
</build>
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</plugin>
</plugins>
</reporting>
<profiles>
<profile>
<id>spring</id>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/libs-release-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
<profile>
<id>coverage</id>
<activation>
<property>
<name>env.TRAVIS</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.9</version>
<executions>
<execution>
<id>agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

Binary file not shown.

View File

@@ -0,0 +1,106 @@
<?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-scheam-registry-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-schema-registry-client</artifactId>
<properties>
<avro.version>1.8.1</avro.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.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>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support-internal</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-schema-registry-core</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-avro</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.avro</groupId>
<artifactId>avro-maven-plugin</artifactId>
<version>${avro.version}</version>
<executions>
<execution>
<phase>generate-test-sources</phase>
<goals>
<goal>schema</goal>
</goals>
</execution>
</executions>
<configuration>
<outputDirectory>${project.basedir}/target/generated-test-sources
</outputDirectory>
<testOutputDirectory>
${project.basedir}/target/generated-test-sources
</testOutputDirectory>
<testSourceDirectory>${project.basedir}/src/test/resources/schemas
</testSourceDirectory>
<testIncludes>
<testInclude>**/*.avsc</testInclude>
</testIncludes>
<imports>
<import>
${project.basedir}/src/test/resources/schemas/imports/Email.avsc
</import>
<import>
${project.basedir}/src/test/resources/schemas/imports/Sms.avsc
</import>
<import>
${project.basedir}/src/test/resources/schemas/imports/PushNotification.avsc
</import>
</imports>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry;
import org.apache.avro.Schema;
/**
* Stores a {@link Schema} together with its String representation.
*
* Helps to avoid unnecessary parsing of schema textual representation, as well as calls
* to {@link org.apache.avro.Schema} toString method which is very expensive due the
* utilization of {@link com.fasterxml.jackson.databind.ObjectMapper} to output a JSON
* representation of the schema.
*
* Once a schema is found for any Class, be it a POJO or a
* {@link org.apache.avro.generic.GenericContainer}, both textual representation as well
* as the {@link org.apache.avro.Schema} will be stored within this class.
*
* @author Vinicius Carvalho
*
*/
public class ParsedSchema {
private final Schema schema;
private final String representation;
private SchemaRegistrationResponse registration;
public ParsedSchema(Schema schema) {
this.schema = schema;
this.representation = schema.toString();
}
public Schema getSchema() {
return this.schema;
}
public String getRepresentation() {
return this.representation;
}
public SchemaRegistrationResponse getRegistration() {
return this.registration;
}
public void setRegistration(SchemaRegistrationResponse registration) {
this.registration = registration;
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry;
/**
* @author Vinicius Carvalho
*/
public class SchemaNotFoundException extends RuntimeException {
public SchemaNotFoundException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry;
import org.springframework.util.Assert;
/**
* References a schema through its subject and version.
*
* @author Marius Bogoevici
*/
public class SchemaReference {
private String subject;
private int version;
private String format;
public SchemaReference(String subject, int version, String format) {
Assert.hasText(subject, "cannot be empty");
Assert.isTrue(version > 0, "must be a positive integer");
Assert.hasText(format, "cannot be empty");
this.subject = subject;
this.version = version;
this.format = format;
}
public String getSubject() {
return this.subject;
}
public void setSubject(String subject) {
Assert.hasText(subject, "cannot be empty");
this.subject = subject;
}
public int getVersion() {
return this.version;
}
public void setVersion(int version) {
Assert.isTrue(version > 0, "must be a positive integer");
this.version = version;
}
public String getFormat() {
return this.format;
}
public void setFormat(String format) {
Assert.hasText(format, "cannot be empty");
this.format = format;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SchemaReference that = (SchemaReference) o;
if (this.version != that.version) {
return false;
}
if (!this.subject.equals(that.subject)) {
return false;
}
return this.format.equals(that.format);
}
@Override
public int hashCode() {
int result = this.subject.hashCode();
result = 31 * result + this.version;
result = 31 * result + this.format.hashCode();
return result;
}
@Override
public String toString() {
return "SchemaReference{" + "subject='" + this.subject + '\'' + ", version="
+ this.version + ", format='" + this.format + '\'' + '}';
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry;
/**
* @author Marius Bogoevici
*/
public class SchemaRegistrationResponse {
private int id;
private SchemaReference schemaReference;
public int getId() {
return this.id;
}
public void setId(int id) {
this.id = id;
}
public SchemaReference getSchemaReference() {
return this.schemaReference;
}
public void setSchemaReference(SchemaReference schemaReference) {
this.schemaReference = schemaReference;
}
}

View File

@@ -0,0 +1,146 @@
/*
* 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.schema.registry.avro;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import org.apache.avro.Schema;
import org.apache.avro.io.DatumWriter;
import org.apache.avro.io.Encoder;
import org.apache.avro.io.EncoderFactory;
import org.springframework.core.io.Resource;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.AbstractMessageConverter;
import org.springframework.messaging.converter.MessageConversionException;
import org.springframework.util.MimeType;
/**
* Base class for Apache Avro
* {@link org.springframework.messaging.converter.MessageConverter} implementations.
*
* @author Marius Bogoevici
* @author Vinicius Carvalho
* @author Sercan Karaoglu
* @author Ish Mahajan
*/
public abstract class AbstractAvroMessageConverter extends AbstractMessageConverter {
/**
* common parser will let user to import external schemas.
*/
private Schema.Parser schemaParser = new Schema.Parser();
private AvroSchemaServiceManager avroSchemaServiceManager;
@Deprecated
protected AbstractAvroMessageConverter(MimeType supportedMimeType) {
this(Collections.singletonList(supportedMimeType), new AvroSchemaServiceManagerImpl());
}
protected AbstractAvroMessageConverter(MimeType supportedMimeType, AvroSchemaServiceManager avroSchemaServiceManager) {
this(Collections.singletonList(supportedMimeType), avroSchemaServiceManager);
}
@Deprecated
protected AbstractAvroMessageConverter(Collection<MimeType> supportedMimeTypes) {
this(supportedMimeTypes, new AvroSchemaServiceManagerImpl());
setContentTypeResolver(new OriginalContentTypeResolver());
}
protected AbstractAvroMessageConverter(Collection<MimeType> supportedMimeTypes, AvroSchemaServiceManager manager) {
super(supportedMimeTypes);
setContentTypeResolver(new OriginalContentTypeResolver());
this.avroSchemaServiceManager = manager;
}
protected AvroSchemaServiceManager avroSchemaServiceManager() {
return this.avroSchemaServiceManager;
}
protected Schema parseSchema(Resource r) throws IOException {
return this.schemaParser.parse(r.getInputStream());
}
@Override
protected boolean canConvertFrom(Message<?> message, Class<?> targetClass) {
return super.canConvertFrom(message, targetClass)
&& (message.getPayload() instanceof byte[]);
}
@Override
protected Object convertFromInternal(Message<?> message, Class<?> targetClass,
Object conversionHint) {
Object result;
try {
byte[] payload = (byte[]) message.getPayload();
MimeType mimeType = getContentTypeResolver().resolve(message.getHeaders());
if (mimeType == null) {
if (conversionHint instanceof MimeType) {
mimeType = (MimeType) conversionHint;
}
else {
return null;
}
}
Schema writerSchema = resolveWriterSchemaForDeserialization(mimeType);
Schema readerSchema = resolveReaderSchemaForDeserialization(targetClass);
result = avroSchemaServiceManager().readData(targetClass, payload, readerSchema, writerSchema);
}
catch (IOException e) {
throw new MessageConversionException(message, "Failed to read payload", e);
}
return result;
}
@Override
protected Object convertToInternal(Object payload, MessageHeaders headers,
Object conversionHint) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
MimeType hintedContentType = null;
if (conversionHint instanceof MimeType) {
hintedContentType = (MimeType) conversionHint;
}
Schema schema = resolveSchemaForWriting(payload, headers, hintedContentType);
@SuppressWarnings("unchecked")
DatumWriter<Object> writer = avroSchemaServiceManager()
.getDatumWriter(payload.getClass(), schema);
Encoder encoder = EncoderFactory.get().binaryEncoder(baos, null);
writer.write(payload, encoder);
encoder.flush();
}
catch (IOException e) {
throw new MessageConversionException("Failed to write payload", e);
}
return baos.toByteArray();
}
protected abstract Schema resolveSchemaForWriting(Object payload,
MessageHeaders headers, MimeType hintedContentType);
protected abstract Schema resolveWriterSchemaForDeserialization(MimeType mimeType);
protected abstract Schema resolveReaderSchemaForDeserialization(Class<?> targetClass);
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.avro;
import java.lang.reflect.Constructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.cloud.schema.registry.client.SchemaRegistryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
/**
* @author Marius Bogoevici
* @author Vinicius Carvalho
* @author Sercan Karaoglu
* @author Ish Mahajan
*/
@Configuration
@ConditionalOnClass(name = "org.apache.avro.Schema")
@ConditionalOnProperty(value = "spring.cloud.stream.schemaRegistryClient.enabled", matchIfMissing = true)
@ConditionalOnBean(type = "org.springframework.cloud.schema.registry.client.SchemaRegistryClient")
@EnableConfigurationProperties({ AvroMessageConverterProperties.class })
@Import(AvroSchemaServiceManagerImpl.class)
public class AvroMessageConverterAutoConfiguration {
// @Autowired
// private AvroMessageConverterProperties avroMessageConverterProperties;
//// @Autowired
// private AvroSchemaServiceManager avroSchemaServiceManager;
@Bean
@ConditionalOnMissingBean(AvroSchemaRegistryClientMessageConverter.class)
public AvroSchemaRegistryClientMessageConverter avroSchemaMessageConverter(
SchemaRegistryClient schemaRegistryClient, AvroSchemaServiceManager avroSchemaServiceManager,
AvroMessageConverterProperties avroMessageConverterProperties) {
AvroSchemaRegistryClientMessageConverter avroSchemaRegistryClientMessageConverter;
avroSchemaRegistryClientMessageConverter = new AvroSchemaRegistryClientMessageConverter(
schemaRegistryClient, cacheManager(), avroSchemaServiceManager);
avroSchemaRegistryClientMessageConverter.setDynamicSchemaGenerationEnabled(
avroMessageConverterProperties.isDynamicSchemaGenerationEnabled());
if (avroMessageConverterProperties.getReaderSchema() != null) {
avroSchemaRegistryClientMessageConverter.setReaderSchema(
avroMessageConverterProperties.getReaderSchema());
}
if (!ObjectUtils
.isEmpty(avroMessageConverterProperties.getSchemaLocations())) {
avroSchemaRegistryClientMessageConverter.setSchemaLocations(
avroMessageConverterProperties.getSchemaLocations());
}
if (!ObjectUtils
.isEmpty(avroMessageConverterProperties.getSchemaImports())) {
avroSchemaRegistryClientMessageConverter.setSchemaImports(
avroMessageConverterProperties.getSchemaImports());
}
avroSchemaRegistryClientMessageConverter
.setPrefix(avroMessageConverterProperties.getPrefix());
try {
Class<?> clazz = avroMessageConverterProperties
.getSubjectNamingStrategy();
Constructor constructor = ReflectionUtils.accessibleConstructor(clazz);
avroSchemaRegistryClientMessageConverter.setSubjectNamingStrategy(
(SubjectNamingStrategy) constructor.newInstance());
}
catch (Exception ex) {
throw new IllegalStateException("Unable to create SubjectNamingStrategy "
+ avroMessageConverterProperties.getSubjectNamingStrategy()
.toString(),
ex);
}
return avroSchemaRegistryClientMessageConverter;
}
@Bean
@ConditionalOnMissingBean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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.schema.registry.avro;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
/**
* @author Vinicius Carvalho
* @author Sercan Karaoglu
*/
@ConfigurationProperties(prefix = "spring.cloud.stream.schema.avro")
public class AvroMessageConverterProperties {
private boolean dynamicSchemaGenerationEnabled;
private Resource readerSchema;
/**
* The source directory of Apache Avro schema. This schema is used by this converter.
* If this schema depends on other schemas consider defining those those dependent
* ones in the {@link #schemaImports}
* @parameter
*/
private Resource[] schemaLocations;
/**
* A list of files or directories that should be loaded first thus making them
* importable by subsequent schemas. Note that imported files should not reference
* each other.
* @parameter
*/
private Resource[] schemaImports;
private String prefix = "vnd";
private Class<? extends SubjectNamingStrategy> subjectNamingStrategy = DefaultSubjectNamingStrategy.class;
public Resource getReaderSchema() {
return this.readerSchema;
}
public void setReaderSchema(Resource readerSchema) {
Assert.notNull(readerSchema, "cannot be null");
this.readerSchema = readerSchema;
}
public Resource[] getSchemaLocations() {
return this.schemaLocations;
}
public void setSchemaLocations(Resource[] schemaLocations) {
Assert.notEmpty(schemaLocations, "cannot be null");
this.schemaLocations = schemaLocations;
}
public boolean isDynamicSchemaGenerationEnabled() {
return this.dynamicSchemaGenerationEnabled;
}
public void setDynamicSchemaGenerationEnabled(
boolean dynamicSchemaGenerationEnabled) {
this.dynamicSchemaGenerationEnabled = dynamicSchemaGenerationEnabled;
}
public String getPrefix() {
return this.prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public Class<?> getSubjectNamingStrategy() {
return this.subjectNamingStrategy;
}
public void setSubjectNamingStrategy(
Class<? extends SubjectNamingStrategy> subjectNamingStrategy) {
Assert.notNull(subjectNamingStrategy, "cannot be null");
this.subjectNamingStrategy = subjectNamingStrategy;
}
public Resource[] getSchemaImports() {
return this.schemaImports;
}
public void setSchemaImports(Resource[] schemaImports) {
this.schemaImports = schemaImports;
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.avro;
import java.io.IOException;
import java.util.Collection;
import org.apache.avro.Schema;
import org.springframework.core.io.Resource;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* A {@link org.springframework.messaging.converter.MessageConverter} using Apache Avro.
* The schema for serializing and deserializing will be automatically inferred from the
* class for {@link org.apache.avro.specific.SpecificRecord} and regular classes, unless a
* specific schema is set, case in which that schema will be used instead. For converting
* to {@link org.apache.avro.generic.GenericRecord} targets, a schema must be set.s
*
* @author Marius Bogoevici
* @author Ish Mahajan
*/
public class AvroSchemaMessageConverter extends AbstractAvroMessageConverter {
private Schema schema;
/**
* Create a {@link AvroSchemaMessageConverter}. Uses the default {@link MimeType} of
* {@code "application/avro"}.
*/
@Deprecated
public AvroSchemaMessageConverter() {
super(new MimeType("application", "avro"));
}
/**
* Create a {@link AvroSchemaMessageConverter}. Uses the default {@link MimeType} of
* {@code "application/avro"}.
* @param manager for schema management
*/
public AvroSchemaMessageConverter(AvroSchemaServiceManager manager) {
super(new MimeType("application", "avro"), manager);
}
/**
* Create a {@link AvroSchemaMessageConverter}. The converter will be used for the
* provided {@link MimeType}.
* @param supportedMimeType mime type to be supported by
* {@link AvroSchemaMessageConverter}
*/
@Deprecated
public AvroSchemaMessageConverter(MimeType supportedMimeType) {
super(supportedMimeType);
}
/**
* Create a {@link AvroSchemaMessageConverter}. The converter will be used for the
* provided {@link MimeType}.
* @param supportedMimeType mime type to be supported by
* {@link AvroSchemaMessageConverter}
* @param manager for schema management
*/
public AvroSchemaMessageConverter(MimeType supportedMimeType, AvroSchemaServiceManager manager) {
super(supportedMimeType, manager);
}
/**
* Create a {@link AvroSchemaMessageConverter}. The converter will be used for the
* provided {@link MimeType}s.
* @param supportedMimeTypes the mime types supported by this converter
*/
@Deprecated
public AvroSchemaMessageConverter(Collection<MimeType> supportedMimeTypes) {
super(supportedMimeTypes);
}
/**
* Create a {@link AvroSchemaMessageConverter}. The converter will be used for the
* provided {@link MimeType}s.
* @param supportedMimeTypes the mime types supported by this converter
* @param manager for schema management
*/
public AvroSchemaMessageConverter(Collection<MimeType> supportedMimeTypes, AvroSchemaServiceManager manager) {
super(supportedMimeTypes, manager);
}
public Schema getSchema() {
return this.schema;
}
/**
* Sets the Apache Avro schema to be used by this converter.
* @param schema schema to be used by this converter
*/
public void setSchema(Schema schema) {
Assert.notNull(schema, "schema cannot be null");
this.schema = schema;
}
/**
* The location of the Apache Avro schema to be used by this converter.
* @param schemaLocation the location of the schema used by this converter.
*/
public void setSchemaLocation(Resource schemaLocation) {
Assert.notNull(schemaLocation, "schema cannot be null");
try {
this.schema = parseSchema(schemaLocation);
}
catch (IOException e) {
throw new IllegalStateException("Schema cannot be parsed:", e);
}
}
@Override
protected boolean supports(Class<?> clazz) {
return true;
}
@Override
protected Schema resolveWriterSchemaForDeserialization(MimeType mimeType) {
return this.schema;
}
@Override
protected Schema resolveReaderSchemaForDeserialization(Class<?> targetClass) {
return this.schema;
}
@Override
protected Schema resolveSchemaForWriting(Object payload, MessageHeaders headers,
MimeType hintedContentType) {
return this.schema;
}
}

View File

@@ -0,0 +1,424 @@
/*
* 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.schema.registry.avro;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericContainer;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.support.NoOpCacheManager;
import org.springframework.cloud.schema.registry.ParsedSchema;
import org.springframework.cloud.schema.registry.SchemaNotFoundException;
import org.springframework.cloud.schema.registry.SchemaReference;
import org.springframework.cloud.schema.registry.SchemaRegistrationResponse;
import org.springframework.cloud.schema.registry.client.SchemaRegistryClient;
import org.springframework.core.io.Resource;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.util.ObjectUtils;
/**
* A {@link org.springframework.messaging.converter.MessageConverter} for Apache Avro,
* with the ability to publish and retrieve schemas stored in a schema server, allowing
* for schema evolution in applications. The supported content types are in the form
* `application/*+avro`.
*
* During the conversion to a message, the converter will set the 'contentType' header to
* 'application/[prefix].[subject].v[version]+avro', where:
*
* <li>
* <ul>
* <i>prefix</i> is a configurable prefix (default 'vnd');
* </ul>
* <ul>
* <i>subject</i> is a subject derived from the type of the outgoing object - typically
* the class name;
* </ul>
* <ul>
* <i>version</i> is the schema version for the given subject;
* </ul>
* </li>
*
* When converting from a message, the converter will parse the content-type and use it to
* fetch and cache the writer schema using the provided {@link SchemaRegistryClient}.
*
* @author Marius Bogoevici
* @author Vinicius Carvalho
* @author Oleg Zhurakousky
* @author Sercan Karaoglu
* @author Ish Mahajan
*/
public class AvroSchemaRegistryClientMessageConverter extends AbstractAvroMessageConverter
implements InitializingBean {
/**
* Avro format defined in the Mime type.
*/
public static final String AVRO_FORMAT = "avro";
/**
* Pattern for validating the prefix to be used in the publised subtype.
*/
public static final Pattern PREFIX_VALIDATION_PATTERN = Pattern
.compile("[\\p{Alnum}]");
/**
* Spring Cloud Stream schema property prefix.
*/
public static final String CACHE_PREFIX = "org.springframework.cloud.stream.schema";
/**
* Property for reflection cache.
*/
public static final String REFLECTION_CACHE_NAME = CACHE_PREFIX + ".reflectionCache";
/**
* Property for schema cache.
*/
public static final String SCHEMA_CACHE_NAME = CACHE_PREFIX + ".schemaCache";
/**
* Property for reference cache.
*/
public static final String REFERENCE_CACHE_NAME = CACHE_PREFIX + ".referenceCache";
/**
* Default Mime type for Avro.
*/
public static final MimeType DEFAULT_AVRO_MIME_TYPE = new MimeType("application",
"*+" + AVRO_FORMAT);
private static final AvroSchemaServiceManager defaultAvroSchemaServiceManager =
new AvroSchemaServiceManagerImpl();
private final CacheManager cacheManager;
protected Resource[] schemaImports = new Resource[] {};
private Pattern versionedSchema;
private boolean dynamicSchemaGenerationEnabled;
private Schema readerSchema;
private Resource[] schemaLocations;
private SchemaRegistryClient schemaRegistryClient;
private String prefix = "vnd";
private SubjectNamingStrategy subjectNamingStrategy;
/**
* Creates a new instance, configuring it with {@link SchemaRegistryClient} and
* {@link CacheManager}.
* @param schemaRegistryClient the {@link SchemaRegistryClient} used to interact with
* the schema registry server.
* @param cacheManager instance of {@link CacheManager} to cache parsed schemas. If
* caching is not required use {@link NoOpCacheManager}
*/
@Deprecated
public AvroSchemaRegistryClientMessageConverter(
SchemaRegistryClient schemaRegistryClient, CacheManager cacheManager) {
super(Collections.singletonList(DEFAULT_AVRO_MIME_TYPE), defaultAvroSchemaServiceManager);
Assert.notNull(schemaRegistryClient, "cannot be null");
Assert.notNull(cacheManager, "'cacheManager' cannot be null");
this.schemaRegistryClient = schemaRegistryClient;
this.cacheManager = cacheManager;
}
/**
* Creates a new instance, configuring it with {@link SchemaRegistryClient} and
* {@link CacheManager}.
* @param schemaRegistryClient the {@link SchemaRegistryClient} used to interact with
* the schema registry server.
* @param cacheManager instance of {@link CacheManager} to cache parsed schemas. If
* caching is not required use {@link NoOpCacheManager}
* @param manager instance of {@link AvroSchemaServiceManager} to manage schemas.
*/
public AvroSchemaRegistryClientMessageConverter(
SchemaRegistryClient schemaRegistryClient, CacheManager cacheManager, AvroSchemaServiceManager manager) {
super(Collections.singletonList(DEFAULT_AVRO_MIME_TYPE), manager);
Assert.notNull(schemaRegistryClient, "cannot be null");
Assert.notNull(cacheManager, "'cacheManager' cannot be null");
Assert.notNull(manager, "'avroSchemaServiceManager' cannot be null");
this.schemaRegistryClient = schemaRegistryClient;
this.cacheManager = cacheManager;
}
public boolean isDynamicSchemaGenerationEnabled() {
return this.dynamicSchemaGenerationEnabled;
}
/**
* Allows the converter to generate and register schemas automatically. If set to
* false, it only allows the converter to use pre-registered schemas. Default 'true'.
* @param dynamicSchemaGenerationEnabled true if dynamic schema generation is enabled
*/
public void setDynamicSchemaGenerationEnabled(
boolean dynamicSchemaGenerationEnabled) {
this.dynamicSchemaGenerationEnabled = dynamicSchemaGenerationEnabled;
}
/**
* A set of locations where the converter can load schemas from. Schemas provided at
* these locations will be registered automatically.
* @param schemaLocations array of locations
*/
public void setSchemaLocations(Resource[] schemaLocations) {
Assert.notEmpty(schemaLocations, "cannot be empty");
this.schemaLocations = schemaLocations;
}
/**
* A set of schema locations where should be imported first. Schemas provided at these
* locations will be reference, thus they should not reference each other.
* @param schemaImports array of schema imports
*/
public void setSchemaImports(Resource[] schemaImports) {
this.schemaImports = schemaImports;
}
/**
* Set the prefix to be used in the published subtype. Default 'vnd'.
* @param prefix prefix to be set
*/
public void setPrefix(String prefix) {
Assert.hasText(prefix, "Prefix cannot be empty");
Assert.isTrue(!PREFIX_VALIDATION_PATTERN.matcher(this.prefix).matches(),
"Invalid prefix:" + this.prefix);
this.prefix = prefix;
}
public void setReaderSchema(Resource readerSchema) {
Assert.notNull(readerSchema, "cannot be null");
try {
this.readerSchema = parseSchema(readerSchema);
}
catch (IOException e) {
throw new BeanInitializationException("Cannot initialize reader schema", e);
}
}
public void setSubjectNamingStrategy(SubjectNamingStrategy subjectNamingStrategy) {
this.subjectNamingStrategy = subjectNamingStrategy;
}
@Override
public void afterPropertiesSet() throws Exception {
this.versionedSchema = Pattern.compile("application/" + this.prefix
+ "\\.([\\p{Alnum}\\$\\.]+)\\.v(\\p{Digit}+)\\+" + AVRO_FORMAT);
Stream.of(this.schemaImports, this.schemaLocations)
.filter(arr -> !ObjectUtils.isEmpty(arr)).distinct().peek(resources -> {
this.logger.info("Scanning avro schema resources on classpath");
if (this.logger.isInfoEnabled()) {
this.logger.info("Parsing" + this.schemaImports.length);
}
}).flatMap(Arrays::stream).forEach(resource -> {
try {
Schema schema = parseSchema(resource);
if (schema.getType().equals(Schema.Type.UNION)) {
schema.getTypes().forEach(
innerSchema -> registerSchema(resource, innerSchema));
}
else {
registerSchema(resource, schema);
}
}
catch (IOException e) {
if (this.logger.isWarnEnabled()) {
this.logger.warn(
"Failed to parse schema at " + resource.getFilename(),
e);
}
}
});
if (this.cacheManager instanceof NoOpCacheManager) {
this.logger.warn("Schema caching is effectively disabled "
+ "since configured cache manager is a NoOpCacheManager. If this was not "
+ "the intention, please provide the appropriate instance of CacheManager "
+ "(i.e., ConcurrentMapCacheManager).");
}
}
protected String toSubject(Schema schema) {
return this.subjectNamingStrategy.toSubject(schema);
}
@Override
protected boolean supports(Class<?> clazz) {
// we support all types
return true;
}
@Override
protected boolean supportsMimeType(MessageHeaders headers) {
if (super.supportsMimeType(headers)) {
return true;
}
MimeType mimeType = getContentTypeResolver().resolve(headers);
return DEFAULT_AVRO_MIME_TYPE.includes(mimeType);
}
@Override
protected Schema resolveSchemaForWriting(Object payload, MessageHeaders headers,
MimeType hintedContentType) {
Schema schema;
schema = extractSchemaForWriting(payload);
ParsedSchema parsedSchema = this.getCache(REFERENCE_CACHE_NAME)
.get(schema, ParsedSchema.class);
if (parsedSchema == null) {
parsedSchema = new ParsedSchema(schema);
this.getCache(REFERENCE_CACHE_NAME).putIfAbsent(schema,
parsedSchema);
}
if (parsedSchema.getRegistration() == null) {
SchemaRegistrationResponse response = this.schemaRegistryClient.register(
toSubject(schema), AVRO_FORMAT, parsedSchema.getRepresentation());
parsedSchema.setRegistration(response);
}
SchemaReference schemaReference = parsedSchema.getRegistration()
.getSchemaReference();
DirectFieldAccessor dfa = new DirectFieldAccessor(headers);
@SuppressWarnings("unchecked")
Map<String, Object> _headers = (Map<String, Object>) dfa
.getPropertyValue("headers");
_headers.put(MessageHeaders.CONTENT_TYPE,
"application/" + this.prefix + "." + schemaReference.getSubject() + ".v"
+ schemaReference.getVersion() + "+" + AVRO_FORMAT);
return schema;
}
@Override
protected Schema resolveWriterSchemaForDeserialization(MimeType mimeType) {
if (this.readerSchema == null) {
SchemaReference schemaReference = extractSchemaReference(mimeType);
if (schemaReference != null) {
ParsedSchema parsedSchema = this.getCache(REFERENCE_CACHE_NAME)
.get(schemaReference, ParsedSchema.class);
if (parsedSchema == null) {
String schemaContent = this.schemaRegistryClient
.fetch(schemaReference);
if (schemaContent != null) {
Schema schema = new Schema.Parser().parse(schemaContent);
parsedSchema = new ParsedSchema(schema);
this.getCache(REFERENCE_CACHE_NAME)
.putIfAbsent(schemaReference, parsedSchema);
}
}
if (parsedSchema != null) {
return parsedSchema.getSchema();
}
}
}
return this.readerSchema;
}
@Override
protected Schema resolveReaderSchemaForDeserialization(Class<?> targetClass) {
return this.readerSchema;
}
private Schema extractSchemaForWriting(Object payload) {
Schema schema = null;
if (this.logger.isDebugEnabled()) {
this.logger.debug("Obtaining schema for class " + payload.getClass());
}
if (GenericContainer.class.isAssignableFrom(payload.getClass())) {
schema = ((GenericContainer) payload).getSchema();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Avro type detected, using schema from object");
}
}
else {
schema = this.getCache(REFLECTION_CACHE_NAME)
.get(payload.getClass().getName(), Schema.class);
if (schema == null) {
if (!isDynamicSchemaGenerationEnabled()) {
throw new SchemaNotFoundException(String.format(
"No schema found in the local cache for %s, and dynamic schema generation "
+ "is not enabled",
payload.getClass()));
}
else {
schema = super.avroSchemaServiceManager().getSchema(payload.getClass());
}
this.getCache(REFLECTION_CACHE_NAME)
.put(payload.getClass().getName(), schema);
}
}
return schema;
}
private void registerSchema(Resource schemaLocation, Schema schema) {
if (this.logger.isInfoEnabled()) {
this.logger.info(
"Resource " + schemaLocation.getFilename() + " parsed into schema "
+ schema.getNamespace() + "." + schema.getName());
}
this.schemaRegistryClient.register(toSubject(schema), AVRO_FORMAT,
schema.toString());
if (this.logger.isInfoEnabled()) {
this.logger
.info("Schema " + schema.getName() + " registered with id " + schema);
}
this.getCache(REFLECTION_CACHE_NAME)
.put(schema.getNamespace() + "." + schema.getName(), schema);
}
private SchemaReference extractSchemaReference(MimeType mimeType) {
SchemaReference schemaReference = null;
Matcher schemaMatcher = this.versionedSchema.matcher(mimeType.toString());
if (schemaMatcher.find()) {
String subject = schemaMatcher.group(1);
Integer version = Integer.parseInt(schemaMatcher.group(2));
schemaReference = new SchemaReference(subject, version, AVRO_FORMAT);
}
return schemaReference;
}
private Cache getCache(String name) {
Cache cache = this.cacheManager.getCache(name);
Assert.notNull(cache, "Cache by the name '" + name + "' is not present in this CacheManager - '"
+ this.cacheManager + "'. Typically caches are auto-created by the CacheManagers. "
+ "Consider reporting it as an issue to the developer of this CacheManager.");
return cache;
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.avro;
import java.io.IOException;
import org.apache.avro.Schema;
import org.apache.avro.io.DatumReader;
import org.apache.avro.io.DatumWriter;
/**
* Manage a {@link Schema} together with its String representation.
*
* Helps to substitute the default implementation of {@link org.apache.avro.Schema}
* Generation using Custom Avro schema generator
*
* Provide a custom bean definition of {@link AvroSchemaServiceManager} and mark
* it as @Primary to override the default implementation
*
* @author Ish Mahajan
*
*/
public interface AvroSchemaServiceManager {
/**
* get {@link Schema}.
* @param clazz {@link Class} for which schema generation is required
* @return returns avro schema for given class
*/
Schema getSchema(Class<?> clazz);
/**
* get {@link DatumWriter}.
* @param type {@link Class} of java object which needs to be serialized
* @param schema {@link Schema} of object which needs to be serialized
* @return datum writer which can be used to write Avro payload
*/
DatumWriter<Object> getDatumWriter(Class<? extends Object> type, Schema schema);
/**
* get {@link DatumReader}.
* @param type {@link Class} of java object which needs to be serialized
* @param schema {@link Schema} default schema of object which needs to be de-serialized
* @param writerSchema {@link Schema} writerSchema provided at run time
* @return datum reader which can be used to read Avro payload
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
DatumReader<Object> getDatumReader(Class<? extends Object> type, Schema schema, Schema writerSchema);
/**
* read data from avro type payload {@link DatumReader}.
* @param targetClass {@link Class} of java object which needs to be serialized
* @param payload {@link byte} serialized payload of object which needs to be de-serialized
* @param readerSchema {@link Schema} readerSchema of object which needs to be de-serialized
* @param writerSchema {@link Schema} writerSchema used to while serializing payload
* @return java object after reading Avro Payload
* @throws IOException in case of error
*/
Object readData(Class<? extends Object> targetClass, byte[] payload, Schema readerSchema, Schema writerSchema)
throws IOException;
}

View File

@@ -0,0 +1,174 @@
/*
* 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.registry.avro;
import java.io.IOException;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.io.DatumReader;
import org.apache.avro.io.DatumWriter;
import org.apache.avro.io.Decoder;
import org.apache.avro.io.DecoderFactory;
import org.apache.avro.reflect.ReflectData;
import org.apache.avro.reflect.ReflectDatumReader;
import org.apache.avro.reflect.ReflectDatumWriter;
import org.apache.avro.specific.SpecificDatumReader;
import org.apache.avro.specific.SpecificDatumWriter;
import org.apache.avro.specific.SpecificRecord;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.messaging.converter.MessageConversionException;
import org.springframework.stereotype.Component;
/**
* Default Concrete implementation of {@link AvroSchemaServiceManager}.
*
* Helps to substitute the default implementation of {@link org.apache.avro.Schema}
* Generation using Custom Avro schema generator
*
* Provide a custom bean definition of {@link AvroSchemaServiceManager} and mark
* it as @Primary to override this default implementation
*
* @author Ish Mahajan
*
*/
@Component
public class AvroSchemaServiceManagerImpl implements AvroSchemaServiceManager {
protected final Log logger = LogFactory.getLog(this.getClass());
/**
* get {@link Schema}.
* @param clazz {@link Class} for which schema generation
* is required
* @return returns avro schema for given class
*/
@Override
public Schema getSchema(Class<?> clazz) {
return ReflectData.get().getSchema(clazz);
}
/**
* get {@link DatumWriter}.
* @param type {@link Class} of java object which needs to be serialized
* @param schema {@link Schema} of object which needs to be serialized
* @return datum writer which can be used to write Avro payload
*/
@Override
public DatumWriter<Object> getDatumWriter(Class<?> type, Schema schema) {
DatumWriter<Object> writer;
this.logger.debug("Finding correct DatumWriter for type " + type.getName());
if (SpecificRecord.class.isAssignableFrom(type)) {
if (schema != null) {
writer = new SpecificDatumWriter<>(schema);
}
else {
writer = new SpecificDatumWriter(type);
}
}
else if (GenericRecord.class.isAssignableFrom(type)) {
writer = new GenericDatumWriter<>(schema);
}
else {
if (schema != null) {
writer = new ReflectDatumWriter<>(schema);
}
else {
writer = new ReflectDatumWriter(type);
}
}
return writer;
}
/**
* get {@link DatumReader}.
* @param type {@link Class} of java object which needs to be serialized
* @param schema {@link Schema} default schema of object which needs to be de-serialized
* @param writerSchema {@link Schema} writerSchema provided at run time
* @return datum reader which can be used to read Avro payload
*/
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public DatumReader<Object> getDatumReader(Class<?> type, Schema schema, Schema writerSchema) {
DatumReader<Object> reader = null;
if (SpecificRecord.class.isAssignableFrom(type)) {
if (schema != null) {
if (writerSchema != null) {
reader = new SpecificDatumReader<>(writerSchema, schema);
}
else {
reader = new SpecificDatumReader<>(schema);
}
}
else {
reader = new SpecificDatumReader(type);
if (writerSchema != null) {
reader.setSchema(writerSchema);
}
}
}
else if (GenericRecord.class.isAssignableFrom(type)) {
if (schema != null) {
if (writerSchema != null) {
reader = new GenericDatumReader<>(writerSchema, schema);
}
else {
reader = new GenericDatumReader<>(schema);
}
}
else {
if (writerSchema != null) {
reader = new GenericDatumReader(writerSchema);
}
}
}
else {
reader = new ReflectDatumReader(type);
if (writerSchema != null) {
reader.setSchema(writerSchema);
}
}
if (reader == null) {
throw new MessageConversionException("No schema can be inferred from type "
+ type.getName() + " and no schema has been explicitly configured.");
}
return reader;
}
/**
* read data from avro type payload {@link DatumReader}.
* @param clazz {@link Class} of java object which needs to be serialized
* @param payload {@link byte} serialized payload of object which needs to be de-serialized
* @param readerSchema {@link Schema} readerSchema of object which needs to be de-serialized
* @param writerSchema {@link Schema} writerSchema used to while serializing payload
* @return java object after reading Avro Payload
* @throws IOException is thrown in case of error
*/
@Override
public Object readData(Class<? extends Object> clazz, byte[] payload, Schema readerSchema,
Schema writerSchema) throws IOException {
DatumReader<Object> reader = this.getDatumReader(clazz,
readerSchema, writerSchema);
Decoder decoder = DecoderFactory.get().binaryDecoder(payload, null);
return reader.read(null, decoder);
}
}

View File

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

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2017-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.avro;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.ContentTypeResolver;
import org.springframework.util.MimeType;
/**
* @author Vinicius Carvalho
*
* Resolves contentType looking for a originalContentType header first. If not found
* returns the contentType
*
*/
class OriginalContentTypeResolver implements ContentTypeResolver {
private static final String BINDER_ORIGINAL_CONTENT_TYPE = "originalContentType";
private ConcurrentMap<String, MimeType> mimeTypeCache = new ConcurrentHashMap<>();
@Override
public MimeType resolve(MessageHeaders headers) {
Object contentType = headers
.get(BINDER_ORIGINAL_CONTENT_TYPE) != null
? headers.get(BINDER_ORIGINAL_CONTENT_TYPE)
: headers.get(MessageHeaders.CONTENT_TYPE);
MimeType mimeType = null;
if (contentType instanceof MimeType) {
mimeType = (MimeType) contentType;
}
else if (contentType instanceof String) {
mimeType = this.mimeTypeCache.get(contentType);
if (mimeType == null) {
String valueAsString = (String) contentType;
mimeType = MimeType.valueOf(valueAsString);
this.mimeTypeCache.put(valueAsString, mimeType);
}
}
return mimeType;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.avro;
import org.apache.avro.Schema;
/**
* @author José A. Íñigo
* @since 2.2.0
*/
public class QualifiedSubjectNamingStrategy implements SubjectNamingStrategy {
@Override
public String toSubject(Schema schema) {
return schema.getFullName().toLowerCase();
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.schema.registry.avro;
import org.apache.avro.Schema;
/**
* Provides function towards naming schema registry subjects for Avro files.
*
* @author David Kalosi
*/
public interface SubjectNamingStrategy {
/**
* Takes the Avro schema on input and returns the generated subject under which the
* schema should be registered.
* @param schema schema to register
* @return subject name
*/
String toSubject(Schema schema);
}

View File

@@ -0,0 +1,69 @@
/*
* 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.registry.client;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.schema.registry.SchemaReference;
import org.springframework.cloud.schema.registry.SchemaRegistrationResponse;
import org.springframework.util.Assert;
/**
* @author Vinicius Carvalho
*/
public class CachingRegistryClient implements SchemaRegistryClient {
private static final String CACHE_PREFIX = "org.springframework.cloud.schema.registry.client";
private static final String ID_CACHE = CACHE_PREFIX + ".schemaByIdCache";
private static final String REF_CACHE = CACHE_PREFIX + ".schemaByReferenceCache";
private SchemaRegistryClient delegate;
@Autowired
private CacheManager cacheManager;
public CachingRegistryClient(SchemaRegistryClient delegate) {
Assert.notNull(delegate, "The delegate cannot be null");
this.delegate = delegate;
}
@Override
public SchemaRegistrationResponse register(String subject, String format,
String schema) {
SchemaRegistrationResponse response = this.delegate.register(subject, format,
schema);
this.cacheManager.getCache(ID_CACHE).put(response.getId(), schema);
this.cacheManager.getCache(REF_CACHE).put(response.getSchemaReference(), schema);
return response;
}
@Override
@Cacheable(cacheNames = REF_CACHE)
public String fetch(SchemaReference schemaReference) {
return this.delegate.fetch(schemaReference);
}
@Override
@Cacheable(cacheNames = ID_CACHE)
public String fetch(int id) {
return this.delegate.fetch(id);
}
}

View File

@@ -0,0 +1,186 @@
/*
* 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.schema.registry.client;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.schema.registry.SchemaNotFoundException;
import org.springframework.cloud.schema.registry.SchemaReference;
import org.springframework.cloud.schema.registry.SchemaRegistrationResponse;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
/**
* @author Vinicius Carvalho
* @author Marius Bogoevici
* @author Jon Archer
*/
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");
String path = String.format("/subjects/%s/versions", subject);
HttpHeaders headers = new HttpHeaders();
headers.put("Accept", ACCEPT_HEADERS);
headers.add("Content-Type", "application/json");
Integer version = null;
Integer id = null;
String payload = null;
try {
payload = this.mapper
.writeValueAsString(Collections.singletonMap("schema", schema));
}
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 + path,
HttpMethod.POST, request, Map.class);
id = (Integer) response.getBody().get("id");
version = getSubjectVersion(subject, payload);
}
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;
}
/**
* Confluent register API returns the id, but we need the version of a given schema
* subject. After a successful registration we can inquire the server to get the
* version of a schema
* @param subject the schema subject
* @param payload payload to send
* @return the version of the returned schema
*/
private Integer getSubjectVersion(String subject, String payload) {
String path = String.format("/subjects/%s", subject);
HttpHeaders headers = new HttpHeaders();
headers.put("Accept", ACCEPT_HEADERS);
headers.add("Content-Type", "application/json");
Integer version = null;
try {
HttpEntity<String> request = new HttpEntity<>(payload, headers);
ResponseEntity<Map> response = this.template.exchange(this.endpoint + path,
HttpMethod.POST, request, Map.class);
version = (Integer) 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);
}
return version;
}
@Override
public String fetch(SchemaReference schemaReference) {
String path = String.format("/subjects/%s/versions/%d",
schemaReference.getSubject(), schemaReference.getVersion());
HttpHeaders headers = new HttpHeaders();
headers.put("Accept", ACCEPT_HEADERS);
headers.add("Content-Type", "application/vnd.schemaregistry.v1+json");
HttpEntity<String> request = new HttpEntity<>("", headers);
try {
ResponseEntity<Map> response = this.template.exchange(this.endpoint + path,
HttpMethod.GET, request, Map.class);
return (String) response.getBody().get("schema");
}
catch (HttpStatusCodeException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
throw new SchemaNotFoundException(String.format(
"Could not find schema for reference: %s", schemaReference));
}
else {
throw e;
}
}
}
@Override
public String fetch(int id) {
String path = String.format("/schemas/ids/%d", id);
HttpHeaders headers = new HttpHeaders();
headers.put("Accept", ACCEPT_HEADERS);
headers.add("Content-Type", "application/vnd.schemaregistry.v1+json");
HttpEntity<String> request = new HttpEntity<>("", headers);
try {
ResponseEntity<Map> response = this.template.exchange(this.endpoint + path,
HttpMethod.GET, request, Map.class);
return (String) response.getBody().get("schema");
}
catch (HttpStatusCodeException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
throw new SchemaNotFoundException(
String.format("Could not find schema with id: %s", id));
}
else {
throw e;
}
}
}
}

View File

@@ -0,0 +1,109 @@
/*
* 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.schema.registry.client;
import java.util.HashMap;
import java.util.Map;
import org.springframework.cloud.schema.registry.SchemaReference;
import org.springframework.cloud.schema.registry.SchemaRegistrationResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;
/**
* @author Marius Bogoevici
* @author Vinicius Carvalho
*/
public class DefaultSchemaRegistryClient implements SchemaRegistryClient {
private RestTemplate restTemplate;
private String endpoint = "http://localhost:8990";
public DefaultSchemaRegistryClient() {
this(new RestTemplate());
}
public DefaultSchemaRegistryClient(RestTemplate restTemplate) {
Assert.notNull(restTemplate, "'restTemplate' must not be null.");
this.restTemplate = restTemplate;
}
protected String getEndpoint() {
return this.endpoint;
}
public void setEndpoint(String endpoint) {
Assert.hasText(endpoint, "cannot be empty");
this.endpoint = endpoint;
}
protected RestTemplate getRestTemplate() {
return this.restTemplate;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public SchemaRegistrationResponse register(String subject, String format,
String schema) {
Map<String, String> requestBody = new HashMap<>();
requestBody.put("subject", subject);
requestBody.put("format", format);
requestBody.put("definition", schema);
ResponseEntity<Map> responseEntity = this.restTemplate
.postForEntity(this.endpoint, requestBody, Map.class);
if (responseEntity.getStatusCode().is2xxSuccessful()) {
SchemaRegistrationResponse registrationResponse = new SchemaRegistrationResponse();
Map<String, Object> responseBody = (Map<String, Object>) responseEntity
.getBody();
registrationResponse.setId((Integer) responseBody.get("id"));
registrationResponse.setSchemaReference(
new SchemaReference(subject, (Integer) responseBody.get("version"),
responseBody.get("format").toString()));
return registrationResponse;
}
throw new RuntimeException(
"Failed to register schema: " + responseEntity.toString());
}
@SuppressWarnings("rawtypes")
@Override
public String fetch(SchemaReference schemaReference) {
ResponseEntity<Map> responseEntity = this.restTemplate.getForEntity(this.endpoint
+ "/" + schemaReference.getSubject() + "/" + schemaReference.getFormat()
+ "/v" + schemaReference.getVersion(), Map.class);
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
throw new RuntimeException(
"Failed to fetch schema: " + responseEntity.toString());
}
return (String) responseEntity.getBody().get("definition");
}
@SuppressWarnings("rawtypes")
@Override
public String fetch(int id) {
ResponseEntity<Map> responseEntity = this.restTemplate
.getForEntity(this.endpoint + "/schemas/" + id, Map.class);
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
throw new RuntimeException(
"Failed to fetch schema: " + responseEntity.toString());
}
return (String) responseEntity.getBody().get("definition");
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.client;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.schema.registry.client.config.SchemaRegistryClientConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* @author Marius Bogoevici
*/
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Configuration
@Import(SchemaRegistryClientConfiguration.class)
public @interface EnableSchemaRegistryClient {
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.client;
import org.springframework.cloud.schema.registry.SchemaReference;
import org.springframework.cloud.schema.registry.SchemaRegistrationResponse;
/**
* @author Vinicius Carvalho
* @author Marius Bogoevici
*/
public interface SchemaRegistryClient {
/**
* Registers a schema with the remote repository returning the unique identifier
* associated with this schema.
* @param subject the full name of the schema
* @param format format of the schema
* @param schema string representation of the schema
* @return a {@link SchemaRegistrationResponse} representing the result of the
* operation
*/
SchemaRegistrationResponse register(String subject, String format, String schema);
/**
* Retrieves a schema by its reference (subject and version).
* @param schemaReference a {@link SchemaReference} used to identify the target
* schema.
* @return schema
*/
String fetch(SchemaReference schemaReference);
/**
* Retrieves a schema by its identifier.
* @param id the id of the target schema.
* @return schema
*/
String fetch(int id);
}

View File

@@ -0,0 +1,57 @@
/*
* 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.schema.registry.client.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.schema.registry.client.CachingRegistryClient;
import org.springframework.cloud.schema.registry.client.DefaultSchemaRegistryClient;
import org.springframework.cloud.schema.registry.client.SchemaRegistryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
/**
* @author Marius Bogoevici
* @author Vinicius Carvalho
* @author Soby Chacko
*/
@Configuration
@EnableConfigurationProperties(SchemaRegistryClientProperties.class)
public class SchemaRegistryClientConfiguration {
// @Autowired
// private SchemaRegistryClientProperties schemaRegistryClientProperties;
@Bean
@ConditionalOnMissingBean
public SchemaRegistryClient schemaRegistryClient(SchemaRegistryClientProperties schemaRegistryClientProperties) {
DefaultSchemaRegistryClient defaultSchemaRegistryClient = new DefaultSchemaRegistryClient();
if (StringUtils.hasText(schemaRegistryClientProperties.getEndpoint())) {
defaultSchemaRegistryClient
.setEndpoint(schemaRegistryClientProperties.getEndpoint());
}
SchemaRegistryClient client = (schemaRegistryClientProperties.isCached())
? new CachingRegistryClient(defaultSchemaRegistryClient)
: defaultSchemaRegistryClient;
return client;
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.schema.registry.client.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Marius Bogoevici
* @author Vinicius Carvalho
*/
@ConfigurationProperties(prefix = "spring.cloud.stream.schema-registry-client")
public class SchemaRegistryClientProperties {
private String endpoint;
private boolean cached = false;
public String getEndpoint() {
return this.endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public boolean isCached() {
return this.cached;
}
public void setCached(boolean cached) {
this.cached = cached;
}
}

View File

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

View File

@@ -0,0 +1,267 @@
/*
* 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.schema.registry.avro.AvroSchemaMessageConverter;
import org.springframework.cloud.schema.registry.avro.AvroSchemaServiceManager;
import org.springframework.cloud.schema.registry.avro.AvroSchemaServiceManagerImpl;
import org.springframework.cloud.schema.registry.client.SchemaRegistryClient;
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.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
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
public MessageConverter userMessageConverter() throws IOException {
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
AvroSchemaMessageConverter avroSchemaMessageConverter = new AvroSchemaMessageConverter(
MimeType.valueOf("avro/bytes"), manager);
if (this.schemaLocation != null) {
avroSchemaMessageConverter.setSchemaLocation(this.schemaLocation);
}
return avroSchemaMessageConverter;
}
}
}

View File

@@ -0,0 +1,179 @@
/*
* 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.schema.registry.avro.AvroSchemaMessageConverter;
import org.springframework.cloud.schema.registry.avro.AvroSchemaServiceManager;
import org.springframework.cloud.schema.registry.avro.AvroSchemaServiceManagerImpl;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.util.MimeType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
/**
* @author Ish Mahajan
*/
public class AvroSchemaServiceManagerTests {
@SuppressWarnings({ "rawtypes", "unchecked", "resource" })
@Test(expected = DataFileWriter.AppendWriteException.class)
public void testWithDefaultImplementation() throws IOException {
AvroSchemaServiceManager defaultServiceManager = new AvroSchemaServiceManagerImpl();
Schema schema = defaultServiceManager.getSchema(FoodOrder.class);
FoodOrder foodOrder = new FoodOrder();
foodOrder.setRestaurant("Spring Kitchen");
foodOrder.setOrderDescription("avro makhani");
foodOrder.setCustomerAddress("world wide web");
File file = new File("foodorder.avro");
DatumWriter datumWriter = defaultServiceManager.getDatumWriter(foodOrder.getClass(), schema);
DataFileWriter<FoodOrder> dataFileWriter = new DataFileWriter<FoodOrder>(datumWriter);
dataFileWriter.create(schema, file);
dataFileWriter.append(foodOrder);
FoodOrder foodOrder2 = new FoodOrder();
dataFileWriter.append(foodOrder2);
dataFileWriter.close();
DatumReader userDatumReader = defaultServiceManager.getDatumReader(foodOrder.getClass(), schema, schema);
DataFileReader<FoodOrder> dataFileReader = new DataFileReader<FoodOrder>(file, userDatumReader);
FoodOrder foodOrderDeserialized = null;
while (dataFileReader.hasNext()) {
// Reuse user object by passing it to next(). This saves us from
// allocating and garbage collecting many objects for files with
// many items.
foodOrderDeserialized = dataFileReader.next(foodOrderDeserialized);
System.out.println("De-serialised Successfully : " + foodOrderDeserialized);
}
}
@Test
public void testWithCustomImplementation() throws IOException {
AvroSchemaServiceManager manager = new AvroSchemaServiceManager() {
@Override
public Schema getSchema(Class<?> clazz) {
ObjectMapper mapper = new ObjectMapper(new AvroFactory());
AvroSchemaGenerator gen = new AvroSchemaGenerator();
try {
mapper.acceptJsonFormatVisitor(FoodOrder.class, gen);
}
catch (JsonMappingException e) {
fail("Error while setting acceptJsonFormatVisitor {}", e);
}
AvroSchema schemaWrapper = gen.getGeneratedSchema();
return schemaWrapper.getAvroSchema();
}
@Override
public DatumWriter<Object> getDatumWriter(Class<?> type, Schema schema) {
return new AvroSchemaServiceManagerImpl().getDatumWriter(type, schema);
}
@Override
public DatumReader<Object> getDatumReader(Class<?> type, Schema schema, Schema writerSchema) {
return new AvroSchemaServiceManagerImpl().getDatumReader(type, schema, schema);
}
@Override
public Object readData(Class<? extends Object> targetClass, byte[] payload, Schema readerSchema,
Schema writerSchema) throws IOException {
ObjectMapper mapper = new ObjectMapper(new AvroFactory());
AvroSchemaGenerator gen = new AvroSchemaGenerator();
try {
mapper.acceptJsonFormatVisitor(targetClass, gen);
}
catch (JsonMappingException e) {
fail("Error while setting acceptJsonFormatVisitor {}", e);
}
return mapper.readerFor(targetClass)
.with(new AvroSchema(readerSchema))
.readValue(payload);
}
};
FoodOrder foodOrder1 = new FoodOrder();
foodOrder1.setRestaurant("Spring Kitchen");
foodOrder1.setOrderDescription("avro makhani");
foodOrder1.setCustomerAddress("world wide web");
FoodOrder foodOrder2 = new FoodOrder();
foodOrder2.setRestaurant("Spring Kitchen");
Schema schema = manager.getSchema(FoodOrder.class);
AvroMapper mapper = new AvroMapper();
byte[] payload1 = mapper.writer(new AvroSchema(schema)).writeValueAsBytes(foodOrder1);
byte[] payload2 = mapper.writer(new AvroSchema(schema)).writeValueAsBytes(foodOrder2);
foodOrder1 = (FoodOrder) manager.readData(foodOrder1.getClass(), payload1, schema, schema);
foodOrder2 = (FoodOrder) manager.readData(foodOrder1.getClass(), payload2, schema, schema);
assertThat(foodOrder2.getOrderDescription()).isNull();
assertThat(foodOrder2.getCustomerAddress()).isNull();
}
@Test
public void testAvroSchemaMessageConverter() {
AvroSchemaMessageConverter converter = new AvroSchemaMessageConverter();
MimeType mimeType = new MimeType("application", "avro");
assertThat(mimeType).isEqualTo(converter.getSupportedMimeTypes().get(0));
AvroSchemaMessageConverter converter2 = new AvroSchemaMessageConverter(mimeType);
assertThat(mimeType).isEqualTo(converter2.getSupportedMimeTypes().get(0));
AvroSchemaMessageConverter converter3 =
new AvroSchemaMessageConverter(Lists.newArrayList(mimeType));
assertThat(mimeType).isEqualTo(converter3.getSupportedMimeTypes().get(0));
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
AvroSchemaMessageConverter converter4 = new AvroSchemaMessageConverter(manager);
assertThat(mimeType).isEqualTo(converter4.getSupportedMimeTypes().get(0));
AvroSchemaMessageConverter converter5 =
new AvroSchemaMessageConverter(Lists.newArrayList(mimeType), manager);
Schema schema = manager.getSchema(FoodOrder.class);
converter5.setSchema(schema);
assertThat(mimeType).isEqualTo(converter5.getSupportedMimeTypes().get(0));
assertThat(schema).isEqualTo(converter5.getSchema());
}
@Test(expected = SchemaParseException.class)
public void testAvroSchemaMessageConverterException() {
MimeType mimeType = new MimeType("application", "avro");
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
AvroSchemaMessageConverter converter =
new AvroSchemaMessageConverter(Lists.newArrayList(mimeType), manager);
converter.setSchemaLocation(new ByteArrayResource(new byte[2]) {
});
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.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.schema.registry.client.SchemaRegistryClient;
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.test.binder.MessageCollector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Marius Bogoevici
*/
public class AvroStubSchemaRegistryClientMessageConverterTests {
static SchemaRegistryClient stubSchemaRegistryClient = new StubSchemaRegistryClient();
@Test
public void testSendMessage() throws Exception {
ConfigurableApplicationContext sourceContext = SpringApplication.run(
AvroSourceApplication.class, "--server.port=0", "--debug",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.output.contentType=application/*+avro",
"--spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled=true");
Source source = sourceContext.getBean(Source.class);
User1 firstOutboundFoo = new User1();
firstOutboundFoo.setFavoriteColor("foo" + UUID.randomUUID().toString());
firstOutboundFoo.setName("foo" + UUID.randomUUID().toString());
source.output().send(MessageBuilder.withPayload(firstOutboundFoo).build());
MessageCollector sourceMessageCollector = sourceContext
.getBean(MessageCollector.class);
Message<?> outboundMessage = sourceMessageCollector.forChannel(source.output())
.poll(1000, TimeUnit.MILLISECONDS);
ConfigurableApplicationContext barSourceContext = SpringApplication.run(
AvroSourceApplication.class, "--server.port=0",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.output.contentType=application/vnd.user1.v1+avro",
"--spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled=true");
Source barSource = barSourceContext.getBean(Source.class);
User2 firstOutboundUser2 = new User2();
firstOutboundUser2.setFavoriteColor("foo" + UUID.randomUUID().toString());
firstOutboundUser2.setName("foo" + UUID.randomUUID().toString());
barSource.output().send(MessageBuilder.withPayload(firstOutboundUser2).build());
MessageCollector barSourceMessageCollector = barSourceContext
.getBean(MessageCollector.class);
Message<?> barOutboundMessage = barSourceMessageCollector
.forChannel(barSource.output()).poll(1000, TimeUnit.MILLISECONDS);
assertThat(barOutboundMessage).isNotNull();
User2 secondBarOutboundPojo = new User2();
secondBarOutboundPojo.setFavoriteColor("foo" + UUID.randomUUID().toString());
secondBarOutboundPojo.setName("foo" + UUID.randomUUID().toString());
source.output().send(MessageBuilder.withPayload(secondBarOutboundPojo).build());
Message<?> secondBarOutboundMessage = sourceMessageCollector
.forChannel(source.output()).poll(1000, TimeUnit.MILLISECONDS);
ConfigurableApplicationContext sinkContext = SpringApplication.run(
AvroSinkApplication.class, "--server.port=0",
"--spring.jmx.enabled=false");
Sink sink = sinkContext.getBean(Sink.class);
sink.input().send(outboundMessage);
sink.input().send(barOutboundMessage);
sink.input().send(secondBarOutboundMessage);
List<User2> receivedPojos = sinkContext
.getBean(AvroSinkApplication.class).receivedPojos;
assertThat(receivedPojos).hasSize(3);
assertThat(receivedPojos.get(0)).isNotSameAs(firstOutboundFoo);
assertThat(receivedPojos.get(0).getFavoriteColor())
.isEqualTo(firstOutboundFoo.getFavoriteColor());
assertThat(receivedPojos.get(0).getName()).isEqualTo(firstOutboundFoo.getName());
assertThat(receivedPojos.get(0).getFavoritePlace()).isEqualTo("NYC");
assertThat(receivedPojos.get(1)).isNotSameAs(firstOutboundUser2);
assertThat(receivedPojos.get(1).getFavoriteColor())
.isEqualTo(firstOutboundUser2.getFavoriteColor());
assertThat(receivedPojos.get(1).getName())
.isEqualTo(firstOutboundUser2.getName());
assertThat(receivedPojos.get(1).getFavoritePlace()).isEqualTo("Boston");
assertThat(receivedPojos.get(2)).isNotSameAs(secondBarOutboundPojo);
assertThat(receivedPojos.get(2).getFavoriteColor())
.isEqualTo(secondBarOutboundPojo.getFavoriteColor());
assertThat(receivedPojos.get(2).getName())
.isEqualTo(secondBarOutboundPojo.getName());
assertThat(receivedPojos.get(2).getFavoritePlace())
.isEqualTo(secondBarOutboundPojo.getFavoritePlace());
sourceContext.close();
}
@EnableBinding(Source.class)
@EnableAutoConfiguration
public static class AvroSourceApplication {
@Bean
public SchemaRegistryClient schemaRegistryClient() {
return stubSchemaRegistryClient;
}
}
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class AvroSinkApplication {
public List<User2> receivedPojos = new ArrayList<>();
@StreamListener(Sink.INPUT)
public void listen(User2 fooPojo) {
this.receivedPojos.add(fooPojo);
}
@Bean
public SchemaRegistryClient schemaRegistryClient() {
return stubSchemaRegistryClient;
}
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.avro;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.cloud.schema.registry.SchemaNotFoundException;
import org.springframework.cloud.schema.registry.SchemaReference;
import org.springframework.cloud.schema.registry.SchemaRegistrationResponse;
import org.springframework.cloud.schema.registry.avro.AvroSchemaRegistryClientMessageConverter;
import org.springframework.cloud.schema.registry.client.SchemaRegistryClient;
/**
* @author Marius Bogoevici
*/
public class StubSchemaRegistryClient implements SchemaRegistryClient {
private final AtomicInteger index = new AtomicInteger(0);
private final Map<Integer, String> schemasById = new HashMap<>();
private final Map<String, Map<Integer, SchemaWithId>> storedSchemas = new HashMap<>();
@Override
public SchemaRegistrationResponse register(String subject, String format,
String schema) {
if (!this.storedSchemas.containsKey(subject)) {
this.storedSchemas.put(subject, new TreeMap<Integer, SchemaWithId>());
}
Map<Integer, SchemaWithId> schemaVersions = this.storedSchemas.get(subject);
for (Map.Entry<Integer, SchemaWithId> integerSchemaEntry : schemaVersions
.entrySet()) {
if (integerSchemaEntry.getValue().getSchema().equals(schema)) {
SchemaRegistrationResponse schemaRegistrationResponse = new SchemaRegistrationResponse();
schemaRegistrationResponse.setId(integerSchemaEntry.getValue().getId());
schemaRegistrationResponse.setSchemaReference(
new SchemaReference(subject, integerSchemaEntry.getKey(),
AvroSchemaRegistryClientMessageConverter.AVRO_FORMAT));
return schemaRegistrationResponse;
}
}
int nextVersion = schemaVersions.size() + 1;
int id = this.index.incrementAndGet();
schemaVersions.put(nextVersion, new SchemaWithId(id, schema));
SchemaRegistrationResponse schemaRegistrationResponse = new SchemaRegistrationResponse();
schemaRegistrationResponse.setId(this.index.getAndIncrement());
schemaRegistrationResponse.setSchemaReference(new SchemaReference(subject,
nextVersion, AvroSchemaRegistryClientMessageConverter.AVRO_FORMAT));
this.schemasById.put(id, schema);
return schemaRegistrationResponse;
}
@Override
public String fetch(SchemaReference schemaReference) {
if (!AvroSchemaRegistryClientMessageConverter.AVRO_FORMAT
.equals(schemaReference.getFormat())) {
throw new IllegalArgumentException("Only 'avro' is supported by this client");
}
if (!this.storedSchemas.containsKey(schemaReference.getSubject())) {
throw new SchemaNotFoundException("Not found: " + schemaReference);
}
if (!this.storedSchemas.get(schemaReference.getSubject())
.containsKey(schemaReference.getVersion())) {
throw new SchemaNotFoundException("Not found: " + schemaReference);
}
return this.storedSchemas.get(schemaReference.getSubject())
.get(schemaReference.getVersion()).getSchema();
}
@Override
public String fetch(int id) {
return this.schemasById.get(id);
}
static class SchemaWithId {
int id;
String schema;
SchemaWithId(int id, String schema) {
this.id = id;
this.schema = schema;
}
public int getId() {
return this.id;
}
public String getSchema() {
return this.schema;
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.schema.registry.client.SchemaRegistryClient;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
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.schema.registry.avro.QualifiedSubjectNamingStrategy",
"--spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled=true");
Source source = sourceContext.getBean(Source.class);
User1 user1 = new User1();
user1.setFavoriteColor("foo" + UUID.randomUUID().toString());
user1.setName("foo" + UUID.randomUUID().toString());
source.output().send(MessageBuilder.withPayload(user1).build());
MessageCollector barSourceMessageCollector = sourceContext
.getBean(MessageCollector.class);
Message<?> message = barSourceMessageCollector.forChannel(source.output())
.poll(1000, TimeUnit.MILLISECONDS);
assertThat(message.getHeaders().get("contentType")).isEqualTo(MimeType.valueOf(
"application/vnd.org.springframework.cloud.schema.avro.User1.v1+avro"));
}
@EnableBinding(Source.class)
@EnableAutoConfiguration
public static class AvroSourceApplication {
@Bean
public SchemaRegistryClient schemaRegistryClient() {
return stubSchemaRegistryClient;
}
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.avro;
import org.apache.avro.reflect.Nullable;
/**
* @author Marius Bogoevici
*/
public class User1 {
@Nullable
private String name;
private int favoriteNumber;
@Nullable
private String favoriteColor;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getFavoriteNumber() {
return this.favoriteNumber;
}
public void setFavoriteNumber(int favoriteNumber) {
this.favoriteNumber = favoriteNumber;
}
public String getFavoriteColor() {
return this.favoriteColor;
}
public void setFavoriteColor(String favoriteColor) {
this.favoriteColor = favoriteColor;
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.avro;
import org.apache.avro.reflect.AvroDefault;
import org.apache.avro.reflect.Nullable;
/**
* @author Marius Bogoevici
*/
public class User2 {
@Nullable
private String name;
private int favoriteNumber;
@Nullable
private String favoriteColor;
@AvroDefault("\"NYC\"")
private String favoritePlace = "Boston";
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getFavoriteNumber() {
return this.favoriteNumber;
}
public void setFavoriteNumber(int favoriteNumber) {
this.favoriteNumber = favoriteNumber;
}
public String getFavoriteColor() {
return this.favoriteColor;
}
public void setFavoriteColor(String favoriteColor) {
this.favoriteColor = favoriteColor;
}
public String getFavoritePlace() {
return this.favoritePlace;
}
public void setFavoritePlace(String favoritePlace) {
this.favoritePlace = favoritePlace;
}
}

View File

@@ -0,0 +1,177 @@
/*
* 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.schema.registry.SchemaNotFoundException;
import org.springframework.cloud.schema.registry.SchemaReference;
import org.springframework.cloud.schema.registry.SchemaRegistrationResponse;
import org.springframework.cloud.schema.registry.client.ConfluentSchemaRegistryClient;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.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
*/
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/subjects/user/versions"))
.andExpect(method(HttpMethod.POST))
.andExpect(header("Content-Type", "application/json"))
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
.andRespond(withSuccess("{\"id\":101}", MediaType.APPLICATION_JSON));
this.mockRestServiceServer
.expect(requestTo("http://localhost:8081/subjects/user"))
.andExpect(method(HttpMethod.POST))
.andExpect(header("Content-Type", "application/json"))
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
.andRespond(withSuccess("{\"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/subjects/user/versions"))
.andExpect(method(HttpMethod.POST))
.andExpect(header("Content-Type", "application/json"))
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
.andRespond(withBadRequest());
ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(
this.restTemplate);
SchemaRegistrationResponse response = client.register("user", "avro", "<>");
}
@Test
public void registerIncompatibleSchema() {
this.mockRestServiceServer
.expect(requestTo("http://localhost:8081/subjects/user/versions"))
.andExpect(method(HttpMethod.POST))
.andExpect(header("Content-Type", "application/json"))
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
.andRespond(withStatus(HttpStatus.CONFLICT));
ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(
this.restTemplate);
Exception expected = null;
try {
SchemaRegistrationResponse response = client.register("user", "avro", "{}");
}
catch (Exception e) {
expected = e;
}
assertThat(expected instanceof RuntimeException).isTrue();
assertThat(expected.getCause() instanceof HttpStatusCodeException).isTrue();
this.mockRestServiceServer.verify();
}
@Test
public void responseErrorFetch() {
this.mockRestServiceServer
.expect(requestTo("http://localhost:8081/subjects/user/versions"))
.andExpect(method(HttpMethod.POST))
.andExpect(header("Content-Type", "application/json"))
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
.andRespond(withSuccess("{\"id\":101}", MediaType.APPLICATION_JSON));
this.mockRestServiceServer
.expect(requestTo("http://localhost:8081/subjects/user"))
.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 instanceof RuntimeException).isTrue();
assertThat(expected.getCause() instanceof HttpStatusCodeException).isTrue();
this.mockRestServiceServer.verify();
}
@Test
public void findByReference() {
this.mockRestServiceServer
.expect(requestTo("http://localhost:8081/subjects/user/versions/1"))
.andExpect(method(HttpMethod.GET))
.andExpect(
header("Content-Type", "application/vnd.schemaregistry.v1+json"))
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
.andRespond(withSuccess("{\"schema\":\"\"}", MediaType.APPLICATION_JSON));
ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(
this.restTemplate);
SchemaReference reference = new SchemaReference("user", 1, "avro");
String schema = client.fetch(reference);
assertThat(schema).isEqualTo("");
this.mockRestServiceServer.verify();
}
@Test(expected = SchemaNotFoundException.class)
public void schemaNotFound() {
this.mockRestServiceServer
.expect(requestTo("http://localhost:8081/subjects/user/versions/1"))
.andExpect(method(HttpMethod.GET))
.andExpect(
header("Content-Type", "application/vnd.schemaregistry.v1+json"))
.andExpect(header("Accept", "application/vnd.schemaregistry.v1+json"))
.andRespond(withStatus(HttpStatus.NOT_FOUND));
ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(
this.restTemplate);
SchemaReference reference = new SchemaReference("user", 1, "avro");
String schema = client.fetch(reference);
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.avro.domain;
/**
* @author Ish Mahajan
*/
public class FoodOrder {
private String restaurant;
private String customerAddress;
private String orderDescription;
public String getRestaurant() {
return restaurant;
}
public void setRestaurant(String restaurant) {
this.restaurant = restaurant;
}
public String getCustomerAddress() {
return customerAddress;
}
public void setCustomerAddress(String customerAddress) {
this.customerAddress = customerAddress;
}
public String getOrderDescription() {
return orderDescription;
}
public void setOrderDescription(String orderDescription) {
this.orderDescription = orderDescription;
}
}

View File

@@ -0,0 +1,234 @@
/*
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.serialization;
import java.io.ByteArrayOutputStream;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import example.avro.Command;
import example.avro.Email;
import example.avro.PushNotification;
import example.avro.Sms;
import example.avro.User;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.io.DatumWriter;
import org.apache.avro.io.Encoder;
import org.apache.avro.io.EncoderFactory;
import org.apache.avro.specific.SpecificDatumWriter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.support.NoOpCacheManager;
import org.springframework.cloud.schema.registry.EnableSchemaRegistryServer;
import org.springframework.cloud.schema.registry.SchemaReference;
import org.springframework.cloud.schema.registry.avro.AvroSchemaRegistryClientMessageConverter;
import org.springframework.cloud.schema.registry.avro.AvroSchemaServiceManager;
import org.springframework.cloud.schema.registry.avro.AvroSchemaServiceManagerImpl;
import org.springframework.cloud.schema.registry.avro.DefaultSubjectNamingStrategy;
import org.springframework.cloud.schema.registry.client.DefaultSchemaRegistryClient;
import org.springframework.cloud.schema.registry.client.SchemaRegistryClient;
import org.springframework.cloud.stream.binder.BinderHeaders;
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(
ServerApplication.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;
}
@SpringBootApplication
@EnableSchemaRegistryServer
public static class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
}

View File

@@ -0,0 +1,284 @@
/*
* 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.serialization;
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.autoconfigure.SpringBootApplication;
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.schema.avro.StubSchemaRegistryClient;
import org.springframework.cloud.schema.avro.User1;
import org.springframework.cloud.schema.avro.User2;
import org.springframework.cloud.schema.registry.EnableSchemaRegistryServer;
import org.springframework.cloud.schema.registry.avro.AvroSchemaRegistryClientMessageConverter;
import org.springframework.cloud.schema.registry.avro.AvroSchemaServiceManager;
import org.springframework.cloud.schema.registry.avro.AvroSchemaServiceManagerImpl;
import org.springframework.cloud.schema.registry.client.DefaultSchemaRegistryClient;
import org.springframework.cloud.schema.registry.client.EnableSchemaRegistryClient;
import org.springframework.cloud.schema.registry.client.SchemaRegistryClient;
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.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.serialization.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(
ServerApplication.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
AvroSchemaRegistryClientMessageConverter avroSchemaRegistryClientMessageConverter() {
AvroSchemaServiceManager manager = new AvroSchemaServiceManagerImpl();
return new AvroSchemaRegistryClientMessageConverter(
new DefaultSchemaRegistryClient(), new NoOpCacheManager(), manager);
}
@Bean
ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
@SpringBootApplication
@EnableSchemaRegistryServer
public static class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(AvroMessageConverterSerializationTests.ServerApplication.class, args);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
{
"namespace":"org.springframework.cloud.stream.samples",
"name": "Status",
"type" : "record",
"fields": [
{"name": "id", "type": "string"},
{"name": "text", "type": "string"},
{"name": "timestamp", "type": "long"}
]
}

View File

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

View File

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

View File

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

View File

View File

@@ -0,0 +1,45 @@
<?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-schema-registry-core</artifactId>
<parent>
<artifactId>spring-cloud-scheam-registry-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<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>
</dependencies>
</project>

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.schema.registry.config.SchemaServerConfiguration;
import org.springframework.context.annotation.Import;
/**
* Enables the schema registry server enpoints.
*
* @author Vinicius Carvalho
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(SchemaServerConfiguration.class)
public @interface EnableSchemaRegistryServer {
}

View File

@@ -0,0 +1,65 @@
/*
* 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.registry.config;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.domain.EntityScanPackages;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.schema.registry.controllers.ServerController;
import org.springframework.cloud.schema.registry.model.Schema;
import org.springframework.cloud.schema.registry.repository.SchemaRepository;
import org.springframework.cloud.schema.registry.support.AvroSchemaValidator;
import org.springframework.cloud.schema.registry.support.SchemaValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/**
* @author Vinicius Carvalho
* @author Soby Chacko
*/
@Configuration
@EnableJpaRepositories(basePackageClasses = SchemaRepository.class)
@EnableConfigurationProperties(SchemaServerProperties.class)
@Import(ServerController.class)
public class SchemaServerConfiguration {
@Bean
public static BeanFactoryPostProcessor entityScanPackagesPostProcessor() {
return beanFactory -> {
if (beanFactory instanceof BeanDefinitionRegistry) {
EntityScanPackages.register((BeanDefinitionRegistry) beanFactory,
Collections
.singletonList(Schema.class.getPackage().getName()));
}
};
}
@Bean
public Map<String, SchemaValidator> schemaValidators() {
Map<String, SchemaValidator> validatorMap = new HashMap<>();
validatorMap.put("avro", new AvroSchemaValidator());
return validatorMap;
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.registry.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Vinicius Carvalho
* @author Ilayaperumal Gopinathan
*/
@ConfigurationProperties("spring.cloud.stream.schema.server")
public class SchemaServerProperties {
/**
* Prefix for configuration resource paths (default is empty). Useful when embedding
* in another application when you don't want to change the context path or servlet
* path.
*/
private String path;
/**
* Boolean flag to enable/disable schema deletion.
*/
private boolean allowSchemaDeletion;
public String getPath() {
return this.path;
}
public void setPath(String path) {
this.path = path;
}
public boolean isAllowSchemaDeletion() {
return this.allowSchemaDeletion;
}
public void setAllowSchemaDeletion(boolean allowSchemaDeletion) {
this.allowSchemaDeletion = allowSchemaDeletion;
}
}

View File

@@ -0,0 +1,220 @@
/*
* 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.schema.registry.controllers;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.cloud.schema.registry.config.SchemaServerProperties;
import org.springframework.cloud.schema.registry.model.Schema;
import org.springframework.cloud.schema.registry.repository.SchemaRepository;
import org.springframework.cloud.schema.registry.support.InvalidSchemaException;
import org.springframework.cloud.schema.registry.support.SchemaDeletionNotAllowedException;
import org.springframework.cloud.schema.registry.support.SchemaNotFoundException;
import org.springframework.cloud.schema.registry.support.SchemaValidator;
import org.springframework.cloud.schema.registry.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
* @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 == 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) {
Optional<Schema> schema = this.repository.findById(id);
if (!schema.isPresent()) {
throw new SchemaNotFoundException("Could not find Schema");
}
return new ResponseEntity<>(schema.get(), HttpStatus.OK);
}
@RequestMapping(method = RequestMethod.GET, produces = "application/json", path = "/{subject}/{format}")
public ResponseEntity<List<Schema>> findBySubjectAndVersion(
@PathVariable("subject") String subject,
@PathVariable("format") String format) {
List<Schema> schemas = this.repository
.findBySubjectAndFormatOrderByVersion(subject, format);
if (schemas == null || schemas.size() == 0) {
throw new SchemaNotFoundException(String.format(
"No schemas found for subject %s and format %s", subject, format));
}
return new ResponseEntity<List<Schema>>(schemas, HttpStatus.OK);
}
@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();
}
}
private void deleteSchema(Schema schema) {
if (schema == null) {
throw new SchemaNotFoundException("Could not find Schema");
}
this.repository.delete(schema);
}
@ExceptionHandler(UnsupportedFormatException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Format not supported")
public void unsupportedFormat(UnsupportedFormatException ex) {
}
@ExceptionHandler(InvalidSchemaException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Invalid schema")
public void invalidSchema(InvalidSchemaException ex) {
}
@ExceptionHandler(SchemaNotFoundException.class)
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Schema not found")
public void schemaNotFound(SchemaNotFoundException ex) {
}
@ExceptionHandler(SchemaDeletionNotAllowedException.class)
@ResponseStatus(value = HttpStatus.METHOD_NOT_ALLOWED, reason = "Schema deletion is not permitted")
public void schemaDeletionNotPermitted(SchemaDeletionNotAllowedException ex) {
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.model;
/**
* @author Vinicius Carvalho
*/
public enum Compatibility {
/**
* Backward compatibiltity.
*/
BACKWARD,
/**
* Forward compatibility.
*/
FORWARD,
/**
* Full compatibility.
*/
FULL,
/**
* Lack of compatibility.
*/
INCOMPATIBLE;
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
/**
* @author Vinicius Carvalho
*
* Represents a persisted schema entity.
*/
@Entity
@Table(name = "SCHEMA_REPOSITORY")
public class Schema {
@Id
@GeneratedValue
@Column(name = "ID")
private Integer id;
@Column(name = "VERSION", nullable = false)
private Integer version;
@Column(name = "SUBJECT", nullable = false)
private String subject;
@Column(name = "FORMAT", nullable = false)
private String format;
@Lob
@Column(name = "DEFINITION", nullable = false, length = 8192)
private String definition;
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getVersion() {
return this.version;
}
public void setVersion(Integer version) {
this.version = version;
}
public String getSubject() {
return this.subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getFormat() {
return this.format;
}
public void setFormat(String format) {
this.format = format;
}
public String getDefinition() {
return this.definition;
}
public void setDefinition(String definition) {
this.definition = definition;
}
}

View File

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

View File

@@ -0,0 +1,69 @@
/*
* 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.registry.support;
import java.util.List;
import org.apache.avro.SchemaParseException;
import org.springframework.cloud.schema.registry.model.Compatibility;
import org.springframework.cloud.schema.registry.model.Schema;
/**
* @author Vinicius Carvalho
*/
public class AvroSchemaValidator implements SchemaValidator {
@Override
public boolean isValid(String definition) {
boolean result = true;
try {
new org.apache.avro.Schema.Parser().parse(definition);
}
catch (SchemaParseException ex) {
result = false;
}
return result;
}
@Override
public Compatibility compatibilityCheck(String source, String other) {
return null;
}
@Override
public Schema match(List<Schema> schemas, String definition) {
Schema result = null;
org.apache.avro.Schema source = new org.apache.avro.Schema.Parser()
.parse(definition);
for (Schema s : schemas) {
org.apache.avro.Schema target = new org.apache.avro.Schema.Parser()
.parse(s.getDefinition());
if (target.equals(source)) {
result = s;
break;
}
}
return result;
}
@Override
public String getFormat() {
return "avro";
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.support;
/**
* @author Vinicius Carvalho
*/
public class InvalidSchemaException extends RuntimeException {
public InvalidSchemaException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.support;
/**
* @author Ilayaperumal Gopinathan
*/
public class SchemaDeletionNotAllowedException extends RuntimeException {
public SchemaDeletionNotAllowedException(String message) {
super(message);
}
public SchemaDeletionNotAllowedException() {
super("Schema Deletion Not Allowed");
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.support;
/**
* @author Vinicius Carvalho
*/
public class SchemaNotFoundException extends RuntimeException {
public SchemaNotFoundException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.support;
import java.util.List;
import org.springframework.cloud.schema.registry.model.Compatibility;
import org.springframework.cloud.schema.registry.model.Schema;
/**
* @author Vinicius Carvalho
*
* Provides utility methods to validate, check compatibility and match schemas of
* different implementations
*/
public interface SchemaValidator {
/**
* Verifies if a definition is a valid schema.
* @param definition - The textual representation of the schema file
* @return true if valid, false otherwise
*/
boolean isValid(String definition);
/**
* Checks for compatibility between two schemas @see Compatibility class for types
* This method may not be supported for certain formats.
* @param source - The textual representation of the schema to tested
* @param other - The textual representation of the other schema to tested
* @return {@link Compatibility}
*/
Compatibility compatibilityCheck(String source, String other);
/**
* Return the Schema that is represented by the definition.
* @param schemas List of schemas to be tested
* @param definition Textual representation of the schema
* @return A full Schema object with identifier and subject properties
*/
Schema match(List<Schema> schemas, String definition);
String getFormat();
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.support;
/**
* @author Vinicius Carvalho
*/
public class UnsupportedFormatException extends RuntimeException {
public UnsupportedFormatException(String message) {
super(message);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.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.schema.registry.EnableSchemaRegistryServer;
import org.springframework.context.ConfigurableApplicationContext;
/**
* @author Marius Bogoevici
*/
public class EntityScanningTestsWithEntityScan {
@Test
public void testApplicationWithEmbeddedSchemaRegistryServerOutsideOfRootPackage() {
final ConfigurableApplicationContext context = SpringApplication
.run(CustomApplicationEmbeddingSchemaServer.class, "--server.port=0");
context.close();
}
@EnableAutoConfiguration
@EnableSchemaRegistryServer
@EntityScan(basePackages = "org.springframework.cloud.schema.registry.entityScanning.domain")
public static class CustomApplicationEmbeddingSchemaServer {
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.entityScanning.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
/**
* @author Marius Bogoevici
*/
@Entity
public class TestEntity {
@Id
private long id;
@Column(name = "name")
private String name;
public long getId() {
return this.id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,50 @@
<?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-schema-registry-server</artifactId>
<parent>
<artifactId>spring-cloud-scheam-registry-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-schema-registry-core</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2016-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.schema.registry.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.schema.registry.EnableSchemaRegistryServer;
/**
* @author Vinicius Carvalho
*/
// @checkstyle:off
@SpringBootApplication
@EnableSchemaRegistryServer
public class SchemaRegistryServerApplication {
public static void main(String[] args) {
SpringApplication.run(SchemaRegistryServerApplication.class, args);
}
}
// @checkstyle:on

View File

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

View File

@@ -0,0 +1,284 @@
/*
* 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.schema.registry.server;
import java.util.List;
import org.assertj.core.api.Assertions;
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.web.client.TestRestTemplate;
import org.springframework.cloud.schema.registry.config.SchemaServerProperties;
import org.springframework.cloud.schema.registry.model.Schema;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.context.WebApplicationContext;
/**
* @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 = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
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;
@Autowired
private SchemaServerProperties schemaServerProperties;
@Autowired
private WebApplicationContext wac;
@Test
public void testUnsupportedFormat() throws Exception {
Schema schema = new Schema();
schema.setFormat("spring");
schema.setSubject("boot");
ResponseEntity<Schema> response = this.client
.postForEntity("http://localhost:8990/", schema, Schema.class);
Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
@Test
public void testInvalidSchema() throws Exception {
Schema schema = new Schema();
schema.setFormat("avro");
schema.setSubject("boot");
schema.setDefinition("{}");
ResponseEntity<Schema> response = this.client
.postForEntity("http://localhost:8990/", schema, Schema.class);
Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
@Test
public void testUserSchemaV1() throws Exception {
Schema schema = new Schema();
schema.setFormat("avro");
schema.setSubject("org.springframework.cloud.stream.schema.User");
schema.setDefinition(this.USER_SCHEMA_V1);
ResponseEntity<Schema> response = this.client
.postForEntity("http://localhost:8990/", schema, Schema.class);
Assertions.assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
Assertions.assertThat(response.getBody().getVersion()).isEqualTo(new Integer(1));
List<String> location = response.getHeaders().get(HttpHeaders.LOCATION);
Assertions.assertThat(location).isNotNull();
ResponseEntity<Schema> persistedSchema = this.client.getForEntity(location.get(0),
Schema.class);
Assertions.assertThat(persistedSchema.getBody().getId())
.isEqualTo(response.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(this.USER_SCHEMA_V1);
Schema schema2 = new Schema();
schema2.setFormat("avro");
schema2.setSubject("org.springframework.cloud.stream.schema.User");
schema2.setDefinition(this.USER_SCHEMA_V2);
ResponseEntity<Schema> response = this.client
.postForEntity("http://localhost:8990/", schema, Schema.class);
Assertions.assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
Assertions.assertThat(response.getBody().getVersion()).isEqualTo(new Integer(1));
List<String> location = response.getHeaders().get(HttpHeaders.LOCATION);
Assertions.assertThat(location).isNotNull();
ResponseEntity<Schema> response2 = this.client
.postForEntity("http://localhost:8990/", schema2, Schema.class);
Assertions.assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
Assertions.assertThat(response2.getBody().getVersion()).isEqualTo(new Integer(2));
List<String> location2 = response2.getHeaders().get(HttpHeaders.LOCATION);
Assertions.assertThat(location2).isNotNull();
}
@Test
public void testIdempotentRegistration() throws Exception {
Schema schema = new Schema();
schema.setFormat("avro");
schema.setSubject("org.springframework.cloud.stream.schema.User");
schema.setDefinition(this.USER_SCHEMA_V1);
ResponseEntity<Schema> response = this.client
.postForEntity("http://localhost:8990/", schema, Schema.class);
Assertions.assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
Assertions.assertThat(response.getBody().getVersion()).isEqualTo(new Integer(1));
List<String> location = response.getHeaders().get(HttpHeaders.LOCATION);
Assertions.assertThat(location).isNotNull();
ResponseEntity<Schema> response2 = this.client
.postForEntity("http://localhost:8990/", schema, Schema.class);
Assertions.assertThat(response2.getBody().getId()).isEqualTo(response.getBody().getId());
}
@Test
public void testSchemaNotfound() throws Exception {
ResponseEntity<Schema> response = this.client
.getForEntity("http://localhost:8990/foo/avro/v42", Schema.class);
Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void testSchemaDeletionBySubjectFormatVersion() throws Exception {
Schema schema = new Schema();
schema.setFormat("avro");
schema.setSubject("test");
schema.setDefinition(this.USER_SCHEMA_V1);
ResponseEntity<Schema> response1 = this.client
.postForEntity("http://localhost:8990/", schema, Schema.class);
Assertions.assertThat(response1.getStatusCode().is2xxSuccessful()).isTrue();
this.schemaServerProperties.setAllowSchemaDeletion(true);
this.client.delete("http://localhost:8990/test/avro/v1");
ResponseEntity<Schema> response2 = this.client
.getForEntity("http://localhost:8990/test/avro/v1", Schema.class);
Assertions.assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void testSchemaDeletionById() throws Exception {
Schema schema = new Schema();
schema.setFormat("avro");
schema.setSubject("test");
schema.setDefinition(this.USER_SCHEMA_V1);
ResponseEntity<Schema> response1 = this.client
.postForEntity("http://localhost:8990/", schema, Schema.class);
Assertions.assertThat(response1.getStatusCode().is2xxSuccessful()).isTrue();
ResponseEntity<Schema> response2 = this.client
.getForEntity("http://localhost:8990/test/avro/v1", Schema.class);
Assertions.assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.OK);
this.schemaServerProperties.setAllowSchemaDeletion(true);
this.client.delete("http://localhost:8990/schemas/1");
ResponseEntity<Schema> response3 = this.client
.getForEntity("http://localhost:8990/test/avro/1", Schema.class);
Assertions.assertThat(response3.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void testSchemaDeletionBySubject() throws Exception {
Schema schema1 = new Schema();
schema1.setFormat("avro");
schema1.setSubject("test");
schema1.setDefinition(this.USER_SCHEMA_V1);
ResponseEntity<Schema> response1 = this.client
.postForEntity("http://localhost:8990/", schema1, Schema.class);
Assertions.assertThat(response1.getStatusCode().is2xxSuccessful()).isTrue();
Assertions.assertThat(this.client
.getForEntity("http://localhost:8990/test/avro/v1", Schema.class)
.getStatusCode()).isEqualTo(HttpStatus.OK);
this.client.getForEntity("http://localhost:8990/test/avro/1", Schema.class);
Schema schema2 = new Schema();
schema2.setFormat("avro");
schema2.setSubject("test");
schema2.setDefinition(this.USER_SCHEMA_V2);
ResponseEntity<Schema> response2 = this.client
.postForEntity("http://localhost:8990/", schema2, Schema.class);
Assertions.assertThat(response2.getStatusCode().is2xxSuccessful()).isTrue();
Assertions.assertThat(this.client
.getForEntity("http://localhost:8990/test/avro/v2", Schema.class)
.getStatusCode()).isEqualTo(HttpStatus.OK);
this.schemaServerProperties.setAllowSchemaDeletion(true);
this.client.delete("http://localhost:8990/test");
ResponseEntity<Schema> response4 = this.client
.getForEntity("http://localhost:8990/test/avro/v1", Schema.class);
Assertions.assertThat(response4.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
ResponseEntity<Schema> response5 = this.client
.getForEntity("http://localhost:8990/test/avro/v2", Schema.class);
Assertions.assertThat(response5.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void testSchemaDeletionNotAllowed() throws Exception {
Schema schema = new Schema();
schema.setFormat("avro");
schema.setSubject("test");
schema.setDefinition(this.USER_SCHEMA_V1);
ResponseEntity<Schema> response1 = this.client
.postForEntity("http://localhost:8990/", schema, Schema.class);
Assertions.assertThat(response1.getStatusCode().is2xxSuccessful()).isTrue();
ResponseEntity<Object> deleteBySubjectFormatVersion = this.client.exchange(
"http://localhost:8990/test/avro/v1", HttpMethod.DELETE, null,
Object.class);
Assertions.assertThat(deleteBySubjectFormatVersion.getStatusCode())
.isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
ResponseEntity<Object> deleteBySubject = this.client.exchange(
"http://localhost:8990/test", HttpMethod.DELETE, null, Object.class);
Assertions.assertThat(deleteBySubject.getStatusCode())
.isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
ResponseEntity<Object> deleteById = this.client.exchange(
"http://localhost:8990/schemas/1", HttpMethod.DELETE, null, Object.class);
Assertions.assertThat(deleteById.getStatusCode()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
}
@Test
public void testFindSchemasBySubjectAndVersion() throws Exception {
Schema v1 = new Schema();
v1.setFormat("avro");
v1.setSubject("test");
v1.setDefinition(this.USER_SCHEMA_V1);
ResponseEntity<Schema> response1 = this.client
.postForEntity("http://localhost:8990/", v1, Schema.class);
Assertions.assertThat(response1.getStatusCode().is2xxSuccessful()).isTrue();
Schema v2 = new Schema();
v2.setFormat("avro");
v2.setSubject("test");
v2.setDefinition(this.USER_SCHEMA_V2);
ResponseEntity<Schema> response2 = this.client
.postForEntity("http://localhost:8990/", v2, Schema.class);
Assertions.assertThat(response2.getStatusCode().is2xxSuccessful()).isTrue();
ResponseEntity<List<Schema>> schemaResponse = this.client.exchange(
"http://localhost:8990/test/avro", HttpMethod.GET, null,
new ParameterizedTypeReference<List<Schema>>() {
});
Assertions.assertThat(schemaResponse.getStatusCode().is2xxSuccessful()).isTrue();
Assertions.assertThat(schemaResponse.getBody().size()).isEqualTo(2);
}
}