= 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