Initial commit
Migrating schema registry code from Spring Cloud Stream core into its own code base
This commit is contained in:
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal 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
1
.mvn/jvm.config
Normal file
@@ -0,0 +1 @@
|
||||
-Xmx1024m -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom
|
||||
1
.mvn/maven.config
Normal file
1
.mvn/maven.config
Normal 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
110
.mvn/wrapper/MavenWrapperDownloader.java
vendored
Executable 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
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Executable file
Binary file not shown.
1
.mvn/wrapper/maven-wrapper.properties
vendored
Executable file
1
.mvn/wrapper/maven-wrapper.properties
vendored
Executable 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
67
docs/pom.xml
Normal 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>
|
||||
42
docs/src/main/asciidoc/README.adoc
Normal file
42
docs/src/main/asciidoc/README.adoc
Normal 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
330
docs/src/main/asciidoc/ghpages.sh
Executable 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
|
||||
BIN
docs/src/main/asciidoc/images/schema_reading.png
Normal file
BIN
docs/src/main/asciidoc/images/schema_reading.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
docs/src/main/asciidoc/images/schema_resolution.png
Normal file
BIN
docs/src/main/asciidoc/images/schema_resolution.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
16
docs/src/main/asciidoc/index.adoc
Normal file
16
docs/src/main/asciidoc/index.adoc
Normal 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
|
||||
25
docs/src/main/asciidoc/sagan-index.adoc
Normal file
25
docs/src/main/asciidoc/sagan-index.adoc
Normal 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.
|
||||
|
||||
|
||||
|
||||
366
docs/src/main/asciidoc/spring-cloud-schema-registry.adoc
Normal file
366
docs/src/main/asciidoc/spring-cloud-schema-registry.adoc
Normal 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.
|
||||
37
docs/src/main/ruby/generate_readme.sh
Executable file
37
docs/src/main/ruby/generate_readme.sh
Executable 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
286
mvnw
vendored
Executable 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
161
mvnw.cmd
vendored
Executable 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
208
pom.xml
Normal 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>
|
||||
0
spring-cloud-schema-registry-client/.jdk8
Normal file
0
spring-cloud-schema-registry-client/.jdk8
Normal file
BIN
spring-cloud-schema-registry-client/foodorder.avro
Normal file
BIN
spring-cloud-schema-registry-client/foodorder.avro
Normal file
Binary file not shown.
106
spring-cloud-schema-registry-client/pom.xml
Normal file
106
spring-cloud-schema-registry-client/pom.xml
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + '\'' + '}';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.schema.registry.avro.AvroMessageConverterAutoConfiguration
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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]) {
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"namespace":"example.avro",
|
||||
"name": "Email",
|
||||
"type": "record",
|
||||
"fields":[
|
||||
{
|
||||
"name":"addressTo",
|
||||
"type":"string"
|
||||
},
|
||||
{
|
||||
"name":"title",
|
||||
"type":"string"
|
||||
},
|
||||
{
|
||||
"name":"text",
|
||||
"type":"string"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"namespace":"example.avro",
|
||||
"name": "PushNotification",
|
||||
"type": "record",
|
||||
"fields":[
|
||||
{
|
||||
"name":"arn",
|
||||
"type":"string"
|
||||
},
|
||||
{
|
||||
"name":"text",
|
||||
"type":"string"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"namespace":"example.avro",
|
||||
"name": "Sms",
|
||||
"type": "record",
|
||||
"fields":[
|
||||
{
|
||||
"name":"phoneNumber",
|
||||
"type":"string"
|
||||
},{
|
||||
"name":"text",
|
||||
"type":"string"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
@@ -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"]}
|
||||
|
||||
]
|
||||
}
|
||||
@@ -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"]}
|
||||
|
||||
]
|
||||
}
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
0
spring-cloud-schema-registry-core/.jdk8
Normal file
0
spring-cloud-schema-registry-core/.jdk8
Normal file
45
spring-cloud-schema-registry-core/pom.xml
Normal file
45
spring-cloud-schema-registry-core/pom.xml
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
spring:
|
||||
application:
|
||||
name: SchemaRegistryServer
|
||||
server:
|
||||
port: 8990
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
0
spring-cloud-schema-registry-server/.jdk8
Normal file
0
spring-cloud-schema-registry-server/.jdk8
Normal file
50
spring-cloud-schema-registry-server/pom.xml
Normal file
50
spring-cloud-schema-registry-server/pom.xml
Normal 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>
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
spring:
|
||||
application:
|
||||
name: SchemaRegistryServer
|
||||
server:
|
||||
port: 8990
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user