From f989249c2c43d348cdaeef071bb241eed80ef714 Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Fri, 19 Jun 2026 17:12:02 +1200 Subject: [PATCH] Fix for ebean-gradle-plugin issue #43 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root Cause The regression from 16.4.0 → 16.5.0+ is in ebean-agent, not the plugin code itself. What changed: The new EbeanEnhanceTask (introduced via PR #41) captures sourceSet.output.classesDirs at Gradle configuration time (afterEvaluate). The Kotlin kapt plugin may not have registered its output directory — which contains META-INF/ebean-generated-info.mf — into classesDirs at that point. The old plugin (16.4.0) evaluated classesDirs inside a doLast hook at task execution time, when all kapt outputs were already registered. Effect (chain of failures): 1. AgentManifest can't find ebean-generated-info.mf → entityPackages is empty 2. DetectQueryBean.isQueryBean("QRoleEntity") iterates empty entityPackages → returns false 3. AgentManifest.isDetectQueryBean("QRoleEntity") → false 4. EnhanceContext.isQueryBean() returns false immediately (the null-fallback is unreachable) 5. MethodAdapter does not replace GETFIELD permissions with _permissions() 6. Original Kotlin getter body runs → null-check on uninitialised lateinit field → UninitializedPropertyAccessException Fix File: ebean-agent/src/main/java/io/ebean/enhance/querybean/DetectQueryBean.java Added an empty-packages fallback in isQueryBean(): when entityPackages is empty (manifest not found), trust naming convention alone — any class in a .../query/Q... or .../query/assoc/Q... package is treated as a query bean. This is consistent with FilterQueryBean.detectOnAll which already applies the same logic. With the fix: - manifest.isDetectQueryBean("QRoleEntity") → true (naming convention match) - reader.get(true, owner, classLoader) reads bytes from enhancedDir - classMeta.isQueryBean() → true (@TypeQueryBean annotation present) - GETFIELD is replaced by _permissions() → enhancement works correctly --- .../enhance/querybean/DetectQueryBean.java | 9 ++++ .../querybean/DetectQueryBeanTest.java | 53 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 ebean-agent/src/test/java/io/ebean/enhance/querybean/DetectQueryBeanTest.java diff --git a/ebean-agent/src/main/java/io/ebean/enhance/querybean/DetectQueryBean.java b/ebean-agent/src/main/java/io/ebean/enhance/querybean/DetectQueryBean.java index 90a8622b..354129b8 100644 --- a/ebean-agent/src/main/java/io/ebean/enhance/querybean/DetectQueryBean.java +++ b/ebean-agent/src/main/java/io/ebean/enhance/querybean/DetectQueryBean.java @@ -54,12 +54,21 @@ public boolean isEmpty() { /** * Return true if this class is a query bean using naming conventions for query beans. + *

+ * When no entity packages are configured (e.g. ebean.mf was not accessible to the + * classloader — a known Gradle Kotlin KAPT scenario), fall back to naming-convention + * detection only, consistent with {@code FilterQueryBean.detectOnAll} behaviour. + *

*/ public boolean isQueryBean(String owner) { int subPackagePos = owner.lastIndexOf("/query/"); if (subPackagePos > -1) { String suffix = owner.substring(subPackagePos); if (isQueryBeanSuffix(suffix)) { + if (entityPackages.isEmpty()) { + // No entity packages loaded (manifest not found) — trust naming convention. + return true; + } String domainPackage = owner.substring(0, subPackagePos + 1); return isEntityBeanPackage(domainPackage); } diff --git a/ebean-agent/src/test/java/io/ebean/enhance/querybean/DetectQueryBeanTest.java b/ebean-agent/src/test/java/io/ebean/enhance/querybean/DetectQueryBeanTest.java new file mode 100644 index 00000000..f4d445fa --- /dev/null +++ b/ebean-agent/src/test/java/io/ebean/enhance/querybean/DetectQueryBeanTest.java @@ -0,0 +1,53 @@ +package io.ebean.enhance.querybean; + +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DetectQueryBeanTest { + + @Test + void isQueryBean_withPackages_matches() { + DetectQueryBean detect = new DetectQueryBean(); + detect.addAll(Set.of("de.worldinsight.wision")); + + assertThat(detect.isQueryBean("de/worldinsight/wision/auth/roles/query/QRoleEntity")).isTrue(); + assertThat(detect.isQueryBean("de/worldinsight/wision/domain/query/QCustomer")).isTrue(); + assertThat(detect.isQueryBean("de/worldinsight/wision/auth/roles/query/RoleEntity")).isFalse(); // no Q prefix + assertThat(detect.isQueryBean("com/other/query/QFoo")).isFalse(); // wrong package + } + + @Test + void isQueryBean_withPackages_noMatch_returnsFalse() { + DetectQueryBean detect = new DetectQueryBean(); + detect.addAll(Set.of("de.worldinsight.wision")); + + assertThat(detect.isQueryBean("com/other/query/QFoo")).isFalse(); + assertThat(detect.isQueryBean("de/worldinsight/wision/RoleEntity")).isFalse(); // not in query sub-package + } + + @Test + void isQueryBean_emptyPackages_namingConventionFallback() { + // When no packages loaded (ebean.mf not found — Gradle Kotlin KAPT scenario), + // trust naming convention: .../query/Q... classes are treated as query beans. + DetectQueryBean detect = new DetectQueryBean(); + assertThat(detect.isEmpty()).isTrue(); + + assertThat(detect.isQueryBean("de/worldinsight/wision/auth/roles/query/QRoleEntity")).isTrue(); + assertThat(detect.isQueryBean("com/example/domain/query/QCustomer")).isTrue(); + assertThat(detect.isQueryBean("com/example/domain/query/assoc/QAssocAddress")).isTrue(); + } + + @Test + void isQueryBean_emptyPackages_nonQueryClass_returnsFalse() { + DetectQueryBean detect = new DetectQueryBean(); + assertThat(detect.isEmpty()).isTrue(); + + assertThat(detect.isQueryBean("de/worldinsight/wision/auth/roles/RoleEntity")).isFalse(); + assertThat(detect.isQueryBean("de/worldinsight/wision/auth/roles/query/RoleRepository")).isFalse(); // no Q prefix + assertThat(detect.isQueryBean("com/example/SomeClass")).isFalse(); + } +} +