diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..76b8b44e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +# Change these settings to your own preference +indent_style = space +indent_size = 2 +# We recommend you to keep these unchanged +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +max_line_length = 140 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..a217b347 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: maven + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml new file mode 100644 index 00000000..e29f6f92 --- /dev/null +++ b/.github/workflows/development.yml @@ -0,0 +1,61 @@ +name: Development branches + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + name: Build and run tests on JDK 11 + steps: + # Checkout the code + - name: Checkout code + uses: actions/checkout@v2 + + - name: Expose branch name + run: echo ${{ github.ref }} + + # Setup the cache + - name: Cache .m2 + uses: actions/cache@v1 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven + + # Setup JDK and Maven + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + server-id: ossrh + server-username: OSS_CENTRAL_USERNAME # env variable for Maven Central + server-password: OSS_CENTRAL_PASSWORD # env variable for Maven Central + + # Prepare + - name: Prepare Maven Wrapper + run: chmod +x ./mvnw + + # Build + - name: Build with Maven + run: ./mvnw clean verify -U -B -T4 + + # I-Test + - name: Run I-Test + run: ./mvnw verify -Pitest -U -B -T4 + + # Code analysis + - name: Upload coverage to Codecov + if: github.event_name == 'push' && github.actor != 'dependabot[bot]' + uses: codecov/codecov-action@v1.0.2 + with: + token: ${{secrets.CODECOV_TOKEN}} + + - name: Upload coverage to Codacy + if: github.event_name == 'push' && github.actor != 'dependabot[bot]' + uses: codacy/codacy-coverage-reporter-action@v1 + with: + project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} + coverage-reports: lib/coverage-aggregate/target/site/jacoco-aggregate/jacoco.xml diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml new file mode 100644 index 00000000..8d9e4017 --- /dev/null +++ b/.github/workflows/master.yml @@ -0,0 +1,70 @@ +name: Produces and releases artifacts + +on: + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + name: Build and run tests on JDK ${{ matrix.java }} + steps: + - name: Checkout code + uses: actions/checkout@v2 + + # Restore the cache first + - name: Cache .m2 + uses: actions/cache@v1 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven + + # Get GPG private key into GPG + - name: Import GPG Owner Trust + run: echo ${{ secrets.GPG_OWNERTRUST }} | base64 --decode | gpg --import-ownertrust + + - name: Import GPG key + run: echo ${{ secrets.GPG_SECRET_KEYS }} | base64 --decode | gpg --import --no-tty --batch --yes + + # Setup JDK and .m2/settings.xml + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + server-id: ossrh + server-username: OSS_CENTRAL_USERNAME # env variable for Maven Central + server-password: OSS_CENTRAL_PASSWORD # env variable for Maven Central + + # Prepare + - name: Prepare Maven Wrapper + run: chmod +x ./mvnw + + # Build + - name: Build with Maven + run: ./mvnw clean verify -U -B -T4 + + # I-Test + - name: Run I-Test + run: ./mvnw verify -Pitest -U -B -T4 + + # Publish release + - name: Deploy a new release version to Maven Central + run: ./mvnw clean deploy -B -DskipTests -DskipExamples -Prelease -Dgpg.keyname="${{ secrets.GPG_KEYNAME }}" -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" + env: + OSS_CENTRAL_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + OSS_CENTRAL_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + + # Code analysis + - name: Upolad coverage information + uses: codecov/codecov-action@v2 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Upload coverage to Codacy + uses: codacy/codacy-coverage-reporter-action@v1 + with: + project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} + coverage-reports: lib/coverage-aggregate/target/site/jacoco-aggregate/jacoco.xml diff --git a/.gitignore b/.gitignore index 0e13eebb..d5f6528c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,26 @@ buildNumber.properties .mvn/timing.properties # https://github.com/takari/maven-wrapper#usage-without-binary-jar .mvn/wrapper/maven-wrapper.jar + +_tmp/ +_old/ + +.attach_* +*.iml +.idea/ +.classpath +.project +.settings/ +.meta-data/ +.java-version +.DS_Store +.vscode/ + +**/.tmp/ +.repository + +docs/ + +javacore.*.txt +Snap.*.trc + diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..b901097f --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * 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/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".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 directory '" + 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 { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + 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(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..ffdc10e5 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/README.md b/README.md index 3a40e471..152bd3ad 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ -# avro-adapter +# avro-registry-adapter + +[](https://github.com/holixon/avro-registry-adapter/actions) +[](https://www.codacy.com/gh/holixon/avro-registry-adapter/dashboard?utm_source=github.com&utm_medium=referral&utm_content=holixon/avro-registry-adapter&utm_campaign=Badge_Grade) +[](https://codecov.io/gh/holixon/avro-registry-adapter) +[](https://maven-badges.herokuapp.com/maven-central/io.holixon.avro/avro-registry-adapter-bom) +[](https://holisticon.de/) + Convenient support for working with avro serialization on the JVM + +## Modules + +### `avro-registry-adapter-bom` + +Bill of material that lists all modules for convenient dependency definition. + +### `avro-registry-adapter-api` + +Defines core interfaces and helpers to simplify and generalize working with apache avro. +These interfaces do not rely on the default serialization of the avro lib, so they can be implemented against various +registries and serialization strategies. + +### `avro-registry-adapter-default` + +Implements `avro-registry-adapter-api` following the default avro lib serialization and deserialization specifications. + +### `avro-registry-adapter-apicurio-rest` + +Implements a [AvroSchemaRegistry](./extension/api/src/main/kotlin/AvroSchemaRegistry.kt) that uses the [apicurio-registry-client](https://github.com/Apicurio/apicurio-registry/tree/master/client) to connect to the apicurio registry via REST. diff --git a/bom/pom.xml b/bom/pom.xml new file mode 100644 index 00000000..58934b4f --- /dev/null +++ b/bom/pom.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + 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> + + <parent> + <groupId>io.holixon.avro._</groupId> + <artifactId>avro-registry-adapter</artifactId> + <version>0.0.1</version> + </parent> + + <groupId>io.holixon.avro</groupId> + <artifactId>avro-registry-adapter-bom</artifactId> + <packaging>pom</packaging> + + <properties> + <avro-registry-adapter.version>${project.version}</avro-registry-adapter.version> + <avro.version>1.10.2</avro.version> + </properties> + + <dependencyManagement> + <dependencies> + <dependency> + <artifactId>avro-registry-adapter-api</artifactId> + <groupId>${project.groupId}</groupId> + <version>${avro-registry-adapter.version}</version> + </dependency> + + <dependency> + <artifactId>avro-registry-adapter-default</artifactId> + <groupId>${project.groupId}</groupId> + <version>${avro-registry-adapter.version}</version> + </dependency> + + <dependency> + <artifactId>avro-registry-adapter-apicurio</artifactId> + <groupId>${project.groupId}</groupId> + <version>${avro-registry-adapter.version}</version> + </dependency> + + <!-- AVRO --> + <dependency> + <groupId>org.apache.avro</groupId> + <artifactId>avro</artifactId> + <version>${avro.version}</version> + <scope>provided</scope> + </dependency> + + </dependencies> + </dependencyManagement> + + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.avro</groupId> + <artifactId>avro-maven-plugin</artifactId> + <version>${avro.version}</version> + </plugin> + </plugins> + </pluginManagement> + </build> +</project> diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 00000000..bb023ed2 --- /dev/null +++ b/detekt.yml @@ -0,0 +1,26 @@ +# +# see https://github.com/arturbosch/detekt/blob/master/detekt-cli/src/main/resources/default-detekt-config.yml +# +comments: + active: true + excludes: "examples/**,**/test/**,**/examples/**,**/*Test.kt,*/src/test/kotlin/**/*.kt,**/*Stages.kt,**/*ITest.kt" + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!:]$) + UndocumentedPublicClass: + active: true + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + UndocumentedPublicFunction: + active: true + +performance: + active: true + SpreadOperator: + active: false diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml new file mode 100644 index 00000000..dc12fa93 --- /dev/null +++ b/examples/docker-compose.yml @@ -0,0 +1,6 @@ +version: '3.1' +services: + registry: + image: apicurio/apicurio-registry-mem:2.0.0.Final + ports: + - "7777:8080" diff --git a/examples/pom.xml b/examples/pom.xml new file mode 100644 index 00000000..26051a10 --- /dev/null +++ b/examples/pom.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + 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> + + <parent> + <groupId>io.holixon.avro._</groupId> + <artifactId>avro-registry-adapter-parent</artifactId> + <version>0.0.1</version> + <relativePath>../parent/pom.xml</relativePath> + </parent> + + <artifactId>examples</artifactId> + <packaging>pom</packaging> + + <properties> + <jacoco.skip>true</jacoco.skip> + <maven.javadoc.skip>true</maven.javadoc.skip> + <maven.sources.skip>true</maven.sources.skip> + <maven.install.skip>true</maven.install.skip> + <gpg.skip>true</gpg.skip> + + <springdoc.version>1.5.6</springdoc.version> + </properties> + + <dependencies> + <!-- No dependencies here, define explicitly in example --> + </dependencies> + +</project> diff --git a/extension/api/pom.xml b/extension/api/pom.xml new file mode 100644 index 00000000..00cf08ee --- /dev/null +++ b/extension/api/pom.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + 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> + + <parent> + <groupId>io.holixon.avro._</groupId> + <artifactId>extension</artifactId> + <version>0.0.1</version> + </parent> + + <groupId>io.holixon.avro</groupId> + <artifactId>avro-registry-adapter-api</artifactId> + <description>API of the registry and Avro helpers.</description> + + <properties> + <jacoco.skip>false</jacoco.skip> + </properties> +</project> diff --git a/extension/api/src/main/kotlin/AvroAdapterApi.kt b/extension/api/src/main/kotlin/AvroAdapterApi.kt new file mode 100644 index 00000000..b82a06fc --- /dev/null +++ b/extension/api/src/main/kotlin/AvroAdapterApi.kt @@ -0,0 +1,78 @@ +package io.holixon.avro.adapter.api + +import io.holixon.avro.adapter.api.type.AvroSchemaInfoData +import org.apache.avro.Schema +import org.apache.avro.specific.SpecificData +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.util.* +import java.util.function.BiFunction +import java.util.function.Function +import java.util.function.Predicate +import kotlin.reflect.KClass + +/** + * Returns a unique id for the given schema that is used to load the schema from a repository. + */ +fun interface SchemaIdSupplier : Function<Schema, AvroSchemaId> + +/** + * Search schema based on schemaId. + */ +fun interface SchemaResolver : Function<AvroSchemaId, Optional<AvroSchemaWithId>> + +/** + * Takes encoded avro bytes (containing schema reference) and return decoded payload and the resolved schema. + */ +fun interface SingleObjectDecoder : Function<AvroSingleObjectEncoded, AvroPayloadAndSchema> + +/** + * Use given Schema information and bytecode payload to return decoded bytes. + */ +fun interface SingleObjectEncoder : BiFunction<AvroSchemaWithId, ByteArray, AvroSingleObjectEncoded> + +/** + * Returns the revision for a given schema. + */ +fun interface SchemaRevisionResolver : Function<Schema, Optional<AvroSchemaRevision>> + +/** + * Returns `true` if the [ByteBuffer] conforms to the singleObject encoding specification. + */ +fun interface IsAvroSingleObjectEncodedPredicate : Predicate<ByteBuffer> + +/** + * Global utility methods. + */ +object AvroAdapterApi { + + fun Schema.byteContent() = this.toString().byteInputStream(StandardCharsets.UTF_8) + + /** + * Determines the revision of a given schema by reading the String value of the given object-property. + */ + @JvmStatic + fun propertyBasedSchemaRevisionResolver(propertyKey: String): SchemaRevisionResolver = + SchemaRevisionResolver { schema -> Optional.ofNullable(schema.getObjectProp(propertyKey) as String?) } + + @JvmStatic + fun schemaForClass(recordClass: Class<*>): Schema = SpecificData(recordClass.classLoader).getSchema(recordClass) + + @JvmStatic + fun schemaForClass(recordClass: KClass<*>) = schemaForClass(recordClass.java) + + @JvmStatic + fun Schema.extractSchemaInfo(schemaRevisionResolver: SchemaRevisionResolver) = AvroSchemaInfoData( + namespace = namespace, + name = name, + revision = schemaRevisionResolver.apply(this).orElse(null) + ) + + /** + * Creates a schema resolver out of a read-only registry. + * @return [SchemaResolver] derived from registry. + */ + fun AvroSchemaReadOnlyRegistry.schemaResolver() = SchemaResolver { schemaId -> this@schemaResolver.findById(schemaId) } + +} + diff --git a/extension/api/src/main/kotlin/AvroAdapterTypes.kt b/extension/api/src/main/kotlin/AvroAdapterTypes.kt new file mode 100644 index 00000000..1f1b3ab2 --- /dev/null +++ b/extension/api/src/main/kotlin/AvroAdapterTypes.kt @@ -0,0 +1,149 @@ +package io.holixon.avro.adapter.api + +import org.apache.avro.Schema + +/** + * The unique id of a schema artifact, published to a repo. + */ +typealias AvroSchemaId = String + +/** + * The version of a schema. + */ +typealias AvroSchemaRevision = String + +/** + * Message encoded as [Single Object](https://avro.apache.org/docs/current/spec.html#single_object_encoding) ByteArray. + */ +typealias AvroSingleObjectEncoded = ByteArray + +/** + * The encoded message. This is only the payload data, + * so no marker header and encoded schemaId are present. + */ +typealias AvroSingleObjectPayload = ByteArray + +/** + * Wrapper type for [AvroSchemaId] and the encoded message [AvroSingleObjectPayload]. + */ +interface AvroPayloadAndSchemaId { + val schemaId: AvroSchemaId + val payload: AvroSingleObjectPayload + + operator fun component1() = schemaId + operator fun component2() = payload +} + +/** + * Wrapper type containing the [AvroSingleObjectPayload], the [Schema] and the artifacts [AvroSchemaId]. + */ +interface AvroPayloadAndSchema { + val schema: AvroSchemaWithId + val payload: AvroSingleObjectPayload + + operator fun component1() = schema + operator fun component2() = payload +} + +/** + * The schema info provides the relevant identifiers for a schema: + * + * * context (aka namespace) + * * name + * * revision + * + */ +interface AvroSchemaInfo { + + companion object { + /** + * Default separator used in canonical name. + */ + const val NAME_SEPARATOR = "." + } + + /** + * Schema namespace. + */ + val namespace: String + + /** + * Schema name. + */ + val name: String + + /** + * Optional revision. + */ + val revision: AvroSchemaRevision? + + /** + * Canonical schema revision. + */ + val canonicalName: String + get() = "$namespace$NAME_SEPARATOR$name" +} + +/** + * Tuple wrapping the schema and its id. + */ +interface AvroSchemaWithId : AvroSchemaInfo { + /** + * Id of the schema. + */ + val schemaId: AvroSchemaId + + /** + * Avro schema. + */ + val schema: Schema + + operator fun component1() = schemaId + operator fun component2() = schema + operator fun component3() = revision +} + +// FIXME: implement meta +// interface AvroSchemaMeta { +// val name: String? +// +// val description: String? +// +// val labels: List<String>? +// +// @JsonProperty("createdBy") +// private val createdBy: String? = null +// +// @JsonProperty("createdOn") +// private val createdOn: Long = 0 +// +// @JsonProperty("modifiedBy") +// private val modifiedBy: String? = null +// +// @JsonProperty("modifiedOn") +// private val modifiedOn: Long = 0 +// +// @JsonProperty("id") +// @JsonPropertyDescription("") +// private val id: String? = null +// +// @JsonProperty("version") +// @JsonPropertyDescription("") +// private val version: Int? = null +// +// @JsonProperty("type") +// @JsonPropertyDescription("") +// private val type: io.apicurio.registry.types.ArtifactType? = null +// +// @JsonProperty("globalId") +// @JsonPropertyDescription("") +// private val globalId: Long? = null +// +// @JsonProperty("state") +// @JsonPropertyDescription("Describes the state of an artifact or artifact version. The following states\nare possible:\n\n* ENABLED\n* DISABLED\n* DEPRECATED\n") +// private val state: ArtifactState? = null +// +// @JsonProperty("properties") +// @JsonPropertyDescription("A set of name-value properties for an artifact or artifact version.") +// private val properties: Map<String, String>? = null +// } diff --git a/extension/api/src/main/kotlin/AvroSchemaIncompatibilityResolver.kt b/extension/api/src/main/kotlin/AvroSchemaIncompatibilityResolver.kt new file mode 100644 index 00000000..e731a0e7 --- /dev/null +++ b/extension/api/src/main/kotlin/AvroSchemaIncompatibilityResolver.kt @@ -0,0 +1,19 @@ +package io.holixon.avro.adapter.api + +import org.apache.avro.Schema +import org.apache.avro.SchemaCompatibility +import org.apache.avro.message.SchemaStore + +/** + * Resolves incompatibility check result and delivers the reader schema used by decoding. + */ +fun interface AvroSchemaIncompatibilityResolver { + /** + * Resolves possible schema incompatibility. + * @return reader schema to use. + */ + fun resolve( + readerSchema: Schema, + writerSchema: Schema + ): Schema +} diff --git a/extension/api/src/main/kotlin/AvroSchemaRegistry.kt b/extension/api/src/main/kotlin/AvroSchemaRegistry.kt new file mode 100644 index 00000000..f3d2b8af --- /dev/null +++ b/extension/api/src/main/kotlin/AvroSchemaRegistry.kt @@ -0,0 +1,42 @@ +package io.holixon.avro.adapter.api + +import org.apache.avro.Schema +import java.util.* + +/** + * The Schema Registry is responsible for storing and retrieving arvo schema files. + */ +interface AvroSchemaRegistry : AvroSchemaReadOnlyRegistry { + + /** + * Stores a new [Schema] (version) in the repository. + */ + fun register(schema: Schema): AvroSchemaWithId + +} + +/** + * The Schema Registry is responsible for retrieving arvo schema files. + */ +interface AvroSchemaReadOnlyRegistry { + /** + * Finds a stored [Schema] based on its unique [AvroSchemaId] (e.g. its fingerprint). + */ + fun findById(schemaId: AvroSchemaId): Optional<AvroSchemaWithId> + + /** + * Finds a stored [Schema] based on its derived info. + */ + fun findByInfo(info: AvroSchemaInfo): Optional<AvroSchemaWithId> + + /** + * Finds all stored [Schema]s based on their namespaces and names (e.g. FQN). + */ + fun findAllByCanonicalName(namespace: String, name: String): List<AvroSchemaWithId> + + /** + * Simply lists all stored [Schema]s. + */ + fun findAll(): List<AvroSchemaWithId> + +} diff --git a/extension/api/src/main/kotlin/converter/SpecificRecordToSingleObjectConverter.kt b/extension/api/src/main/kotlin/converter/SpecificRecordToSingleObjectConverter.kt new file mode 100644 index 00000000..eda7e953 --- /dev/null +++ b/extension/api/src/main/kotlin/converter/SpecificRecordToSingleObjectConverter.kt @@ -0,0 +1,22 @@ +package io.holixon.avro.adapter.api.converter + +import io.holixon.avro.adapter.api.AvroSingleObjectEncoded +import org.apache.avro.specific.SpecificRecordBase + +/** + * Converts a typed SpecificRecord to single object bytes. + */ +interface SpecificRecordToSingleObjectConverter { + + /** + * Converts instance of SpecificRecord to bytes containing the SchemaId. + */ + fun <T: SpecificRecordBase> encode(data: T) : AvroSingleObjectEncoded + + /** + * Extracts SchemaId from given bytes amd converts the contents of the bytes payload + * to a SpecificRecord instance of that writer schema. + */ + fun <T: SpecificRecordBase> decode(bytes: AvroSingleObjectEncoded) : T + +} diff --git a/extension/api/src/main/kotlin/ext/ByteArrayExt.kt b/extension/api/src/main/kotlin/ext/ByteArrayExt.kt new file mode 100644 index 00000000..37eb7f44 --- /dev/null +++ b/extension/api/src/main/kotlin/ext/ByteArrayExt.kt @@ -0,0 +1,77 @@ +package io.holixon.avro.adapter.api.ext + +import java.nio.ByteBuffer + +/** + * Extension functions for [ByteArray] and [ByteBuffer] that simplify + * with avro encoded data. + */ +object ByteArrayExt { + + /** + * Converts a byte array into its hexadecimal string representation + * e.g. for the V1_HEADER => [C3 01] + * + * @param separator - what to print between the bytes, defaults to " " + * @param prefix - start of string, defaults to "[" + * @param suffix - end of string, defaults to "]" + * @return the hexadecimal string representation of the input byte array + */ + @JvmStatic + fun ByteArray.toHexString(): String = this.joinToString( + separator = " ", + prefix = "[", + postfix = "]" + ) { "%02X".format(it) } + + /** + * @return [ByteBuffer.wrap] for given array + */ + fun ByteArray.buffer(): ByteBuffer = ByteBuffer.wrap(this) + + /** + * Splits an array in `n+1` parts, where `n` is the number of given indexes. + * The first slice reaches from 0 to index_0, the last slice from index_n to end. + * + * @param indexes - positive ints, must be sorted + * @return list of `n+1` byte arrays, each having a size of the diff between the indexes. + */ + fun ByteArray.split(vararg indexes: Int): List<ByteArray> { + require(indexes.none { it < 0 || it > size - 1 }) { "all indexes have to match '0 < index < size-1', was: indexes=${indexes.toList()}, size=$size" } + require(indexes.toList() == indexes.sorted()) { "indexes must be ordered, was: ${indexes.toList()}" } + require(indexes.size == indexes.distinct().size) { "indexes must be unique, was: ${indexes.toList()}" } + + val allButLast = + indexes.fold(0 to emptyList<ByteArray>()) { pair, nextIndex -> nextIndex to pair.second + this.copyOfRange(pair.first, nextIndex) } + return allButLast.second + this.copyOfRange(allButLast.first, size) + } + + /** + * @see ByteArray.split(indexes) + */ + fun ByteBuffer.split(vararg indexes: Int): List<ByteArray> = this.array().split(*indexes) + + /** + * Reads [size] bytes from given buffer, starting at given position. + * Ensures that the original buffer position is kept. + * + * @param position - the position to start reading + * @param size - how many bytes should be read. if not given, all remaining bytes are read + * @return bytesArray containg [size] bytes starting at [position] + */ + fun ByteBuffer.extract(position: Int, size: Int? = null): ByteArray { + val originalPosition = this.position() + try { + this.position(position) + require(size == null || size > 0) { "size < 1: ($size < 1)" } + val maxSize = remaining() + require(size == null || size <= maxSize) { "Cannot extract from position=$position, size=$size, remaining=${this.remaining()}" } + + val bytes = ByteArray(size ?: this.remaining()) + this.get(bytes) + return bytes + } finally { + this.position(originalPosition) + } + } +} diff --git a/extension/api/src/main/kotlin/ext/FunctionalExt.kt b/extension/api/src/main/kotlin/ext/FunctionalExt.kt new file mode 100644 index 00000000..3528d8bd --- /dev/null +++ b/extension/api/src/main/kotlin/ext/FunctionalExt.kt @@ -0,0 +1,12 @@ +package io.holixon.avro.adapter.api.ext + +import java.util.function.Function + +/** + * Collection of function extensions. + */ +object FunctionalExt { + + operator fun <T:Any, R:Any> Function<T,R>.invoke(t:T) :R = this.apply(t) + +} diff --git a/extension/api/src/main/kotlin/type/AvroPayloadAndSchemaData.kt b/extension/api/src/main/kotlin/type/AvroPayloadAndSchemaData.kt new file mode 100644 index 00000000..77e828f2 --- /dev/null +++ b/extension/api/src/main/kotlin/type/AvroPayloadAndSchemaData.kt @@ -0,0 +1,32 @@ +package io.holixon.avro.adapter.api.type + +import io.holixon.avro.adapter.api.AvroPayloadAndSchema +import io.holixon.avro.adapter.api.AvroSchemaWithId +import io.holixon.avro.adapter.api.AvroSingleObjectEncoded + +/** + * Data class implementing [AvroPayloadAndSchema]. + */ +data class AvroPayloadAndSchemaData( + override val schema: AvroSchemaWithId, + override val payload: AvroSingleObjectEncoded +) : AvroPayloadAndSchema { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AvroPayloadAndSchemaData + + if (schema != other.schema) return false + if (!payload.contentEquals(other.payload)) return false + + return true + } + + override fun hashCode(): Int { + var result = schema.hashCode() + result = 31 * result + payload.contentHashCode() + return result + } +} diff --git a/extension/api/src/main/kotlin/type/AvroPayloadAndSchemaIdData.kt b/extension/api/src/main/kotlin/type/AvroPayloadAndSchemaIdData.kt new file mode 100644 index 00000000..6174297c --- /dev/null +++ b/extension/api/src/main/kotlin/type/AvroPayloadAndSchemaIdData.kt @@ -0,0 +1,32 @@ +package io.holixon.avro.adapter.api.type + +import io.holixon.avro.adapter.api.AvroPayloadAndSchemaId +import io.holixon.avro.adapter.api.AvroSingleObjectPayload +import io.holixon.avro.adapter.api.AvroSchemaId + +/** + * Data class implementation of the [AvroPayloadAndSchemaId]. + */ +data class AvroPayloadAndSchemaIdData( + override val schemaId : AvroSchemaId, + override val payload : AvroSingleObjectPayload +) : AvroPayloadAndSchemaId { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AvroPayloadAndSchemaIdData + + if (schemaId != other.schemaId) return false + if (!payload.contentEquals(other.payload)) return false + + return true + } + + override fun hashCode(): Int { + var result = schemaId.hashCode() + result = 31 * result + payload.contentHashCode() + return result + } + +} diff --git a/extension/api/src/main/kotlin/type/AvroSchemaInfoData.kt b/extension/api/src/main/kotlin/type/AvroSchemaInfoData.kt new file mode 100644 index 00000000..16a14e18 --- /dev/null +++ b/extension/api/src/main/kotlin/type/AvroSchemaInfoData.kt @@ -0,0 +1,13 @@ +package io.holixon.avro.adapter.api.type + +import io.holixon.avro.adapter.api.AvroSchemaInfo +import io.holixon.avro.adapter.api.AvroSchemaRevision + +/** + * Data class implementing [AvroSchemaInfo]. + */ +data class AvroSchemaInfoData( + override val namespace: String, + override val name: String, + override val revision: AvroSchemaRevision? +) : AvroSchemaInfo diff --git a/extension/api/src/main/kotlin/type/AvroSchemaWithIdData.kt b/extension/api/src/main/kotlin/type/AvroSchemaWithIdData.kt new file mode 100644 index 00000000..2249daac --- /dev/null +++ b/extension/api/src/main/kotlin/type/AvroSchemaWithIdData.kt @@ -0,0 +1,29 @@ +package io.holixon.avro.adapter.api.type + +import io.holixon.avro.adapter.api.AvroSchemaWithId +import io.holixon.avro.adapter.api.AvroSchemaId +import io.holixon.avro.adapter.api.AvroSchemaRevision +import org.apache.avro.Schema + +/** + * Data class implementing [AvroSchemaWithId]. + */ +data class AvroSchemaWithIdData( + override val schemaId: AvroSchemaId, + override val schema: Schema, + override val revision: AvroSchemaRevision?, + override val namespace: String, + override val name: String +) : AvroSchemaWithId { + constructor( + schemaId: AvroSchemaId, + schema: Schema, + revision: AvroSchemaRevision? = null + ) : this( + schemaId = schemaId, + schema = schema, + revision = revision, + namespace = schema.namespace, + name = schema.name + ) +} diff --git a/extension/api/src/main/resources/.gitkeep b/extension/api/src/main/resources/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/extension/api/src/test/kotlin/AvroAdapterApiTestHelper.kt b/extension/api/src/test/kotlin/AvroAdapterApiTestHelper.kt new file mode 100644 index 00000000..e9326f97 --- /dev/null +++ b/extension/api/src/test/kotlin/AvroAdapterApiTestHelper.kt @@ -0,0 +1,2 @@ +package io.holixon.avro.adapter.api + diff --git a/extension/api/src/test/kotlin/ext/ByteArrayExtTest.kt b/extension/api/src/test/kotlin/ext/ByteArrayExtTest.kt new file mode 100644 index 00000000..e4e5c697 --- /dev/null +++ b/extension/api/src/test/kotlin/ext/ByteArrayExtTest.kt @@ -0,0 +1,80 @@ +package io.holixon.avro.adapter.api.ext + +import io.holixon.avro.adapter.api.ext.ByteArrayExt.extract +import io.holixon.avro.adapter.api.ext.ByteArrayExt.split +import io.holixon.avro.adapter.api.ext.ByteArrayExt.buffer +import io.holixon.avro.adapter.api.ext.ByteArrayExt.toHexString +import mu.KLogging +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +internal class ByteArrayExtTest { + companion object : KLogging() + + private val helloBytes: ByteArray = "Hello World!".encodeToByteArray() + + @Test + internal fun `to hex String`() { + assertThat(helloBytes.toHexString()).isEqualTo("[48 65 6C 6C 6F 20 57 6F 72 6C 64 21]") + assertThat(byteArrayOf(-61, 1).toHexString()).isEqualTo("[C3 01]") + } + + @Test + internal fun `extract from byte buffer`() { + val buffer = helloBytes.buffer() + buffer.position(7) + + // extract 3,4,5 + assertThat(buffer.extract(2, 3).toHexString()).isEqualTo("[6C 6C 6F]") + assertThat(buffer.position()).isEqualTo(7) + + // fails when extracting too much + assertThatThrownBy { buffer.extract(0, 100) } + .isInstanceOf(IllegalArgumentException::class.javaObjectType) + .hasMessage("Cannot extract from position=0, size=100, remaining=${helloBytes.size}") + assertThat(buffer.position()).isEqualTo(7) + + // fails when extracting too much + assertThatThrownBy { buffer.extract(0, 100) } + .isInstanceOf(IllegalArgumentException::class.javaObjectType) + .hasMessage("Cannot extract from position=0, size=100, remaining=${helloBytes.size}") + assertThat(buffer.position()).isEqualTo(7) + + assertThatThrownBy { byteArrayOf(-61).buffer().extract(1, 1) } + .isInstanceOf(IllegalArgumentException::class.javaObjectType) + .hasMessage("Cannot extract from position=1, size=1, remaining=0") + + // fails when pos < 0 + assertThatThrownBy { buffer.extract(-1, 5) } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("newPosition < 0: (-1 < 0)") + + // fails when size < 1 + assertThatThrownBy { buffer.extract(0, 0) } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("size < 1: (0 < 1)") + + // extract after + assertThat(buffer.extract(9).toHexString()).isEqualTo("[6C 64 21]") + } + + @Test + internal fun `split byteArray`() { + assertThatThrownBy { helloBytes.split(-1) }.hasMessage("all indexes have to match '0 < index < size-1', was: indexes=[-1], size=12") + .isInstanceOf(IllegalArgumentException::class.java) + assertThatThrownBy { helloBytes.split(100) }.hasMessage("all indexes have to match '0 < index < size-1', was: indexes=[100], size=12") + .isInstanceOf(IllegalArgumentException::class.java) + assertThatThrownBy { helloBytes.split(10, 5) }.hasMessage("indexes must be ordered, was: [10, 5]") + .isInstanceOf(IllegalArgumentException::class.java) + assertThatThrownBy { helloBytes.split(10, 10) }.hasMessage("indexes must be unique, was: [10, 10]") + .isInstanceOf(IllegalArgumentException::class.java) + + val parts = helloBytes.split(5, 10) + + assertThat(parts).hasSize(3) + assertThat(parts[0].toHexString()).isEqualTo("[48 65 6C 6C 6F]") + assertThat(parts[1].toHexString()).isEqualTo("[20 57 6F 72 6C]") + assertThat(parts[2].toHexString()).isEqualTo("[64 21]") + } +} diff --git a/extension/api/src/test/kotlin/type/AvroSchemaInfoTest.kt b/extension/api/src/test/kotlin/type/AvroSchemaInfoTest.kt new file mode 100644 index 00000000..3fb4255d --- /dev/null +++ b/extension/api/src/test/kotlin/type/AvroSchemaInfoTest.kt @@ -0,0 +1,21 @@ +package io.holixon.avro.adapter.api.type + +import io.holixon.avro.adapter.api.AvroSchemaInfo +import io.holixon.avro.lib.test.AvroAdapterTestLib +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class AvroSchemaInfoTest { + + private val sampleEventSchema = AvroAdapterTestLib.loadArvoResource("test.fixture.SampleEvent-v4711") + + @Test + internal fun `avro schema info`() { + val data: AvroSchemaInfo = AvroSchemaInfoData(namespace = "foo", name = "bar", revision = "1") + + assertThat(data.namespace).isEqualTo("foo") + assertThat(data.name).isEqualTo("bar") + assertThat(data.revision).isEqualTo("1") + assertThat(data.canonicalName).isEqualTo("foo.bar") + } +} diff --git a/extension/api/src/test/kotlin/type/AvroSchemaWithIdTest.kt b/extension/api/src/test/kotlin/type/AvroSchemaWithIdTest.kt new file mode 100644 index 00000000..f50f65e8 --- /dev/null +++ b/extension/api/src/test/kotlin/type/AvroSchemaWithIdTest.kt @@ -0,0 +1,30 @@ +package io.holixon.avro.adapter.api.type + +import io.holixon.avro.adapter.api.AvroAdapterApi +import io.holixon.avro.lib.test.AvroAdapterTestLib +import org.apache.avro.Schema +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class AvroSchemaWithIdTest { + + private val sampleEventSchema = AvroAdapterTestLib.loadArvoResource("test.fixture.SampleEvent-v4711") + private val schemaRevisionResolver = AvroAdapterApi.propertyBasedSchemaRevisionResolver("revision") + + @Test + internal fun `read schema and derive values`() { + val schema = Schema.Parser().parse(sampleEventSchema) + + val avroSchema = AvroSchemaWithIdData( + schemaId = "1", + schema = schema, + revision = schemaRevisionResolver.apply(schema).orElse(null) + ) + + assertThat(avroSchema.schemaId).isEqualTo("1") + assertThat(avroSchema.revision).isEqualTo("4711") + assertThat(avroSchema.name).isEqualTo("SampleEvent") + assertThat(avroSchema.namespace).isEqualTo("test.fixture") + assertThat(avroSchema.canonicalName).isEqualTo("test.fixture.SampleEvent") + } +} diff --git a/extension/api/src/test/resources/.gitkeep b/extension/api/src/test/resources/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/extension/default/pom.xml b/extension/default/pom.xml new file mode 100644 index 00000000..9d7295a3 --- /dev/null +++ b/extension/default/pom.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + 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> + + <parent> + <groupId>io.holixon.avro._</groupId> + <artifactId>extension</artifactId> + <version>0.0.1</version> + </parent> + + <groupId>io.holixon.avro</groupId> + <artifactId>avro-registry-adapter-default</artifactId> + <description>Implementation of serialization using Avro library defaults and useful defaults for the adapter API.</description> + + <properties> + <jacoco.skip>false</jacoco.skip> + </properties> + + <dependencies> + <dependency> + <groupId>io.holixon.avro</groupId> + <artifactId>avro-registry-adapter-api</artifactId> + </dependency> + </dependencies> + +</project> diff --git a/extension/default/src/main/kotlin/AvroAdapterDefault.kt b/extension/default/src/main/kotlin/AvroAdapterDefault.kt new file mode 100644 index 00000000..23fb613c --- /dev/null +++ b/extension/default/src/main/kotlin/AvroAdapterDefault.kt @@ -0,0 +1,124 @@ +package io.holixon.avro.adapter.common + +import io.holixon.avro.adapter.api.AvroPayloadAndSchemaId +import io.holixon.avro.adapter.api.AvroSchemaWithId +import io.holixon.avro.adapter.api.AvroSingleObjectEncoded +import io.holixon.avro.adapter.api.SchemaIdSupplier +import io.holixon.avro.adapter.api.ext.ByteArrayExt.buffer +import io.holixon.avro.adapter.api.ext.ByteArrayExt.extract +import io.holixon.avro.adapter.api.ext.ByteArrayExt.split +import io.holixon.avro.adapter.api.ext.ByteArrayExt.toHexString +import io.holixon.avro.adapter.api.type.AvroPayloadAndSchemaIdData +import io.holixon.avro.adapter.common.AvroAdapterDefault.DecoderSpecificRecordClassResolver +import io.holixon.avro.adapter.common.converter.DefaultSchemaCompatibilityResolver +import io.holixon.avro.adapter.common.registry.InMemoryAvroSchemaRegistry +import org.apache.avro.Schema +import org.apache.avro.SchemaNormalization +import org.apache.avro.specific.SpecificRecordBase +import org.apache.avro.util.ClassUtils +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.util.function.Function + +/** + * Collection of default adapter helper functions. + */ +object AvroAdapterDefault { + + const val PROPERTY_REVISION = "revision" + + /** + * Marker bytes according to Avro schema specification v1. + */ + @JvmField + val AVRO_V1_HEADER = byteArrayOf(-61, 1) // [C3 01] + private val AVRO_HEADER_LENGTH = AVRO_V1_HEADER.size + Long.SIZE_BYTES + + /** + * Read payload and schema id from Avro single object encoded ball. + */ + fun AvroSingleObjectEncoded.readPayloadAndSchemaId(): AvroPayloadAndSchemaId { + require(this.size > AVRO_HEADER_LENGTH) { "Single object encoded bytes must have at least length > $AVRO_HEADER_LENGTH, was: $size." } + require(this.isAvroSingleObjectEncoded()) { "Single object encoded bytes need to start with ${AVRO_V1_HEADER.toHexString()}." } + + val (_, idBytes, payloadBytes) = this.split(AVRO_V1_HEADER.size, AVRO_HEADER_LENGTH) + + return AvroPayloadAndSchemaIdData( + schemaId = "${idBytes.readLong()}", + payload = payloadBytes + ) + } + + private fun ByteArray.readLong(): Long { + require(this.size == Long.SIZE_BYTES) { "Size must be exactly Long.SIZE_BYTES (${Long.SIZE_BYTES}." } + return this.buffer().order(ByteOrder.LITTLE_ENDIAN).long + } + + @JvmStatic + fun ByteBuffer.isAvroSingleObjectEncoded(): Boolean = extract(0, AVRO_V1_HEADER.size).contentEquals(AVRO_V1_HEADER) + + @JvmStatic + fun ByteArray.isAvroSingleObjectEncoded(): Boolean = buffer().isAvroSingleObjectEncoded() + + @JvmField + val schemaRevisionResolver = DefaultSchemaRevisionResolver() + + /** + * Implements [SchemaIdSupplier] by using SchemaNormalization#parsingFingerprint64(Schema). + */ + @JvmField + val schemaIdSupplier = DefaultSchemaIdSupplier() + + /** + * Create a in-memory schema registry using [SchemaNormalization.parsingFingerprint64] and [DefaultSchemaRevisionResolver]. + */ + fun inMemorySchemaRegistry() = InMemoryAvroSchemaRegistry( + schemaIdSupplier = schemaIdSupplier, + schemaRevisionResolver = schemaRevisionResolver + ) + + /** + * Reflective access using the method of generated specific record to access byte buffer representation. + */ + @JvmStatic + fun SpecificRecordBase.toByteBuffer(): ByteBuffer = this.javaClass.getDeclaredMethod("toByteBuffer").invoke(this) as ByteBuffer + + /** + * Reflective access using the method of generated specific record to access byte array representation. + * @see toByteBuffer + */ + @JvmStatic + fun SpecificRecordBase.toByteArray(): ByteArray = this.toByteBuffer().array() + + /** + * Reflective access using the method of generated specific record to access specific record base representation. + */ + @JvmStatic + fun Class<SpecificRecordBase>.fromByteArray(bytes: ByteArray) = this.getDeclaredMethod("fromByteBuffer", ByteBuffer::class.java) + .invoke(null, ByteBuffer.wrap(bytes)) as SpecificRecordBase + + /** + * Retrieves the schema from generated class extending specific record class. + */ + @JvmStatic + fun Class<SpecificRecordBase>.getSchema() = this.getDeclaredField("SCHEMA$").get(null) as Schema + + /** + * Resolver for a concrete class used by decoding of Avro single object into Avro specific record. + */ + fun interface DecoderSpecificRecordClassResolver : Function<AvroSchemaWithId, Class<SpecificRecordBase>> + + /** + * Default implementation using [Class.forName]. + */ + @Suppress("UNCHECKED_CAST") + val reflectionBasedDecoderSpecificRecordClassResolver = DecoderSpecificRecordClassResolver { + ClassUtils.forName(it.canonicalName) as Class<SpecificRecordBase> + } + + /** + * Default schema compatibility resolver throwing exception on any incompatibility. + */ + @JvmField + val defaultSchemaCompatibilityResolver = DefaultSchemaCompatibilityResolver() +} diff --git a/extension/default/src/main/kotlin/DefaultSchemaIdSupplier.kt b/extension/default/src/main/kotlin/DefaultSchemaIdSupplier.kt new file mode 100644 index 00000000..f69c8147 --- /dev/null +++ b/extension/default/src/main/kotlin/DefaultSchemaIdSupplier.kt @@ -0,0 +1,13 @@ +package io.holixon.avro.adapter.common + +import io.holixon.avro.adapter.api.AvroSchemaId +import io.holixon.avro.adapter.api.SchemaIdSupplier +import io.holixon.avro.adapter.common.ext.SchemaExt.fingerprint +import org.apache.avro.Schema + +/** + * Delivers schema id based on fingerprint. + */ +class DefaultSchemaIdSupplier : SchemaIdSupplier { + override fun apply(schema: Schema): AvroSchemaId = schema.fingerprint.toString() +} diff --git a/extension/default/src/main/kotlin/DefaultSchemaRevisionResolver.kt b/extension/default/src/main/kotlin/DefaultSchemaRevisionResolver.kt new file mode 100644 index 00000000..fa345418 --- /dev/null +++ b/extension/default/src/main/kotlin/DefaultSchemaRevisionResolver.kt @@ -0,0 +1,17 @@ +package io.holixon.avro.adapter.common + +import io.holixon.avro.adapter.api.AvroAdapterApi +import io.holixon.avro.adapter.api.AvroSchemaRevision +import io.holixon.avro.adapter.api.SchemaRevisionResolver +import org.apache.avro.Schema +import java.util.* + +/** + * Default schema revision resolver based on a class property. + */ +class DefaultSchemaRevisionResolver : SchemaRevisionResolver { + private val propertyBasedResolver = AvroAdapterApi.propertyBasedSchemaRevisionResolver(AvroAdapterDefault.PROPERTY_REVISION) + + override fun apply(schema: Schema): Optional<AvroSchemaRevision> = propertyBasedResolver.apply(schema) + +} diff --git a/extension/default/src/main/kotlin/DefaultSchemaStore.kt b/extension/default/src/main/kotlin/DefaultSchemaStore.kt new file mode 100644 index 00000000..847d7f52 --- /dev/null +++ b/extension/default/src/main/kotlin/DefaultSchemaStore.kt @@ -0,0 +1,16 @@ +package io.holixon.avro.adapter.common + +import io.holixon.avro.adapter.api.SchemaResolver +import org.apache.avro.Schema +import org.apache.avro.message.SchemaStore + +/** + * Schema store using the schema resolver. + */ +class DefaultSchemaStore(private val schemaResolver: SchemaResolver) : SchemaStore { + + override fun findByFingerprint(fingerprint: Long): Schema? = schemaResolver + .apply(fingerprint.toString()) + .map { it.schema } + .orElse(null) +} diff --git a/extension/default/src/main/kotlin/converter/DefaultSchemaCompatibilityResolver.kt b/extension/default/src/main/kotlin/converter/DefaultSchemaCompatibilityResolver.kt new file mode 100644 index 00000000..7b9a0ff5 --- /dev/null +++ b/extension/default/src/main/kotlin/converter/DefaultSchemaCompatibilityResolver.kt @@ -0,0 +1,47 @@ +package io.holixon.avro.adapter.common.converter + +import io.holixon.avro.adapter.api.AvroSchemaIncompatibilityResolver +import io.holixon.avro.adapter.api.ext.FunctionalExt.invoke +import io.holixon.avro.adapter.common.AvroAdapterDefault.schemaIdSupplier +import org.apache.avro.Schema +import org.apache.avro.SchemaCompatibility +import org.apache.avro.SchemaCompatibility.SchemaCompatibilityType.* + +/** + * Detects and possibly resolves schema incompatibilities. + */ +class DefaultSchemaCompatibilityResolver @JvmOverloads constructor( + val ignoredIncompatibilityTypes: Set<SchemaCompatibility.SchemaIncompatibilityType> = setOf() +) : AvroSchemaIncompatibilityResolver { + + override fun resolve(readerSchema: Schema, writerSchema: Schema): Schema { + + // check compatibility + val compatibility = SchemaCompatibility.checkReaderWriterCompatibility(readerSchema, writerSchema) + return when (compatibility.result.compatibility!!) { + COMPATIBLE -> readerSchema + INCOMPATIBLE -> if (filterIgnored(compatibility.result.incompatibilities.map { it.type }).isEmpty()) { + readerSchema + } else { + throw IllegalArgumentException( + "Reader schema[${readerSchema.schemaId}] is not compatible with Writer schema[${writerSchema.schemaId}]. The incompatibilities are: ${ + filterIgnored( + compatibility.result.incompatibilities.map { it.type }) + }" + ) + } + RECURSION_IN_PROGRESS -> throw IllegalArgumentException( + "Recursion in progress for compatibility check for Reader schema[${readerSchema.schemaId}] and Writer schema[${writerSchema.schemaId}]." + ) + } + } + + private val Schema.schemaId get() = schemaIdSupplier(this) + + /** + * Filters found incompatibilities by ignoring the types provided in [ignoredIncompatibilityTypes]. + */ + fun filterIgnored(incompatibilities: List<SchemaCompatibility.SchemaIncompatibilityType>) = (incompatibilities - ignoredIncompatibilityTypes) + +} + diff --git a/extension/default/src/main/kotlin/converter/DefaultSpecificRecordToSingleObjectConverter.kt b/extension/default/src/main/kotlin/converter/DefaultSpecificRecordToSingleObjectConverter.kt new file mode 100644 index 00000000..690eee27 --- /dev/null +++ b/extension/default/src/main/kotlin/converter/DefaultSpecificRecordToSingleObjectConverter.kt @@ -0,0 +1,52 @@ +package io.holixon.avro.adapter.common.converter + +import io.holixon.avro.adapter.api.AvroSchemaIncompatibilityResolver +import io.holixon.avro.adapter.api.AvroSingleObjectEncoded +import io.holixon.avro.adapter.api.SchemaResolver +import io.holixon.avro.adapter.api.converter.SpecificRecordToSingleObjectConverter +import io.holixon.avro.adapter.common.AvroAdapterDefault +import io.holixon.avro.adapter.common.AvroAdapterDefault.DecoderSpecificRecordClassResolver +import io.holixon.avro.adapter.common.AvroAdapterDefault.getSchema +import io.holixon.avro.adapter.common.AvroAdapterDefault.readPayloadAndSchemaId +import io.holixon.avro.adapter.common.AvroAdapterDefault.toByteArray +import io.holixon.avro.adapter.common.DefaultSchemaStore +import org.apache.avro.message.BinaryMessageDecoder +import org.apache.avro.specific.SpecificData +import org.apache.avro.specific.SpecificRecordBase + +/** + * Converts any instance derived from [SpecificRecordBase] (generated from avsc) to a [ByteArray] that follows the format specified + * in the [avro specs](https://avro.apache.org/docs/current/spec.html#single_object_encoding). + */ +class DefaultSpecificRecordToSingleObjectConverter @JvmOverloads constructor( + private val schemaResolver: SchemaResolver, + private val decoderSpecificRecordClassResolver: DecoderSpecificRecordClassResolver = AvroAdapterDefault.reflectionBasedDecoderSpecificRecordClassResolver, + private val schemaIncompatibilityResolver: AvroSchemaIncompatibilityResolver = AvroAdapterDefault.defaultSchemaCompatibilityResolver +) : SpecificRecordToSingleObjectConverter { + + private val schemaStore = DefaultSchemaStore(schemaResolver) + + override fun <T : SpecificRecordBase> encode(data: T): AvroSingleObjectEncoded = data.toByteArray() + + + override fun <T : SpecificRecordBase> decode(bytes: AvroSingleObjectEncoded): T { + + // get the reader schema id from the single object encoded bytes + val schemaId = bytes.readPayloadAndSchemaId().schemaId + // load writer schema info from schema resolver + val writerSchemaWithId = + schemaResolver.apply(schemaId).orElseThrow { IllegalArgumentException("Can not resolve writer schema for id=$schemaId.") } + // we have to assume that the namespace and name of the message payload did not change, so we try to load the class based on the schema info + // of the writer schema. This might lead to another (earlier or later) revision, but the canonical name should not have changed. + val targetClass: Class<SpecificRecordBase> = decoderSpecificRecordClassResolver.apply(writerSchemaWithId) + // get reader schema from the class + val readerSchema = targetClass.getSchema() + + // resolve incompatibilities if any and return the resulting reader schema + val resolvedReaderSchema = schemaIncompatibilityResolver.resolve(readerSchema, writerSchemaWithId.schema) + + // construct decoder and decode + return BinaryMessageDecoder<T>(SpecificData(), resolvedReaderSchema, schemaStore).decode(bytes) + } +} + diff --git a/extension/default/src/main/kotlin/converter/SingleObjectToJson.kt b/extension/default/src/main/kotlin/converter/SingleObjectToJson.kt new file mode 100644 index 00000000..2f36b7b6 --- /dev/null +++ b/extension/default/src/main/kotlin/converter/SingleObjectToJson.kt @@ -0,0 +1,42 @@ +package io.holixon.avro.adapter.common.converter + +import io.holixon.avro.adapter.api.AvroSingleObjectEncoded +import io.holixon.avro.adapter.api.SchemaResolver +import io.holixon.avro.adapter.common.AvroAdapterDefault.readPayloadAndSchemaId +import org.apache.avro.Schema +import org.apache.avro.generic.GenericDatumReader +import org.apache.avro.generic.GenericDatumWriter +import org.apache.avro.io.* +import java.io.ByteArrayOutputStream +import java.nio.charset.StandardCharsets +import java.util.function.Function + +/** + * Uses the schema encoded in the single object bytes to create a generic datum and convert it to json. + */ +class SingleObjectToJson(private val schemaResolver: SchemaResolver) : Function<ByteArray, String> { + + override fun apply(avroSingleObject: AvroSingleObjectEncoded): String { + val (schemaId, payload) = avroSingleObject.readPayloadAndSchemaId() + val writerSchema = schemaResolver.apply(schemaId).orElseThrow().schema + + val genericDatum = readGenericDatum(writerSchema, payload) + + return writeToJson(writerSchema, genericDatum) + } + + private fun writeToJson(schema: Schema, genericDatum: Any): String = ByteArrayOutputStream().use { + val writer: DatumWriter<Any> = GenericDatumWriter(schema) + val encoder = EncoderFactory.get().jsonEncoder(schema, it, false) + writer.write(genericDatum, encoder) + encoder.flush() + it.flush() + return String(it.toByteArray(), StandardCharsets.UTF_8) + } + + private fun readGenericDatum(schema: Schema, avroBinary: ByteArray): Any { + val datumReader: DatumReader<Any> = GenericDatumReader(schema) + val decoder: Decoder = DecoderFactory.get().binaryDecoder(avroBinary, null) + return datumReader.read(null, decoder) + } +} diff --git a/extension/default/src/main/kotlin/ext/SchemaExt.kt b/extension/default/src/main/kotlin/ext/SchemaExt.kt new file mode 100644 index 00000000..1bf008d8 --- /dev/null +++ b/extension/default/src/main/kotlin/ext/SchemaExt.kt @@ -0,0 +1,13 @@ +package io.holixon.avro.adapter.common.ext + +import org.apache.avro.Schema +import org.apache.avro.SchemaNormalization + +/** + * Extension functions to retrieve schema fingerprint. + */ +object SchemaExt { + + val Schema.fingerprint get() = SchemaNormalization.parsingFingerprint64(this) + +} diff --git a/extension/default/src/main/kotlin/registry/CompositeAvroSchemaReadOnlyRegistry.kt b/extension/default/src/main/kotlin/registry/CompositeAvroSchemaReadOnlyRegistry.kt new file mode 100644 index 00000000..14b1fb9a --- /dev/null +++ b/extension/default/src/main/kotlin/registry/CompositeAvroSchemaReadOnlyRegistry.kt @@ -0,0 +1,66 @@ +package io.holixon.avro.adapter.common.registry + +import io.holixon.avro.adapter.api.* +import java.util.* + +/** + * Composite Avro registry allowing registry composition. + */ +class CompositeAvroSchemaReadOnlyRegistry( + private val registries: List<AvroSchemaReadOnlyRegistry> +) : AvroSchemaReadOnlyRegistry { + + /** + * Using registries or read-only registries as varargs. + */ + constructor(vararg registry: AvroSchemaRegistry) : this(registry.asList()) + + /** + * Using registries or read-only registries as varargs. + */ + constructor(vararg registry: AvroSchemaReadOnlyRegistry) : this(registry.asList()) + + init { + require(registries.isNotEmpty()) { "Composite Avro Schema Registry must contain at least one registry." } + } + + override fun findById(schemaId: AvroSchemaId): Optional<AvroSchemaWithId> { + for (registry in registries) { + val result = registry.findById(schemaId) + if (result.isPresent) { + return result + } + } + return Optional.empty() + } + + override fun findByInfo(info: AvroSchemaInfo): Optional<AvroSchemaWithId> { + for (registry in registries) { + val result = registry.findByInfo(info) + if (result.isPresent) { + return result + } + } + return Optional.empty() + } + + override fun findAllByCanonicalName(namespace: String, name: String): List<AvroSchemaWithId> { + for (registry in registries) { + val result = registry.findAllByCanonicalName(namespace, name) + if (result.isNotEmpty()) { + return result + } + } + return listOf() + } + + override fun findAll(): List<AvroSchemaWithId> { + for (registry in registries) { + val result = registry.findAll() + if (result.isNotEmpty()) { + return result + } + } + return listOf() + } +} diff --git a/extension/default/src/main/kotlin/registry/CompositeAvroSchemaRegistry.kt b/extension/default/src/main/kotlin/registry/CompositeAvroSchemaRegistry.kt new file mode 100644 index 00000000..3ca90cdf --- /dev/null +++ b/extension/default/src/main/kotlin/registry/CompositeAvroSchemaRegistry.kt @@ -0,0 +1,42 @@ +package io.holixon.avro.adapter.common.registry + +import io.holixon.avro.adapter.api.AvroSchemaReadOnlyRegistry +import io.holixon.avro.adapter.api.AvroSchemaRegistry +import io.holixon.avro.adapter.api.AvroSchemaWithId +import org.apache.avro.Schema + +/** + * Composite registry delegating the registration to a dedicated registry and using a composite read only registry for schema retrieval. + */ +class CompositeAvroSchemaRegistry( + private val registry: AvroSchemaRegistry, + private val compositeAvroSchemaReadOnlyRegistry: CompositeAvroSchemaReadOnlyRegistry +) : AvroSchemaReadOnlyRegistry by compositeAvroSchemaReadOnlyRegistry, AvroSchemaRegistry { + + /** + * Convenience constructor for a composite registry. + * @param readOnlyRegistries list of read-only registries used for finding schemas. + * @param registry registry used for schema registration. + */ + constructor(registry: AvroSchemaRegistry, vararg readOnlyRegistries: AvroSchemaReadOnlyRegistry) : this( + registry, + readOnlyRegistries.toList() + ) + + /** + * Convenience constructor for a composite registry. + * @param readOnlyRegistries list of read-only registries used for finding schemas. + * @param registry registry used for schema registration. + */ + constructor(registry: AvroSchemaRegistry, readOnlyRegistries: List<AvroSchemaReadOnlyRegistry>) : this( + registry, + CompositeAvroSchemaReadOnlyRegistry(readOnlyRegistries) + ) + + /* + * Delegate registration to the registry. + */ + override fun register(schema: Schema): AvroSchemaWithId { + return registry.register(schema) + } +} diff --git a/extension/default/src/main/kotlin/registry/InMemoryAvroSchemaRegistry.kt b/extension/default/src/main/kotlin/registry/InMemoryAvroSchemaRegistry.kt new file mode 100644 index 00000000..1d019174 --- /dev/null +++ b/extension/default/src/main/kotlin/registry/InMemoryAvroSchemaRegistry.kt @@ -0,0 +1,58 @@ +package io.holixon.avro.adapter.common.registry + +import io.holixon.avro.adapter.api.* +import io.holixon.avro.adapter.api.AvroAdapterApi.extractSchemaInfo +import io.holixon.avro.adapter.api.type.AvroSchemaWithIdData +import org.apache.avro.Schema +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** + * An implementation of [AvroSchemaRegistry] that does not use persistent storage. + * Obviously, this is primarily meant for testing and should not be used for real life projects. + */ +class InMemoryAvroSchemaRegistry( + private val store: ConcurrentHashMap<String, Pair<AvroSchemaInfo, Schema>>, + val schemaIdSupplier: SchemaIdSupplier, + val schemaRevisionResolver: SchemaRevisionResolver +) : AvroSchemaRegistry, AutoCloseable { + + /** + * Create instance with default empty [ConcurrentHashMap] as store. + */ + constructor(schemaIdSupplier: SchemaIdSupplier, schemaRevisionResolver: SchemaRevisionResolver) : this(ConcurrentHashMap(), schemaIdSupplier, schemaRevisionResolver) + + override fun register(schema: Schema): AvroSchemaWithId { + val info = schema.extractSchemaInfo(schemaRevisionResolver) + + return findByInfo(info) + .orElseGet { + val id = schemaIdSupplier.apply(schema) + store[id] = info to schema + findById(id).get() + } + } + + override fun findById(schemaId: AvroSchemaId): Optional<AvroSchemaWithId> = Optional.ofNullable( + store[schemaId] + ).map { AvroSchemaWithIdData(schemaId, it.second, it.first.revision) } + + override fun findByInfo(info: AvroSchemaInfo): Optional<AvroSchemaWithId> = Optional.ofNullable(store.entries + .filter { it.value.first == info } + .map { it.toSchemaData() } + .firstOrNull()) + + override fun findAllByCanonicalName(namespace: String, name: String): List<AvroSchemaWithId> = store + .filter { it.value.first.name == name } + .filter { it.value.first.namespace == namespace } + .map { it.toSchemaData() } + + override fun findAll(): List<AvroSchemaWithId> = store.map { it.toSchemaData() } + + override fun close() { + store.clear() + } + + private fun Map.Entry<AvroSchemaId, Pair<AvroSchemaInfo, Schema>>.toSchemaData() = AvroSchemaWithIdData(key, value.second) + +} diff --git a/extension/default/src/main/resources/.gitkeep b/extension/default/src/main/resources/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/extension/default/src/test/java/io/holixon/avro/adapter/registry/AvroAdapterApiTest.java b/extension/default/src/test/java/io/holixon/avro/adapter/registry/AvroAdapterApiTest.java new file mode 100644 index 00000000..4015f310 --- /dev/null +++ b/extension/default/src/test/java/io/holixon/avro/adapter/registry/AvroAdapterApiTest.java @@ -0,0 +1,39 @@ +package io.holixon.avro.adapter.registry; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.holixon.avro.adapter.api.AvroAdapterApi; +import io.holixon.avro.adapter.api.AvroSchemaRegistry; +import io.holixon.avro.adapter.api.SchemaIdSupplier; +import io.holixon.avro.adapter.api.SchemaRevisionResolver; +import io.holixon.avro.adapter.common.registry.InMemoryAvroSchemaRegistry; +import io.holixon.avro.lib.test.schema.SampleEventV4711; +import org.apache.avro.Schema; +import org.apache.avro.SchemaNormalization; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class AvroAdapterApiTest { + + public static final Schema SAMPLE_4711 = SampleEventV4711.INSTANCE.getSchema(); + + private static final SchemaRevisionResolver SCHEMA_REVISION_RESOLVER = AvroAdapterApi.propertyBasedSchemaRevisionResolver("revision"); + private static final SchemaIdSupplier SCHEMA_ID_SUPPLIER = schema -> String.valueOf(SchemaNormalization.parsingFingerprint64(schema)); + + private final AvroSchemaRegistry schemaRegistry = new InMemoryAvroSchemaRegistry(SCHEMA_ID_SUPPLIER, SCHEMA_REVISION_RESOLVER); + + @BeforeEach + void setUp() { + assertThat(schemaRegistry.findAll()).isEmpty(); + } + + @Test + void registerReturnsSchemaWithId() { + var registered = schemaRegistry.register(SAMPLE_4711); + + assertThat(schemaRegistry.findAll()).hasSize(1); + assertThat(registered.getSchemaId()).isEqualTo(String.valueOf(SampleEventV4711.INSTANCE.getSchemaData().getFingerPrint())); + } + + +} diff --git a/extension/default/src/test/kotlin/AvroAdapterApiTestHelper.kt b/extension/default/src/test/kotlin/AvroAdapterApiTestHelper.kt new file mode 100644 index 00000000..99061e51 --- /dev/null +++ b/extension/default/src/test/kotlin/AvroAdapterApiTestHelper.kt @@ -0,0 +1,15 @@ +package io.holixon.avro.adapter.common + +import io.holixon.avro.adapter.api.AvroAdapterApi +import io.holixon.avro.adapter.api.SchemaIdSupplier +import org.apache.avro.SchemaNormalization + +/** + * Test helper. + */ +object AvroAdapterApiTestHelper { + + val schemaIdSupplier = SchemaIdSupplier { schema -> SchemaNormalization.parsingFingerprint64(schema).toString() } + + val schemaRevisionResolver = AvroAdapterApi.propertyBasedSchemaRevisionResolver("revision") +} diff --git a/extension/default/src/test/kotlin/AvroAdapterDefaultTest.kt b/extension/default/src/test/kotlin/AvroAdapterDefaultTest.kt new file mode 100644 index 00000000..da9e6ba9 --- /dev/null +++ b/extension/default/src/test/kotlin/AvroAdapterDefaultTest.kt @@ -0,0 +1,53 @@ +package io.holixon.avro.adapter.common + +import io.holixon.avro.adapter.api.ext.ByteArrayExt.toHexString +import io.holixon.avro.adapter.common.AvroAdapterDefault.isAvroSingleObjectEncoded +import io.holixon.avro.adapter.common.AvroAdapterDefault.readPayloadAndSchemaId +import io.holixon.avro.adapter.common.AvroAdapterDefault.toByteArray +import io.holixon.avro.adapter.common.ext.SchemaExt.fingerprint +import io.holixon.avro.lib.test.AvroAdapterTestLib +import mu.KLogging +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +internal class AvroAdapterDefaultTest { + companion object : KLogging() + + private val bytes = AvroAdapterTestLib.sampleFoo.toByteArray() + + @Test + internal fun `read payload and schemaId from encoded bytes`() { + logger.info { bytes.toHexString() } + + // too short + assertThatThrownBy { "foo".encodeToByteArray().readPayloadAndSchemaId() }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("Single object encoded bytes must have at least length > 10, was: 3.") + + // long enough, but does not start with V1_Header + assertThatThrownBy { + "hello my precious world".encodeToByteArray().readPayloadAndSchemaId() + }.isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("Single object encoded bytes need to start with [C3 01].") + + val payloadAndSchemaId = bytes.readPayloadAndSchemaId() + assertThat(payloadAndSchemaId.schemaId).isEqualTo(AvroAdapterTestLib.sampleFoo.schema.fingerprint.toString()) + assertThat(payloadAndSchemaId.payload.toHexString()).isEqualTo("[06 66 6F 6F]") + } + + @Test + internal fun `extract schemaId and payload`() { + val (schemaId, payload) = bytes.readPayloadAndSchemaId() + + assertThat(schemaId).isEqualTo(AvroAdapterTestLib.sampleEventFingerprint.toString()) + assertThat(payload.toHexString()).isEqualTo("[06 66 6F 6F]") + } + + @Test + internal fun `is avro single object encoded`() { + assertThat(bytes.isAvroSingleObjectEncoded()).isTrue + + assertThat("foo".encodeToByteArray().isAvroSingleObjectEncoded()).isFalse + } + +} diff --git a/extension/default/src/test/kotlin/converter/DefaultSchemaCompatibilityResolverTest.kt b/extension/default/src/test/kotlin/converter/DefaultSchemaCompatibilityResolverTest.kt new file mode 100644 index 00000000..db85e904 --- /dev/null +++ b/extension/default/src/test/kotlin/converter/DefaultSchemaCompatibilityResolverTest.kt @@ -0,0 +1,35 @@ +package io.holixon.avro.adapter.common.converter + +import org.apache.avro.SchemaCompatibility +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class DefaultSchemaCompatibilityResolverTest { + + + @Test + fun `check ignored incompatibilities empty`() { + val converter = DefaultSchemaCompatibilityResolver() + assertThat( + converter.filterIgnored(listOf(SchemaCompatibility.SchemaIncompatibilityType.NAME_MISMATCH)) + ).isNotEmpty + } + + @Test + fun `check ignored incompatibilities are matched`() { + val converter = DefaultSchemaCompatibilityResolver(setOf(SchemaCompatibility.SchemaIncompatibilityType.NAME_MISMATCH)) + assertThat( + converter.filterIgnored(listOf(SchemaCompatibility.SchemaIncompatibilityType.NAME_MISMATCH)) + ).isEmpty() + } + + + @Test + fun `check empty incompatibilities are ok`() { + val converter = DefaultSchemaCompatibilityResolver(setOf(SchemaCompatibility.SchemaIncompatibilityType.NAME_MISMATCH)) + assertThat( + converter.filterIgnored(listOf()) + ).isEmpty() + } + +} diff --git a/extension/default/src/test/kotlin/converter/DefaultSpecificRecordToSingleObjectConverterTest.kt b/extension/default/src/test/kotlin/converter/DefaultSpecificRecordToSingleObjectConverterTest.kt new file mode 100644 index 00000000..99f3caa7 --- /dev/null +++ b/extension/default/src/test/kotlin/converter/DefaultSpecificRecordToSingleObjectConverterTest.kt @@ -0,0 +1,109 @@ +package io.holixon.avro.adapter.common.converter + +import io.holixon.avro.adapter.api.AvroAdapterApi.schemaResolver +import io.holixon.avro.adapter.common.AvroAdapterDefault +import io.holixon.avro.adapter.common.AvroAdapterDefault.DecoderSpecificRecordClassResolver +import io.holixon.avro.adapter.common.AvroAdapterDefault.reflectionBasedDecoderSpecificRecordClassResolver +import io.holixon.avro.lib.test.AvroAdapterTestLib +import io.holixon.avro.lib.test.schema.SampleEventV4713 +import org.apache.avro.SchemaCompatibility +import org.apache.avro.SchemaCompatibility.SchemaIncompatibilityType.NAME_MISMATCH +import org.apache.avro.specific.SpecificRecordBase +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test +import test.fixture.SampleEvent +import test.fixture.SampleEventWithAdditionalFieldWithDefault + +/** + * Test for converter. + */ +internal class DefaultSpecificRecordToSingleObjectConverterTest { + + private val registry = AvroAdapterDefault.inMemorySchemaRegistry() + + @Test + internal fun `encode and decode same writer and reader schema`() { + + registry.register(AvroAdapterTestLib.schemaSampleEvent4711) + val converter = createConverter() + + val data = AvroAdapterTestLib.sampleFoo + val encoded = converter.encode(data) + + val decoded: SampleEvent = converter.decode(encoded) + + assertThat(decoded).isEqualTo(data) + } + + @Test + internal fun `encode and decode different writer and reader schema`() { + + val reg1 = registry.register(AvroAdapterTestLib.schemaSampleEvent4711) + val reg2 = registry.register(AvroAdapterTestLib.schemaSampleEvent4713) + + @Suppress("UNCHECKED_CAST") + val converter = createConverter(decoderSpecificRecordClassResolver = { + Class.forName(SampleEventV4713.addSuffix(it.canonicalName)) as Class<SpecificRecordBase> + }) + + val data = AvroAdapterTestLib.sampleFoo + val encoded = converter.encode(data) + + assertThatThrownBy { + converter.decode<SampleEventWithAdditionalFieldWithDefault>(encoded) + }.hasMessage("Reader schema[${reg2.schemaId}] is not compatible with Writer schema[${reg1.schemaId}]. The incompatibilities are: [NAME_MISMATCH]") + + } + + + @Test + internal fun `encode and decode different writer and reader schema ignoring NAME_MISMATCH`() { + + registry.register(AvroAdapterTestLib.schemaSampleEvent4711) + registry.register(AvroAdapterTestLib.schemaSampleEvent4713) + + @Suppress("UNCHECKED_CAST") + val converter = createConverter(decoderSpecificRecordClassResolver = { + Class.forName(SampleEventV4713.addSuffix(it.canonicalName)) as Class<SpecificRecordBase> + }, ignoredIncompatibilities = setOf(NAME_MISMATCH)) + + val data = AvroAdapterTestLib.sampleFoo + val encoded = converter.encode(data) + + val decoded: SampleEventWithAdditionalFieldWithDefault = converter.decode(encoded) + + assertThat(decoded).isEqualTo(AvroAdapterTestLib.sampleFooWithAdditionalFieldWithDefault) + + } + + @Test + internal fun `encode and decode different writer=4713 and reader=4711 schema ignoring NAME_MISMATCH`() { + + registry.register(AvroAdapterTestLib.schemaSampleEvent4711) + registry.register(AvroAdapterTestLib.schemaSampleEvent4713) + + @Suppress("UNCHECKED_CAST") + val converter = createConverter(decoderSpecificRecordClassResolver = { + Class.forName(SampleEventV4713.removeSuffix(it.canonicalName)) as Class<SpecificRecordBase> + }, ignoredIncompatibilities = setOf(NAME_MISMATCH)) + + val data = AvroAdapterTestLib.sampleFooWithAdditionalFieldWithDefault + val encoded = converter.encode(data) + + val decoded: SampleEvent = converter.decode(encoded) + + assertThat(decoded).isEqualTo(AvroAdapterTestLib.sampleFoo) + } + + private fun createConverter( + decoderSpecificRecordClassResolver: DecoderSpecificRecordClassResolver = reflectionBasedDecoderSpecificRecordClassResolver, + ignoredIncompatibilities: Set<SchemaCompatibility.SchemaIncompatibilityType> = setOf() + ) = + DefaultSpecificRecordToSingleObjectConverter( + registry.schemaResolver(), + decoderSpecificRecordClassResolver, + DefaultSchemaCompatibilityResolver(ignoredIncompatibilities) + ) + +} diff --git a/extension/default/src/test/kotlin/converter/SingleObjectToJsonTest.kt b/extension/default/src/test/kotlin/converter/SingleObjectToJsonTest.kt new file mode 100644 index 00000000..079e1b40 --- /dev/null +++ b/extension/default/src/test/kotlin/converter/SingleObjectToJsonTest.kt @@ -0,0 +1,28 @@ +package io.holixon.avro.adapter.common.converter + +import io.holixon.avro.adapter.api.AvroAdapterApi.schemaResolver +import io.holixon.avro.adapter.common.AvroAdapterDefault +import io.holixon.avro.adapter.common.AvroAdapterDefault.toByteArray +import io.holixon.avro.lib.test.AvroAdapterTestLib +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + + +internal class SingleObjectToJsonTest { + + private val sample = AvroAdapterTestLib.sampleFoo + + private val registry = AvroAdapterDefault.inMemorySchemaRegistry().apply { + register(sample.schema) + } + + private val bytes = sample.toByteArray() + private val expectedJson = sample.toString().replace("\\s".toRegex(), "") + + private val fn = SingleObjectToJson(registry.schemaResolver()) + + @Test + internal fun `convert bytes to json`() { + assertThat(fn.apply(bytes)).isEqualTo(expectedJson) + } +} diff --git a/extension/default/src/test/kotlin/registry/CompositeRegistryTest.kt b/extension/default/src/test/kotlin/registry/CompositeRegistryTest.kt new file mode 100644 index 00000000..e80c4de2 --- /dev/null +++ b/extension/default/src/test/kotlin/registry/CompositeRegistryTest.kt @@ -0,0 +1,140 @@ +package io.holixon.avro.adapter.common.registry + +import io.holixon.avro.adapter.api.AvroSchemaReadOnlyRegistry +import io.holixon.avro.adapter.api.AvroSchemaRegistry +import io.holixon.avro.adapter.api.AvroSchemaWithId +import io.holixon.avro.adapter.api.type.AvroSchemaInfoData +import io.holixon.avro.adapter.api.type.AvroSchemaWithIdData +import io.holixon.avro.adapter.common.AvroAdapterApiTestHelper +import io.holixon.avro.lib.test.schema.SampleEventV4711 +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.kotlin.* +import java.util.* + +internal class CompositeRegistryTest { + + + private val schema4711 = + AvroSchemaWithIdData(AvroAdapterApiTestHelper.schemaIdSupplier.apply(SampleEventV4711.schema), SampleEventV4711.schema) + + private val fastRoRegistry: AvroSchemaReadOnlyRegistry = mock() + private val mediumRwRegistry: AvroSchemaRegistry = mock() + private val slowRwRegistry: AvroSchemaRegistry = mock() + + private val registry: AvroSchemaRegistry = CompositeAvroSchemaRegistry(slowRwRegistry, fastRoRegistry, mediumRwRegistry, slowRwRegistry) + + @Test + fun `should deliver from the first registry if found`() { + mockRegistryFindMethods(fastRoRegistry, schema4711) + mockRegistryFindMethods(mediumRwRegistry, null) + mockRegistryFindMethods(slowRwRegistry, null) + + callRegistryFindMethodsAndFindResult() + + verifyCallsExecuted(fastRoRegistry, true) + verifyCallsExecuted(mediumRwRegistry, false) + verifyCallsExecuted(slowRwRegistry, false) + } + + @Test + fun `should deliver from the second registry if not found in the first`() { + mockRegistryFindMethods(fastRoRegistry, null) + mockRegistryFindMethods(mediumRwRegistry, schema4711) + mockRegistryFindMethods(slowRwRegistry, null) + + callRegistryFindMethodsAndFindResult() + + verifyCallsExecuted(fastRoRegistry, true) + verifyCallsExecuted(mediumRwRegistry, true) + verifyCallsExecuted(slowRwRegistry, false) + } + + @Test + fun `should deliver from the third registry if not found in the first and second`() { + mockRegistryFindMethods(fastRoRegistry, null) + mockRegistryFindMethods(mediumRwRegistry, null) + mockRegistryFindMethods(slowRwRegistry, schema4711) + + callRegistryFindMethodsAndFindResult() + + verifyCallsExecuted(fastRoRegistry, true) + verifyCallsExecuted(mediumRwRegistry, true) + verifyCallsExecuted(slowRwRegistry, true) + } + + @Test + fun `should ask all registries if not found`() { + mockRegistryFindMethods(fastRoRegistry, null) + mockRegistryFindMethods(mediumRwRegistry, null) + mockRegistryFindMethods(slowRwRegistry, null) + + callRegistryFindMethodsAndDontFindResult() + + verifyCallsExecuted(fastRoRegistry, true) + verifyCallsExecuted(mediumRwRegistry, true) + verifyCallsExecuted(slowRwRegistry, true) + } + + @Test + fun `should register schema in the designated registry`() { + whenever(slowRwRegistry.register(any())).thenReturn(schema4711) + + registry.register(schema4711.schema) + + verify(slowRwRegistry).register(schema4711.schema) + verifyZeroInteractions(mediumRwRegistry) + } + + /** + * Stubbing of methods. + */ + private fun mockRegistryFindMethods(registry: AvroSchemaReadOnlyRegistry, schemaWithId: AvroSchemaWithId?) { + if (schemaWithId != null) { + whenever(registry.findById(any())).thenReturn(Optional.of(schemaWithId)) + whenever(registry.findByInfo(any())).thenReturn(Optional.of(schemaWithId)) + whenever(registry.findAll()).thenReturn(listOf(schemaWithId)) + whenever(registry.findAllByCanonicalName(any(), any())).thenReturn(listOf(schemaWithId)) + } else { + whenever(registry.findById(any())).thenReturn(Optional.empty()) + whenever(registry.findByInfo(any())).thenReturn(Optional.empty()) + whenever(registry.findAll()).thenReturn(listOf()) + whenever(registry.findAllByCanonicalName(any(), any())).thenReturn(listOf()) + } + } + + /** + * Verification that the call has been (or not) executed. + */ + private fun verifyCallsExecuted(registry: AvroSchemaReadOnlyRegistry, executed: Boolean) { + if (executed) { + verify(registry).findAll() + verify(registry).findAllByCanonicalName(schema4711.namespace, schema4711.name) + verify(registry).findById(schema4711.schemaId) + verify(registry).findByInfo(AvroSchemaInfoData(schema4711.namespace, schema4711.name, null)) + } else { + verifyNoMoreInteractions(registry) + } + } + + /** + * Actual call of the registry methods. + */ + private fun callRegistryFindMethodsAndFindResult() { + assertThat(registry.findAll()).isNotEmpty.containsExactly(schema4711) + assertThat(registry.findAllByCanonicalName(schema4711.namespace, schema4711.name)).isNotEmpty.containsExactly(schema4711) + assertThat(registry.findById(schema4711.schemaId)).isPresent.get().isEqualTo(schema4711) + assertThat(registry.findByInfo(AvroSchemaInfoData(schema4711.namespace, schema4711.name, null))).isPresent.get().isEqualTo(schema4711) + } + + /** + * Actual call of the registry methods. + */ + private fun callRegistryFindMethodsAndDontFindResult() { + assertThat(registry.findAll()).isEmpty() + assertThat(registry.findAllByCanonicalName(schema4711.namespace, schema4711.name)).isEmpty() + assertThat(registry.findById(schema4711.schemaId)).isEmpty + assertThat(registry.findByInfo(AvroSchemaInfoData(schema4711.namespace, schema4711.name, null))).isEmpty + } + +} diff --git a/extension/default/src/test/kotlin/registry/InMemoryAvroSchemaRegistryTest.kt b/extension/default/src/test/kotlin/registry/InMemoryAvroSchemaRegistryTest.kt new file mode 100644 index 00000000..368b0063 --- /dev/null +++ b/extension/default/src/test/kotlin/registry/InMemoryAvroSchemaRegistryTest.kt @@ -0,0 +1,58 @@ +package io.holixon.avro.adapter.common.registry + +import io.holixon.avro.lib.test.AvroAdapterTestLib +import io.holixon.avro.adapter.common.AvroAdapterApiTestHelper.schemaIdSupplier +import io.holixon.avro.adapter.common.AvroAdapterApiTestHelper.schemaRevisionResolver +import org.apache.avro.Schema +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class InMemoryAvroSchemaRegistryTest { + + private val registry = InMemoryAvroSchemaRegistry( + schemaIdSupplier = schemaIdSupplier, + schemaRevisionResolver = schemaRevisionResolver + ) + + private val schema = Schema.Parser().parse(AvroAdapterTestLib.loadArvoResource("test.fixture.SampleEvent-v4711")) + + private val schemaId = schemaIdSupplier.apply(schema) + + @BeforeEach + internal fun setUp() { + assertThat(registry.findAll()).isEmpty() + } + + @Test + internal fun `register returns schemaWithId`() { + val registered = registry.register(schema) + + assertThat(registry.findAll()).hasSize(1) + assertThat(registered.schemaId).isEqualTo(schemaId) + assertThat(registered.schema).isEqualTo(schema) + assertThat(registered.canonicalName).isEqualTo("test.fixture.SampleEvent") + assertThat(registered.revision).isEqualTo("4711") + } + + @Test + internal fun `find by context and name`() { + assertThat(registry.findAllByCanonicalName("test.fixture", "SampleEvent")).isEmpty() + registry.register(schema) + + assertThat(registry.findAllByCanonicalName("test.fixture", "SampleEvent")).hasSize(1) + } + + @Test + internal fun `can register schema and find by id`() { + registry.register(schema) + assertThat(registry.findAll()).isNotEmpty + + val found = registry.findById(schemaId).orElseThrow() + + assertThat(found.schemaId).isEqualTo(schemaId) + assertThat(found.schema).isEqualTo(schema) + assertThat(found.revision).isEqualTo("4711") + } + +} diff --git a/extension/default/src/test/resources/.gitkeep b/extension/default/src/test/resources/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/extension/pom.xml b/extension/pom.xml new file mode 100644 index 00000000..74a6b910 --- /dev/null +++ b/extension/pom.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + 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> + + <parent> + <groupId>io.holixon.avro._</groupId> + <artifactId>avro-registry-adapter-parent</artifactId> + <version>0.0.1</version> + <relativePath>../parent/pom.xml</relativePath> + </parent> + + <artifactId>extension</artifactId> + <packaging>pom</packaging> + <description>Parent pom for the extension.</description> + + <modules> + <module>api</module> + <module>default</module> + <module>registry/apicurio-rest</module> + </modules> + + <dependencies> + <dependency> + <groupId>org.jetbrains.kotlin</groupId> + <artifactId>kotlin-stdlib-jdk8</artifactId> + </dependency> + <dependency> + <groupId>org.jetbrains.kotlin</groupId> + <artifactId>kotlin-reflect</artifactId> + </dependency> + <dependency> + <groupId>org.apache.avro</groupId> + <artifactId>avro</artifactId> + </dependency> + + <dependency> + <groupId>io.holixon.avro._</groupId> + <artifactId>avro-registry-adapter-lib-test</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + +</project> diff --git a/extension/registry/apicurio-rest/pom.xml b/extension/registry/apicurio-rest/pom.xml new file mode 100644 index 00000000..b082441d --- /dev/null +++ b/extension/registry/apicurio-rest/pom.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + 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> + + <parent> + <groupId>io.holixon.avro._</groupId> + <artifactId>extension</artifactId> + <version>0.0.1</version> + <relativePath>../../pom.xml</relativePath> + </parent> + + <groupId>io.holixon.avro</groupId> + <artifactId>avro-registry-adapter-apicurio</artifactId> + <description>Adapter implementation using Apicurio Schema registry.</description> + + <properties> + <jacoco.skip>false</jacoco.skip> + </properties> + + <dependencies> + <dependency> + <groupId>io.holixon.avro</groupId> + <artifactId>avro-registry-adapter-api</artifactId> + </dependency> + <dependency> + <groupId>io.apicurio</groupId> + <artifactId>apicurio-registry-client</artifactId> + <version>${apicurio.version}</version> + </dependency> + + <dependency> + <groupId>io.holixon.avro</groupId> + <artifactId>avro-registry-adapter-default</artifactId> + <scope>test</scope> + </dependency> + + </dependencies> +</project> diff --git a/extension/registry/apicurio-rest/src/main/kotlin/ApicurioAvroSchemaRegistry.kt b/extension/registry/apicurio-rest/src/main/kotlin/ApicurioAvroSchemaRegistry.kt new file mode 100644 index 00000000..7bf02b01 --- /dev/null +++ b/extension/registry/apicurio-rest/src/main/kotlin/ApicurioAvroSchemaRegistry.kt @@ -0,0 +1,105 @@ +package io.holixon.avro.adapter.registry.apicurio + +import io.apicurio.registry.rest.client.RegistryClient +import io.apicurio.registry.rest.v2.beans.ArtifactMetaData +import io.apicurio.registry.rest.v2.beans.EditableMetaData +import io.apicurio.registry.rest.v2.beans.IfExists +import io.apicurio.registry.types.ArtifactType +import io.holixon.avro.adapter.api.* +import io.holixon.avro.adapter.api.AvroAdapterApi.byteContent +import io.holixon.avro.adapter.api.type.AvroSchemaWithIdData +import io.holixon.avro.adapter.registry.apicurio.AvroAdapterApicurioRest.description +import org.apache.avro.Schema +import org.slf4j.LoggerFactory +import java.io.InputStream +import java.util.* + +/** + * Implementation of the registry using Apicurio. + */ +class ApicurioAvroSchemaRegistry( + private val client: RegistryClient, + private val schemaIdSupplier: SchemaIdSupplier, + private val schemaRevisionResolver: SchemaRevisionResolver +) : AvroSchemaRegistry { + companion object { + const val KEY_NAME = "name" + const val KEY_NAMESPACE = "namespace" + const val KEY_CANONICAL_NAME = "canonicalName" + const val KEY_REVISION = "revision" + const val DEFAULT_GROUP = "default" // TODO: should this be configurable? + } + + private val logger = LoggerFactory.getLogger(ApicurioAvroSchemaRegistry::class.java) + + override fun register(schema: Schema): AvroSchemaWithId { + val content = schema.byteContent() + val schemaId = schemaIdSupplier.apply(schema) + val revision = schemaRevisionResolver.apply(schema).orElse(null) + + + val metaData: ArtifactMetaData = client.createArtifact(DEFAULT_GROUP, schemaId, ArtifactType.AVRO, IfExists.RETURN_OR_UPDATE, content) + logger.trace("Registered schema and received the following metadata: $metaData") + + client.updateArtifactMetaData(DEFAULT_GROUP, schemaId, EditableMetaData().apply { + name = schema.name + description = schema.description() + properties = mapOf( + KEY_NAME to schema.name, + KEY_NAMESPACE to schema.namespace, + KEY_CANONICAL_NAME to schema.fullName, + KEY_REVISION to revision + ) + }) + + logger.info("meta date: ${client.getArtifactMetaData(DEFAULT_GROUP, schemaId)}") + + return AvroSchemaWithIdData( + schemaId = schemaId, + schema = schema, + revision = revision + ) + } + + override fun findById(schemaId: AvroSchemaId): Optional<AvroSchemaWithId> { + + val schema = client.getLatestArtifact(DEFAULT_GROUP, schemaId).schema() + return Optional.of( + AvroSchemaWithIdData( + schemaId = schemaId, + schema = schema, + revision = schemaRevisionResolver.apply(schema).orElse(null) + ) + ) + } + + override fun findByInfo(info: AvroSchemaInfo): Optional<AvroSchemaWithId> { + return findAllByCanonicalName(info.namespace, info.name).singleOrNull { info.revision == it.revision } + .let { Optional.ofNullable(it) } + } + + override fun findAllByCanonicalName(namespace: String, name: String): List<AvroSchemaWithId> { + return client.listArtifactsInGroup(DEFAULT_GROUP).artifacts + .asSequence() + .filter { it.name == name } + .map { client.getArtifactMetaData(DEFAULT_GROUP, it.id) } + .filter { it.namespace().orElse("") == namespace } + .map { findById(it.id) } + .filter { it.isPresent } + .map { it.get() } + .toList() + } + + override fun findAll(): List<AvroSchemaWithId> { + val artifactIds = client.listArtifactsInGroup(DEFAULT_GROUP).artifacts + return artifactIds.map { findById(it.id) }.filter { it.isPresent }.map { it.get() } + } + + private fun InputStream.schema(): Schema = this.bufferedReader(Charsets.UTF_8).use { + val text = it.readText() + Schema.Parser().parse(text) + } + + private fun ArtifactMetaData.revision(): AvroSchemaRevision? = this.properties[KEY_REVISION] + private fun ArtifactMetaData.namespace(): Optional<String> = Optional.ofNullable(this.properties[KEY_NAMESPACE]) +} diff --git a/extension/registry/apicurio-rest/src/main/kotlin/AvroAdapterApicurioRest.kt b/extension/registry/apicurio-rest/src/main/kotlin/AvroAdapterApicurioRest.kt new file mode 100644 index 00000000..a399b41b --- /dev/null +++ b/extension/registry/apicurio-rest/src/main/kotlin/AvroAdapterApicurioRest.kt @@ -0,0 +1,28 @@ +package io.holixon.avro.adapter.registry.apicurio + +import io.apicurio.registry.rest.client.RegistryClient +import io.apicurio.registry.rest.client.RegistryClientFactory +import org.apache.avro.Schema + +/** + * + * REST endpoint (version 1.3.x): `http://$host:port/api` + * REST endpoint (version 2.0.x): `http://$host:port/api/registry/v2` + * + */ +object AvroAdapterApicurioRest { + + @JvmOverloads + @JvmStatic + fun registryApiUrl(host: String, port: Int, https: Boolean = false) = "http${if (https) "s" else ""}://$host:$port/apis/registry/v2" + + @JvmOverloads + @JvmStatic + fun registryRestClient(host: String, port: Int, https: Boolean = false): RegistryClient = registryRestClient(registryApiUrl(host, port, https)) + + @JvmStatic + fun registryRestClient(apiUrl : String): RegistryClient = RegistryClientFactory.create(apiUrl) + + fun Schema.description(): String? = this.doc + +} diff --git a/extension/registry/apicurio-rest/src/main/resources/.gitkeep b/extension/registry/apicurio-rest/src/main/resources/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/extension/registry/apicurio-rest/src/test/kotlin/ApicurioAvroSchemaRegistryITest.kt b/extension/registry/apicurio-rest/src/test/kotlin/ApicurioAvroSchemaRegistryITest.kt new file mode 100644 index 00000000..40cd7d1a --- /dev/null +++ b/extension/registry/apicurio-rest/src/test/kotlin/ApicurioAvroSchemaRegistryITest.kt @@ -0,0 +1,114 @@ +package io.holixon.avro.adapter.registry.apicurio + +import io.holixon.avro.adapter.api.type.AvroSchemaInfoData +import io.holixon.avro.adapter.common.AvroAdapterDefault +import io.holixon.avro.adapter.registry.apicurio.ApicurioRegistryTestContainer.Companion.EXPOSED_PORT +import io.holixon.avro.lib.test.AvroAdapterTestLib +import io.holixon.avro.lib.test.schema.SampleEventV4711 +import mu.KLogging +import org.apache.avro.Schema +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.output.Slf4jLogConsumer +import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers + +/** + * Test container for apicurio tests. + */ +class ApicurioRegistryTestContainer : GenericContainer<ApicurioRegistryTestContainer>("apicurio/apicurio-registry-mem:2.0.0.Final") { + companion object { + const val EXPOSED_PORT = 8080 + } + + /** + * Delivers the REST client for Apicurio access. + */ + fun restClient() = AvroAdapterApicurioRest.registryRestClient(containerIpAddress, getMappedPort(EXPOSED_PORT)) +} + +@Testcontainers +internal class ApicurioAvroSchemaRegistryITest { + companion object : KLogging() { + + @Container + @JvmStatic + val CONTAINER = ApicurioRegistryTestContainer().apply { + withExposedPorts(EXPOSED_PORT) + withLogConsumer(Slf4jLogConsumer(logger)) + setWaitStrategy(HostPortWaitStrategy()) + } + } + + private val registryClient by lazy { + ApicurioAvroSchemaRegistry( + CONTAINER.restClient(), + AvroAdapterDefault.schemaIdSupplier, + AvroAdapterDefault.schemaRevisionResolver + ) + } + + @Test + internal fun `find by id`() { + val schema: Schema = SampleEventV4711.schema + val fingerprint = AvroAdapterDefault.schemaIdSupplier.apply(schema) + + val created = registryClient.register(schema) + logger.info { "created: $created" } + + val (schemaId, foundSchema, revision) = registryClient.findById(fingerprint).orElseThrow() + + assertThat(schemaId).isEqualTo(fingerprint) + assertThat(foundSchema).isEqualTo(schema) + assertThat(revision).isEqualTo("4711") + } + + @Test + internal fun `find by info`() { + val schema: Schema = AvroAdapterTestLib.schemaSampleEvent4711 + val fingerprint = AvroAdapterDefault.schemaIdSupplier.apply(schema) + + val created = registryClient.register(schema) + logger.info { "created: $created" } + + val found = registryClient.findByInfo( + AvroSchemaInfoData( + namespace = schema.namespace, + name = schema.name, + revision = "4711" + ) + ) + + assertThat(found).isNotEmpty + assertThat(found.get().schemaId).isEqualTo(fingerprint) + + assertThat( + registryClient.findByInfo( + AvroSchemaInfoData( + namespace = schema.namespace, + name = schema.name, + revision = "4712" + ) + ) + ).isEmpty + } + + @Test + internal fun `find by context and name`() { + val schema = SampleEventV4711.schema + val fingerprint = SampleEventV4711.schemaData.fingerPrint.toString() + + val created = registryClient.register(schema) + logger.info { "created: $created" } + + val found = registryClient.findAllByCanonicalName( + namespace = schema.namespace, + name = schema.name + ) + + assertThat(found).hasSize(1) + assertThat(found.first().schemaId).isEqualTo(fingerprint) + } +} diff --git a/extension/registry/apicurio-rest/src/test/resources/.gitkeep b/extension/registry/apicurio-rest/src/test/resources/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 00000000..03f45228 --- /dev/null +++ b/lib/README.md @@ -0,0 +1,6 @@ +# Module: /lib + +Code/Resources of submodules beyond this module are not part of the +release build. + +They are supposed to be used for testing and example code. diff --git a/lib/coverage-aggregate/pom.xml b/lib/coverage-aggregate/pom.xml new file mode 100644 index 00000000..f3b9efe1 --- /dev/null +++ b/lib/coverage-aggregate/pom.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + 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> + + <parent> + <groupId>io.holixon.avro._</groupId> + <artifactId>lib</artifactId> + <version>0.0.1</version> + </parent> + + <artifactId>avro-registry-adapter-lib-coverage-aggregator</artifactId> + <description>Aggregates coverage reports from JUnit test an I-Tests.</description> + + <properties> + <jacoco.skip>false</jacoco.skip> + <deploy.skip>true</deploy.skip> + </properties> + + <!-- Coverage aggregates works based on dependencies defined here --> + <dependencies> + <dependency> + <groupId>io.holixon.avro</groupId> + <artifactId>avro-registry-adapter-api</artifactId> + </dependency> + <dependency> + <groupId>io.holixon.avro</groupId> + <artifactId>avro-registry-adapter-default</artifactId> + </dependency> + <dependency> + <groupId>io.holixon.avro</groupId> + <artifactId>avro-registry-adapter-apicurio</artifactId> + </dependency> + <dependency> + <groupId>io.holixon.avro._</groupId> + <artifactId>avro-registry-adapter-lib-test</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <executions> + <execution> + <id>report-aggregate</id> + <phase>verify</phase> + <goals> + <goal>report-aggregate</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/lib/pom.xml b/lib/pom.xml new file mode 100644 index 00000000..03fb2db3 --- /dev/null +++ b/lib/pom.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + 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> + + <parent> + <groupId>io.holixon.avro._</groupId> + <artifactId>avro-registry-adapter-parent</artifactId> + <version>0.0.1</version> + <relativePath>../parent/pom.xml</relativePath> + </parent> + + <artifactId>lib</artifactId> + <packaging>pom</packaging> + + <modules> + <module>test</module> + <module>coverage-aggregate</module> + </modules> +</project> diff --git a/lib/test/README.md b/lib/test/README.md new file mode 100644 index 00000000..ea681c3b --- /dev/null +++ b/lib/test/README.md @@ -0,0 +1,6 @@ +# Module: /lib/test + +This module provides a common test scope dependency and configuration setup, +used by all extension sub modules and examples. + +This avoids redundant declaration of dependencies over and over again. diff --git a/lib/test/pom.xml b/lib/test/pom.xml new file mode 100644 index 00000000..150fba6b --- /dev/null +++ b/lib/test/pom.xml @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + 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> + + <parent> + <groupId>io.holixon.avro._</groupId> + <artifactId>lib</artifactId> + <version>0.0.1</version> + </parent> + + <artifactId>avro-registry-adapter-lib-test</artifactId> + <description>Internally used test library</description> + + <properties> + <jacoco.skip>false</jacoco.skip> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.avro</groupId> + <artifactId>avro</artifactId> + </dependency> + <dependency> + <groupId>org.jetbrains.kotlin</groupId> + <artifactId>kotlin-stdlib-jdk8</artifactId> + </dependency> + <dependency> + <groupId>io.github.microutils</groupId> + <artifactId>kotlin-logging-jvm</artifactId> + <version>${kotlin-logging.version}</version> + </dependency> + + + <!-- all test dependencies are on the compile scope, because this is a test lib which is used on the test scope --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <scope>compile</scope> + <version>5.7.2</version> + </dependency> + + <dependency> + <groupId>org.jetbrains.kotlin</groupId> + <artifactId>kotlin-test-junit5</artifactId> + <scope>compile</scope> + </dependency> + + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>compile</scope> + <version>3.10.0</version> + </dependency> + + <dependency> + <groupId>org.mockito.kotlin</groupId> + <artifactId>mockito-kotlin</artifactId> + <version>3.2.0</version> + <scope>compile</scope> + </dependency> + + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>junit-jupiter</artifactId> + <scope>compile</scope> + <version>1.15.3</version> + </dependency> + + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <version>1.7.30</version> + <scope>compile</scope> + </dependency> + + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <version>3.19.0</version> + <scope>compile</scope> + </dependency> + + <dependency> + <groupId>org.awaitility</groupId> + <artifactId>awaitility-kotlin</artifactId> + <version>4.1.0</version> + <scope>compile</scope> + </dependency> + + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.avro</groupId> + <artifactId>avro-maven-plugin</artifactId> + <version>${avro.version}</version> + <executions> + <execution> + <phase>generate-sources</phase> + <goals> + <goal>schema</goal> + </goals> + <configuration> + <sourceDirectory>${project.basedir}/src/main/resources/avro/</sourceDirectory> + <outputDirectory>${project.build.directory}/generated-sources/avro/</outputDirectory> + <stringType>String</stringType> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/lib/test/src/main/kotlin/AvroAdapterTestLib.kt b/lib/test/src/main/kotlin/AvroAdapterTestLib.kt new file mode 100644 index 00000000..66ca92a2 --- /dev/null +++ b/lib/test/src/main/kotlin/AvroAdapterTestLib.kt @@ -0,0 +1,30 @@ +package io.holixon.avro.lib.test + +import io.holixon.avro.lib.test.schema.SampleEventV4711 +import io.holixon.avro.lib.test.schema.SampleEventV4713 +import org.apache.avro.Schema +import test.fixture.SampleEvent +import test.fixture.SampleEventWithAdditionalFieldWithDefault + +/** + * Test support helper. + */ +object AvroAdapterTestLib { + + val schemaSampleEvent4711 = SampleEventV4711.schema + val schemaSampleEvent4713 = SampleEventV4713.schema + + fun loadResource(resName:String): String = {}::class.java.getResource(resName.trailingSlash())?.readText() ?: throw IllegalStateException("resource not found: $resName") + fun loadArvoResource(avroFileName:String): String = loadResource("/avro/$avroFileName.avsc") + + private fun String.trailingSlash() = if (startsWith("/")) this else "/$this" + + fun loadSchema(resName:String): Schema = Schema.Parser().parse(loadArvoResource(resName)) + + val sampleFoo = SampleEvent("foo") + val sampleFooWithAdditionalFieldWithDefault = SampleEventWithAdditionalFieldWithDefault("foo", "default value") + const val sampleFooHex = "[C3 01 CC 98 1F E7 56 D4 1C A5 06 66 6F 6F]" + + const val sampleEventFingerprint = -6549126288393660212L + +} diff --git a/lib/test/src/main/kotlin/schema/SampleEvents.kt b/lib/test/src/main/kotlin/schema/SampleEvents.kt new file mode 100644 index 00000000..2e75221d --- /dev/null +++ b/lib/test/src/main/kotlin/schema/SampleEvents.kt @@ -0,0 +1,73 @@ +package io.holixon.avro.lib.test.schema + +import io.holixon.avro.lib.test.AvroAdapterTestLib +import org.apache.avro.generic.GenericData +import org.apache.avro.generic.GenericRecord + +const val SCHEMA_CONTEXT = "test.fixture" +const val SCHEMA_NAME = "SampleEvent" + +/** + * Test schema provider. + */ +object SampleEventV4711 : TestSchemaDataProvider { + const val REVISION = "4711" + + override val schemaJson by lazy { AvroAdapterTestLib.loadArvoResource("$SCHEMA_CONTEXT.$SCHEMA_NAME-v$REVISION") } +} + +/** + * Test schema provider. + */ +object SampleEventV4712 : TestSchemaDataProvider { + const val REVISION = "4712" + + override val schemaJson: String = """{ + "type": "record", + "namespace": "$SCHEMA_CONTEXT", + "name": "$SCHEMA_NAME", + "revision": "$REVISION", + "doc": "used for testing, has optional field", + "fields": [ + { + "name": "value", + "doc": "this is the first value, it is required", + "type": { + "type": "string", + "avro.java.string": "String" + } + }, + { + "name": "anotherValue", + "doc": "this is the second value, it is optional", + "type": [ + { + "type": "string", + "avro.java.string": "String" + }, + "null" + ], + "default": "null" + } + ], + "__comment": "some additional comment" + }""" + + fun create(value: String, anotherValue:String?= null) : GenericRecord = GenericData.Record(schema).apply { + put("value", value) + anotherValue?.also { put("anotherValue", anotherValue) } + } +} + +/** + * Test provider. + */ +object SampleEventV4713 : TestSchemaDataProvider { + const val REVISION = "4713" + const val SUFFIX = "WithAdditionalFieldWithDefault" + + fun addSuffix(fqn: String) = fqn + SUFFIX + fun removeSuffix(fqn:String) = fqn.removeSuffix(SUFFIX) + + override val schemaJson by lazy { AvroAdapterTestLib.loadArvoResource("$SCHEMA_CONTEXT.$SCHEMA_NAME-v$REVISION") } +} diff --git a/lib/test/src/main/kotlin/schema/TestSchemaData.kt b/lib/test/src/main/kotlin/schema/TestSchemaData.kt new file mode 100644 index 00000000..c20b1838 --- /dev/null +++ b/lib/test/src/main/kotlin/schema/TestSchemaData.kt @@ -0,0 +1,36 @@ +package io.holixon.avro.lib.test.schema + +import org.apache.avro.Schema +import org.apache.avro.SchemaNormalization + +data class TestSchemaData( + val schema: Schema, + val name: String, + val namespace: String, + val doc: String, + val fingerPrint: Long, + val revision: String? +) { + constructor(schema: Schema) : this( + schema = schema, + name = schema.name, + namespace = schema.namespace, + doc = schema.doc, + fingerPrint = SchemaNormalization.parsingFingerprint64(schema), + revision = schema.getObjectProp("revision") as String? + ) + + constructor(schemaJson: String) : this(Schema.Parser().parse(schemaJson)) +} + +interface TestSchemaDataProvider { + val schemaJson: String + + val schemaData: TestSchemaData + get() = TestSchemaData(schemaJson) + + val schema: Schema + get() = schemaData.schema + + fun toJson() = schema.toString(true) +} diff --git a/lib/test/src/main/resources/avro/test.fixture.SampleEvent-v4711.avsc b/lib/test/src/main/resources/avro/test.fixture.SampleEvent-v4711.avsc new file mode 100644 index 00000000..dd1c2552 --- /dev/null +++ b/lib/test/src/main/resources/avro/test.fixture.SampleEvent-v4711.avsc @@ -0,0 +1,17 @@ +{ + "type": "record", + "namespace": "test.fixture", + "name": "SampleEvent", + "doc": "a sample event for testing", + "revision": "4711", + "fields": [ + { + "name": "value", + "type": { + "type": "string", + "avro.java.string": "String" + } + } + ], + "__comment": "additional comment" +} diff --git a/lib/test/src/main/resources/avro/test.fixture.SampleEvent-v4713.avsc b/lib/test/src/main/resources/avro/test.fixture.SampleEvent-v4713.avsc new file mode 100644 index 00000000..e8f67cae --- /dev/null +++ b/lib/test/src/main/resources/avro/test.fixture.SampleEvent-v4713.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "test.fixture", + "name": "SampleEventWithAdditionalFieldWithDefault", + "doc": "a sample event for testing", + "revision": "4713", + "fields": [ + { + "name": "value", + "type": { + "type": "string", + "avro.java.string": "String" + } + }, + { + "name": "otherValue", + "type": { + "type": "string", + "avro.java.string": "String" + }, + "default": "default value" + } + ], + "__comment": "additional comment" +} diff --git a/lib/test/src/main/resources/simplelogger.properties b/lib/test/src/main/resources/simplelogger.properties new file mode 100644 index 00000000..c80d8479 --- /dev/null +++ b/lib/test/src/main/resources/simplelogger.properties @@ -0,0 +1,6 @@ +org.slf4j.simpleLogger.defaultLogLevel = info + +org.slf4j.simpleLogger.showShortLogName = true +org.slf4j.simpleLogger.levelInBrackets = true + +#org.slf4j.simpleLogger.log.org.camunda.bpm.engine.persistence = warn diff --git a/lib/test/src/test/kotlin/AvroAdapterTestLibTest.kt b/lib/test/src/test/kotlin/AvroAdapterTestLibTest.kt new file mode 100644 index 00000000..f6f267e9 --- /dev/null +++ b/lib/test/src/test/kotlin/AvroAdapterTestLibTest.kt @@ -0,0 +1,45 @@ +package io.holixon.avro.lib.test + +import io.holixon.avro.lib.test.schema.SampleEventV4711 +import io.holixon.avro.lib.test.schema.SampleEventV4712 +import org.apache.avro.generic.GenericData +import org.apache.avro.generic.GenericRecord +import org.apache.avro.specific.SpecificData +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import test.fixture.SampleEvent + +internal class AvroAdapterTestLibTest { + + @Test + internal fun `schema matches`() { + assertThat(SampleEvent.`SCHEMA$`).isEqualTo(SampleEventV4711.schema) + } + + @Test + internal fun `create specific from generic`() { + val generic = GenericData.Record(SampleEvent.`SCHEMA$`).apply { + put("value", "foo") + } + + val specific = SpecificData.get().deepCopy(SampleEvent.`SCHEMA$`, generic) as SampleEvent + + assertThat(specific.value).isEqualTo("foo") + } + + @Test + internal fun `generic record from schema 4712`() { + + println(SampleEventV4712.create("foo")) + println(SampleEventV4712.create(value = "foo", anotherValue = "bar")) + println(SampleEventV4712.create(value = "foo", anotherValue = null)) + + val record4712: GenericRecord = SampleEventV4712.create("foo") + + println("rec: $record4712") + + val sd4711 = SpecificData.get().deepCopy(SampleEventV4711.schema, record4712) as SampleEvent + + println("spec: $sd4711") + } +} diff --git a/lib/test/src/test/kotlin/schema/SampleEventV4711Test.kt b/lib/test/src/test/kotlin/schema/SampleEventV4711Test.kt new file mode 100644 index 00000000..7df01f52 --- /dev/null +++ b/lib/test/src/test/kotlin/schema/SampleEventV4711Test.kt @@ -0,0 +1,16 @@ +package io.holixon.avro.lib.test.schema + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SampleEventV4711Test { + + @Test + internal fun `load from resource`() { + val schemaData = SampleEventV4711.schemaData + + assertThat(schemaData.name).isEqualTo("SampleEvent") + assertThat(schemaData.revision).isEqualTo("4711") + assertThat(schemaData.doc).isEqualTo("a sample event for testing") + } +} diff --git a/lib/test/src/test/kotlin/schema/SampleEventV4712Test.kt b/lib/test/src/test/kotlin/schema/SampleEventV4712Test.kt new file mode 100644 index 00000000..ea03712b --- /dev/null +++ b/lib/test/src/test/kotlin/schema/SampleEventV4712Test.kt @@ -0,0 +1,16 @@ +package io.holixon.avro.lib.test.schema + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SampleEventV4712Test { + + @Test + internal fun `parse schema data`() { + val schemaData = SampleEventV4712.schemaData + + assertThat(schemaData.name).isEqualTo("SampleEvent") + + assertThat(schemaData.schema.fields.map { it.name() }).containsExactlyInAnyOrder("value", "anotherValue") + } +} diff --git a/mvnw b/mvnw new file mode 100755 index 00000000..41c0f0c2 --- /dev/null +++ b/mvnw @@ -0,0 +1,310 @@ +#!/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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven 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)`" +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 + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + 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 $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + 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 + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +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 "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 00000000..86115719 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,182 @@ +@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 http://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 Maven 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 keystroke 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 by 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.5.6/maven-wrapper-0.5.6.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% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%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% diff --git a/parent/pom.xml b/parent/pom.xml new file mode 100644 index 00000000..2263d0ba --- /dev/null +++ b/parent/pom.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + 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> + + <parent> + <groupId>io.holixon.avro._</groupId> + <artifactId>avro-registry-adapter</artifactId> + <version>0.0.1</version> + </parent> + + <artifactId>avro-registry-adapter-parent</artifactId> + <packaging>pom</packaging> + + <properties> + <avro.version>1.10.2</avro.version> + <kotlin-logging.version>2.0.6</kotlin-logging.version> + <apicurio.version>2.0.0.Final</apicurio.version> + </properties> + + <dependencyManagement> + <dependencies> + <!-- KOTLIN --> + <dependency> + <groupId>org.jetbrains.kotlin</groupId> + <artifactId>kotlin-bom</artifactId> + <version>${kotlin.version}</version> + <scope>import</scope> + <type>pom</type> + </dependency> + + <dependency> + <groupId>io.holixon.avro</groupId> + <artifactId>avro-registry-adapter-bom</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + + <dependency> + <groupId>io.holixon.avro._</groupId> + <artifactId>avro-registry-adapter-lib-test</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + </dependencyManagement> + +</project> diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..75f09551 --- /dev/null +++ b/pom.xml @@ -0,0 +1,586 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + 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> + + <groupId>io.holixon.avro._</groupId> + <artifactId>avro-registry-adapter</artifactId> + <version>0.0.1</version> + <url>https://github.com/holixon/avro-registry-adapter/</url> + <packaging>pom</packaging> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> + <java.version>11.0</java.version> + + <kotlin.version>1.5.10</kotlin.version> + + <pattern.class.itest>**/*ITest.*</pattern.class.itest> + <pattern.package.itest>**/itest/**/*.*</pattern.package.itest> + + <!-- Skip instrumentalization by default --> + <jacoco.skip>true</jacoco.skip> + </properties> + + <modules> + <module>parent</module> + <module>lib</module> + <module>extension</module> + <module>bom</module> + <!-- More modules in profile examples --> + </modules> + + <build> + <defaultGoal>clean jacoco:prepare-agent package</defaultGoal> + <resources> + <!-- ignore .gitkeep marker files --> + <resource> + <directory>src/main/resources</directory> + <excludes> + <exclude>.gitkeep</exclude> + </excludes> + </resource> + </resources> + + <pluginManagement> + <plugins> + <plugin> + <!-- cleaning --> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-clean-plugin</artifactId> + <version>3.1.0</version> + </plugin> + + <!-- Coverage metering --> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <version>0.8.7</version> + <executions> + <execution> + <id>pre-unit-test</id> + <goals> + <goal>prepare-agent</goal> + </goals> + </execution> + <execution> + <id>pre-integration-test</id> + <goals> + <goal>prepare-agent-integration</goal> + </goals> + </execution> + </executions> + </plugin> + + <plugin> + <!-- java compiler --> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.8.1</version> + <configuration> + <encoding>${project.build.sourceEncoding}</encoding> + <release>11</release> + </configuration> + <executions> + <!-- Replacing default-compile as it is treated specially by maven --> + <execution> + <id>default-compile</id> + <phase>none</phase> + </execution> + <!-- Replacing default-testCompile as it is treated specially by maven --> + <execution> + <id>default-testCompile</id> + <phase>none</phase> + </execution> + <execution> + <id>java-compile</id> + <phase>compile</phase> + <goals> + <goal>compile</goal> + </goals> + </execution> + <execution> + <id>java-test-compile</id> + <phase>test-compile</phase> + <goals> + <goal>testCompile</goal> + </goals> + </execution> + </executions> + </plugin> + + <plugin> + <!-- kotlin compiler --> + <groupId>org.jetbrains.kotlin</groupId> + <artifactId>kotlin-maven-plugin</artifactId> + <version>${kotlin.version}</version> + <configuration> + <jvmTarget>11</jvmTarget> + <compilerPlugins> + <plugin>spring</plugin> + <plugin>no-arg</plugin> + <plugin>all-open</plugin> + </compilerPlugins> + <pluginOptions> + <!-- Each annotation is placed on its own line --> + <option>all-open:annotation=com.tngtech.jgiven.integration.spring.JGivenStage</option> + </pluginOptions> + </configuration> + + <executions> + <execution> + <id>compile</id> + <phase>compile</phase> + <goals> + <goal>compile</goal> + </goals> + <configuration> + <sourceDirs> + <sourceDir>${project.basedir}/src/main/kotlin</sourceDir> + </sourceDirs> + </configuration> + </execution> + <execution> + <id>test-compile</id> + <phase>test-compile</phase> + <goals> + <goal>test-compile</goal> + </goals> + <configuration> + <sourceDirs> + <sourceDir>${project.basedir}/src/test/kotlin</sourceDir> + <sourceDir>${project.basedir}/src/test/java</sourceDir> + </sourceDirs> + </configuration> + </execution> + </executions> + + <dependencies> + <dependency> + <groupId>org.jetbrains.kotlin</groupId> + <artifactId>kotlin-maven-allopen</artifactId> + <version>${kotlin.version}</version> + </dependency> + <dependency> + <groupId>org.jetbrains.kotlin</groupId> + <artifactId>kotlin-maven-noarg</artifactId> + <version>${kotlin.version}</version> + </dependency> + </dependencies> + </plugin> + + <plugin> + <!-- resources --> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-resources-plugin</artifactId> + <version>3.2.0</version> + <configuration> + <encoding>UTF-8</encoding> + </configuration> + </plugin> + + <!-- Unit Tests --> + <plugin> + <artifactId>maven-surefire-plugin</artifactId> + <version>3.0.0-M5</version> + <configuration> + <useSystemClassLoader>false</useSystemClassLoader> + <shutdown>kill</shutdown> + <runOrder>random</runOrder> + <excludes> + <exclude>${pattern.class.itest}</exclude> + <exclude>${pattern.package.itest}</exclude> + </excludes> + <!-- Sets the VM argument line used when unit tests are run. --> + <!-- prevent the annoying ForkedBooter process from stealing window focus on Mac OS --> + <!-- suppress UnresolvedMavenProperty --> + <argLine>--illegal-access=permit -Djava.awt.headless=true ${argLine} -XX:+StartAttachListener -Xmx1024m + </argLine> + <systemPropertyVariables> + <jgiven.report.dir>${project.build.directory}/jgiven-reports</jgiven.report.dir> + </systemPropertyVariables> + </configuration> + </plugin> + + <!-- ITests --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + <version>3.0.0-M5</version> + <executions> + <execution> + <goals> + <goal>integration-test</goal> + </goals> + </execution> + <execution> + <id>verify results</id> + <phase>verify</phase> + <goals> + <goal>verify</goal> + </goals> + </execution> + </executions> + <configuration> + <includes> + <include>${pattern.class.itest}</include> + <include>${pattern.package.itest}</include> + </includes> + <!-- Sets the VM argument line used when unit tests are run. --> + <!-- prevent the annoying ForkedBooter process from stealing window focus on Mac OS --> + <!-- suppress UnresolvedMavenProperty --> + <argLine>-Djava.awt.headless=true --illegal-access=permit ${argLine} -XX:+StartAttachListener</argLine> + <runOrder>random</runOrder> + </configuration> + </plugin> + + <!-- javadoc for kotlin --> + <plugin> + <groupId>org.jetbrains.dokka</groupId> + <artifactId>dokka-maven-plugin</artifactId> + <version>1.4.32</version> + <executions> + <execution> + <phase>package</phase> + <id>attach-javadocs</id> + <goals> + <goal>javadocJar</goal> + </goals> + </execution> + </executions> + <configuration> + <jdkVersion>11</jdkVersion> + <reportUndocumented>false</reportUndocumented> + <skipEmptyPackages>true</skipEmptyPackages> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <version>3.3.0</version> + <configuration> + <failOnError>false</failOnError> + <doclint>none</doclint> + </configuration> + <executions> + <execution> + <id>attach-javadocs</id> + <goals> + <goal>jar</goal> + </goals> + </execution> + </executions> + </plugin> + + <!-- source from kotlin --> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <version>3.2.0</version> + <executions> + <execution> + <phase>generate-sources</phase> + <goals> + <goal>add-source</goal> + </goals> + <configuration> + <sources> + <source>${project.basedir}/src/main/kotlin</source> + </sources> + </configuration> + </execution> + </executions> + </plugin> + + <!-- attach sources --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-source-plugin</artifactId> + <version>3.2.1</version> + <executions> + <execution> + <id>attach-sources</id> + <phase>package</phase> + <goals> + <goal>jar</goal> + </goals> + <configuration> + <attach>true</attach> + <forceCreation>true</forceCreation> + </configuration> + </execution> + </executions> + </plugin> + + <!-- gitflow --> + <plugin> + <groupId>com.amashchenko.maven.plugin</groupId> + <artifactId>gitflow-maven-plugin</artifactId> + <version>1.16.0</version> + <configuration> + <gitFlowConfig> + <productionBranch>main</productionBranch> + <developmentBranch>develop</developmentBranch> + <featureBranchPrefix>feature/</featureBranchPrefix> + <releaseBranchPrefix>release/</releaseBranchPrefix> + <hotfixBranchPrefix>hotfix/</hotfixBranchPrefix> + <supportBranchPrefix>support/</supportBranchPrefix> + <origin>origin</origin> + </gitFlowConfig> + <useSnapshotInHotfix>true</useSnapshotInHotfix> + <useSnapshotInRelease>true</useSnapshotInRelease> + <keepBranch>false</keepBranch> + <pushRemote>true</pushRemote> + </configuration> + </plugin> + + <!-- To sign the artifacts --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-gpg-plugin</artifactId> + <version>3.0.1</version> + <configuration> + <gpgArguments> + <arg>--batch</arg> + <arg>--yes</arg> + <arg>--pinentry-mode</arg> + <arg>loopback</arg> + </gpgArguments> + </configuration> + <executions> + <execution> + <id>sign-artifacts</id> + <phase>verify</phase> + <goals> + <goal>sign</goal> + </goals> + </execution> + </executions> + </plugin> + + <!-- Deploy --> + <plugin> + <artifactId>maven-deploy-plugin</artifactId> + <version>3.0.0-M1</version> + <configuration> + <skip>true</skip> + </configuration> + </plugin> + <plugin> + <groupId>org.sonatype.plugins</groupId> + <artifactId>nexus-staging-maven-plugin</artifactId> + <version>1.6.8</version> + <configuration> + <autoReleaseAfterClose>true</autoReleaseAfterClose> + <serverId>ossrh</serverId> + <nexusUrl>https://oss.sonatype.org/</nexusUrl> + </configuration> + <executions> + <execution> + <id>default-deploy</id> + <phase>deploy</phase> + <goals> + <goal>deploy</goal> + </goals> + </execution> + </executions> + </plugin> + + <!-- Install --> + <plugin> + <artifactId>maven-install-plugin</artifactId> + <version>2.5.2</version> + </plugin> + + <!-- Enforce --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-enforcer-plugin</artifactId> + <version>3.0.0-M3</version> + <executions> + <execution> + <id>enforce-maven</id> + <goals> + <goal>enforce</goal> + </goals> + <configuration> + <rules> + <requireMavenVersion> + <version>3.6.0</version> + </requireMavenVersion> + <requireJavaVersion> + <version>11</version> + </requireJavaVersion> + </rules> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </pluginManagement> + + <plugins> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-enforcer-plugin</artifactId> + </plugin> + + <plugin> + <groupId>org.jetbrains.kotlin</groupId> + <artifactId>kotlin-maven-plugin</artifactId> + </plugin> + </plugins> + + </build> + + <profiles> + <profile> + <id>itest</id> + <activation> + <activeByDefault>false</activeByDefault> + </activation> + <build> + <defaultGoal>verify</defaultGoal> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <!-- Skip Unit test execution on ITest profile run --> + <skipTests>true</skipTests> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + </plugin> + </plugins> + </build> + </profile> + + <!-- + Profile creating all artifacts: JARs, POMs, Sources, JavaDoc and all signatures. + --> + <profile> + <id>release</id> + <activation> + <property> + <name>release</name> + </property> + </activation> + <build> + <plugins> + <!-- Kdoc / JavaDoc --> + <plugin> + <groupId>org.jetbrains.dokka</groupId> + <artifactId>dokka-maven-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + </plugin> + <!-- Sources for Java and Kotlin --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-source-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + </plugin> + <!-- Sign it all --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-gpg-plugin</artifactId> + </plugin> + <!-- Deploy it all --> + <plugin> + <groupId>org.sonatype.plugins</groupId> + <artifactId>nexus-staging-maven-plugin</artifactId> + </plugin> + </plugins> + </build> + </profile> + + <profile> + <!-- + Profile for building examples + --> + <id>examples</id> + <activation> + <property> + <name>!skipExamples</name> + </property> + </activation> + <modules> + <module>examples</module> + </modules> + </profile> + </profiles> + + <!-- Defaults to make OSS Sonatype happy --> + <name>${project.artifactId}</name> + <description>Avro Registry Adapter</description> + + <licenses> + <license> + <name>The Apache Software License, Version 2.0</name> + <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url> + </license> + </licenses> + + <scm> + <connection>scm:git:git@github.com:holixon/avro-registry-adapter.git</connection> + <url>scm:git:git@github.com:holixon/avro-registry-adapter.git</url> + <developerConnection>scm:git:git@github.com:holixon/avro-registry-adapter.git</developerConnection> + <tag>HEAD</tag> + </scm> + + <distributionManagement> + <site> + <id>README</id> + <url>https://github.com/holixon/avro-registry-adapter</url> + </site> + <snapshotRepository> + <id>ossrh</id> + <url>https://oss.sonatype.org/content/repositories/snapshots</url> + </snapshotRepository> + <repository> + <id>ossrh</id> + <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url> + </repository> + </distributionManagement> + + <developers> + <developer> + <id>jangalinski</id> + <name>Jan Galinski</name> + <roles> + <role>Product Owner</role> + <role>Developer</role> + </roles> + <organization>Holisticon AG</organization> + <organizationUrl>https://holisticon.de</organizationUrl> + </developer> + <developer> + <id>zambrovski</id> + <name>Simon Zambrovski</name> + <roles> + <role>Product Owner</role> + <role>Developer</role> + </roles> + <organization>Holisticon AG</organization> + <organizationUrl>https://holisticon.de</organizationUrl> + </developer> + + </developers> + +</project>