Model generator

This commit is contained in:
2026-05-15 12:51:34 +02:00
parent dd09c8dbcd
commit 8905773132
11 changed files with 818 additions and 46 deletions

46
.idea/compiler.xml generated
View File

@@ -61,52 +61,6 @@
<entry name="$USER_HOME$/.m2/peak-repo/org/antlr/antlr4-runtime/4.13.2/antlr4-runtime-4.13.2.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/net/bytebuddy/byte-buddy/1.17.8/byte-buddy-1.17.8.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/jboss/logging/jboss-logging/3.6.1.Final/jboss-logging-3.6.1.Final.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/mapstruct/mapstruct-processor/1.6.3/mapstruct-processor-1.6.3.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/errorprone/error_prone_core/2.37.0/error_prone_core-2.37.0.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/errorprone/error_prone_annotation/2.37.0/error_prone_annotation-2.37.0.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/errorprone/error_prone_check_api/2.37.0/error_prone_check_api-2.37.0.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/io/github/java-diff-utils/java-diff-utils/4.12/java-diff-utils-4.12.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/github/kevinstern/software-and-algorithms/1.0/software-and-algorithms-1.0.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/github/ben-manes/caffeine/caffeine/3.0.5/caffeine-3.0.5.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/googlejavaformat/google-java-format/1.24.0/google-java-format-1.24.0.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/pcollections/pcollections/4.0.1/pcollections-4.0.1.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/guava/guava/32.1.1-jre/guava-32.1.1-jre.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/checkerframework/checker-qual/3.33.0/checker-qual-3.33.0.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/j2objc/j2objc-annotations/2.8/j2objc-annotations-2.8.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/auto/auto-common/1.2.2/auto-common-1.2.2.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/io/github/eisop/dataflow-errorprone/3.41.0-eisop1/dataflow-errorprone-3.41.0-eisop1.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/auto/value/auto-value-annotations/1.9/auto-value-annotations-1.9.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/errorprone/error_prone_annotations/2.37.0/error_prone_annotations-2.37.0.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/protobuf/protobuf-java/3.25.5/protobuf-java-3.25.5.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/google/auto/service/auto-service-annotations/1.0.1/auto-service-annotations-1.0.1.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/javax/inject/javax.inject/1/javax.inject-1.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/uber/nullaway/nullaway/0.13.1/nullaway-0.13.1.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/checkerframework/dataflow-nullaway/3.53.0/dataflow-nullaway-3.53.0.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/hibernate/orm/hibernate-processor/7.2.4.Final/hibernate-processor-7.2.4.Final.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/hibernate/orm/hibernate-core/7.2.4.Final/hibernate-core-7.2.4.Final.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/persistence/jakarta.persistence-api/3.2.0/jakarta.persistence-api-3.2.0.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/transaction/jakarta.transaction-api/2.0.1/jakarta.transaction-api-2.0.1.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/fasterxml/classmate/1.7.1/classmate-1.7.1.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/hibernate/models/hibernate-models/1.0.1/hibernate-models-1.0.1.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/io/smallrye/jandex/3.3.2/jandex-3.3.2.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/xml/bind/jakarta.xml.bind-api/4.0.4/jakarta.xml.bind-api-4.0.4.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/activation/jakarta.activation-api/2.1.4/jakarta.activation-api-2.1.4.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/glassfish/jaxb/jaxb-runtime/4.0.6/jaxb-runtime-4.0.6.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/glassfish/jaxb/jaxb-core/4.0.6/jaxb-core-4.0.6.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/eclipse/angus/angus-activation/2.0.3/angus-activation-2.0.3.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/glassfish/jaxb/txw2/4.0.6/txw2-4.0.6.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/com/sun/istack/istack-commons-runtime/4.1.2/istack-commons-runtime-4.1.2.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/validation/jakarta.validation-api/3.1.1/jakarta.validation-api-3.1.1.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/annotation/jakarta.annotation-api/3.0.0/jakarta.annotation-api-3.0.0.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/antlr/antlr4-runtime/4.13.2/antlr4-runtime-4.13.2.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/net/bytebuddy/byte-buddy/1.17.8/byte-buddy-1.17.8.jar" />
<entry name="$USER_HOME$/.m2/peak-repo/org/jboss/logging/jboss-logging/3.6.1.Final/jboss-logging-3.6.1.Final.jar" />
</processorPath>
<module name="peakup-backend" />
</profile>

View File

@@ -35,6 +35,11 @@
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.20.1</version>
</dependency>
<dependency>
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
<version>1.13.0</version>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,17 @@
package com.peakup.tools.generator;
import jakarta.annotation.Nullable;
public class ElementCollectionDefinition {
public String fieldName;
public String collectionTable;
public String joinColumn;
/** Simple type name: "String" for scalar collections, or an embeddable class name. */
public String valueType;
/** Column name for scalar (String) collections; null for embeddable collections. */
public @Nullable String valueColumn;
public @Nullable String orderColumn;
/** Inline embeddable definition — generated alongside the entity when present. */
public @Nullable EmbeddableDefinition embeddable;
}

View File

@@ -0,0 +1,10 @@
package com.peakup.tools.generator;
import java.util.ArrayList;
import java.util.List;
public class EmbeddableDefinition {
public String className;
public List<FieldDefinition> fields = new ArrayList<>();
}

View File

@@ -0,0 +1,10 @@
package com.peakup.tools.generator;
import java.util.ArrayList;
import java.util.List;
public class EnumDefinition {
public String className;
public List<EnumValueDefinition> values = new ArrayList<>();
}

View File

@@ -0,0 +1,7 @@
package com.peakup.tools.generator;
public class EnumValueDefinition {
public String name;
public int key;
}

View File

@@ -0,0 +1,13 @@
package com.peakup.tools.generator;
import jakarta.annotation.Nullable;
public class FieldDefinition {
public String name;
public String javaType;
public boolean nullable;
public String columnName;
public @Nullable String columnDefinition;
public @Nullable String jdbcTypeCode;
}

View File

@@ -0,0 +1,20 @@
package com.peakup.tools.generator;
import java.util.ArrayList;
import java.util.List;
public class ModelDefinition {
public String modelName;
public String packageBase;
public String tableName;
/** "AbstractEntity" or "AbstractTimestampedEntity" */
public String baseClass;
public String apiPath;
/** When true, adds @JsonInclude(JsonInclude.Include.NON_NULL) to the entity. */
public boolean jsonIncludeNonNull;
public List<FieldDefinition> fields = new ArrayList<>();
public List<ElementCollectionDefinition> elementCollections = new ArrayList<>();
public List<OneToManyDefinition> oneToManyRelations = new ArrayList<>();
public List<EnumDefinition> enums = new ArrayList<>();
}

View File

@@ -0,0 +1,659 @@
package com.peakup.tools.generator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import javax.lang.model.element.Modifier;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Generates a full domain slice (model, DAO, service, resource) from a JSON definition file.
*
* <p>Run from project root:
* <pre>
* JAVA_HOME=/home/ingo/.jdks/temurin-21.0.7 mvn -f peakup-tools/pom.xml \
* -s /home/ingo/.m2/settings_peak.xml -Dmaven.repo.local=/home/ingo/.m2/peak-repo \
* compile exec:java -Dexec.mainClass=com.peakup.tools.generator.ModelGeneratorTool \
* "-Dexec.args=/path/to/definition.json [outputDir]"
* </pre>
*
* <p>Default outputDir: peakup-backend/src/main/java (relative to working directory).
*/
public class ModelGeneratorTool {
private static final String COMMON_PKG = "com.peakup.common";
// -----------------------------------------------------------------------
// Entry point
// -----------------------------------------------------------------------
public static void main(final String[] args) throws Exception {
if (args.length < 1) {
System.err.println("Usage: ModelGeneratorTool <definition.json> [outputDir]");
System.exit(1);
}
final Path definitionPath = Paths.get(args[0]);
final Path outputDir = args.length >= 2
? Paths.get(args[1])
: Paths.get(System.getProperty("user.dir")).resolve("peakup-backend/src/main/java");
final ModelDefinition def = new ObjectMapper().readValue(definitionPath.toFile(), ModelDefinition.class);
System.out.println("Generating: " + def.modelName + " → " + outputDir.toAbsolutePath());
final ModelGeneratorTool tool = new ModelGeneratorTool();
final Set<String> enumNames = def.enums.stream().map(e -> e.className).collect(Collectors.toSet());
for (final EnumDefinition enumDef : def.enums) {
write(tool.generateEnum(enumDef, def.packageBase + ".model"), outputDir, "model/" + enumDef.className);
}
for (final ElementCollectionDefinition ec : def.elementCollections) {
if (ec.embeddable != null) {
write(tool.generateEmbeddable(ec.embeddable, def.packageBase + ".model"), outputDir, "model/" + ec.embeddable.className);
}
}
write(tool.generateModel(def, enumNames), outputDir, "model/" + def.modelName);
write(tool.generateDAO(def), outputDir, "dao/" + def.modelName + "DAO");
write(tool.generateService(def), outputDir, "service/" + def.modelName + "Service");
write(tool.generateResource(def), outputDir, def.modelName + "Resource");
System.out.println("Done.");
}
private static void write(final JavaFile file, final Path outputDir, final String label) throws Exception {
file.writeTo(outputDir);
System.out.println(" " + label + ".java");
}
// -----------------------------------------------------------------------
// Enum
// -----------------------------------------------------------------------
private JavaFile generateEnum(final EnumDefinition enumDef, final String modelPackage) {
final ClassName selfClass = ClassName.get(modelPackage, enumDef.className);
final ClassName persistableEnum = ClassName.get(COMMON_PKG + ".model", "PersistableEnum");
final ClassName abstractConverter = ClassName.get(COMMON_PKG + ".model", "AbstractEnumConverter");
final TypeSpec.Builder enumBuilder = TypeSpec.enumBuilder(enumDef.className)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(persistableEnum, selfClass));
for (final EnumValueDefinition value : enumDef.values) {
enumBuilder.addEnumConstant(value.name, TypeSpec.anonymousClassBuilder("$L", value.key).build());
}
enumBuilder.addField(FieldSpec.builder(TypeName.INT, "key", Modifier.PRIVATE, Modifier.FINAL).build());
enumBuilder.addMethod(MethodSpec.constructorBuilder()
.addParameter(TypeName.INT, "key")
.addStatement("this.key = key")
.build());
enumBuilder.addMethod(MethodSpec.methodBuilder("getKey")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.get(Integer.class))
.addStatement("return key")
.build());
final TypeSpec converterClass = TypeSpec.classBuilder("Converter")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.persistence", "Converter"))
.addMember("autoApply", "$L", true)
.build())
.superclass(ParameterizedTypeName.get(abstractConverter, selfClass))
.addMethod(MethodSpec.methodBuilder("getEnumByKey")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(selfClass)
.addParameter(ParameterSpec.builder(ClassName.get(Integer.class), "key").addModifiers(Modifier.FINAL).build())
.addStatement("return $T.getEnumByKey(key, $T.values())", persistableEnum, selfClass)
.build())
.build();
enumBuilder.addType(converterClass);
return JavaFile.builder(modelPackage, enumBuilder.build()).skipJavaLangImports(true).build();
}
// -----------------------------------------------------------------------
// Embeddable
// -----------------------------------------------------------------------
private JavaFile generateEmbeddable(final EmbeddableDefinition embeddable, final String modelPackage) {
final TypeSpec.Builder classBuilder = TypeSpec.classBuilder(embeddable.className)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(ClassName.get("jakarta.persistence", "Embeddable"))
.addAnnotation(ClassName.get("lombok", "Data"));
for (final FieldDefinition field : embeddable.fields) {
classBuilder.addField(buildColumnField(field, modelPackage, Set.of()));
}
classBuilder.addMethod(buildHibernateConstructor());
return JavaFile.builder(modelPackage, classBuilder.build()).skipJavaLangImports(true).build();
}
// -----------------------------------------------------------------------
// Entity model
// -----------------------------------------------------------------------
private JavaFile generateModel(final ModelDefinition def, final Set<String> enumNames) {
final String modelPackage = def.packageBase + ".model";
final ClassName modelClass = ClassName.get(modelPackage, def.modelName);
final TypeSpec.Builder classBuilder = TypeSpec.classBuilder(def.modelName)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(ClassName.get("jakarta.persistence", "Entity"))
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.persistence", "Table"))
.addMember("name", "$S", def.tableName)
.build())
.addAnnotation(ClassName.get("lombok", "Data"))
.addAnnotation(AnnotationSpec.builder(ClassName.get("lombok", "EqualsAndHashCode"))
.addMember("callSuper", "$L", true)
.build())
.superclass(ClassName.get(COMMON_PKG + ".model", def.baseClass));
if (def.jsonIncludeNonNull) {
classBuilder.addAnnotation(AnnotationSpec.builder(
ClassName.get("com.fasterxml.jackson.annotation", "JsonInclude"))
.addMember("value", "$T.NON_NULL",
ClassName.get("com.fasterxml.jackson.annotation", "JsonInclude", "Include"))
.build());
}
for (final FieldDefinition field : def.fields) {
classBuilder.addField(buildColumnField(field, modelPackage, enumNames));
}
addElementCollectionFields(classBuilder, def, modelPackage);
addOneToManyFields(classBuilder, def, modelPackage);
classBuilder.addMethod(buildMergeFromMethod(def, modelClass));
classBuilder.addMethod(buildHibernateConstructor());
return JavaFile.builder(modelPackage, classBuilder.build()).skipJavaLangImports(true).build();
}
private void addElementCollectionFields(final TypeSpec.Builder classBuilder,
final ModelDefinition def, final String modelPackage) {
for (final ElementCollectionDefinition ec : def.elementCollections) {
final TypeName elementType = "String".equals(ec.valueType)
? ClassName.get(String.class)
: ClassName.get(modelPackage, ec.valueType);
final FieldSpec.Builder fieldBuilder = FieldSpec.builder(
ParameterizedTypeName.get(ClassName.get(List.class), elementType),
ec.fieldName, Modifier.PUBLIC)
.addAnnotation(ClassName.get("com.fasterxml.jackson.annotation", "JsonIgnore"))
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.persistence", "ElementCollection"))
.addMember("fetch", "$T.LAZY", ClassName.get("jakarta.persistence", "FetchType"))
.build())
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.persistence", "CollectionTable"))
.addMember("name", "$S", ec.collectionTable)
.addMember("joinColumns", "@$T(name = $S)",
ClassName.get("jakarta.persistence", "JoinColumn"), ec.joinColumn)
.build())
.initializer("new $T<>()", ArrayList.class);
if (ec.orderColumn != null) {
fieldBuilder.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.persistence", "OrderColumn"))
.addMember("name", "$S", ec.orderColumn)
.build());
}
if (ec.valueColumn != null) {
fieldBuilder.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.persistence", "Column"))
.addMember("name", "$S", ec.valueColumn)
.build());
}
classBuilder.addField(fieldBuilder.build());
}
}
private void addOneToManyFields(final TypeSpec.Builder classBuilder,
final ModelDefinition def, final String modelPackage) {
for (final OneToManyDefinition otm : def.oneToManyRelations) {
final ClassName entityClass = resolveClassName(otm.entityClass, modelPackage);
final FieldSpec.Builder fieldBuilder = FieldSpec.builder(
ParameterizedTypeName.get(ClassName.get(List.class), entityClass),
otm.fieldName, Modifier.PUBLIC)
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.persistence", "OneToMany"))
.addMember("fetch", "$T.EAGER", ClassName.get("jakarta.persistence", "FetchType"))
.build())
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.persistence", "JoinColumn"))
.addMember("name", "$S", otm.joinColumn)
.build())
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.persistence", "OrderBy"))
.addMember("value", "$S", otm.orderBy)
.build())
.initializer("new $T<>()", ArrayList.class);
if (otm.fetchMode != null) {
fieldBuilder.addAnnotation(AnnotationSpec.builder(ClassName.get("org.hibernate.annotations", "Fetch"))
.addMember("value", "$T.$L",
ClassName.get("org.hibernate.annotations", "FetchMode"), otm.fetchMode)
.build());
}
classBuilder.addField(fieldBuilder.build());
}
}
private MethodSpec buildMergeFromMethod(final ModelDefinition def, final ClassName modelClass) {
final MethodSpec.Builder builder = MethodSpec.methodBuilder("mergeFrom")
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(ParameterSpec.builder(modelClass, "source").addModifiers(Modifier.FINAL).build());
for (final FieldDefinition field : def.fields) {
builder.addStatement("this.$L = source.$L", field.name, field.name);
}
return builder.build();
}
// -----------------------------------------------------------------------
// DAO
// -----------------------------------------------------------------------
private JavaFile generateDAO(final ModelDefinition def) {
final String modelPackage = def.packageBase + ".model";
final String daoPackage = def.packageBase + ".dao";
final ClassName modelClass = ClassName.get(modelPackage, def.modelName);
final TypeSpec.Builder classBuilder = TypeSpec.classBuilder(def.modelName + "DAO")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(ClassName.get("jakarta.enterprise.context", "ApplicationScoped"))
.superclass(ParameterizedTypeName.get(
ClassName.get(COMMON_PKG + ".dao", "AbstractDAO"), modelClass));
classBuilder.addMethod(MethodSpec.methodBuilder("getEntityClass")
.addAnnotation(Override.class)
.addModifiers(Modifier.PROTECTED)
.returns(ParameterizedTypeName.get(ClassName.get(Class.class), modelClass))
.addStatement("return $T.class", modelClass)
.build());
if (hasAthleteId(def)) {
classBuilder.addMethod(buildGetByAthleteIdDAO(modelClass));
}
return JavaFile.builder(daoPackage, classBuilder.build()).skipJavaLangImports(true).build();
}
private MethodSpec buildGetByAthleteIdDAO(final ClassName modelClass) {
return MethodSpec.methodBuilder("getByAthleteId")
.addModifiers(Modifier.PUBLIC)
.returns(ParameterizedTypeName.get(ClassName.get(List.class), modelClass))
.addParameter(ParameterSpec.builder(TypeName.LONG, "athleteId").addModifiers(Modifier.FINAL).build())
.addStatement("final $T cb = em.getCriteriaBuilder()",
ClassName.get("jakarta.persistence.criteria", "CriteriaBuilder"))
.addStatement("final $T<$T> cq = cb.createQuery($T.class)",
ClassName.get("jakarta.persistence.criteria", "CriteriaQuery"), modelClass, modelClass)
.addStatement("final $T<$T> root = cq.from($T.class)",
ClassName.get("jakarta.persistence.criteria", "Root"), modelClass, modelClass)
.addStatement("cq.select(root).where(cb.equal(root.get($S), athleteId))", "athleteId")
.addStatement("return em.createQuery(cq).getResultList()")
.build();
}
// -----------------------------------------------------------------------
// Service
// -----------------------------------------------------------------------
private JavaFile generateService(final ModelDefinition def) {
final String modelPackage = def.packageBase + ".model";
final String daoPackage = def.packageBase + ".dao";
final String servicePackage = def.packageBase + ".service";
final ClassName modelClass = ClassName.get(modelPackage, def.modelName);
final ClassName daoClass = ClassName.get(daoPackage, def.modelName + "DAO");
final ClassName transactional = ClassName.get("jakarta.transaction", "Transactional");
final String daoFieldName = toLowerFirst(def.modelName) + "DAO";
final String entityParam = toLowerFirst(def.modelName);
final TypeSpec.Builder classBuilder = TypeSpec.classBuilder(def.modelName + "Service")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(ClassName.get("jakarta.enterprise.context", "ApplicationScoped"))
.superclass(ParameterizedTypeName.get(
ClassName.get(COMMON_PKG + ".service", "AbstractService"), modelClass, daoClass));
classBuilder.addField(FieldSpec.builder(daoClass, daoFieldName)
.addAnnotation(ClassName.get("jakarta.inject", "Inject"))
.addAnnotation(suppressNullAwayInit())
.build());
classBuilder.addMethod(MethodSpec.methodBuilder("getDAO")
.addAnnotation(Override.class)
.addModifiers(Modifier.PROTECTED)
.returns(daoClass)
.addStatement("return $L", daoFieldName)
.build());
classBuilder.addMethod(MethodSpec.methodBuilder("getById")
.addAnnotation(Override.class)
.addAnnotation(transactional)
.addModifiers(Modifier.PUBLIC)
.returns(modelClass)
.addParameter(ParameterSpec.builder(TypeName.LONG, "id").addModifiers(Modifier.FINAL).build())
.addStatement("return super.getById(id)")
.build());
classBuilder.addMethod(MethodSpec.methodBuilder("getAll")
.addAnnotation(Override.class)
.addAnnotation(transactional)
.addModifiers(Modifier.PUBLIC)
.returns(ParameterizedTypeName.get(ClassName.get(List.class), modelClass))
.addStatement("return super.getAll()")
.build());
classBuilder.addMethod(MethodSpec.methodBuilder("insert")
.addAnnotation(Override.class)
.addAnnotation(transactional)
.addModifiers(Modifier.PUBLIC)
.addParameter(ParameterSpec.builder(modelClass, entityParam).addModifiers(Modifier.FINAL).build())
.returns(TypeName.VOID)
.addStatement("super.insert($L)", entityParam)
.build());
classBuilder.addMethod(MethodSpec.methodBuilder("update")
.addAnnotation(Override.class)
.addAnnotation(transactional)
.addModifiers(Modifier.PUBLIC)
.addParameter(ParameterSpec.builder(modelClass, entityParam).addModifiers(Modifier.FINAL).build())
.returns(TypeName.VOID)
.addStatement("super.update($L)", entityParam)
.build());
classBuilder.addMethod(MethodSpec.methodBuilder("delete")
.addAnnotation(Override.class)
.addAnnotation(transactional)
.addModifiers(Modifier.PUBLIC)
.addParameter(ParameterSpec.builder(modelClass, entityParam).addModifiers(Modifier.FINAL).build())
.returns(TypeName.VOID)
.addStatement("super.delete($L)", entityParam)
.build());
if (hasAthleteId(def)) {
classBuilder.addMethod(MethodSpec.methodBuilder("getByAthleteId")
.addAnnotation(transactional)
.addModifiers(Modifier.PUBLIC)
.returns(ParameterizedTypeName.get(ClassName.get(List.class), modelClass))
.addParameter(ParameterSpec.builder(TypeName.LONG, "athleteId").addModifiers(Modifier.FINAL).build())
.addStatement("return $L.getByAthleteId(athleteId)", daoFieldName)
.build());
}
return JavaFile.builder(servicePackage, classBuilder.build()).skipJavaLangImports(true).build();
}
// -----------------------------------------------------------------------
// Resource
// -----------------------------------------------------------------------
private JavaFile generateResource(final ModelDefinition def) {
final String modelPackage = def.packageBase + ".model";
final String servicePackage = def.packageBase + ".service";
final ClassName modelClass = ClassName.get(modelPackage, def.modelName);
final ClassName serviceClass = ClassName.get(servicePackage, def.modelName + "Service");
final ClassName restResponse = ClassName.get("org.jboss.resteasy.reactive", "RestResponse");
final ClassName noResultException = ClassName.get("jakarta.persistence", "NoResultException");
final ClassName response = ClassName.get("jakarta.ws.rs.core", "Response");
final ClassName mediaType = ClassName.get("jakarta.ws.rs.core", "MediaType");
final String serviceFieldName = toLowerFirst(def.modelName) + "Service";
final String entityParam = toLowerFirst(def.modelName);
final TypeSpec.Builder classBuilder = TypeSpec.classBuilder(def.modelName + "Resource")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.ws.rs", "Path"))
.addMember("value", "$S", def.apiPath)
.build())
.addAnnotation(ClassName.get("jakarta.enterprise.context", "ApplicationScoped"))
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.ws.rs", "Produces"))
.addMember("value", "$T.APPLICATION_JSON", mediaType)
.build())
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.ws.rs", "Consumes"))
.addMember("value", "$T.APPLICATION_JSON", mediaType)
.build())
.superclass(ClassName.get(COMMON_PKG, "AbstractResource"));
classBuilder.addField(FieldSpec.builder(serviceClass, serviceFieldName)
.addAnnotation(ClassName.get("jakarta.inject", "Inject"))
.addAnnotation(suppressNullAwayInit())
.build());
addGetEndpoint(classBuilder, def, modelClass, serviceClass, serviceFieldName, restResponse);
classBuilder.addMethod(buildGetByIdEndpoint(modelClass, serviceFieldName, restResponse, noResultException));
classBuilder.addMethod(buildCreateEndpoint(modelClass, serviceFieldName, entityParam, restResponse, response));
classBuilder.addMethod(buildUpdateEndpoint(modelClass, serviceFieldName, entityParam, restResponse, noResultException));
classBuilder.addMethod(buildDeleteEndpoint(modelClass, serviceFieldName, restResponse, noResultException));
return JavaFile.builder(def.packageBase, classBuilder.build()).skipJavaLangImports(true).build();
}
private void addGetEndpoint(final TypeSpec.Builder classBuilder, final ModelDefinition def,
final ClassName modelClass, final ClassName serviceClass,
final String serviceFieldName, final ClassName restResponse) {
final TypeName listResponse = ParameterizedTypeName.get(restResponse,
ParameterizedTypeName.get(ClassName.get(List.class), modelClass));
if (hasAthleteId(def)) {
classBuilder.addMethod(MethodSpec.methodBuilder("getByAthlete")
.addAnnotation(ClassName.get("jakarta.ws.rs", "GET"))
.addModifiers(Modifier.PUBLIC)
.returns(listResponse)
.addParameter(ParameterSpec.builder(ClassName.get(Long.class), "athleteId")
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.ws.rs", "QueryParam"))
.addMember("value", "$S", "athleteId").build())
.addAnnotation(ClassName.get("jakarta.annotation", "Nullable"))
.addModifiers(Modifier.FINAL)
.build())
.beginControlFlow("if (athleteId == null)")
.addStatement("throw new $T($S)",
ClassName.get("jakarta.ws.rs", "BadRequestException"),
"athleteId query parameter is required")
.endControlFlow()
.addStatement("return $T.ok($L.getByAthleteId(athleteId))", restResponse, serviceFieldName)
.build());
} else {
classBuilder.addMethod(MethodSpec.methodBuilder("getAll")
.addAnnotation(ClassName.get("jakarta.ws.rs", "GET"))
.addModifiers(Modifier.PUBLIC)
.returns(listResponse)
.addStatement("return $T.ok($L.getAll())", restResponse, serviceFieldName)
.build());
}
}
private MethodSpec buildGetByIdEndpoint(final ClassName modelClass, final String serviceFieldName,
final ClassName restResponse, final ClassName noResultException) {
return MethodSpec.methodBuilder("getById")
.addAnnotation(ClassName.get("jakarta.ws.rs", "GET"))
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.ws.rs", "Path"))
.addMember("value", "$S", "/{id}").build())
.addModifiers(Modifier.PUBLIC)
.returns(ParameterizedTypeName.get(restResponse, modelClass))
.addParameter(buildIdPathParam())
.beginControlFlow("try")
.addStatement("return $T.ok($L.getById(id))", restResponse, serviceFieldName)
.nextControlFlow("catch (final $T e)", noResultException)
.addStatement("return $T.notFound()", restResponse)
.endControlFlow()
.build();
}
private MethodSpec buildCreateEndpoint(final ClassName modelClass, final String serviceFieldName,
final String entityParam, final ClassName restResponse,
final ClassName response) {
return MethodSpec.methodBuilder("create")
.addAnnotation(ClassName.get("jakarta.ws.rs", "POST"))
.addModifiers(Modifier.PUBLIC)
.returns(ParameterizedTypeName.get(restResponse, modelClass))
.addParameter(ParameterSpec.builder(modelClass, entityParam).addModifiers(Modifier.FINAL).build())
.addStatement("$L.insert($L)", serviceFieldName, entityParam)
.addStatement("return $T.status($T.Status.CREATED, $L)", restResponse, response, entityParam)
.build();
}
private MethodSpec buildUpdateEndpoint(final ClassName modelClass, final String serviceFieldName,
final String entityParam, final ClassName restResponse,
final ClassName noResultException) {
return MethodSpec.methodBuilder("update")
.addAnnotation(ClassName.get("jakarta.ws.rs", "PUT"))
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.ws.rs", "Path"))
.addMember("value", "$S", "/{id}").build())
.addModifiers(Modifier.PUBLIC)
.returns(ParameterizedTypeName.get(restResponse, modelClass))
.addParameter(buildIdPathParam())
.addParameter(ParameterSpec.builder(modelClass, entityParam).addModifiers(Modifier.FINAL).build())
.beginControlFlow("try")
.addStatement("final $T existing = $L.getById(id)", modelClass, serviceFieldName)
.addStatement("existing.mergeFrom($L)", entityParam)
.addStatement("$L.update(existing)", serviceFieldName)
.addStatement("return $T.ok(existing)", restResponse)
.nextControlFlow("catch (final $T e)", noResultException)
.addStatement("return $T.notFound()", restResponse)
.endControlFlow()
.build();
}
private MethodSpec buildDeleteEndpoint(final ClassName modelClass, final String serviceFieldName,
final ClassName restResponse, final ClassName noResultException) {
return MethodSpec.methodBuilder("delete")
.addAnnotation(ClassName.get("jakarta.ws.rs", "DELETE"))
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.ws.rs", "Path"))
.addMember("value", "$S", "/{id}").build())
.addModifiers(Modifier.PUBLIC)
.returns(ParameterizedTypeName.get(restResponse, ClassName.get(Void.class)))
.addParameter(buildIdPathParam())
.beginControlFlow("try")
.addStatement("final $T existing = $L.getById(id)", modelClass, serviceFieldName)
.addStatement("$L.delete(existing)", serviceFieldName)
.addStatement("return $T.noContent()", restResponse)
.nextControlFlow("catch (final $T e)", noResultException)
.addStatement("return $T.notFound()", restResponse)
.endControlFlow()
.build();
}
// -----------------------------------------------------------------------
// Shared field builders
// -----------------------------------------------------------------------
private FieldSpec buildColumnField(final FieldDefinition field, final String modelPackage,
final Set<String> enumNames) {
final TypeName typeName = resolveTypeName(field.javaType, modelPackage, enumNames);
final AnnotationSpec.Builder colAnn = AnnotationSpec.builder(ClassName.get("jakarta.persistence", "Column"))
.addMember("name", "$S", field.columnName);
if (!field.nullable && !isPrimitive(field.javaType)) {
colAnn.addMember("nullable", "$L", false);
}
if (field.columnDefinition != null) {
colAnn.addMember("columnDefinition", "$S", field.columnDefinition);
}
final FieldSpec.Builder fieldBuilder = FieldSpec.builder(typeName, field.name, Modifier.PUBLIC)
.addAnnotation(colAnn.build());
if (field.nullable) {
fieldBuilder.addAnnotation(ClassName.get("jakarta.annotation", "Nullable"));
}
if ("ARRAY".equals(field.jdbcTypeCode)) {
fieldBuilder.addAnnotation(AnnotationSpec.builder(ClassName.get("org.hibernate.annotations", "JdbcTypeCode"))
.addMember("value", "$T.ARRAY", ClassName.get("org.hibernate.type", "SqlTypes"))
.build());
}
return fieldBuilder.build();
}
private MethodSpec buildHibernateConstructor() {
return MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "NullAway.Init")
.build())
.addComment("hibernate")
.build();
}
private ParameterSpec buildIdPathParam() {
return ParameterSpec.builder(TypeName.LONG, "id")
.addAnnotation(AnnotationSpec.builder(ClassName.get("jakarta.ws.rs", "PathParam"))
.addMember("value", "$S", "id").build())
.addModifiers(Modifier.FINAL)
.build();
}
private AnnotationSpec suppressNullAwayInit() {
return AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "NullAway.Init")
.build();
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
private static TypeName resolveTypeName(final String javaType, final String modelPackage,
final Set<String> localTypes) {
return switch (javaType) {
case "int" -> TypeName.INT;
case "long" -> TypeName.LONG;
case "double" -> TypeName.DOUBLE;
case "float" -> TypeName.FLOAT;
case "boolean" -> TypeName.BOOLEAN;
case "Integer" -> ClassName.get(Integer.class);
case "Long" -> ClassName.get(Long.class);
case "Double" -> ClassName.get(Double.class);
case "Float" -> ClassName.get(Float.class);
case "Boolean" -> ClassName.get(Boolean.class);
case "String" -> ClassName.get(String.class);
case "int[]" -> ArrayTypeName.of(TypeName.INT);
case "long[]" -> ArrayTypeName.of(TypeName.LONG);
case "double[]" -> ArrayTypeName.of(TypeName.DOUBLE);
case "String[]" -> ArrayTypeName.of(ClassName.get(String.class));
case "Instant" -> ClassName.get("java.time", "Instant");
case "LocalDate" -> ClassName.get("java.time", "LocalDate");
default -> resolveClassName(javaType, modelPackage);
};
}
private static ClassName resolveClassName(final String className, final String modelPackage) {
if (className.contains(".")) {
final int lastDot = className.lastIndexOf('.');
return ClassName.get(className.substring(0, lastDot), className.substring(lastDot + 1));
}
return ClassName.get(modelPackage, className);
}
private static boolean isPrimitive(final String javaType) {
return switch (javaType) {
case "int", "long", "double", "float", "boolean" -> true;
default -> false;
};
}
private static boolean hasAthleteId(final ModelDefinition def) {
return def.fields.stream().anyMatch(f -> "athleteId".equals(f.name));
}
private static String toLowerFirst(final String name) {
if (name == null || name.isEmpty()) {
return name;
}
return Character.toLowerCase(name.charAt(0)) + name.substring(1);
}
}

View File

@@ -0,0 +1,17 @@
package com.peakup.tools.generator;
import jakarta.annotation.Nullable;
public class OneToManyDefinition {
public String fieldName;
/**
* Simple or fully-qualified class name of the target entity.
* Simple names are resolved against the model package; FQNs are used as-is.
*/
public String entityClass;
public String joinColumn;
public String orderBy;
/** Hibernate FetchMode constant name, e.g. "SUBSELECT". Null omits @Fetch. */
public @Nullable String fetchMode;
}

View File

@@ -0,0 +1,60 @@
{
"modelName": "TrainingNote",
"packageBase": "com.peakup.trainingnote",
"tableName": "training_note",
"baseClass": "AbstractTimestampedEntity",
"apiPath": "/api/training-notes",
"jsonIncludeNonNull": false,
"fields": [
{ "name": "athleteId", "javaType": "Long", "nullable": false, "columnName": "athlete_id" },
{ "name": "title", "javaType": "String", "nullable": false, "columnName": "title" },
{ "name": "body", "javaType": "String", "nullable": true, "columnName": "body", "columnDefinition": "TEXT" },
{ "name": "noteType", "javaType": "NoteTypeEnum", "nullable": false, "columnName": "note_type" },
{ "name": "streamTypes", "javaType": "String[]", "nullable": true, "columnName": "stream_types", "columnDefinition": "text[]", "jdbcTypeCode": "ARRAY" }
],
"elementCollections": [
{
"fieldName": "tags",
"collectionTable": "training_note_tag",
"joinColumn": "training_note_id",
"valueType": "String",
"valueColumn": "tag",
"orderColumn": null,
"embeddable": null
},
{
"fieldName": "attachments",
"collectionTable": "training_note_attachment",
"joinColumn": "training_note_id",
"valueType": "NoteAttachment",
"valueColumn": null,
"orderColumn": "sort_index",
"embeddable": {
"className": "NoteAttachment",
"fields": [
{ "name": "url", "javaType": "String", "nullable": false, "columnName": "url" },
{ "name": "filename", "javaType": "String", "nullable": true, "columnName": "filename" }
]
}
}
],
"oneToManyRelations": [
{
"fieldName": "steps",
"entityClass": "NoteStep",
"joinColumn": "training_note_id",
"orderBy": "sortIndex ASC",
"fetchMode": "SUBSELECT"
}
],
"enums": [
{
"className": "NoteTypeEnum",
"values": [
{ "name": "GENERAL", "key": 1 },
{ "name": "RACE", "key": 2 },
{ "name": "RECOVERY", "key": 3 }
]
}
]
}