annotations = new LinkedHashSet<>();
- annotations.add(ENTITY);
- annotations.add(EMBEDDABLE);
- annotations.add(CONVERTER);
- annotations.add(EBEAN_COMPONENT);
- annotations.add(MODULEINFO);
- return annotations;
+ ProcessingContext.init(processingEnv);
}
@Override
@@ -45,27 +50,33 @@ public SourceVersion getSupportedSourceVersion() {
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
- processingContext.readModuleInfo();
+ APContext.setProjectModuleElement(annotations, roundEnv);
+ ProcessingContext.readModuleInfo();
int count = processEntities(roundEnv);
processOthers(roundEnv);
- final int loaded = processingContext.complete();
+ final int loaded = ProcessingContext.complete();
+ initModuleInfoBean();
+ if (count > 0) {
+ String msg =
+ "Ebean APT generated %s query beans, loaded %s others - META-INF/ebean-generated-info.mf entity-packages: %s";
+ logNote(msg, count, loaded, ProcessingContext.getAllEntityPackages());
+ }
if (roundEnv.processingOver()) {
writeModuleInfoBean();
- }
- if (count > 0) {
- String msg = "Ebean APT generated %s query beans, loaded %s others - META-INF/ebean-generated-info.mf entity-packages: %s";
- processingContext.logNote(msg, count, loaded, processingContext.getAllEntityPackages());
+ ProcessingContext.validateModule();
+ ProcessingContext.clear();
}
return true;
}
private int processEntities(RoundEnvironment roundEnv) {
int count = 0;
- for (Element element : roundEnv.getElementsAnnotatedWith(processingContext.embeddableAnnotation())) {
+
+ for (Element element : getElements(roundEnv, EmbeddablePrism.PRISM_TYPE)) {
generateQueryBeans(element);
count++;
}
- for (Element element : roundEnv.getElementsAnnotatedWith(processingContext.entityAnnotation())) {
+ for (Element element : getElements(roundEnv, EntityPrism.PRISM_TYPE)) {
generateQueryBeans(element);
count++;
}
@@ -73,35 +84,52 @@ private int processEntities(RoundEnvironment roundEnv) {
}
private void processOthers(RoundEnvironment round) {
- processOthers(round, processingContext.converterAnnotation());
- processOthers(round, processingContext.componentAnnotation());
+ getElements(round, ConverterPrism.PRISM_TYPE).forEach(ProcessingContext::addOther);
+ getElements(round, EbeanComponentPrism.PRISM_TYPE).forEach(ProcessingContext::addOther);
}
- private void processOthers(RoundEnvironment roundEnv, TypeElement otherType) {
- if (otherType != null) {
- for (Element element : roundEnv.getElementsAnnotatedWith(otherType)) {
- processingContext.addOther(element);
+ private Set extends Element> getElements(RoundEnvironment round, String name) {
+ return Optional.ofNullable(typeElement(name))
+ .map(round::getElementsAnnotatedWith)
+ .orElse(Set.of());
+ }
+
+ private void initModuleInfoBean() {
+ try {
+ if (moduleWriter == null) {
+ moduleWriter = new SimpleModuleInfoWriter();
}
+ } catch (FilerException e) {
+ logWarn("FilerException trying to write EntityClassRegister: " + e);
+ } catch (Throwable e) {
+ logError("Failed to write EntityClassRegister error:" + e + " stack:" + Arrays.toString(e.getStackTrace()));
}
}
private void writeModuleInfoBean() {
try {
- new SimpleModuleInfoWriter(processingContext).write();
- } catch (FilerException e) {
- processingContext.logWarn(null, "FilerException trying to write EntityClassRegister: " + e);
- } catch (Throwable e) {
- processingContext.logError(null, "Failed to write EntityClassRegister error:" + e + " stack:" + Arrays.toString(e.getStackTrace()));
+ if (moduleWriter == null) {
+ logError("EntityClassRegister was not initialised and not written");
+ } else {
+ moduleWriter.write();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ logError(
+ "Failed to write EntityClassRegister error:"
+ + e
+ + " stack:"
+ + Arrays.toString(e.getStackTrace()));
}
}
private void generateQueryBeans(Element element) {
try {
- SimpleQueryBeanWriter beanWriter = new SimpleQueryBeanWriter((TypeElement) element, processingContext);
+ SimpleQueryBeanWriter beanWriter =
+ new SimpleQueryBeanWriter((TypeElement) element);
beanWriter.writeRootBean();
- } catch (Throwable e) {
- processingContext.logError(element, "Error generating query beans: " + e);
+ } catch (Exception e) {
+ logError(element, "Error generating query beans: " + e);
}
}
-
}
diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/ReadModuleInfo.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/ReadModuleInfo.java
deleted file mode 100644
index 01f19a3044..0000000000
--- a/querybean-generator/src/main/java/io/ebean/querybean/generator/ReadModuleInfo.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package io.ebean.querybean.generator;
-
-import javax.lang.model.element.AnnotationMirror;
-import javax.lang.model.element.AnnotationValue;
-import javax.lang.model.element.Element;
-import javax.lang.model.element.ExecutableElement;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Helper to read the current/existing prefixed entity classes.
- *
- * These are added back on partial compile.
- */
-class ReadModuleInfo {
-
- private final ProcessingContext ctx;
-
- public ReadModuleInfo(ProcessingContext ctx) {
- this.ctx = ctx;
- }
-
- ModuleMeta read(Element element) {
- final List extends AnnotationMirror> mirrors = element.getAnnotationMirrors();
- for (AnnotationMirror mirror : mirrors) {
- final String name = mirror.getAnnotationType().asElement().toString();
- if (Constants.MODULEINFO.equals(name)) {
- List entities = readEntities("entities", mirror);
- List other = readEntities("other", mirror);
- return new ModuleMeta(entities, other);
- }
- }
- return null;
- }
-
- @SuppressWarnings("unchecked")
- private List readEntities(String key, AnnotationMirror mirror) {
- final Map extends ExecutableElement, ? extends AnnotationValue> elementValues = mirror.getElementValues();
- final Set extends Map.Entry extends ExecutableElement, ? extends AnnotationValue>> entries = elementValues.entrySet();
-
- for (Map.Entry extends ExecutableElement, ? extends AnnotationValue> entry : entries) {
- if (key.equals(entry.getKey().getSimpleName().toString())) {
- return readAttributes(entry.getValue());
- }
- }
- return Collections.emptyList();
- }
-
- @SuppressWarnings("unchecked")
- private List readAttributes(AnnotationValue value) {
- final Object entitiesValue = value.getValue();
- if (entitiesValue != null) {
- try {
- List vals = new ArrayList<>();
- List coll = (List) entitiesValue;
- for (AnnotationValue annotationValue : coll) {
- vals.add((String) annotationValue.getValue());
- }
- return vals;
- } catch (Exception e) {
- ctx.logError(null, "Error reading ModuleInfo annotation, err " + e);
- }
- }
- return Collections.emptyList();
- }
-}
diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java
index 917e34b029..022e0b2a3c 100644
--- a/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java
+++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java
@@ -1,7 +1,7 @@
package io.ebean.querybean.generator;
-import javax.tools.FileObject;
-import javax.tools.JavaFileObject;
+import static io.ebean.querybean.generator.APContext.logError;
+
import java.io.IOException;
import java.io.Writer;
import java.util.LinkedHashSet;
@@ -9,26 +9,25 @@
import java.util.Set;
import java.util.StringJoiner;
+import javax.tools.FileObject;
+import javax.tools.JavaFileObject;
+
/**
* Write the source code for the factory.
*/
class SimpleModuleInfoWriter {
- private final ProcessingContext processingContext;
-
private final String factoryPackage;
- private final String factoryShortName;
+ private final String factoryShortName= "EbeanEntityRegister";
private final String factoryFullName;
private final JavaFileObject javaFileObject;
private Append writer;
- SimpleModuleInfoWriter(ProcessingContext processingContext) throws IOException {
- this.processingContext = processingContext;
- this.factoryPackage = processingContext.getFactoryPackage();
- this.factoryShortName = "EbeanEntityRegister";
+ SimpleModuleInfoWriter() throws IOException {
+ this.factoryPackage = ProcessingContext.getFactoryPackage();
this.factoryFullName = factoryPackage + "." + factoryShortName;
- this.javaFileObject = processingContext.createWriter(factoryFullName);
+ this.javaFileObject = APContext.createSourceFile(factoryFullName);
}
void write() throws IOException {
@@ -44,22 +43,23 @@ void write() throws IOException {
private void writeServicesFile() {
try {
- FileObject jfo = processingContext.createMetaInfServicesWriter();
+ FileObject jfo = ProcessingContext.createMetaInfServicesWriter();
if (jfo != null) {
Writer writer = jfo.openWriter();
+ ProcessingContext.addService(factoryFullName);
writer.write(factoryFullName);
writer.close();
}
} catch (IOException e) {
- processingContext.logError(null, "Failed to write services file " + e.getMessage());
+ logError("Failed to write services file " + e.getMessage());
}
}
private void writeManifestFile() {
try {
- final Set allEntityPackages = processingContext.getAllEntityPackages();
+ final Set allEntityPackages = ProcessingContext.getAllEntityPackages();
if (!allEntityPackages.isEmpty()) {
- FileObject jfo = processingContext.createManifestWriter();
+ FileObject jfo = ProcessingContext.createManifestWriter();
if (jfo != null) {
Writer writer = jfo.openWriter();
writer.write("generated-by: Ebean query bean generator\n");
@@ -69,7 +69,7 @@ private void writeManifestFile() {
}
}
} catch (IOException e) {
- processingContext.logError(null, "Failed to write services file " + e.getMessage());
+ logError("Failed to write services file " + e.getMessage());
}
}
@@ -84,13 +84,13 @@ private String manifestEntityPackages(Set allEntityPackages) {
private void writeNativeImageFile() {
try {
- Set allEntities = new LinkedHashSet<>(processingContext.getDbEntities());
- for (Set value : processingContext.getOtherDbEntities().values()) {
+ Set allEntities = new LinkedHashSet<>(ProcessingContext.getDbEntities());
+ for (Set value : ProcessingContext.getOtherDbEntities().values()) {
allEntities.addAll(value);
}
if (!allEntities.isEmpty()) {
- FileObject jfo = processingContext.createNativeImageWriter(factoryPackage + ".ebean-entity");
+ FileObject jfo = ProcessingContext.createNativeImageWriter(factoryPackage + ".ebean-entity");
if (jfo != null) {
boolean first = true;
Writer writer = jfo.openWriter();
@@ -111,7 +111,7 @@ private void writeNativeImageFile() {
}
}
} catch (IOException e) {
- processingContext.logError(null, "Failed to write services file " + e.getMessage());
+ logError("Failed to write services file " + e.getMessage());
}
}
@@ -130,7 +130,7 @@ private void writePackage() {
void buildAtContextModule(Append writer) {
writer.append(Constants.AT_GENERATED).eol();
writer.append("@ModuleInfo(");
- if (processingContext.hasOtherClasses()) {
+ if (ProcessingContext.hasOtherClasses()) {
writer.append("other={%s}, ", otherClasses());
}
writer.append("entities={%s}", prefixEntities());
@@ -138,11 +138,11 @@ void buildAtContextModule(Append writer) {
}
private String otherClasses() {
- return quoteTypes(processingContext.getOtherClasses());
+ return quoteTypes(ProcessingContext.getOtherClasses());
}
private String prefixEntities() {
- return quoteTypes(processingContext.getPrefixEntities());
+ return quoteTypes(ProcessingContext.getPrefixEntities());
}
private String quoteTypes(Set otherClasses) {
@@ -158,9 +158,9 @@ private void writeStartClass() {
writer.append("public class %s implements EntityClassRegister {", factoryShortName).eol().eol();
writeMethodOtherClasses();
- writeMethodEntityClasses(processingContext.getDbEntities(), null);
+ writeMethodEntityClasses(ProcessingContext.getDbEntities(), null);
- final Map> otherDbEntities = processingContext.getOtherDbEntities();
+ final Map> otherDbEntities = ProcessingContext.getOtherDbEntities();
for (Map.Entry> otherDb : otherDbEntities.entrySet()) {
writeMethodEntityClasses(otherDb.getValue(), otherDb.getKey());
}
@@ -171,11 +171,11 @@ private void writeStartClass() {
private void writeMethodOtherClasses() {
writeMethodComment("Register AttributeConverter etc", "");
writer.append(" private List> otherClasses() {").eol();
- if (!processingContext.hasOtherClasses()) {
+ if (!ProcessingContext.hasOtherClasses()) {
writer.append(" return Collections.emptyList();").eol();
} else {
writer.append(" List> others = new ArrayList<>();").eol();
- for (String otherType : processingContext.getOtherClasses()) {
+ for (String otherType : ProcessingContext.getOtherClasses()) {
writer.append(" others.add(%s.class);", otherType).eol();
}
writer.append(" return others;").eol();
@@ -192,14 +192,14 @@ private void writeMethodEntityClasses(Set dbEntities, String dbName) {
writeMethodComment("Entities with no @DbName", dbName);
}
writer.append(" private List> %s() {", method).eol();
- if (dbEntities.isEmpty() && !processingContext.hasOtherClasses()) {
+ if (dbEntities.isEmpty() && !ProcessingContext.hasOtherClasses()) {
writer.append(" return Collections.emptyList();").eol();
} else {
writer.append(" List> entities = new ArrayList<>();").eol();
for (String dbEntity : dbEntities) {
writer.append(" entities.add(%s.class);", dbEntity).eol();
}
- if (processingContext.hasOtherClasses()) {
+ if (ProcessingContext.hasOtherClasses()) {
writer.append(" entities.addAll(otherClasses());").eol();
}
writer.append(" return entities;").eol();
diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleQueryBeanWriter.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleQueryBeanWriter.java
index c642146835..4f8887ac0e 100644
--- a/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleQueryBeanWriter.java
+++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleQueryBeanWriter.java
@@ -1,10 +1,9 @@
package io.ebean.querybean.generator;
-import javax.lang.model.element.TypeElement;
-import javax.lang.model.element.VariableElement;
-import javax.lang.model.type.TypeMirror;
-import javax.tools.JavaFileObject;
+import static io.ebean.querybean.generator.APContext.asTypeElement;
+import static io.ebean.querybean.generator.APContext.createSourceFile;
+
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
@@ -12,6 +11,11 @@
import java.util.Set;
import java.util.TreeSet;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeMirror;
+import javax.tools.JavaFileObject;
+
/**
* A simple implementation that generates and writes query beans.
*/
@@ -22,7 +26,6 @@ class SimpleQueryBeanWriter {
private final TypeElement element;
private final TypeElement implementsInterface;
private String implementsInterfaceShortName;
- private final ProcessingContext processingContext;
private final String dbName;
private final String beanFullName;
private final boolean isEntity;
@@ -34,25 +37,24 @@ class SimpleQueryBeanWriter {
private final boolean fullyQualify;
private Append writer;
- SimpleQueryBeanWriter(TypeElement element, ProcessingContext processingContext) {
+ SimpleQueryBeanWriter(TypeElement element) {
this.element = element;
- this.processingContext = processingContext;
this.beanFullName = element.getQualifiedName().toString();
boolean nested = element.getNestingKind().isNested();
this.destPackage = Util.packageOf(nested, beanFullName) + ".query";
String sn = Util.shortName(nested, beanFullName);
this.shortInnerName = Util.shortName(false, sn);
this.shortName = sn.replace('.', '$');
- this.isEntity = processingContext.isEntity(element);
- this.embeddable = processingContext.isEmbeddable(element);
+ this.isEntity = EntityPrism.isPresent(element);
+ this.embeddable = ProcessingContext.isEmbeddable(element);
this.dbName = findDbName();
this.implementsInterface = initInterface(element);
- this.fullyQualify = processingContext.isNameClash(shortName);
+ this.fullyQualify = ProcessingContext.isNameClash(shortName);
}
private TypeElement initInterface(TypeElement element) {
for (TypeMirror anInterface : element.getInterfaces()) {
- TypeElement e = (TypeElement)processingContext.asElement(anInterface);
+ TypeElement e = asTypeElement(anInterface);
String name = e.getQualifiedName().toString();
if (!name.startsWith("java") && !name.startsWith("io.ebean")) {
return e;
@@ -62,7 +64,7 @@ private TypeElement initInterface(TypeElement element) {
}
private String findDbName() {
- return processingContext.findDbName(element);
+ return ProcessingContext.findDbName(element);
}
private boolean isEntity() {
@@ -88,8 +90,8 @@ private void gatherPropertyDetails() {
* Includes properties from mapped super classes and usual inheritance.
*/
private void addClassProperties() {
- for (VariableElement field : processingContext.allFields(element)) {
- PropertyType type = processingContext.getPropertyType(field);
+ for (VariableElement field : ProcessingContext.allFields(element)) {
+ PropertyType type = ProcessingContext.getPropertyType(field);
if (type != null) {
type.addImports(importTypes, fullyQualify);
properties.add(new PropertyMeta(field.getSimpleName().toString(), type));
@@ -102,7 +104,7 @@ private void addClassProperties() {
*/
void writeRootBean() throws IOException {
gatherPropertyDetails();
- processingContext.addEntity(beanFullName, dbName);
+ ProcessingContext.addEntity(beanFullName, dbName);
writer = new Append(createFileWriter());
writePackage();
writeImports();
@@ -339,15 +341,13 @@ private void writeAssocBeanConstructor(String prefix) {
}
private void writeAssocBeanFetch() {
- if (isEntity()) {
- // inherit the fetch methods
- if (implementsInterface != null) {
- writeAssocBeanExpression(false, "eq", "Is equal to by ID property.");
- writeAssocBeanExpression(true, "eqIfPresent", "Is equal to by ID property if the value is not null, if null no expression is added.");
- writeAssocBeanExpression(false, "in", "IN the given values.", implementsInterfaceShortName + "...", "in");
- writeAssocBeanExpression(false, "inBy", "IN the given interface values.", "Collection extends " + implementsInterfaceShortName + ">", "in");
- writeAssocBeanExpression(true, "inOrEmptyBy", "IN the given interface values if the collection is not empty. No expression is added if the collection is empty..", "Collection extends " + implementsInterfaceShortName + ">", "inOrEmpty");
- }
+ // inherit the fetch methods
+ if (isEntity() && (implementsInterface != null)) {
+ writeAssocBeanExpression(false, "eq", "Is equal to by ID property.");
+ writeAssocBeanExpression(true, "eqIfPresent", "Is equal to by ID property if the value is not null, if null no expression is added.");
+ writeAssocBeanExpression(false, "in", "IN the given values.", implementsInterfaceShortName + "...", "in");
+ writeAssocBeanExpression(false, "inBy", "IN the given interface values.", "Collection extends " + implementsInterfaceShortName + ">", "in");
+ writeAssocBeanExpression(true, "inOrEmptyBy", "IN the given interface values if the collection is not empty. No expression is added if the collection is empty..", "Collection extends " + implementsInterfaceShortName + ">", "inOrEmpty");
}
}
@@ -358,7 +358,7 @@ private void writeAssocBeanExpression(boolean nullable,String expression, String
private void writeAssocBeanExpression(boolean nullable, String expression, String comment, String param, String actualExpression) {
final String nullableAnnotation = nullable ? "@Nullable " : "";
String values = expression.startsWith("in") ? "values" : "value";
- String castVarargs = expression.equals("in") ? "(Object[])" : "";
+ String castVarargs = "in".equals(expression) ? "(Object[])" : "";
writer.append(" /**").eol();
writer.append(" * ").append(comment).eol();
writer.append(" */").eol();
@@ -388,7 +388,7 @@ private void writePackage() {
}
private Writer createFileWriter() throws IOException {
- JavaFileObject jfo = processingContext.createWriter(destPackage + "." + "Q" + shortName, element);
+ JavaFileObject jfo = createSourceFile(destPackage + "." + "Q" + shortName, element);
return jfo.openWriter();
}
diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/package-info.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/package-info.java
new file mode 100644
index 0000000000..95cc2e7594
--- /dev/null
+++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/package-info.java
@@ -0,0 +1,15 @@
+@GeneratePrism(io.ebean.annotation.DbArray.class)
+@GeneratePrism(io.ebean.annotation.DbJson.class)
+@GeneratePrism(io.ebean.annotation.DbJsonB.class)
+@GeneratePrism(io.ebean.annotation.DbName.class)
+@GeneratePrism(io.ebean.annotation.EbeanComponent.class)
+@GeneratePrism(io.ebean.config.ModuleInfo.class)
+
+@GeneratePrism(jakarta.persistence.Converter.class)
+@GeneratePrism(jakarta.persistence.Embeddable.class)
+@GeneratePrism(jakarta.persistence.Entity.class)
+@GeneratePrism(jakarta.persistence.ManyToMany.class)
+@GeneratePrism(jakarta.persistence.OneToMany.class)
+package io.ebean.querybean.generator;
+
+import io.avaje.prism.GeneratePrism;
diff --git a/querybean-generator/src/main/java/module-info.java b/querybean-generator/src/main/java/module-info.java
index f864e9528e..3bce8926e4 100644
--- a/querybean-generator/src/main/java/module-info.java
+++ b/querybean-generator/src/main/java/module-info.java
@@ -1,7 +1,13 @@
module io.ebean.querybean.generator {
- provides javax.annotation.processing.Processor with io.ebean.querybean.generator.Processor;
-
requires java.compiler;
requires java.sql;
+
+ requires static io.avaje.prism;
+ requires static io.ebean.api;
+ requires static io.ebean.annotation;
+ requires static jakarta.persistence.api;
+
+ provides javax.annotation.processing.Processor with io.ebean.querybean.generator.Processor;
+
}
diff --git a/querybean-generator/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/querybean-generator/src/main/resources/META-INF/services/javax.annotation.processing.Processor
deleted file mode 100644
index 0ca63ab195..0000000000
--- a/querybean-generator/src/main/resources/META-INF/services/javax.annotation.processing.Processor
+++ /dev/null
@@ -1 +0,0 @@
-io.ebean.querybean.generator.Processor
\ No newline at end of file
diff --git a/querybean-generator/src/test/java/io/ebean/querybean/generator/QueryBeanProcessorTest.java b/querybean-generator/src/test/java/io/ebean/querybean/generator/QueryBeanProcessorTest.java
new file mode 100644
index 0000000000..3c2ccbbf65
--- /dev/null
+++ b/querybean-generator/src/test/java/io/ebean/querybean/generator/QueryBeanProcessorTest.java
@@ -0,0 +1,70 @@
+package io.ebean.querybean.generator;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+
+import javax.tools.JavaCompiler;
+import javax.tools.JavaCompiler.CompilationTask;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+class QueryBeanProcessorTest {
+
+ @AfterEach
+ void deleteGeneratedFiles() throws Exception {
+
+ Paths.get("ebean-generated-info.mf").toAbsolutePath().toFile().delete();
+ Paths.get("io.ebean.config.EntityClassRegister").toAbsolutePath().toFile().delete();
+ Paths.get("reflect-config.json").toAbsolutePath().toFile().delete();
+ Files.walk(Paths.get("io").toAbsolutePath())
+ .sorted(Comparator.reverseOrder())
+ .map(Path::toFile)
+ .forEach(File::delete);
+ }
+
+ // uncomment this to debug the processor for diagnosing issues
+ // @Test
+ void testGeneration() throws Exception {
+ final String source =
+ Paths.get("src/test/java/io/ebean/querybean/generator/entities")
+ .toAbsolutePath()
+ .toString();
+
+ final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ final StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null);
+
+ manager.setLocation(StandardLocation.SOURCE_PATH, List.of(new File(source)));
+
+ final Set fileKinds = Set.of(Kind.SOURCE);
+
+ final Iterable files =
+ manager.list(StandardLocation.SOURCE_PATH, "", fileKinds, true);
+
+ final CompilationTask task =
+ compiler.getTask(
+ new PrintWriter(System.out),
+ null,
+ null,
+ List.of("--release=" + Integer.getInteger("java.specification.version")),
+ null,
+ files);
+ task.setProcessors(List.of(new Processor()));
+
+ assertThat(task.call()).isTrue();
+ }
+}
diff --git a/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/ArticleEntity.java b/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/ArticleEntity.java
new file mode 100644
index 0000000000..7b95f3f46f
--- /dev/null
+++ b/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/ArticleEntity.java
@@ -0,0 +1,127 @@
+package io.ebean.querybean.generator.entities;
+
+import io.ebean.Model;
+import io.ebean.annotation.WhenCreated;
+import io.ebean.annotation.WhenModified;
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Entity
+@Table(name = "realworld.article")
+public class ArticleEntity extends Model {
+
+ @Id @GeneratedValue private UUID id;
+
+ @WhenCreated
+ @Column(nullable = false, updatable = false)
+ private LocalDateTime createdAt;
+
+ @WhenModified
+ @Column(nullable = false)
+ private LocalDateTime updatedAt;
+
+ @ManyToOne(fetch = FetchType.EAGER)
+ @JoinColumn(name = "user_id", nullable = false)
+ private UserEntity author;
+
+ @Column(nullable = false)
+ private String slug;
+
+ @Column(nullable = false)
+ private String title;
+
+ @Column(nullable = false)
+ private String description;
+
+ @Column(nullable = false)
+ private String body;
+
+ @OneToMany(mappedBy = "article", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
+ private final List tags = new ArrayList<>();
+
+ public UUID id() {
+ return id;
+ }
+
+ public ArticleEntity id(UUID id) {
+ this.id = id;
+ return this;
+ }
+
+ public UserEntity author() {
+ return author;
+ }
+
+ public ArticleEntity author(UserEntity user) {
+ this.author = user;
+ return this;
+ }
+
+ public String slug() {
+ return slug;
+ }
+
+ public ArticleEntity slug(String slug) {
+ this.slug = slug;
+ return this;
+ }
+
+ public String title() {
+ return title;
+ }
+
+ public ArticleEntity title(String title) {
+ this.title = title;
+ return this;
+ }
+
+ public String description() {
+ return description;
+ }
+
+ public ArticleEntity description(String description) {
+ this.description = description;
+ return this;
+ }
+
+ public String body() {
+ return body;
+ }
+
+ public ArticleEntity body(String body) {
+ this.body = body;
+ return this;
+ }
+
+ public List tags() {
+ return tags;
+ }
+
+ public LocalDateTime createdAt() {
+ return createdAt;
+ }
+
+ public void createdAt(LocalDateTime createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public LocalDateTime updatedAt() {
+ return updatedAt;
+ }
+
+ public void updatedAt(LocalDateTime updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+}
diff --git a/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/ArticleTags.java b/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/ArticleTags.java
new file mode 100644
index 0000000000..058fe04cdf
--- /dev/null
+++ b/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/ArticleTags.java
@@ -0,0 +1,72 @@
+package io.ebean.querybean.generator.entities;
+
+import java.util.UUID;
+
+import io.ebean.Model;
+import jakarta.persistence.Embeddable;
+import jakarta.persistence.EmbeddedId;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+
+@Entity
+@Table(name = "realworld.article_tag")
+public class ArticleTags extends Model {
+
+ @EmbeddedId private ArticleTagId id;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "article_id", nullable = false, insertable = false, updatable = false)
+ private final ArticleEntity article;
+
+ @ManyToOne(fetch = FetchType.EAGER)
+ @JoinColumn(name = "tag_id", nullable = false, insertable = false, updatable = false)
+ private final TagEntity tag;
+
+ public ArticleTags(ArticleEntity article, TagEntity tag) {
+ this.article = article;
+ this.tag = tag;
+ }
+
+ public ArticleEntity article() {
+ return article;
+ }
+
+ public TagEntity tag() {
+ return tag;
+ }
+
+ public ArticleTagId id() {
+ return id;
+ }
+
+ public void id(ArticleTagId id) {
+ this.id = id;
+ }
+
+ @Embeddable
+ public static class ArticleTagId {
+ UUID articleId;
+ UUID tagId;
+
+ public ArticleTagId(UUID articleId, UUID tagId) {}
+
+ public UUID getArticleId() {
+ return articleId;
+ }
+
+ public void setArticleId(UUID articleId) {
+ this.articleId = articleId;
+ }
+
+ public UUID getTagId() {
+ return tagId;
+ }
+
+ public void setTagId(UUID tagId) {
+ this.tagId = tagId;
+ }
+ }
+}
diff --git a/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/CommentEntity.java b/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/CommentEntity.java
new file mode 100644
index 0000000000..a3c1a785d0
--- /dev/null
+++ b/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/CommentEntity.java
@@ -0,0 +1,52 @@
+package io.ebean.querybean.generator.entities;
+
+import io.ebean.Model;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.NamedAttributeNode;
+import jakarta.persistence.NamedEntityGraph;
+import jakarta.persistence.Table;
+
+@Entity
+@Table(name = "comments")
+@NamedEntityGraph(name = "fetch-author", attributeNodes = @NamedAttributeNode("author"))
+public class CommentEntity extends Model {
+
+ @Column(nullable = false)
+ private String body;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "author_id", nullable = false)
+ private UserEntity author;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "article_id", nullable = false)
+ private ArticleEntity article;
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ public UserEntity getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(UserEntity author) {
+ this.author = author;
+ }
+
+ public ArticleEntity getArticle() {
+ return article;
+ }
+
+ public void setArticle(ArticleEntity article) {
+ this.article = article;
+ }
+}
diff --git a/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/FavoriteEntity.java b/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/FavoriteEntity.java
new file mode 100644
index 0000000000..c49283e91c
--- /dev/null
+++ b/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/FavoriteEntity.java
@@ -0,0 +1,40 @@
+package io.ebean.querybean.generator.entities;
+
+import io.ebean.Model;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import java.util.UUID;
+
+@Entity
+@Table(name = "favorites")
+public class FavoriteEntity extends Model {
+
+ @Id UUID id;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ private ArticleEntity article;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ private UserEntity user;
+
+ public ArticleEntity article() {
+ return article;
+ }
+
+ public FavoriteEntity article(ArticleEntity article) {
+ this.article = article;
+ return this;
+ }
+
+ public UserEntity user() {
+ return user;
+ }
+
+ public FavoriteEntity user(UserEntity user) {
+ this.user = user;
+ return this;
+ }
+}
diff --git a/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/TagEntity.java b/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/TagEntity.java
new file mode 100644
index 0000000000..dc06b59a23
--- /dev/null
+++ b/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/TagEntity.java
@@ -0,0 +1,27 @@
+package io.ebean.querybean.generator.entities;
+
+import io.ebean.Model;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import java.util.UUID;
+
+@Entity
+@Table(name = "realworld.tag")
+public class TagEntity extends Model {
+ @Id private final UUID id;
+ private final String name;
+
+ public TagEntity(UUID id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public UUID id() {
+ return id;
+ }
+
+ public String name() {
+ return name;
+ }
+}
diff --git a/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/UserEntity.java b/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/UserEntity.java
new file mode 100644
index 0000000000..daa1cca30f
--- /dev/null
+++ b/querybean-generator/src/test/java/io/ebean/querybean/generator/entities/UserEntity.java
@@ -0,0 +1,86 @@
+package io.ebean.querybean.generator.entities;
+
+import io.ebean.Model;
+import io.ebean.annotation.Encrypted;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import java.util.UUID;
+
+@Entity
+@Table(name = "realworld.user")
+public class UserEntity extends Model {
+
+ @Id @GeneratedValue private UUID id;
+
+ @Column(nullable = false, unique = true)
+ private String username;
+
+ @Encrypted
+ @Column(nullable = false)
+ private String passwordHash;
+
+ @Column(nullable = false, unique = true)
+ private String email;
+
+ @Column(nullable = false)
+ private String bio;
+
+ private String image;
+
+ public UUID id() {
+ return id;
+ }
+
+ public UserEntity id(UUID id) {
+ this.id = id;
+ return this;
+ }
+
+ public String username() {
+ return username;
+ }
+
+ public UserEntity username(String username) {
+ this.username = username;
+ return this;
+ }
+
+ public String passwordHash() {
+ return passwordHash;
+ }
+
+ public UserEntity passwordHash(String passwordHash) {
+ this.passwordHash = passwordHash;
+ return this;
+ }
+
+ public String email() {
+ return email;
+ }
+
+ public UserEntity email(String email) {
+ this.email = email;
+ return this;
+ }
+
+ public String bio() {
+ return bio;
+ }
+
+ public UserEntity bio(String bio) {
+ this.bio = bio;
+ return this;
+ }
+
+ public String image() {
+ return image;
+ }
+
+ public UserEntity image(String image) {
+ this.image = image;
+ return this;
+ }
+}