diff --git a/lang/java/.gitignore b/lang/java/.gitignore index 5a536dfcc82..250e79d5ba7 100644 --- a/lang/java/.gitignore +++ b/lang/java/.gitignore @@ -20,3 +20,6 @@ dependency-reduced-pom.xml mapred/userlogs/ tools/userlogs/ +gradle-plugin/build +gradle-plugin/.kotlin +gradle-plugin/.gradle diff --git a/lang/java/gradle-plugin/README.md b/lang/java/gradle-plugin/README.md new file mode 100644 index 00000000000..eb6607a911b --- /dev/null +++ b/lang/java/gradle-plugin/README.md @@ -0,0 +1,77 @@ +# Avro Gradle plugin (in development) + +Gradle plugin that generates Java code from Avro schemas + +## Version +`0.0.2` + +first beta + +`0.0.5` + +Possible breaking change: rename `CompileSchemaTask` to `CompileAvroSchemaTask` + +Add logical type factories + +Now released on Gradle plugin portal: https://plugins.gradle.org/plugin/eu.eventloopsoftware.avro-gradle-plugin + +`0.0.7` + +It is not needed to add `tasks.named("compileKotlin") { dependsOn(tasks.named("avroGenerateJavaClasses")) }` any more + +`0.0.8` + +Add `sourceZipFiles` property to add zip files with schemas in them +pu + +## Usage + +### Add avro extension +In `build.gradle.kts`: + +### Add plugin + +```kotlin +plugins { + id("eu.eventloopsoftware.avro-gradle-plugin") version "0.0.8" +} +``` +### Add Avro dependency + +```kotlin +implementation("org.apache.avro:avro:1.12.1") +``` + +### Configure Avro Gradle plugin + +```kotlin +avro { + sourceDirectory = "src/main/avro" + // All properties are available in `GradlePluginExtension.kt` +} +``` + +### Generate Java classes + +`./gradlew avroGenerateJavaClasses` + + +## Example project that uses the Apache Avro gradle-plugin +https://codeberg.org/frevib/use-gradle-plugin-test + +## FAQ + +#### How can I benefit from Kotlin's null safety? +Use `createNullSafeAnnotations = true` and Java getters will be annotated with +`@org.jetbrains.annotations.NotNull`/ `@org.jetbrains.annotations.Nullable`. This way +Kotlin will recognize which value is nullable. + +#### I get my Avro schemas from a Maven dependency, how can I add JAR files that contain schemas? +Use `sourceZipFiles = listOf("file_path")`, e.g. + +```kotlin +avro { + sourceZipFiles = listOf("/home/user/.gradle/caches/modules-2/files-2.1/eu.eventloopsoftware.group-id/artifact-id/1.0.0/92ac3d0533de9dd79ac35373c892ebaa01763d4d/jar_with_schemas-1.0.0.jar") +} +``` + diff --git a/lang/java/gradle-plugin/build.gradle.kts b/lang/java/gradle-plugin/build.gradle.kts new file mode 100644 index 00000000000..45ce2a159ca --- /dev/null +++ b/lang/java/gradle-plugin/build.gradle.kts @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + kotlin("jvm") version "2.2.10" + `java-gradle-plugin` + id("com.gradle.plugin-publish") version "2.0.0" +} + +group = "eu.eventloopsoftware" +version = "0.0.9-SNAPSHOT" + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + // TODO: for release use ${version} + implementation("org.apache.avro:avro-compiler:1.12.1") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin-api:2.3.0") + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} + + +gradlePlugin { + plugins { + website = "https://avro.apache.org/" + vcsUrl = "https://github.com/apache/avro.git" + register("gradlePlugin") { + id = "eu.eventloopsoftware.avro-gradle-plugin" + displayName = "Avro Gradle Plugin" + description = "Avro Gradle plugin for generating Java code" + tags = listOf("avro", "kotlin", "java", "apache") + implementationClass = "eu.eventloopsoftware.avro.gradle.plugin.AvroGradlePlugin" + } + } +} diff --git a/lang/java/gradle-plugin/gradle/wrapper/gradle-wrapper.jar b/lang/java/gradle-plugin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..1b33c55baab Binary files /dev/null and b/lang/java/gradle-plugin/gradle/wrapper/gradle-wrapper.jar differ diff --git a/lang/java/gradle-plugin/gradle/wrapper/gradle-wrapper.properties b/lang/java/gradle-plugin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..23449a2b543 --- /dev/null +++ b/lang/java/gradle-plugin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/lang/java/gradle-plugin/gradlew b/lang/java/gradle-plugin/gradlew new file mode 100755 index 00000000000..23d15a93670 --- /dev/null +++ b/lang/java/gradle-plugin/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +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 + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/lang/java/gradle-plugin/gradlew.bat b/lang/java/gradle-plugin/gradlew.bat new file mode 100644 index 00000000000..db3a6ac207e --- /dev/null +++ b/lang/java/gradle-plugin/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lang/java/gradle-plugin/pom.xml b/lang/java/gradle-plugin/pom.xml new file mode 100644 index 00000000000..c2547e3491b --- /dev/null +++ b/lang/java/gradle-plugin/pom.xml @@ -0,0 +1,187 @@ + + + + 4.0.0 + + + avro-parent + org.apache.avro + 1.13.0-SNAPSHOT + ../pom.xml + + + gradle-plugin + pom + + Apache Avro Gradle Plugin + Gradle plugin for Avro IDL and Specific API Compilers + + + ${project.parent.parent.basedir} + 3.3.0 + + true + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + + run-gradle-task-assemble + compile + + exec + + + ./gradlew + + assemble + -i + + + + + run-gradle-task-test + test + + exec + + + ./gradlew + + test + -i + + + + + run-gradle-task-build + package + + exec + + + ./gradlew + + build + -i + + + + + run-gradle-task-publish + deploy + + exec + + + ./gradlew + + publishPlugins + -i + + + + + + + + + + + windows + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + run-gradle-task-assemble + compile + + exec + + + gradlew.bat + + assemble + -i + + + + + run-gradle-task-test + test + + exec + + + gradlew.bat + + test + -i + + + + + run-gradle-task-build + package + + exec + + + gradlew.bat + + build + -i + + + + + run-gradle-task-publish + deploy + + exec + + + gradlew.bat + + publishPlugins + -i + + + + + + + + + + + diff --git a/lang/java/gradle-plugin/settings.gradle.kts b/lang/java/gradle-plugin/settings.gradle.kts new file mode 100644 index 00000000000..d8028b29b2e --- /dev/null +++ b/lang/java/gradle-plugin/settings.gradle.kts @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} +rootProject.name = "avro-gradle-plugin" + + + diff --git a/lang/java/gradle-plugin/src/main/kotlin/eu/eventloopsoftware/avro/gradle/plugin/AvroGradlePlugin.kt b/lang/java/gradle-plugin/src/main/kotlin/eu/eventloopsoftware/avro/gradle/plugin/AvroGradlePlugin.kt new file mode 100644 index 00000000000..6cdf8ecd6cf --- /dev/null +++ b/lang/java/gradle-plugin/src/main/kotlin/eu/eventloopsoftware/avro/gradle/plugin/AvroGradlePlugin.kt @@ -0,0 +1,128 @@ +package eu.eventloopsoftware.avro.gradle.plugin + +import eu.eventloopsoftware.avro.gradle.plugin.extension.AvroGradlePluginExtension +import eu.eventloopsoftware.avro.gradle.plugin.tasks.AbstractCompileTask +import eu.eventloopsoftware.avro.gradle.plugin.tasks.CompileAvroSchemaTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.provider.Property +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmExtension +import java.io.File + +abstract class AvroGradlePlugin : Plugin { + + override fun apply(project: Project) { + project.logger.info("Running Avro Gradle plugin for project: ${project.name}") + + val extension = project.extensions.create("avro", AvroGradlePluginExtension::class.java) + + // Required so that we can get the sourceSets from the java extension below. + project.pluginManager.apply("java") + + val compileAvroSchemaTask = + project.tasks.register("avroGenerateJavaClasses", CompileAvroSchemaTask::class.java) { compileSchemaTask -> + configurePlugin( + compileSchemaTask, + extension, + project, + extension.sourceDirectory, + extension.outputDirectory, + project.configurations.getByName("runtimeClasspath").files + ) + } + + val compileTestAvroSchemaTask = project.tasks.register( + "avroGenerateTestJavaClasses", + CompileAvroSchemaTask::class.java, + ) { compileSchemaTask -> + configurePlugin( + compileSchemaTask, + extension, + project, + extension.testSourceDirectory, + extension.testOutputDirectory, + project.configurations.getByName("testRuntimeClasspath").files + ) + } + + // Add generated code before compilation + project.pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { + addGeneratedSourcesToKotlinProject(project, compileAvroSchemaTask, compileTestAvroSchemaTask) + } + + project.plugins.withType(JavaPlugin::class.java) { + addGeneratedSourcesToJavaProject(project, compileAvroSchemaTask, compileTestAvroSchemaTask) + } + } + + private fun configurePlugin( + compileTask: AbstractCompileTask, + extension: AvroGradlePluginExtension, + project: Project, + sourceDirectory: Property, + outputDirectory: Property, + classPathFiles: Set + ) { + compileTask.outputDirectory.set(project.layout.buildDirectory.dir(outputDirectory)) + compileTask.fieldVisibility.set(extension.fieldVisibility) + compileTask.testExcludes.set(extension.testExcludes) + compileTask.stringType.set(extension.stringType) + compileTask.velocityToolsClassesNames.set(extension.velocityToolsClassesNames.get()) + compileTask.templateDirectory.set(extension.templateDirectory) + compileTask.recordSpecificClass.set(extension.recordSpecificClass) + compileTask.errorSpecificClass.set(extension.errorSpecificClass) + compileTask.createOptionalGetters.set(extension.createOptionalGetters) + compileTask.gettersReturnOptional.set(extension.gettersReturnOptional) + compileTask.createSetters.set(extension.createSetters) + compileTask.createNullSafeAnnotations.set(extension.createNullSafeAnnotations) + compileTask.nullSafeAnnotationNullable.set(extension.nullSafeAnnotationNullable) + compileTask.nullSafeAnnotationNotNull.set(extension.nullSafeAnnotationNotNull) + compileTask.optionalGettersForNullableFieldsOnly.set(extension.optionalGettersForNullableFieldsOnly) + compileTask.customConversions.set(extension.customConversions) + compileTask.customLogicalTypeFactories.set(extension.customLogicalTypeFactories) + compileTask.enableDecimalLogicalType.set(extension.enableDecimalLogicalType) + + + when (compileTask) { + is CompileAvroSchemaTask -> { + compileTask.schemaFiles.from(project.fileTree(sourceDirectory).apply { + setIncludes(listOf("**/*.avsc")) + setExcludes(extension.excludes.get()) + }) + extension.sourceZipFiles.get().forEach { zipPath -> + compileTask.schemaFiles.from( + project.zipTree(zipPath).matching { it.include(setOf("**/*.avsc")) } + ) + } + compileTask.runtimeClassPathFileCollection.from(classPathFiles) + + } + + else -> TODO() + } + + } +} + +private fun addGeneratedSourcesToJavaProject( + project: Project, + compileTask: TaskProvider, + compileTestTask: TaskProvider +) { + val sourceSets = project.extensions.getByType(JavaPluginExtension::class.java).sourceSets + sourceSets.getByName("main").java.srcDir(compileTask.flatMap { it.outputDirectory }) + sourceSets.getByName("test").java.srcDir(compileTestTask.flatMap { it.outputDirectory }) +} + +private fun addGeneratedSourcesToKotlinProject( + project: Project, + compileTask: TaskProvider, + compileTestTask: TaskProvider, +) { + val sourceSets = project.extensions.getByType(KotlinJvmExtension::class.java).sourceSets + sourceSets.getByName("main").kotlin.srcDir(compileTask.flatMap { it.outputDirectory }) + sourceSets.getByName("test").kotlin.srcDir(compileTestTask.flatMap { it.outputDirectory }) +} diff --git a/lang/java/gradle-plugin/src/main/kotlin/eu/eventloopsoftware/avro/gradle/plugin/extension/AvroGradlePluginExtension.kt b/lang/java/gradle-plugin/src/main/kotlin/eu/eventloopsoftware/avro/gradle/plugin/extension/AvroGradlePluginExtension.kt new file mode 100644 index 00000000000..f534e3dc4ae --- /dev/null +++ b/lang/java/gradle-plugin/src/main/kotlin/eu/eventloopsoftware/avro/gradle/plugin/extension/AvroGradlePluginExtension.kt @@ -0,0 +1,238 @@ +package eu.eventloopsoftware.avro.gradle.plugin.extension + +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import javax.inject.Inject + +abstract class AvroGradlePluginExtension @Inject constructor(objects: ObjectFactory) { + + /** + * Schema type: "schema", "idl", "protocol" are valid. Default is "schema" + */ + val schemaType: Property = objects.property(String::class.java).convention("schema") + + /** + * The source directory containing Avro schema files. + *

+ * Defaults to {@code src/main/avro}. + */ + val sourceDirectory: Property = objects.property(String::class.java).convention("src/main/avro") + + /** + * A list of zip files that contain Avro schema files. All generated + * Java classes are added to the classpath. + *

+ * Defaults to {@code emptyList()}. + */ + val sourceZipFiles: ListProperty = objects.listProperty(String::class.java).convention(emptyList()) + + /** + * The output directory for the generated Java code. + */ + val outputDirectory: Property = objects.property(String::class.java).convention("generated-sources-avro") + + + /** + * The output directory for the generated test Java code. + */ + val testSourceDirectory: Property = objects.property(String::class.java).convention("src/test/avro") + + /** + * @parameter property="outputDirectory" + * default-value="${project.layout.buildDirectory}/generated-test-sources/avro" + */ + val testOutputDirectory: Property = + objects.property(String::class.java).convention("generated-test-sources-avro") + + + /** + * The field visibility indicator for the fields of the generated class, as + * string values of SpecificCompiler.FieldVisibility. The text is case + * insensitive. + * + * @parameter default-value="PRIVATE" + */ + val fieldVisibility: Property = objects.property(String::class.java).convention("PRIVATE") + + + /** + * A set of Ant-like inclusion patterns used to select files from the source + * directory for processing. The default pattern is different for Schema, + * Protocol and IDL files. + * + * @parameter + */ + val includes: ListProperty = objects.listProperty(String::class.java).convention(emptyList()) + + /** + * A set of Ant-like exclusion patterns used to prevent certain files from being + * processed. By default, this set is empty such that no files are excluded. + * + * @parameter + */ + val excludes: ListProperty = objects.listProperty(String::class.java).convention(emptyList()) + + /** + * A set of Ant-like exclusion patterns used to prevent certain files from being + * processed. By default, this set is empty such that no files are excluded. + * + * @parameter + */ + val testExcludes: ListProperty = objects.listProperty(String::class.java).convention(emptyList()) + + /** + * The Java type to use for Avro strings. May be one of CharSequence, String or + * Utf8. String by default. + * + * @parameter property="stringType" + */ + val stringType: Property = objects.property(String::class.java).convention("String") + + + /** + * The qualified names of classes which the plugin will look up, instantiate + * (through an empty constructor that must exist) and set up to be injected into + * Velocity templates by Avro compiler. + * + * @parameter property="velocityToolsClassesNames" + */ + val velocityToolsClassesNames: ListProperty = + objects.listProperty(String::class.java).convention(emptyList()) + + + /** + * The directory (within the java classpath) that contains the velocity + * templates to use for code generation. The default value points to the + * templates included with the avro-maven-plugin. + * + * @parameter property="templateDirectory" + */ + val templateDirectory: Property = + objects.property(String::class.java).convention("/org/apache/avro/compiler/specific/templates/java/classic/") + + + /** + * Generated record schema classes will extend this class. + * + * @parameter property="recordSpecificClass" + */ + val recordSpecificClass: Property = + objects.property(String::class.java).convention("org.apache.avro.specific.SpecificRecordBase") + + + /** + * Generated error schema classes will extend this class. + * + * @parameter property="errorSpecificClass" + */ + val errorSpecificClass: Property = + objects.property(String::class.java).convention("org.apache.avro.specific.SpecificExceptionBase") + + + /** + * The createOptionalGetters parameter enables generating the getOptional... + * methods that return an Optional of the requested type. This works ONLY on + * Java 8+ + * + * @parameter property="createOptionalGetters" + */ + val createOptionalGetters: Property = objects.property(Boolean::class.java).convention(false) + + /** + * The gettersReturnOptional parameter enables generating get... methods that + * return an Optional of the requested type. This works ONLY on Java 8+ + * + * @parameter property="gettersReturnOptional" + */ + val gettersReturnOptional: Property = objects.property(Boolean::class.java).convention(false) + + /** + * The optionalGettersForNullableFieldsOnly parameter works in conjunction with + * gettersReturnOptional option. If it is set, Optional getters will be + * generated only for fields that are nullable. If the field is mandatory, + * regular getter will be generated. This works ONLY on Java 8+. + * + * @parameter property="optionalGettersForNullableFieldsOnly" + */ + val optionalGettersForNullableFieldsOnly: Property = + objects.property(Boolean::class.java).convention(false) + + + /** + * Determines whether or not to create setters for the fields of the record. The + * default is to create setters. + * + * @parameter default-value="true" + */ + val createSetters: Property = objects.property(Boolean::class.java).convention(true) + + /** + * If set to true, @Nullable and @NotNull annotations are + * added to fields of the record. The default is false. If enabled, JetBrains + * annotations are used by default but other annotations can be specified via + * the nullSafeAnnotationNullable and nullSafeAnnotationNotNull parameters. + * + * @parameter property="createNullSafeAnnotations" + * + * @see [ + * JetBrains nullability annotations](https://www.jetbrains.com/help/idea/annotating-source-code.html.nullability-annotations) + */ + val createNullSafeAnnotations: Property = objects.property(Boolean::class.java).convention(false) + + /** + * Controls which annotation should be added to nullable fields if + * createNullSafeAnnotations is enabled. The default is + * org.jetbrains.annotations.Nullable. + * + * @parameter property="nullSafeAnnotationNullable" + * + * @see [ + * JetBrains nullability annotations](https://www.jetbrains.com/help/idea/annotating-source-code.html.nullability-annotations) + */ + val nullSafeAnnotationNullable: Property = + objects.property(String::class.java).convention("org.jetbrains.annotations.Nullable") + + /** + * Controls which annotation should be added to non-nullable fields if + * createNullSafeAnnotations is enabled. The default is + * org.jetbrains.annotations.NotNull. + * + * @parameter property="nullSafeAnnotationNotNull" + * + * @see [ + * JetBrains nullability annotations](https://www.jetbrains.com/help/idea/annotating-source-code.html.nullability-annotations) + */ + val nullSafeAnnotationNotNull: Property = + objects.property(String::class.java).convention("org.jetbrains.annotations.NotNull") + + /** + * A set of fully qualified class names of custom + * {@link org.apache.avro.Conversion} implementations to add to the compiler. + * The classes must be on the classpath at compile time and whenever the Java + * objects are serialized. + * + * @parameter property="customConversions" + */ + val customConversions: ListProperty = objects.listProperty(String::class.java).convention(emptyList()) + + /** + * A set of fully qualified class names of custom + * [org.apache.avro.LogicalTypes.LogicalTypeFactory] implementations to + * add to the compiler. The classes must be on the classpath at compile time and + * whenever the Java objects are serialized. + * + * @parameter property="customLogicalTypeFactories" + */ + val customLogicalTypeFactories: ListProperty = + objects.listProperty(String::class.java).convention(emptyList()) + + + /** + * Determines whether or not to use Java classes for decimal types + * + * @parameter default-value="false" + */ + val enableDecimalLogicalType: Property = objects.property(Boolean::class.java).convention(false) + +} diff --git a/lang/java/gradle-plugin/src/main/kotlin/eu/eventloopsoftware/avro/gradle/plugin/tasks/AbstractCompileTask.kt b/lang/java/gradle-plugin/src/main/kotlin/eu/eventloopsoftware/avro/gradle/plugin/tasks/AbstractCompileTask.kt new file mode 100644 index 00000000000..95e9c2489b6 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/kotlin/eu/eventloopsoftware/avro/gradle/plugin/tasks/AbstractCompileTask.kt @@ -0,0 +1,166 @@ +package eu.eventloopsoftware.avro.gradle.plugin.tasks + +import org.apache.avro.LogicalTypes +import org.apache.avro.compiler.specific.SpecificCompiler +import org.apache.avro.compiler.specific.SpecificCompiler.FieldVisibility +import org.apache.avro.generic.GenericData +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import java.io.File +import java.io.IOException +import java.net.URL +import java.net.URLClassLoader + +abstract class AbstractCompileTask : DefaultTask() { + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + @get:Input + abstract val fieldVisibility: Property + + @get:Input + abstract val testExcludes: ListProperty + + @get:Input + abstract val stringType: Property + + @get:Input + abstract val velocityToolsClassesNames: ListProperty + + @get:Input + abstract val templateDirectory: Property + + @get:Input + abstract val recordSpecificClass: Property + + @get:Input + abstract val errorSpecificClass: Property + + @get:Input + abstract val createOptionalGetters: Property + + @get:Input + abstract val gettersReturnOptional: Property + + @get:Input + abstract val optionalGettersForNullableFieldsOnly: Property + + @get:Input + abstract val createSetters: Property + + @get:Input + abstract val createNullSafeAnnotations: Property + + @get:Input + abstract val nullSafeAnnotationNullable: Property + + @get:Input + abstract val nullSafeAnnotationNotNull: Property + + @get:Input + abstract val customConversions: ListProperty + + @get:Input + abstract val customLogicalTypeFactories: ListProperty + + @get:Input + abstract val enableDecimalLogicalType: Property + + @get:InputFiles + @get:Classpath + abstract val runtimeClassPathFileCollection: ConfigurableFileCollection + + protected fun doCompile( + sourceFileForModificationDetection: File?, + compiler: SpecificCompiler, + outputDirectory: File + ) { + setCompilerProperties(compiler) + try { + for (customConversion in customConversions.get()) { + compiler.addCustomConversion(Thread.currentThread().getContextClassLoader().loadClass(customConversion)) + } + } catch (e: ClassNotFoundException) { + throw IOException(e) + } + compiler.compileToDestination(sourceFileForModificationDetection, outputDirectory) + } + + + private fun setCompilerProperties(compiler: SpecificCompiler) { + compiler.setTemplateDir(templateDirectory.get()) + compiler.setStringType(GenericData.StringType.valueOf(stringType.get())) + compiler.setFieldVisibility(getFieldV()) + compiler.setCreateOptionalGetters(createOptionalGetters.get()) + compiler.setGettersReturnOptional(gettersReturnOptional.get()) + compiler.setOptionalGettersForNullableFieldsOnly(optionalGettersForNullableFieldsOnly.get()) + compiler.setCreateSetters(createSetters.get()) + compiler.setCreateNullSafeAnnotations(createNullSafeAnnotations.get()) + compiler.setNullSafeAnnotationNullable(nullSafeAnnotationNullable.get()) + compiler.setNullSafeAnnotationNotNull(nullSafeAnnotationNotNull.get()) + compiler.setEnableDecimalLogicalType(enableDecimalLogicalType.get()) + // TODO: likely not needed +// compiler.setOutputCharacterEncoding(project.getProperties().getProperty("project.build.sourceEncoding")) + compiler.setAdditionalVelocityTools(instantiateAdditionalVelocityTools(velocityToolsClassesNames.get())) + compiler.setRecordSpecificClass(recordSpecificClass.get()) + compiler.setErrorSpecificClass(errorSpecificClass.get()) + } + + private fun getFieldV(): FieldVisibility { + try { + val upperCaseFieldVisibility = fieldVisibility.get().trim().uppercase() + return FieldVisibility.valueOf(upperCaseFieldVisibility) + } catch (_: IllegalArgumentException) { + logger.warn("Could not parse field visibility: ${fieldVisibility.get()}, using PRIVATE") + return FieldVisibility.PRIVATE + } + } + + private fun instantiateAdditionalVelocityTools(velocityToolsClassesNames: List): List { + return velocityToolsClassesNames.map { velocityToolClassName -> + try { + Class.forName(velocityToolClassName) + .getDeclaredConstructor() + .newInstance() + } catch (e: Exception) { + throw RuntimeException(e) + } + } + } + + protected fun loadLogicalTypesFactories() = + createClassLoader().use { classLoader -> + customLogicalTypeFactories.get().forEach { factory -> + try { + @Suppress("UNCHECKED_CAST") + val logicalTypeFactoryClass = + classLoader.loadClass(factory) as Class + val factoryInstance = logicalTypeFactoryClass.getDeclaredConstructor().newInstance() + LogicalTypes.register(factoryInstance) + } catch (e: ClassNotFoundException) { + throw IOException(e) + } catch (e: ReflectiveOperationException) { + throw GradleException("Failed to instantiate logical type factory class: $factory", e) + } + } + } + + private fun createClassLoader(): URLClassLoader { + val urls = classPathFileCollection() + return URLClassLoader(urls.toTypedArray(), Thread.currentThread().contextClassLoader) + } + + private fun classPathFileCollection(): List = + runtimeClassPathFileCollection.files.map { it.toURI().toURL() } + + +} diff --git a/lang/java/gradle-plugin/src/main/kotlin/eu/eventloopsoftware/avro/gradle/plugin/tasks/CompileAvroSchemaTask.kt b/lang/java/gradle-plugin/src/main/kotlin/eu/eventloopsoftware/avro/gradle/plugin/tasks/CompileAvroSchemaTask.kt new file mode 100644 index 00000000000..c2d305031c8 --- /dev/null +++ b/lang/java/gradle-plugin/src/main/kotlin/eu/eventloopsoftware/avro/gradle/plugin/tasks/CompileAvroSchemaTask.kt @@ -0,0 +1,66 @@ +package eu.eventloopsoftware.avro.gradle.plugin.tasks + +import org.apache.avro.SchemaParseException +import org.apache.avro.SchemaParser +import org.apache.avro.compiler.specific.SpecificCompiler +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.io.IOException + +abstract class CompileAvroSchemaTask : AbstractCompileTask() { + + @get:InputFiles + @get:SkipWhenEmpty + abstract val schemaFiles: ConfigurableFileCollection + + + @TaskAction + fun compileSchema() { + logger.info("Generating Java files from ${schemaFiles.files.size} Avro schemas...") + + compileSchemas(schemaFiles, outputDirectory.get().asFile) + + logger.info("Done generating Java files from Avro schemas...") + } + + private fun compileSchemas(schemaFileTree: ConfigurableFileCollection, outputDirectory: File) { + val sourceFileForModificationDetection: File? = + schemaFileTree.asFileTree + .files + .filter { file: File -> file.lastModified() > 0 } + .maxBy { it.lastModified() } + + + // Need to register custom logical type factories before schema compilation. + try { + loadLogicalTypesFactories() + } catch (e: IOException) { + throw RuntimeException("Error while loading logical types factories ", e) + } + + try { + val parser = SchemaParser() + for (sourceFile in schemaFileTree.files) { + parser.parse(sourceFile) + } + val schemas = parser.parsedNamedSchemas + + doCompile(sourceFileForModificationDetection, SpecificCompiler(schemas), outputDirectory) + } catch (ex: IOException) { + // TODO: more concrete exceptions + throw RuntimeException( + "IO ex: Error compiling a file in " + schemaFileTree.asPath + " to " + outputDirectory, + ex + ) + } catch (ex: SchemaParseException) { + throw RuntimeException( + "SchemaParse ex Error compiling a file in " + schemaFileTree.asPath + " to " + outputDirectory, + ex + ) + } + } + +} diff --git a/lang/java/gradle-plugin/src/test/avro/AvdlClasspathImport.avdl b/lang/java/gradle-plugin/src/test/avro/AvdlClasspathImport.avdl new file mode 100644 index 00000000000..81bdb609445 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/avro/AvdlClasspathImport.avdl @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace test; + +import idl "avro/User.avdl"; + +/** Ignored Doc Comment */ +/** IDL User */ +record IdlUserWrapper { + union { null, test.IdlUser } wrapped; +} diff --git a/lang/java/gradle-plugin/src/test/avro/User.avdl b/lang/java/gradle-plugin/src/test/avro/User.avdl new file mode 100644 index 00000000000..98de878d9d0 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/avro/User.avdl @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@namespace("test") +protocol IdlTest { + + enum IdlPrivacy { + Public, Private + } + + record IdlUser { + union { null, string } id; + union { null, long } createdOn; + timestamp_ms modifiedOn; + union { null, IdlPrivacy } privacy; + } + +} diff --git a/lang/java/gradle-plugin/src/test/avro/User.avpr b/lang/java/gradle-plugin/src/test/avro/User.avpr new file mode 100644 index 00000000000..6dd8b9b8900 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/avro/User.avpr @@ -0,0 +1,41 @@ +{ + "protocol" : "ProtocolTest", + "namespace" : "test", + "types" : [ + { + "type" : "enum", + "name" : "ProtocolPrivacy", + "symbols" : [ "Public", "Private"] + }, + { + "type": "record", + "namespace": "test", + "name": "ProtocolUser", + "doc": "User Test Bean", + "fields": [ + { + "name": "id", + "type": ["null", "string"], + "default": null + }, + { + "name": "createdOn", + "type": ["null", "long"], + "default": null + }, + { + "name": "privacy", + "type": ["null", "ProtocolPrivacy"], + "default": null + }, + { + "name": "modifiedOn", + "type": { + "type": "long", + "logicalType": "timestamp-millis" + } + } + ] + } + ] +} diff --git a/lang/java/gradle-plugin/src/test/avro/User.avsc b/lang/java/gradle-plugin/src/test/avro/User.avsc new file mode 100644 index 00000000000..a93e0d13f21 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/avro/User.avsc @@ -0,0 +1,45 @@ +{ + "type": "record", + "namespace": "test", + "name": "SchemaUser", + "doc": "User Test Bean", + "fields": [ + { + "name": "id", + "type": ["null", "string"], + "default": null + }, + { + "name": "createdOn", + "type": ["null", "long"], + "default": null + }, + { + "name": "privacy", + "type": ["null", { + "type": "enum", + "name": "SchemaPrivacy", + "namespace": "test", + "symbols" : ["Public","Private"] + }], + "default": null + }, + { + "name": "privacyImported", + "type": ["null", "test.PrivacyImport"], + "default": null + }, + { + "name": "privacyDirectImport", + "type": ["null", "test.PrivacyDirectImport"], + "default": null + }, + { + "name": "time", + "type": { + "type": "long", + "logicalType": "timestamp-millis" + } + } + ] +} diff --git a/lang/java/gradle-plugin/src/test/avro/directImport/PrivacyDirectImport.avsc b/lang/java/gradle-plugin/src/test/avro/directImport/PrivacyDirectImport.avsc new file mode 100644 index 00000000000..a5b62959206 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/avro/directImport/PrivacyDirectImport.avsc @@ -0,0 +1,7 @@ +{ + "type": "enum", + "namespace": "test", + "name": "PrivacyDirectImport", + "doc": "Privacy Test Enum", + "symbols" : ["Public","Private"] +} diff --git a/lang/java/gradle-plugin/src/test/avro/extends/Custom.avsc b/lang/java/gradle-plugin/src/test/avro/extends/Custom.avsc new file mode 100644 index 00000000000..63056e5d17f --- /dev/null +++ b/lang/java/gradle-plugin/src/test/avro/extends/Custom.avsc @@ -0,0 +1,18 @@ +{ + "type": "record", + "namespace": "test", + "name": "SchemaCustom", + "doc": "Custom Test Bean", + "fields": [ + { + "name": "id", + "type": ["null", "string"], + "default": null + }, + { + "name": "createdOn", + "type": ["null", "long"], + "default": null + } + ] +} diff --git a/lang/java/gradle-plugin/src/test/avro/imports/PrivacyImport.avsc b/lang/java/gradle-plugin/src/test/avro/imports/PrivacyImport.avsc new file mode 100644 index 00000000000..f454f1d3996 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/avro/imports/PrivacyImport.avsc @@ -0,0 +1,7 @@ +{ + "type": "enum", + "namespace": "test", + "name": "PrivacyImport", + "doc": "Privacy Test Enum", + "symbols" : ["Public","Private"] +} diff --git a/lang/java/gradle-plugin/src/test/avro/multipleSchemas/ApplicationEvent.avsc b/lang/java/gradle-plugin/src/test/avro/multipleSchemas/ApplicationEvent.avsc new file mode 100644 index 00000000000..efc7fbf6139 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/avro/multipleSchemas/ApplicationEvent.avsc @@ -0,0 +1,44 @@ +{ + "namespace": "model", + "type": "record", + "doc": "", + "name": "ApplicationEvent", + "fields": [ + { + "name": "applicationId", + "type": "string", + "doc": "Application ID" + }, + { + "name": "status", + "type": "string", + "doc": "Application Status" + }, + { + "name": "documents", + "type": ["null", { + "type": "array", + "items": "model.DocumentInfo" + }], + "doc": "", + "default": null + }, + { + "name": "response", + "type": { + "namespace": "model", + "type": "record", + "doc": "", + "name": "MyResponse", + "fields": [ + { + "name": "isSuccessful", + "type": "boolean", + "doc": "Indicator for successful or unsuccessful call" + } + ] + } + } + ] + +} diff --git a/lang/java/gradle-plugin/src/test/avro/multipleSchemas/DocumentInfo.avsc b/lang/java/gradle-plugin/src/test/avro/multipleSchemas/DocumentInfo.avsc new file mode 100644 index 00000000000..95dd4243ea6 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/avro/multipleSchemas/DocumentInfo.avsc @@ -0,0 +1,19 @@ +{ + "namespace": "model", + "type": "record", + "doc": "", + "name": "DocumentInfo", + "fields": [ + { + "name": "documentId", + "type": "string", + "doc": "Document ID" + }, + { + "name": "filePath", + "type": "string", + "doc": "Document Path" + } + ] + +} diff --git a/lang/java/gradle-plugin/src/test/avro/multipleSchemas/MyResponse.avsc b/lang/java/gradle-plugin/src/test/avro/multipleSchemas/MyResponse.avsc new file mode 100644 index 00000000000..ac6d08291d9 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/avro/multipleSchemas/MyResponse.avsc @@ -0,0 +1,14 @@ +{ + "namespace": "model", + "type": "record", + "doc": "", + "name": "MyResponse", + "fields": [ + { + "name": "isSuccessful", + "type": "boolean", + "doc": "Indicator for successful or unsuccessful call" + } + ] + +} diff --git a/lang/java/gradle-plugin/src/test/kotlin/eu/eventloopsoftware/avro/gradle/plugin/SchemaCompileTaskTest.kt b/lang/java/gradle-plugin/src/test/kotlin/eu/eventloopsoftware/avro/gradle/plugin/SchemaCompileTaskTest.kt new file mode 100644 index 00000000000..a415ea8f96c --- /dev/null +++ b/lang/java/gradle-plugin/src/test/kotlin/eu/eventloopsoftware/avro/gradle/plugin/SchemaCompileTaskTest.kt @@ -0,0 +1,270 @@ +package eu.eventloopsoftware.avro.gradle.plugin + +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import kotlin.io.path.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@ExperimentalPathApi +class SchemaCompileTaskTest { + + @TempDir + lateinit var tempDir: Path + + @Test + fun `plugin executes avroGenerateJavaClasses task successfully`() { + // given + val tempSettingsFile = tempDir.resolve("settings.gradle.kts") + val tempBuildFile = tempDir.resolve("build.gradle.kts") + val tempAvroSrcDir = tempDir.resolve("src/test/avro").createDirectories() + + val testAvroFiles = Path.of("src/test/avro") + val testAvroOutPutDir = Path.of("generated-sources/avro") + + val testOutPutDirectory = tempDir.resolve("build/$testAvroOutPutDir/test") + + testAvroFiles.copyToRecursively( + tempAvroSrcDir, + overwrite = true, + followLinks = false + ) + + tempSettingsFile.writeText("") + tempBuildFile.writeText( + """ + plugins { + id("eu.eventloopsoftware.avro-gradle-plugin") + } + + avro { + schemaType = "schema" + sourceDirectory = "$testAvroFiles" + outputDirectory = "$testAvroOutPutDir" + } + """.trimIndent() + ) + + // when + val result = GradleRunner.create() + .withProjectDir(tempDir.toFile()) + .withArguments("avroGenerateJavaClasses") + .withPluginClasspath() + .forwardOutput() // to see printLn in code + .build() + + val expectedFiles = setOf( + "SchemaPrivacy.java", + "SchemaUser.java", + "PrivacyImport.java", + "SchemaCustom.java", + "PrivacyDirectImport.java" + ) + + // then + assertEquals(TaskOutcome.SUCCESS, result.task(":avroGenerateJavaClasses")?.outcome) + assertFilesExist(testOutPutDirectory, expectedFiles) + + val schemaUserContent = testOutPutDirectory.resolve("SchemaUser.java").readText() + assertTrue(schemaUserContent.contains("java.time.Instant")) + } + + + @Test + fun `plugin executes avroGenerateTestJavaClasses task successfully - for files in test directory`() { + // given + val tempSettingsFile = tempDir.resolve("settings.gradle.kts") + val tempBuildFile = tempDir.resolve("build.gradle.kts") + val tempAvroSrcDir = tempDir.resolve("src/test/avro").createDirectories() + + val testAvroFiles = Path.of("src/test/avro") + val testAvroOutPutDir = Path.of("generated-test-sources-avro") + + val testOutPutDirectory = tempDir.resolve("build/$testAvroOutPutDir/test") + + testAvroFiles.copyToRecursively( + tempAvroSrcDir, + overwrite = true, + followLinks = false + ) + + tempSettingsFile.writeText("") + tempBuildFile.writeText( + """ + plugins { + id("eu.eventloopsoftware.avro-gradle-plugin") + } + + avro { + schemaType = "schema" + testSourceDirectory = "$testAvroFiles" + testOutputDirectory = "$testAvroOutPutDir" + } + """.trimIndent() + ) + + // when + val result = GradleRunner.create() + .withProjectDir(tempDir.toFile()) + .withArguments("avroGenerateTestJavaClasses") + .withPluginClasspath() + .forwardOutput() // to see printLn in code + .build() + + val expectedFiles = setOf( + "SchemaPrivacy.java", + "SchemaUser.java", + "PrivacyImport.java", + "SchemaCustom.java", + "PrivacyDirectImport.java" + ) + + // then + assertEquals(TaskOutcome.SUCCESS, result.task(":avroGenerateTestJavaClasses")?.outcome) + assertFilesExist(testOutPutDirectory, expectedFiles) + + val schemaUserContent = testOutPutDirectory.resolve("SchemaUser.java").readText() + assertTrue(schemaUserContent.contains("java.time.Instant")) + } + + @Test + fun `plugin executes avroGenerateJavaClasses task successfully - with Velocity class names`() { + // given + val tempSettingsFile = tempDir.resolve("settings.gradle.kts") + val tempBuildFile = tempDir.resolve("build.gradle.kts") + val tempAvroSrcDir = tempDir.resolve("src/test/avro").createDirectories() + val tempVelocityToolClassesDir = tempDir.resolve("src/test/resources/templates").createDirectories() + + val testAvroFilesDir = Path.of("src/test/avro") + val testAvroOutPutDir = Path.of("generated-sources-avro") + val testVelocityToolClassesDir = Path.of("src/test/resources/templates") + + val testOutPutDirectory = tempDir.resolve("build/$testAvroOutPutDir/test") + + testAvroFilesDir.copyToRecursively( + tempAvroSrcDir, + overwrite = true, + followLinks = false + ) + + testVelocityToolClassesDir.copyToRecursively( + tempVelocityToolClassesDir, + overwrite = true, + followLinks = false + ) + + tempSettingsFile.writeText("") + tempBuildFile.writeText( + """ + plugins { + id("eu.eventloopsoftware.avro-gradle-plugin") + } + + avro { + schemaType = "schema" + sourceDirectory = "$testAvroFilesDir" + outputDirectory = "$testAvroOutPutDir" + templateDirectory = "${tempDir.resolve(testVelocityToolClassesDir).toString() + "/"}" + velocityToolsClassesNames = listOf("java.lang.String") + } + """.trimIndent() + ) + + // when + val result = GradleRunner.create() + .withProjectDir(tempDir.toFile()) + .withArguments("avroGenerateJavaClasses") + .withPluginClasspath() + .forwardOutput() // to see printLn in code + .build() + + val expectedFiles = setOf( + "SchemaPrivacy.java", + "SchemaUser.java", + "PrivacyImport.java", + "SchemaCustom.java", + "PrivacyDirectImport.java" + ) + + // then + assertEquals(TaskOutcome.SUCCESS, result.task(":avroGenerateJavaClasses")?.outcome) + assertFilesExist(testOutPutDirectory, expectedFiles) + + val schemaUserContent = testOutPutDirectory.resolve("SchemaUser.java").readText() + assertTrue(schemaUserContent.contains("It works!")) + } + + @Test + fun `plugin executes avroGenerateJavaClasses task successfully - custom recordSpecificClass`() { + // given + val tempSettingsFile = tempDir.resolve("settings.gradle.kts") + val tempBuildFile = tempDir.resolve("build.gradle.kts") + val tempAvroSrcDir = tempDir.resolve("src/test/avro").createDirectories() + + val testAvroFiles = Path.of("src/test/avro") + val testAvroOutPutDir = Path.of("generated-sources/avro") + + val testOutPutDirectory = tempDir.resolve("build/$testAvroOutPutDir/test") + + testAvroFiles.copyToRecursively( + tempAvroSrcDir, + overwrite = true, + followLinks = false + ) + + tempSettingsFile.writeText("") + tempBuildFile.writeText( + """ + plugins { + id("eu.eventloopsoftware.avro-gradle-plugin") + } + + avro { + schemaType = "schema" + sourceDirectory = "$testAvroFiles" + outputDirectory = "$testAvroOutPutDir" + recordSpecificClass = "org.apache.avro.custom.CustomRecordBase" + } + """.trimIndent() + ) + + // when + val result = GradleRunner.create() + .withProjectDir(tempDir.toFile()) + .withArguments("avroGenerateJavaClasses") + .withPluginClasspath() + .forwardOutput() // to see printLn in code + .build() + + // then + assertEquals(TaskOutcome.SUCCESS, result.task(":avroGenerateJavaClasses")?.outcome) + + val outPutFile = testOutPutDirectory.resolve("SchemaCustom.java") + assertTrue(outPutFile.toFile().exists()) + + val extendsLines = outPutFile.readLines() + .filter { line -> line.contains("class SchemaCustom extends ") } + assertEquals(1, extendsLines.size) + + val extendLine = extendsLines[0] + assertTrue(extendLine.contains(" org.apache.avro.custom.CustomRecordBase ")) + assertFalse(extendLine.contains("org.apache.avro.specific.SpecificRecordBase")) + } + + + private fun assertFilesExist(directory: Path, expectedFiles: Set) { + assertTrue(directory.exists(), "Directory $directory does not exist") + assertTrue(expectedFiles.isNotEmpty()) + + val filesInDirectory: Set = directory + .listDirectoryEntries() + .map { it.fileName.toString() }.toSet() + + assertEquals(expectedFiles, filesInDirectory) + } + +} diff --git a/lang/java/gradle-plugin/src/test/resources/templates/enum.vm b/lang/java/gradle-plugin/src/test/resources/templates/enum.vm new file mode 100644 index 00000000000..fbb32a9583a --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/templates/enum.vm @@ -0,0 +1,19 @@ +## +## Licensed to the Apache Software Foundation (ASF) under one +## or more contributor license agreements. See the NOTICE file +## distributed with this work for additional information +## regarding copyright ownership. The ASF licenses this file +## to you under the Apache License, Version 2.0 (the +## "License"); you may not use this file except in compliance +## with the License. You may obtain a copy of the License at +## +## https://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +## No need to place anything here by now. File must exist, though. diff --git a/lang/java/gradle-plugin/src/test/resources/templates/protocol.vm b/lang/java/gradle-plugin/src/test/resources/templates/protocol.vm new file mode 100644 index 00000000000..fbb32a9583a --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/templates/protocol.vm @@ -0,0 +1,19 @@ +## +## Licensed to the Apache Software Foundation (ASF) under one +## or more contributor license agreements. See the NOTICE file +## distributed with this work for additional information +## regarding copyright ownership. The ASF licenses this file +## to you under the Apache License, Version 2.0 (the +## "License"); you may not use this file except in compliance +## with the License. You may obtain a copy of the License at +## +## https://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +## No need to place anything here by now. File must exist, though. diff --git a/lang/java/gradle-plugin/src/test/resources/templates/record.vm b/lang/java/gradle-plugin/src/test/resources/templates/record.vm new file mode 100644 index 00000000000..859bc062968 --- /dev/null +++ b/lang/java/gradle-plugin/src/test/resources/templates/record.vm @@ -0,0 +1,21 @@ +## +## Licensed to the Apache Software Foundation (ASF) under one +## or more contributor license agreements. See the NOTICE file +## distributed with this work for additional information +## regarding copyright ownership. The ASF licenses this file +## to you under the Apache License, Version 2.0 (the +## "License"); you may not use this file except in compliance +## with the License. You may obtain a copy of the License at +## +## https://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +/** + * $string.concat("It works!") + */ diff --git a/lang/java/pom.xml b/lang/java/pom.xml index 87f3c200fed..3f99c6d98a9 100644 --- a/lang/java/pom.xml +++ b/lang/java/pom.xml @@ -78,6 +78,7 @@ idl compiler maven-plugin + gradle-plugin ipc ipc-jetty ipc-netty diff --git a/pom.xml b/pom.xml index 8b64416d391..9e902c54691 100644 --- a/pom.xml +++ b/pom.xml @@ -524,6 +524,9 @@ lang/py/userlogs/** lang/py/docs/build/** lang/ruby/Manifest + **/.gradle/** + lang/java/gradle-plugin/gradle/** + lang/java/gradle-plugin/build/** CHANGES.txt DIST_README.txt