Skip to content

Commit 287c3cd

Browse files
authored
Merge pull request #294 from tomo-yamaguchi-cre/fix/codegen-superclass-classloader
Enable resolution of Entity superclassName from the project classpath
2 parents eb21c2b + c84dbd3 commit 287c3cd

9 files changed

Lines changed: 309 additions & 1 deletion

File tree

codegen-superclass-test/.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/.classpath
2+
/.gradle/
3+
/.project
4+
/.settings/
5+
/bin/
6+
/build/
7+
/target/
8+
/.metadata
9+
/.idea/
10+
.factorypath
11+
/.claude/
12+
/data/
13+
/.apt_generated/
14+
15+
/src/main/java/codegen
16+
/src/main/kotlin/codegen
17+
/src/main/resources/META-INF/codegen
18+
/src/test/java/codegen
19+
/src/test/kotlin/codegen
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
plugins {
2+
java
3+
alias(libs.plugins.doma.compile)
4+
id("org.domaframework.doma.codegen")
5+
}
6+
7+
java {
8+
toolchain.languageVersion.set(JavaLanguageVersion.of(17))
9+
}
10+
11+
repositories {
12+
mavenLocal()
13+
mavenCentral()
14+
}
15+
16+
dependencies {
17+
implementation(libs.doma.core)
18+
annotationProcessor(libs.doma.processor)
19+
20+
testImplementation(platform(libs.junit.bom))
21+
testImplementation(libs.junit.jupiter.api)
22+
testRuntimeOnly(libs.junit.jupiter.engine)
23+
testRuntimeOnly(libs.junit.platform.launcher)
24+
25+
testImplementation(libs.h2)
26+
27+
domaCodeGen(libs.h2)
28+
// Add compiled classes to domaCodeGen classpath for superclass resolution
29+
domaCodeGen(sourceSets.main.get().output)
30+
}
31+
32+
val initScript = file("init_h2.sql")
33+
val _url = "jdbc:h2:mem:superclass_test;INIT=RUNSCRIPT FROM 'file:${initScript.absolutePath}'"
34+
val _user = ""
35+
val _password = ""
36+
37+
domaCodeGen {
38+
register("superclass") {
39+
val basePackage = "codegen"
40+
url = _url
41+
user = _user
42+
password = _password
43+
schemaName = "PUBLIC"
44+
entity {
45+
packageName = "${basePackage}.entity"
46+
// Use AbstractEntity as superclass
47+
superclassName = "base.AbstractEntity"
48+
// Use AbstractEntityListener as listener superclass
49+
listenerSuperclassName = "base.AbstractEntityListener"
50+
}
51+
dao {
52+
packageName = "${basePackage}.dao"
53+
}
54+
}
55+
}
56+
57+
tasks {
58+
// Step 1: Create a task to compile only base classes first
59+
val compileBaseClasses = register<JavaCompile>("compileBaseClasses") {
60+
source = fileTree("src/main/java") {
61+
include("**/base/**")
62+
}
63+
classpath = configurations.compileClasspath.get()
64+
destinationDirectory.set(layout.buildDirectory.dir("classes/java/main"))
65+
}
66+
67+
// Step 2: domaCodeGen depends on base classes being compiled
68+
matching { it.name.startsWith("domaCodeGen") }.configureEach {
69+
dependsOn(compileBaseClasses)
70+
}
71+
72+
// Step 3: Full compilation depends on code generation
73+
compileJava {
74+
dependsOn("domaCodeGenSuperclassAll")
75+
}
76+
77+
test {
78+
useJUnitPlatform()
79+
}
80+
81+
val deleteSrc = register("deleteSrc") {
82+
doLast {
83+
// Delete only generated code, not base classes
84+
delete("src/main/java/codegen")
85+
delete("src/main/kotlin/codegen")
86+
delete("src/main/resources/META-INF/codegen")
87+
delete("src/test/java/codegen")
88+
delete("src/test/kotlin/codegen")
89+
}
90+
}
91+
92+
clean {
93+
dependsOn(deleteSrc)
94+
}
95+
}
96+
97+
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
-- Create test tables with superclass fields
2+
3+
CREATE TABLE EMPLOYEE (
4+
ID BIGINT PRIMARY KEY AUTO_INCREMENT,
5+
NAME VARCHAR(100) NOT NULL,
6+
EMAIL VARCHAR(100),
7+
DEPARTMENT VARCHAR(50),
8+
SALARY DECIMAL(10, 2),
9+
-- Superclass fields
10+
CREATED_BY VARCHAR(50),
11+
CREATED_AT TIMESTAMP,
12+
UPDATED_BY VARCHAR(50),
13+
UPDATED_AT TIMESTAMP,
14+
DELETED BOOLEAN DEFAULT FALSE
15+
);
16+
17+
CREATE TABLE DEPARTMENT (
18+
ID BIGINT PRIMARY KEY AUTO_INCREMENT,
19+
DEPT_NAME VARCHAR(100) NOT NULL,
20+
LOCATION VARCHAR(100),
21+
-- Superclass fields
22+
CREATED_BY VARCHAR(50),
23+
CREATED_AT TIMESTAMP,
24+
UPDATED_BY VARCHAR(50),
25+
UPDATED_AT TIMESTAMP,
26+
DELETED BOOLEAN DEFAULT FALSE
27+
);
28+
29+
CREATE TABLE PROJECT (
30+
ID BIGINT PRIMARY KEY AUTO_INCREMENT,
31+
PROJECT_NAME VARCHAR(200) NOT NULL,
32+
START_DATE DATE,
33+
END_DATE DATE,
34+
BUDGET DECIMAL(15, 2),
35+
-- Superclass fields
36+
CREATED_BY VARCHAR(50),
37+
CREATED_AT TIMESTAMP,
38+
UPDATED_BY VARCHAR(50),
39+
UPDATED_AT TIMESTAMP,
40+
DELETED BOOLEAN DEFAULT FALSE
41+
);
42+
43+
-- Insert sample data
44+
INSERT INTO EMPLOYEE (NAME, EMAIL, DEPARTMENT, SALARY, CREATED_BY, CREATED_AT, UPDATED_BY, UPDATED_AT, DELETED)
45+
VALUES ('John Doe', 'john@example.com', 'Engineering', 75000.00, 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, FALSE);
46+
47+
INSERT INTO DEPARTMENT (DEPT_NAME, LOCATION, CREATED_BY, CREATED_AT, UPDATED_BY, UPDATED_AT, DELETED)
48+
VALUES ('Engineering', 'Building A', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, FALSE);
49+
50+
INSERT INTO PROJECT (PROJECT_NAME, START_DATE, END_DATE, BUDGET, CREATED_BY, CREATED_AT, UPDATED_BY, UPDATED_AT, DELETED)
51+
VALUES ('Project Alpha', '2026-01-01', '2026-12-31', 500000.00, 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, FALSE);
52+
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package base;
2+
3+
import java.io.Serializable;
4+
import java.time.LocalDateTime;
5+
import org.seasar.doma.Column;
6+
7+
/**
8+
* Base entity class with common audit fields.
9+
* This class demonstrates superclass functionality in Doma CodeGen.
10+
*/
11+
public abstract class AbstractEntity implements Serializable {
12+
13+
/** Created by user ID */
14+
@Column(name = "CREATED_BY")
15+
private String createdBy;
16+
17+
/** Created timestamp */
18+
@Column(name = "CREATED_AT")
19+
private LocalDateTime createdAt;
20+
21+
/** Last updated by user ID */
22+
@Column(name = "UPDATED_BY")
23+
private String updatedBy;
24+
25+
/** Last updated timestamp */
26+
@Column(name = "UPDATED_AT")
27+
private LocalDateTime updatedAt;
28+
29+
/** Logical delete flag */
30+
@Column(name = "DELETED")
31+
private Boolean deleted;
32+
33+
public String getCreatedBy() {
34+
return createdBy;
35+
}
36+
37+
public void setCreatedBy(String createdBy) {
38+
this.createdBy = createdBy;
39+
}
40+
41+
public LocalDateTime getCreatedAt() {
42+
return createdAt;
43+
}
44+
45+
public void setCreatedAt(LocalDateTime createdAt) {
46+
this.createdAt = createdAt;
47+
}
48+
49+
public String getUpdatedBy() {
50+
return updatedBy;
51+
}
52+
53+
public void setUpdatedBy(String updatedBy) {
54+
this.updatedBy = updatedBy;
55+
}
56+
57+
public LocalDateTime getUpdatedAt() {
58+
return updatedAt;
59+
}
60+
61+
public void setUpdatedAt(LocalDateTime updatedAt) {
62+
this.updatedAt = updatedAt;
63+
}
64+
65+
public Boolean getDeleted() {
66+
return deleted;
67+
}
68+
69+
public void setDeleted(Boolean deleted) {
70+
this.deleted = deleted;
71+
}
72+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package base;
2+
3+
import java.time.LocalDateTime;
4+
import org.seasar.doma.jdbc.entity.EntityListener;
5+
import org.seasar.doma.jdbc.entity.PreInsertContext;
6+
import org.seasar.doma.jdbc.entity.PreUpdateContext;
7+
8+
/**
9+
* Base entity listener that automatically sets audit fields.
10+
*/
11+
public class AbstractEntityListener<E extends AbstractEntity> implements EntityListener<E> {
12+
13+
@Override
14+
public void preInsert(E entity, PreInsertContext<E> context) {
15+
LocalDateTime now = LocalDateTime.now();
16+
entity.setCreatedAt(now);
17+
entity.setUpdatedAt(now);
18+
entity.setCreatedBy("system");
19+
entity.setUpdatedBy("system");
20+
if (entity.getDeleted() == null) {
21+
entity.setDeleted(false);
22+
}
23+
}
24+
25+
@Override
26+
public void preUpdate(E entity, PreUpdateContext<E> context) {
27+
entity.setUpdatedAt(LocalDateTime.now());
28+
entity.setUpdatedBy("system");
29+
}
30+
}
31+
32+

codegen/src/main/java/org/seasar/doma/gradle/codegen/CodeGenPlugin.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ private void connectProperties(CodeGenEntityDescTask task, CodeGenConfig extensi
188188
task.getVersionColumnNamePattern().set(extension.getVersionColumnNamePattern());
189189
task.getLanguageClassResolver().set(extension.getLanguageClassResolver());
190190
task.setEntityConfig(extension.getEntityConfig());
191+
task.setCodeGenClassLoaderProvider(extension.getClassLoaderProvider());
191192
}
192193

193194
private void connectProperties(CodeGenEntityTask task, CodeGenConfig extension) {

codegen/src/main/java/org/seasar/doma/gradle/codegen/extension/CodeGenConfig.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public class CodeGenConfig {
3838

3939
private final String name;
4040

41+
private final Project project;
42+
4143
private final Configuration configuration;
4244

4345
private final Property<GlobalFactory> globalFactory;
@@ -93,6 +95,7 @@ public class CodeGenConfig {
9395
@Inject
9496
public CodeGenConfig(String name, Project project) {
9597
this.name = name;
98+
this.project = project;
9699

97100
this.configuration = project.getConfigurations().getByName(CONFIGURATION_NAME);
98101

@@ -473,4 +476,21 @@ public void sql(Action<SqlConfig> action) {
473476
public void sqlTest(Action<SqlTestConfig> action) {
474477
action.execute(sqlTestConfig);
475478
}
479+
480+
/**
481+
* Returns a Provider that creates a ClassLoader including the domaCodeGen configuration classpath.
482+
* This can be used by tasks to load classes specified in entity configuration.
483+
*
484+
* <p>The ClassLoader is created lazily when the Provider is resolved (during task execution),
485+
* ensuring that all dependencies in the domaCodeGen configuration are properly resolved.
486+
*
487+
* <p>Note: If the domaCodeGen configuration is empty or null, the class's own ClassLoader is returned.
488+
* Otherwise, each call to {@code get()} creates a new URLClassLoader instance. In the latter case,
489+
* the caller is responsible for managing the lifecycle of the returned ClassLoader.
490+
*
491+
* @return a Provider that creates a ClassLoader including the domaCodeGen configuration classpath
492+
*/
493+
public Provider<ClassLoader> getClassLoaderProvider() {
494+
return project.provider(this::createClassLoader);
495+
}
476496
}

codegen/src/main/java/org/seasar/doma/gradle/codegen/task/CodeGenEntityDescTask.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ public class CodeGenEntityDescTask extends DefaultTask {
5555
private final Property<LanguageClassResolver> languageClassResolver =
5656
getProject().getObjects().property(LanguageClassResolver.class);
5757

58+
private Provider<ClassLoader> codeGenClassLoaderProvider;
59+
5860
private EntityConfig entityConfig;
5961

6062
@Internal
@@ -121,6 +123,15 @@ public void setEntityConfig(EntityConfig entityConfig) {
121123
this.entityConfig = entityConfig;
122124
}
123125

126+
@Internal
127+
public Provider<ClassLoader> getCodeGenClassLoaderProvider() {
128+
return codeGenClassLoaderProvider;
129+
}
130+
131+
public void setCodeGenClassLoaderProvider(Provider<ClassLoader> codeGenClassLoaderProvider) {
132+
this.codeGenClassLoaderProvider = codeGenClassLoaderProvider;
133+
}
134+
124135
@TaskAction
125136
public void create() {
126137
EntityPropertyClassNameResolver entityPropertyClassNameResolver =
@@ -165,7 +176,10 @@ private EntityDescFactory createEntityDescFactory(
165176
EntityPropertyDescFactory entityPropertyDescFactory) {
166177
Class<?> superclass = null;
167178
if (entityConfig.getSuperclassName().isPresent()) {
168-
superclass = ClassUtil.forName(entityConfig.getSuperclassName().get(), "superclassName");
179+
ClassLoader classLoader = codeGenClassLoaderProvider != null && codeGenClassLoaderProvider.isPresent()
180+
? codeGenClassLoaderProvider.get()
181+
: getClass().getClassLoader();
182+
superclass = ClassUtil.forName(entityConfig.getSuperclassName().get(), "superclassName", classLoader);
169183
}
170184
return globalFactory
171185
.get()

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ if (releaseVersion != null) {
1111
include("codegen-h2-test")
1212
include("codegen-tc-test")
1313
include("codegen-template-test")
14+
include("codegen-superclass-test")
1415
}

0 commit comments

Comments
 (0)