diff --git a/codegen-superclass-test/.gitignore b/codegen-superclass-test/.gitignore new file mode 100644 index 0000000..cbaaccb --- /dev/null +++ b/codegen-superclass-test/.gitignore @@ -0,0 +1,19 @@ +/.classpath +/.gradle/ +/.project +/.settings/ +/bin/ +/build/ +/target/ +/.metadata +/.idea/ +.factorypath +/.claude/ +/data/ +/.apt_generated/ + +/src/main/java/codegen +/src/main/kotlin/codegen +/src/main/resources/META-INF/codegen +/src/test/java/codegen +/src/test/kotlin/codegen diff --git a/codegen-superclass-test/build.gradle.kts b/codegen-superclass-test/build.gradle.kts new file mode 100644 index 0000000..1ce7ed5 --- /dev/null +++ b/codegen-superclass-test/build.gradle.kts @@ -0,0 +1,97 @@ +plugins { + java + alias(libs.plugins.doma.compile) + id("org.domaframework.doma.codegen") +} + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(17)) +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation(libs.doma.core) + annotationProcessor(libs.doma.processor) + + testImplementation(platform(libs.junit.bom)) + testImplementation(libs.junit.jupiter.api) + testRuntimeOnly(libs.junit.jupiter.engine) + testRuntimeOnly(libs.junit.platform.launcher) + + testImplementation(libs.h2) + + domaCodeGen(libs.h2) + // Add compiled classes to domaCodeGen classpath for superclass resolution + domaCodeGen(sourceSets.main.get().output) +} + +val initScript = file("init_h2.sql") +val _url = "jdbc:h2:mem:superclass_test;INIT=RUNSCRIPT FROM 'file:${initScript.absolutePath}'" +val _user = "" +val _password = "" + +domaCodeGen { + register("superclass") { + val basePackage = "codegen" + url = _url + user = _user + password = _password + schemaName = "PUBLIC" + entity { + packageName = "${basePackage}.entity" + // Use AbstractEntity as superclass + superclassName = "base.AbstractEntity" + // Use AbstractEntityListener as listener superclass + listenerSuperclassName = "base.AbstractEntityListener" + } + dao { + packageName = "${basePackage}.dao" + } + } +} + +tasks { + // Step 1: Create a task to compile only base classes first + val compileBaseClasses = register("compileBaseClasses") { + source = fileTree("src/main/java") { + include("**/base/**") + } + classpath = configurations.compileClasspath.get() + destinationDirectory.set(layout.buildDirectory.dir("classes/java/main")) + } + + // Step 2: domaCodeGen depends on base classes being compiled + matching { it.name.startsWith("domaCodeGen") }.configureEach { + dependsOn(compileBaseClasses) + } + + // Step 3: Full compilation depends on code generation + compileJava { + dependsOn("domaCodeGenSuperclassAll") + } + + test { + useJUnitPlatform() + } + + val deleteSrc = register("deleteSrc") { + doLast { + // Delete only generated code, not base classes + delete("src/main/java/codegen") + delete("src/main/kotlin/codegen") + delete("src/main/resources/META-INF/codegen") + delete("src/test/java/codegen") + delete("src/test/kotlin/codegen") + } + } + + clean { + dependsOn(deleteSrc) + } +} + + diff --git a/codegen-superclass-test/init_h2.sql b/codegen-superclass-test/init_h2.sql new file mode 100644 index 0000000..192c78a --- /dev/null +++ b/codegen-superclass-test/init_h2.sql @@ -0,0 +1,52 @@ +-- Create test tables with superclass fields + +CREATE TABLE EMPLOYEE ( + ID BIGINT PRIMARY KEY AUTO_INCREMENT, + NAME VARCHAR(100) NOT NULL, + EMAIL VARCHAR(100), + DEPARTMENT VARCHAR(50), + SALARY DECIMAL(10, 2), + -- Superclass fields + CREATED_BY VARCHAR(50), + CREATED_AT TIMESTAMP, + UPDATED_BY VARCHAR(50), + UPDATED_AT TIMESTAMP, + DELETED BOOLEAN DEFAULT FALSE +); + +CREATE TABLE DEPARTMENT ( + ID BIGINT PRIMARY KEY AUTO_INCREMENT, + DEPT_NAME VARCHAR(100) NOT NULL, + LOCATION VARCHAR(100), + -- Superclass fields + CREATED_BY VARCHAR(50), + CREATED_AT TIMESTAMP, + UPDATED_BY VARCHAR(50), + UPDATED_AT TIMESTAMP, + DELETED BOOLEAN DEFAULT FALSE +); + +CREATE TABLE PROJECT ( + ID BIGINT PRIMARY KEY AUTO_INCREMENT, + PROJECT_NAME VARCHAR(200) NOT NULL, + START_DATE DATE, + END_DATE DATE, + BUDGET DECIMAL(15, 2), + -- Superclass fields + CREATED_BY VARCHAR(50), + CREATED_AT TIMESTAMP, + UPDATED_BY VARCHAR(50), + UPDATED_AT TIMESTAMP, + DELETED BOOLEAN DEFAULT FALSE +); + +-- Insert sample data +INSERT INTO EMPLOYEE (NAME, EMAIL, DEPARTMENT, SALARY, CREATED_BY, CREATED_AT, UPDATED_BY, UPDATED_AT, DELETED) +VALUES ('John Doe', 'john@example.com', 'Engineering', 75000.00, 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, FALSE); + +INSERT INTO DEPARTMENT (DEPT_NAME, LOCATION, CREATED_BY, CREATED_AT, UPDATED_BY, UPDATED_AT, DELETED) +VALUES ('Engineering', 'Building A', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, FALSE); + +INSERT INTO PROJECT (PROJECT_NAME, START_DATE, END_DATE, BUDGET, CREATED_BY, CREATED_AT, UPDATED_BY, UPDATED_AT, DELETED) +VALUES ('Project Alpha', '2026-01-01', '2026-12-31', 500000.00, 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, FALSE); + diff --git a/codegen-superclass-test/src/main/java/base/AbstractEntity.java b/codegen-superclass-test/src/main/java/base/AbstractEntity.java new file mode 100644 index 0000000..77d13e2 --- /dev/null +++ b/codegen-superclass-test/src/main/java/base/AbstractEntity.java @@ -0,0 +1,72 @@ +package base; + +import java.io.Serializable; +import java.time.LocalDateTime; +import org.seasar.doma.Column; + +/** + * Base entity class with common audit fields. + * This class demonstrates superclass functionality in Doma CodeGen. + */ +public abstract class AbstractEntity implements Serializable { + + /** Created by user ID */ + @Column(name = "CREATED_BY") + private String createdBy; + + /** Created timestamp */ + @Column(name = "CREATED_AT") + private LocalDateTime createdAt; + + /** Last updated by user ID */ + @Column(name = "UPDATED_BY") + private String updatedBy; + + /** Last updated timestamp */ + @Column(name = "UPDATED_AT") + private LocalDateTime updatedAt; + + /** Logical delete flag */ + @Column(name = "DELETED") + private Boolean deleted; + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public Boolean getDeleted() { + return deleted; + } + + public void setDeleted(Boolean deleted) { + this.deleted = deleted; + } +} diff --git a/codegen-superclass-test/src/main/java/base/AbstractEntityListener.java b/codegen-superclass-test/src/main/java/base/AbstractEntityListener.java new file mode 100644 index 0000000..cc45c97 --- /dev/null +++ b/codegen-superclass-test/src/main/java/base/AbstractEntityListener.java @@ -0,0 +1,32 @@ +package base; + +import java.time.LocalDateTime; +import org.seasar.doma.jdbc.entity.EntityListener; +import org.seasar.doma.jdbc.entity.PreInsertContext; +import org.seasar.doma.jdbc.entity.PreUpdateContext; + +/** + * Base entity listener that automatically sets audit fields. + */ +public class AbstractEntityListener implements EntityListener { + + @Override + public void preInsert(E entity, PreInsertContext context) { + LocalDateTime now = LocalDateTime.now(); + entity.setCreatedAt(now); + entity.setUpdatedAt(now); + entity.setCreatedBy("system"); + entity.setUpdatedBy("system"); + if (entity.getDeleted() == null) { + entity.setDeleted(false); + } + } + + @Override + public void preUpdate(E entity, PreUpdateContext context) { + entity.setUpdatedAt(LocalDateTime.now()); + entity.setUpdatedBy("system"); + } +} + + diff --git a/codegen/src/main/java/org/seasar/doma/gradle/codegen/CodeGenPlugin.java b/codegen/src/main/java/org/seasar/doma/gradle/codegen/CodeGenPlugin.java index 00fb1b4..5766c32 100644 --- a/codegen/src/main/java/org/seasar/doma/gradle/codegen/CodeGenPlugin.java +++ b/codegen/src/main/java/org/seasar/doma/gradle/codegen/CodeGenPlugin.java @@ -188,6 +188,7 @@ private void connectProperties(CodeGenEntityDescTask task, CodeGenConfig extensi task.getVersionColumnNamePattern().set(extension.getVersionColumnNamePattern()); task.getLanguageClassResolver().set(extension.getLanguageClassResolver()); task.setEntityConfig(extension.getEntityConfig()); + task.setCodeGenClassLoaderProvider(extension.getClassLoaderProvider()); } private void connectProperties(CodeGenEntityTask task, CodeGenConfig extension) { diff --git a/codegen/src/main/java/org/seasar/doma/gradle/codegen/extension/CodeGenConfig.java b/codegen/src/main/java/org/seasar/doma/gradle/codegen/extension/CodeGenConfig.java index a5cf109..c778a52 100644 --- a/codegen/src/main/java/org/seasar/doma/gradle/codegen/extension/CodeGenConfig.java +++ b/codegen/src/main/java/org/seasar/doma/gradle/codegen/extension/CodeGenConfig.java @@ -38,6 +38,8 @@ public class CodeGenConfig { private final String name; + private final Project project; + private final Configuration configuration; private final Property globalFactory; @@ -93,6 +95,7 @@ public class CodeGenConfig { @Inject public CodeGenConfig(String name, Project project) { this.name = name; + this.project = project; this.configuration = project.getConfigurations().getByName(CONFIGURATION_NAME); @@ -473,4 +476,21 @@ public void sql(Action action) { public void sqlTest(Action action) { action.execute(sqlTestConfig); } + + /** + * Returns a Provider that creates a ClassLoader including the domaCodeGen configuration classpath. + * This can be used by tasks to load classes specified in entity configuration. + * + *

The ClassLoader is created lazily when the Provider is resolved (during task execution), + * ensuring that all dependencies in the domaCodeGen configuration are properly resolved. + * + *

Note: If the domaCodeGen configuration is empty or null, the class's own ClassLoader is returned. + * Otherwise, each call to {@code get()} creates a new URLClassLoader instance. In the latter case, + * the caller is responsible for managing the lifecycle of the returned ClassLoader. + * + * @return a Provider that creates a ClassLoader including the domaCodeGen configuration classpath + */ + public Provider getClassLoaderProvider() { + return project.provider(this::createClassLoader); + } } diff --git a/codegen/src/main/java/org/seasar/doma/gradle/codegen/task/CodeGenEntityDescTask.java b/codegen/src/main/java/org/seasar/doma/gradle/codegen/task/CodeGenEntityDescTask.java index 51dea53..8c2df7e 100644 --- a/codegen/src/main/java/org/seasar/doma/gradle/codegen/task/CodeGenEntityDescTask.java +++ b/codegen/src/main/java/org/seasar/doma/gradle/codegen/task/CodeGenEntityDescTask.java @@ -55,6 +55,8 @@ public class CodeGenEntityDescTask extends DefaultTask { private final Property languageClassResolver = getProject().getObjects().property(LanguageClassResolver.class); + private Provider codeGenClassLoaderProvider; + private EntityConfig entityConfig; @Internal @@ -121,6 +123,15 @@ public void setEntityConfig(EntityConfig entityConfig) { this.entityConfig = entityConfig; } + @Internal + public Provider getCodeGenClassLoaderProvider() { + return codeGenClassLoaderProvider; + } + + public void setCodeGenClassLoaderProvider(Provider codeGenClassLoaderProvider) { + this.codeGenClassLoaderProvider = codeGenClassLoaderProvider; + } + @TaskAction public void create() { EntityPropertyClassNameResolver entityPropertyClassNameResolver = @@ -165,7 +176,10 @@ private EntityDescFactory createEntityDescFactory( EntityPropertyDescFactory entityPropertyDescFactory) { Class superclass = null; if (entityConfig.getSuperclassName().isPresent()) { - superclass = ClassUtil.forName(entityConfig.getSuperclassName().get(), "superclassName"); + ClassLoader classLoader = codeGenClassLoaderProvider != null && codeGenClassLoaderProvider.isPresent() + ? codeGenClassLoaderProvider.get() + : getClass().getClassLoader(); + superclass = ClassUtil.forName(entityConfig.getSuperclassName().get(), "superclassName", classLoader); } return globalFactory .get() diff --git a/settings.gradle.kts b/settings.gradle.kts index 40f2f69..9c3f2fe 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,4 +11,5 @@ if (releaseVersion != null) { include("codegen-h2-test") include("codegen-tc-test") include("codegen-template-test") + include("codegen-superclass-test") }