properties;
+
+ /**
+ * A list of files or directories that should be compiled first thus making them
+ * importable by subsequently compiled schemas. Note that imported files should
+ * not reference each other.
+ *
+ * All paths should be relative to the src/[main|test]/avro directory
+ *
+ * Passed as a comma-separated list.
+ */
+ final String[] imports;
+
+ /**
+ * The Java type to use for Avro strings. May be one of CharSequence, String or
+ * Utf8. CharSequence by default.
+ */
+ final GenericData.StringType stringType;
+
+ /**
+ * The createOptionalGetters parameter enables generating the getOptional...
+ * methods that return an Optional of the requested type. This works ONLY on
+ * Java 8+
+ */
+ final boolean createOptionalGetters;
+
+ /**
+ * Determines whether or not to use Java classes for decimal types, defaults to false
+ */
+ final boolean enableDecimalLogicalType;
+
+ /**
+ * Determines whether or not to create setters for the fields of the record. The
+ * default is to create setters.
+ */
+ final boolean createSetters;
+
+ /**
+ * The gettersReturnOptional parameter enables generating get... methods that
+ * return an Optional of the requested type. This will replace the This works
+ * ONLY on Java 8+
+ */
+ final boolean gettersReturnOptional;
+
+ /**
+ * 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+.
+ */
+ final boolean optionalGettersForNullableFieldsOnly;
+
+ AvroOptions(Map properties, String specificPropertyKey) {
+ this.properties = properties;
+ String imports = prop("avro.codegen." + specificPropertyKey + ".imports", "");
+ this.imports = "".equals(imports) ? EMPTY : imports.split(",");
+
+ stringType = GenericData.StringType.valueOf(prop("avro.codegen.stringType", "String"));
+ createOptionalGetters = getBooleanProperty("avro.codegen.createOptionalGetters", false);
+ enableDecimalLogicalType = getBooleanProperty("avro.codegen.enableDecimalLogicalType", false);
+ createSetters = getBooleanProperty("avro.codegen.createSetters", true);
+ gettersReturnOptional = getBooleanProperty("avro.codegen.gettersReturnOptional", false);
+ optionalGettersForNullableFieldsOnly = getBooleanProperty("avro.codegen.optionalGettersForNullableFieldsOnly",
+ false);
+ }
+
+ private String prop(String propName, String defaultValue) {
+ return properties.getOrDefault(propName, defaultValue);
+ }
+
+ private boolean getBooleanProperty(String propName, boolean defaultValue) {
+ String value = prop(propName, String.valueOf(defaultValue)).toLowerCase(Locale.ROOT);
+ if (Boolean.FALSE.toString().equals(value)) {
+ return false;
+ }
+ if (Boolean.TRUE.toString().equals(value)) {
+ return true;
+ }
+ return defaultValue;
+ }
+ }
+}
diff --git a/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroProtocolCodeGenProvider.java b/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroProtocolCodeGenProvider.java
new file mode 100644
index 0000000000000..fec0652e58af3
--- /dev/null
+++ b/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroProtocolCodeGenProvider.java
@@ -0,0 +1,54 @@
+package io.quarkus.avro.deployment;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import org.apache.avro.Protocol;
+import org.apache.avro.compiler.specific.SpecificCompiler;
+
+import io.quarkus.bootstrap.prebuild.CodeGenException;
+import io.quarkus.deployment.CodeGenProvider;
+
+/**
+ * Avro code generator for Avro Protocol, based on the avro-maven-plugin
+ *
+ * @see AvroCodeGenProviderBase
+ */
+public class AvroProtocolCodeGenProvider extends AvroCodeGenProviderBase implements CodeGenProvider {
+
+ @Override
+ public String providerId() {
+ return "avpr";
+ }
+
+ @Override
+ public String inputExtension() {
+ return "avpr";
+ }
+
+ @Override
+ void init() {
+ }
+
+ void compileSingleFile(Path filePath,
+ Path outputDirectory,
+ AvroOptions options) throws CodeGenException {
+ try {
+ final Protocol protocol = Protocol.parse(filePath.toFile());
+ final SpecificCompiler compiler = new SpecificCompiler(protocol);
+ compiler.setTemplateDir(templateDirectory);
+ compiler.setStringType(options.stringType);
+ compiler.setFieldVisibility(SpecificCompiler.FieldVisibility.PRIVATE);
+ compiler.setCreateOptionalGetters(options.createOptionalGetters);
+ compiler.setGettersReturnOptional(options.gettersReturnOptional);
+ compiler.setOptionalGettersForNullableFieldsOnly(options.optionalGettersForNullableFieldsOnly);
+ compiler.setCreateSetters(options.createSetters);
+ compiler.setEnableDecimalLogicalType(options.enableDecimalLogicalType);
+
+ compiler.setOutputCharacterEncoding("UTF-8");
+ compiler.compileToDestination(filePath.toFile(), outputDirectory.toFile());
+ } catch (IOException e) {
+ new CodeGenException("Failed to compile avro protocole file: " + filePath.toString() + " to Java", e);
+ }
+ }
+}
diff --git a/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroSchemaCodeGenProvider.java b/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroSchemaCodeGenProvider.java
new file mode 100644
index 0000000000000..a274ca77a7fdb
--- /dev/null
+++ b/extensions/avro/deployment/src/main/java/io/quarkus/avro/deployment/AvroSchemaCodeGenProvider.java
@@ -0,0 +1,75 @@
+package io.quarkus.avro.deployment;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+
+import org.apache.avro.Schema;
+import org.apache.avro.compiler.specific.SpecificCompiler;
+
+import io.quarkus.bootstrap.prebuild.CodeGenException;
+import io.quarkus.deployment.CodeGenProvider;
+
+/**
+ * Avro code generator for Avro Schema, based on the avro-maven-plugin
+ *
+ * @see AvroCodeGenProviderBase
+ */
+public class AvroSchemaCodeGenProvider extends AvroCodeGenProviderBase implements CodeGenProvider {
+
+ Schema.Parser schemaParser;
+
+ @Override
+ public String providerId() {
+ return "avsc";
+ }
+
+ @Override
+ public String inputExtension() {
+ return "avsc";
+ }
+
+ void init() {
+ schemaParser = new Schema.Parser();
+ }
+
+ @Override
+ void compileSingleFile(Path filePath,
+ Path outputDirectory,
+ AvroOptions options) throws CodeGenException {
+ final Schema schema;
+
+ File file = filePath.toFile();
+
+ // This is necessary to maintain backward-compatibility. If there are
+ // no imported files then isolate the schemas from each other, otherwise
+ // allow them to share a single schema so reuse and sharing of schema
+ // is possible.
+ try {
+ if (options.imports == null) {
+ schema = new Schema.Parser().parse(file);
+ } else {
+ schema = schemaParser.parse(file);
+ }
+ } catch (IOException e) {
+ throw new CodeGenException("", e);
+ }
+
+ final SpecificCompiler compiler = new SpecificCompiler(schema);
+ compiler.setTemplateDir(templateDirectory);
+ compiler.setStringType(options.stringType);
+ compiler.setFieldVisibility(SpecificCompiler.FieldVisibility.PRIVATE);
+ compiler.setCreateOptionalGetters(options.createOptionalGetters);
+ compiler.setGettersReturnOptional(options.gettersReturnOptional);
+ compiler.setOptionalGettersForNullableFieldsOnly(options.optionalGettersForNullableFieldsOnly);
+ compiler.setCreateSetters(options.createSetters);
+ compiler.setEnableDecimalLogicalType(options.enableDecimalLogicalType);
+ compiler.setOutputCharacterEncoding("UTF-8");
+ try {
+ compiler.compileToDestination(file, outputDirectory.toFile());
+ } catch (IOException e) {
+ throw new CodeGenException("Failed to copy compiled files to output directory " +
+ outputDirectory.toAbsolutePath(), e);
+ }
+ }
+}
diff --git a/extensions/avro/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.CodeGenProvider b/extensions/avro/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.CodeGenProvider
new file mode 100644
index 0000000000000..d27d31cd4e5f8
--- /dev/null
+++ b/extensions/avro/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.CodeGenProvider
@@ -0,0 +1,2 @@
+io.quarkus.avro.deployment.AvroSchemaCodeGenProvider
+io.quarkus.avro.deployment.AvroProtocolCodeGenProvider
\ No newline at end of file
diff --git a/integration-tests/avro-reload/pom.xml b/integration-tests/avro-reload/pom.xml
new file mode 100644
index 0000000000000..ab2490f872f22
--- /dev/null
+++ b/integration-tests/avro-reload/pom.xml
@@ -0,0 +1,81 @@
+
+
+ 4.0.0
+
+
+ io.quarkus
+ quarkus-integration-tests-parent
+ 999-SNAPSHOT
+
+
+ quarkus-avro-reload-test
+ Quarkus - Integration Test - Avro code generation and reload
+
+
+ imports,directImport/PrivacyDirectImport.avsc
+
+
+
+
+ io.quarkus
+ quarkus-avro
+
+
+ io.quarkus
+ quarkus-resteasy-reactive-deployment
+
+
+ io.quarkus
+ quarkus-junit5-internal
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+ io.quarkus
+ quarkus-avro-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
+
+
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+
+
+
+ generate-code
+
+ generate-code
+ generate-code-tests
+ build
+
+
+
+
+
+
+
+
diff --git a/integration-tests/avro-reload/src/test/avro/User.avpr b/integration-tests/avro-reload/src/test/avro/User.avpr
new file mode 100644
index 0000000000000..6dd8b9b890031
--- /dev/null
+++ b/integration-tests/avro-reload/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/integration-tests/avro-reload/src/test/avro/User.avsc b/integration-tests/avro-reload/src/test/avro/User.avsc
new file mode 100644
index 0000000000000..11c4aa7d51ab8
--- /dev/null
+++ b/integration-tests/avro-reload/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/integration-tests/avro-reload/src/test/avro/directImport/PrivacyDirectImport.avsc b/integration-tests/avro-reload/src/test/avro/directImport/PrivacyDirectImport.avsc
new file mode 100644
index 0000000000000..a5b629592068a
--- /dev/null
+++ b/integration-tests/avro-reload/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/integration-tests/avro-reload/src/test/avro/imports/PrivacyImport.avsc b/integration-tests/avro-reload/src/test/avro/imports/PrivacyImport.avsc
new file mode 100644
index 0000000000000..d697a35c47e67
--- /dev/null
+++ b/integration-tests/avro-reload/src/test/avro/imports/PrivacyImport.avsc
@@ -0,0 +1,8 @@
+/* copied from avro-maven-plugin, licensed under Apache Software License */
+{
+ "type": "enum",
+ "namespace": "test",
+ "name": "PrivacyImport",
+ "doc": "Privacy Test Enum",
+ "symbols" : ["Public","Private"]
+}
diff --git a/integration-tests/avro-reload/src/test/java/io/quarkus/avro/deployment/AvroCodeReloadTest.java b/integration-tests/avro-reload/src/test/java/io/quarkus/avro/deployment/AvroCodeReloadTest.java
new file mode 100644
index 0000000000000..c4dcc9a90c4df
--- /dev/null
+++ b/integration-tests/avro-reload/src/test/java/io/quarkus/avro/deployment/AvroCodeReloadTest.java
@@ -0,0 +1,46 @@
+package io.quarkus.avro.deployment;
+
+import static io.restassured.RestAssured.when;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.regex.Pattern;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusDevModeTest;
+
+public class AvroCodeReloadTest {
+ @RegisterExtension
+ public static final QuarkusDevModeTest test = new QuarkusDevModeTest()
+ .setArchiveProducer(
+ () -> ShrinkWrap.create(JavaArchive.class)
+ .addClasses(AvroReloadResource.class))
+ .setCodeGenSources("avro")
+ .setBuildSystemProperty("avro.codegen.avsc.imports", "imports");
+
+ @Test
+ void shouldAlterSchema() throws InterruptedException {
+ assertThat(when().get().body().print().split(",")).containsExactlyInAnyOrder("Public", "Private");
+
+ test.modifyFile("avro/imports/PrivacyImport.avsc",
+ text -> text.replaceAll(Pattern.quote("\"symbols\" : [\"Public\",\"Private\"]"),
+ "\"symbols\" : [\"Public\",\"Private\",\"Default\"]"));
+ Thread.sleep(5000); // to wait for eager reload for code gen sources to happen
+ assertThat(when().get().body().print().split(",")).containsExactlyInAnyOrder("Public", "Private", "Default");
+ }
+
+ @Test
+ void shouldAlterProtocol() throws InterruptedException {
+ assertThat(when().get("/protocol").body().print().split(",")).containsExactlyInAnyOrder("Public", "Private");
+
+ test.modifyFile("avro/User.avpr",
+ text -> text.replaceAll(Pattern.quote("\"symbols\" : [ \"Public\", \"Private\"]"),
+ "\"symbols\" : [ \"Public\", \"Private\", \"Default\"]"));
+ Thread.sleep(5000); // to wait for eager reload for code gen sources to happen
+ assertThat(when().get("/protocol").body().print().split(",")).containsExactlyInAnyOrder("Public", "Private", "Default");
+ }
+
+}
diff --git a/integration-tests/avro-reload/src/test/java/io/quarkus/avro/deployment/AvroReloadResource.java b/integration-tests/avro-reload/src/test/java/io/quarkus/avro/deployment/AvroReloadResource.java
new file mode 100644
index 0000000000000..f93e7744b0ef1
--- /dev/null
+++ b/integration-tests/avro-reload/src/test/java/io/quarkus/avro/deployment/AvroReloadResource.java
@@ -0,0 +1,26 @@
+package io.quarkus.avro.deployment;
+
+import static java.util.stream.Collectors.joining;
+
+import java.util.Arrays;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+
+import test.PrivacyImport;
+import test.ProtocolPrivacy;
+
+@Path("/")
+public class AvroReloadResource {
+ @GET
+ public String getAvailablePrivacyImports() {
+ return Arrays.stream(PrivacyImport.values()).map(String::valueOf).collect(joining(","));
+ }
+
+ @GET
+ @Path("/protocol")
+ public String getAvailableProtocolPrivacies() {
+ return Arrays.stream(ProtocolPrivacy.values()).map(String::valueOf).collect(joining(","));
+ }
+
+}
diff --git a/integration-tests/kafka-avro/pom.xml b/integration-tests/kafka-avro/pom.xml
index b47bbccf3f8b2..f96cd5599ce4f 100644
--- a/integration-tests/kafka-avro/pom.xml
+++ b/integration-tests/kafka-avro/pom.xml
@@ -250,30 +250,13 @@
-
- org.apache.avro
- avro-maven-plugin
- 1.10.2
-
-
- generate-sources
-
- schema
-
-
- src/main/avro
- ${project.build.directory}/generated-sources
- String
-
-
-
-
io.quarkus
quarkus-maven-plugin
+ generate-code
build
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index 952a973d1532c..c932b1d249dd7 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -23,6 +23,7 @@
true
+ avro-reload
bouncycastle
bouncycastle-fips
bouncycastle-fips-jsse
diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusDevModeTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusDevModeTest.java
index bb291e63bd2d7..9af9784a2acfe 100644
--- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusDevModeTest.java
+++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusDevModeTest.java
@@ -16,7 +16,9 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.ServiceLoader;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
@@ -98,6 +100,8 @@ public class QuarkusDevModeTest
private Path testLocation;
private String[] commandLineArgs = new String[0];
+ private final Map buildSystemProperties = new HashMap<>();
+
private static final List compilationProviders;
static {
@@ -136,6 +140,11 @@ public List getLogRecords() {
return inMemoryLogHandler.records;
}
+ public QuarkusDevModeTest setBuildSystemProperty(String name, String value) {
+ buildSystemProperties.put(name, value);
+ return this;
+ }
+
public Object createTestInstance(TestInstanceFactoryContext factoryContext, ExtensionContext extensionContext)
throws TestInstantiationException {
try {
@@ -202,6 +211,7 @@ public void close() throws Throwable {
context.setTest(true);
context.setAbortOnFailedStart(true);
context.getBuildSystemProperties().put("quarkus.banner.enabled", "false");
+ context.getBuildSystemProperties().putAll(buildSystemProperties);
devModeMain = new DevModeMain(context);
devModeMain.start();
ApplicationStateNotification.waitForApplicationStart();