Skip to content

Commit

Permalink
Avro code generation with live reload
Browse files Browse the repository at this point in the history
#fixes 15567
  • Loading branch information
michalszynkiewicz committed Apr 1, 2021
1 parent 660a652 commit 64c0b30
Show file tree
Hide file tree
Showing 17 changed files with 586 additions and 19 deletions.
5 changes: 5 additions & 0 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3414,6 +3414,11 @@
<artifactId>avro</artifactId>
<version>${avro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro-compiler</artifactId>
<version>${avro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public static List<CodeGenData> init(ClassLoader deploymentClassLoader,
codeGenProviderClass = (Class<? extends CodeGenProvider>) deploymentClassLoader
.loadClass(CodeGenProvider.class.getName());
} catch (ClassNotFoundException e) {
throw new CodeGenException("Failde to load CodeGenProvider class from deployment classloader", e);
throw new CodeGenException("Failed to load CodeGenProvider class from deployment classloader", e);
}
for (CodeGenProvider provider : ServiceLoader.load(codeGenProviderClass, deploymentClassLoader)) {
Path outputDir = codeGenOutDir(generatedSourcesDir, provider, sourceRegistrar);
Expand Down
9 changes: 9 additions & 0 deletions extensions/avro/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,20 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-avro</artifactId>
</dependency>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro-compiler</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-deployment</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package io.quarkus.avro.deployment;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.avro.generic.GenericData;
import org.jboss.logging.Logger;

import io.quarkus.bootstrap.prebuild.CodeGenException;
import io.quarkus.deployment.CodeGenContext;
import io.quarkus.deployment.CodeGenProvider;

public abstract class AvroCodeGenProviderBase implements CodeGenProvider {

private static final Logger log = Logger.getLogger(AvroCodeGenProviderBase.class);

/**
* The directory (within the java classpath) that contains the velocity
* templates to use for code generation.
*/
static final String templateDirectory = "/org/apache/avro/compiler/specific/templates/java/classic/";

@Override
public String inputDirectory() {
return "avro";
}

@Override
public boolean trigger(CodeGenContext context) throws CodeGenException {
init();
boolean filesGenerated = false;

AvroOptions options = new AvroOptions(context.properties(), inputExtension());
Path input = context.inputDir();
Path outputDir = context.outDir();

Set<Path> importedPaths = new HashSet<>();

// compile the imports first
for (String imprt : options.imports) {
Path importPath = Paths.get(input.toAbsolutePath().toString(), imprt).toAbsolutePath();
if (Files.isDirectory(importPath)) {
for (Path file : gatherAllFiles(importPath)) {
compileSingleFile(file, outputDir, options);
importedPaths.add(file);
filesGenerated = true;
}
} else {
compileSingleFile(importPath, outputDir, options);
importedPaths.add(importPath);
filesGenerated = true;
}
}

// compile the rest of the files
for (Path file : gatherAllFiles(input)) {
if (!importedPaths.contains(file)) {
compileSingleFile(file, outputDir, options);
filesGenerated = true;
}
}

return filesGenerated;
}

abstract void init();

private Collection<Path> gatherAllFiles(Path importPath) throws CodeGenException {
try {
return Files.find(importPath, 20,
(path, ignored) -> Files.isRegularFile(path) && path.toString().endsWith("." + inputExtension()))
.map(Path::toAbsolutePath)
.collect(Collectors.toList());
} catch (IOException e) {
throw new CodeGenException("Failed to list matching files in " + importPath, e);
}
}

abstract void compileSingleFile(Path importPath, Path outputDir, AvroOptions options) throws CodeGenException;

public static class AvroOptions {

public static final String[] EMPTY = new String[0];

private final Map<String, String> 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.
* <p>
* All paths should be relative to the src/[main|test]/avro directory
* <p>
* 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<String, String> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
io.quarkus.avro.deployment.AvroSchemaCodeGenProvider
io.quarkus.avro.deployment.AvroProtocolCodeGenProvider
Loading

0 comments on commit 64c0b30

Please sign in to comment.