|
|
@@ -0,0 +1,386 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+
|
|
|
+import java.io.InputStream;
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.util.Scanner;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.junit.jupiter.api.Assertions.fail;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 契约级单测:管理端知识库管理(能力键 {@code admin.knowledgeBase.manage})。
|
|
|
+ *
|
|
|
+ * <p><b>契约来源(设计/需求溯源)</b>:
|
|
|
+ * <ul>
|
|
|
+ * <li>需求:{@code tests/user-scenarios/应用场景总览.md} <b>SC-A-08 知识库管理</b>(知识条目 CRUD → 向量同步;
|
|
|
+ * 边界:<b>同步失败重试;大批量分批</b>)与 <b>SC-A-22 精准营养知识库管理</b>(CRUD → 同步;边界:<b>同步失败重试</b>)。</li>
|
|
|
+ * <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.60.8 —— 载体为
|
|
|
+ * {@code KnowledgeBaseController} 系(非开工包猜测的 {@code AdminKnowledgeController}):
|
|
|
+ * {@code /api/admin/knowledge-base}(list/get/save/delete/upload-file/fetch-url/bind-tags/bind-dimensions/backfill-tags)
|
|
|
+ * + {@code KnowledgeBaseVectorController /api/admin/knowledge-base/vector}(stats/documents/sync/delete-source/update-document)
|
|
|
+ * + {@code KnowledgeTagController /api/admin/knowledge-tag}(list/save/delete)
|
|
|
+ * + {@code AdminUnifiedKnowledgeController /api/admin/knowledge-base/unified}(overview/section/update/reset)。</li>
|
|
|
+ * <li>契约核对:{@code docs/superpowers/specs/2026-11-07-admin-ops-contract.md} §1.8/§1.22 —— SC-A-08 载体确认存在,
|
|
|
+ * 写入端点权限粒度「留待测试侧补权限流核验」。</li>
|
|
|
+ * </ul></p>
|
|
|
+ *
|
|
|
+ * <p><b>实现现状(src/main 实测)</b>:
|
|
|
+ * <ul>
|
|
|
+ * <li>✅ <b>已落地</b>:四个 Controller(CRUD/向量/标签/统一知识库)与 {@code DanKnowledgeBaseService} /
|
|
|
+ * {@code UnifiedKnowledgeBaseService} 均存在,schema.sql 含 {@code dan_knowledge_base} / {@code dan_knowledge_tag} /
|
|
|
+ * {@code dan_knowledge_base_tag} / {@code dan_knowledge_base_dimension} / {@code knowledge_base_section} 建表。</li>
|
|
|
+ * <li>❌ <b>未落地(契约缺口,RED)</b>:① 同步失败重试 —— {@code LanggraphSyncService}(已标 deprecated)与
|
|
|
+ * {@code KnowledgeBaseVectorController} 同步路径无任何 retry/重试能力(无 {@code maxRetries}、无
|
|
|
+ * {@code syncWithRetry}、无 {@code @Retryable});② 大批量分批 —— 同步/回填路径无任何 batch 分批能力
|
|
|
+ * (无 {@code syncBatch}、无 {@code BATCH_SIZE});③ 知识库写入端点角色门禁 —— {@code KnowledgeBaseController} /
|
|
|
+ * {@code KnowledgeBaseVectorController} 无控制器内平台角色校验({@code AdminUnifiedKnowledgeController} 已有
|
|
|
+ * {@code allowed} 门禁,故该项仅对 base/vector 缺口红)。</li>
|
|
|
+ * </ul></p>
|
|
|
+ *
|
|
|
+ * <p><b>测试范围</b>:仅用反射 + schema.sql 类路径探测(无 Spring / 无 DB / 无外部服务),
|
|
|
+ * 与同批已合入主链的 {@code TeamProfitHookContractTest} 风格一致。</p>
|
|
|
+ *
|
|
|
+ * <p><b>RED 预期</b>:契约边界行为(同步失败重试 / 大批量分批)与 base/vector 角色门禁未落地,
|
|
|
+ * 对应用例在实现补齐前应失败(红);已落地的契约表面(CRUD/向量/标签/统一端点 + 统一知识库角色门禁)用例为绿,
|
|
|
+ * 作为「已存在」的登记证据。</p>
|
|
|
+ *
|
|
|
+ * <p><b>GREEN 升级指引</b>:实现者在同步路径补「失败重试」({@code maxRetries} 配置 / {@code syncWithRetry} /
|
|
|
+ * {@code @Retryable})、补「大批量分批」({@code syncBatch} / {@code BATCH_SIZE})、并为
|
|
|
+ * {@code KnowledgeBaseController} / {@code KnowledgeBaseVectorController} 补平台角色门禁后,
|
|
|
+ * 对应 RED 用例自动转绿。</p>
|
|
|
+ */
|
|
|
+class AdminKnowledgeBaseManageContractTest {
|
|
|
+
|
|
|
+ /** SC-A-08 载体 Controller 类(§4.60.8) */
|
|
|
+ private static final String KB_CONTROLLER = "com.etotem.cfc.controller.admin.KnowledgeBaseController";
|
|
|
+ private static final String KB_VECTOR_CONTROLLER = "com.etotem.cfc.controller.admin.KnowledgeBaseVectorController";
|
|
|
+ private static final String KB_TAG_CONTROLLER = "com.etotem.cfc.controller.admin.KnowledgeTagController";
|
|
|
+ private static final String KB_UNIFIED_CONTROLLER = "com.etotem.cfc.controller.admin.AdminUnifiedKnowledgeController";
|
|
|
+
|
|
|
+ /** SC-A-08/SC-A-22 Service 载体 */
|
|
|
+ private static final String KB_SERVICE = "com.etotem.cfc.service.DanKnowledgeBaseService";
|
|
|
+ private static final String UNIFIED_KB_SERVICE = "com.etotem.cfc.service.UnifiedKnowledgeBaseService";
|
|
|
+ private static final String SYNC_SERVICE = "com.etotem.cfc.service.LanggraphSyncService";
|
|
|
+
|
|
|
+ /** §4.60.8 端点子路径(Controller 内 {@code @PostMapping} 值) */
|
|
|
+ private static final String[] KB_CRUD_ENDPOINTS = {
|
|
|
+ "/list", "/get", "/save", "/delete", "/upload-file", "/fetch-url",
|
|
|
+ "/bind-tags", "/bind-dimensions", "/backfill-tags"
|
|
|
+ };
|
|
|
+ private static final String[] KB_VECTOR_ENDPOINTS = {
|
|
|
+ "/stats", "/documents", "/sync", "/delete-source", "/update-document"
|
|
|
+ };
|
|
|
+ private static final String[] KB_TAG_ENDPOINTS = {"/list", "/save", "/delete"};
|
|
|
+ private static final String[] KB_UNIFIED_ENDPOINTS = {"/overview", "/section", "/update", "/reset"};
|
|
|
+
|
|
|
+ /** schema.sql 应包含的知识库相关表 */
|
|
|
+ private static final String[] KB_TABLES = {
|
|
|
+ "dan_knowledge_base", "dan_knowledge_tag", "dan_knowledge_base_tag",
|
|
|
+ "dan_knowledge_base_dimension", "knowledge_base_section"
|
|
|
+ };
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 正常流:契约表面已落地(登记证据,GREEN)
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-1:KnowledgeBaseController 存在并承载知识条目 CRUD 端点(§4.60.8)")
|
|
|
+ void knowledgeBaseController_surfaceExists() {
|
|
|
+ Class<?> c = loadClass(KB_CONTROLLER);
|
|
|
+ assertNotNull(c, "[admin.knowledgeBase.manage] KnowledgeBaseController 类不存在(§4.60.8)。");
|
|
|
+ assertEndpoints(c, KB_CRUD_ENDPOINTS, "KnowledgeBaseController", "/api/admin/knowledge-base");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-2:KnowledgeBaseVectorController 存在并承载向量库端点(§4.60.8)")
|
|
|
+ void knowledgeBaseVectorController_surfaceExists() {
|
|
|
+ Class<?> c = loadClass(KB_VECTOR_CONTROLLER);
|
|
|
+ assertNotNull(c, "[admin.knowledgeBase.manage] KnowledgeBaseVectorController 类不存在(§4.60.8)。");
|
|
|
+ assertEndpoints(c, KB_VECTOR_ENDPOINTS, "KnowledgeBaseVectorController", "/api/admin/knowledge-base/vector");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-3:KnowledgeTagController 存在并承载知识标签字典端点(§4.60.8)")
|
|
|
+ void knowledgeTagController_surfaceExists() {
|
|
|
+ Class<?> c = loadClass(KB_TAG_CONTROLLER);
|
|
|
+ assertNotNull(c, "[admin.knowledgeBase.manage] KnowledgeTagController 类不存在(§4.60.8)。");
|
|
|
+ assertEndpoints(c, KB_TAG_ENDPOINTS, "KnowledgeTagController", "/api/admin/knowledge-tag");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-4:AdminUnifiedKnowledgeController 存在并承载统一知识库端点(§4.60.8)")
|
|
|
+ void adminUnifiedKnowledgeController_surfaceExists() {
|
|
|
+ Class<?> c = loadClass(KB_UNIFIED_CONTROLLER);
|
|
|
+ assertNotNull(c, "[admin.knowledgeBase.manage] AdminUnifiedKnowledgeController 类不存在(§4.60.8)。");
|
|
|
+ assertEndpoints(c, KB_UNIFIED_ENDPOINTS, "AdminUnifiedKnowledgeController", "/api/admin/knowledge-base/unified");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-5:知识库 Service 载体存在(DanKnowledgeBaseService / UnifiedKnowledgeBaseService)")
|
|
|
+ void knowledgeServices_surfaceExists() {
|
|
|
+ Class<?> svc = loadClass(KB_SERVICE);
|
|
|
+ assertNotNull(svc, "[admin.knowledgeBase.manage] DanKnowledgeBaseService 类不存在。");
|
|
|
+ assertTrue(hasMethod(svc, "saveWithAssociations"), "[admin.knowledgeBase.manage] "
|
|
|
+ + "DanKnowledgeBaseService 应提供 saveWithAssociations(保存+关联标签/维度)。");
|
|
|
+ assertTrue(hasMethod(svc, "getWithAssociations"), "[admin.knowledgeBase.manage] "
|
|
|
+ + "DanKnowledgeBaseService 应提供 getWithAssociations(详情+关联)。");
|
|
|
+ assertTrue(hasMethod(svc, "backfillTags"), "[admin.knowledgeBase.manage] "
|
|
|
+ + "DanKnowledgeBaseService 应提供 backfillTags(标签/维度回填)。");
|
|
|
+
|
|
|
+ Class<?> uni = loadClass(UNIFIED_KB_SERVICE);
|
|
|
+ assertNotNull(uni, "[admin.knowledgeBase.manage] UnifiedKnowledgeBaseService 类不存在。");
|
|
|
+ assertTrue(hasMethod(uni, "getModuleOverviewDetailed"), "[admin.knowledgeBase.manage] "
|
|
|
+ + "UnifiedKnowledgeBaseService 应提供 getModuleOverviewDetailed(模块概览)。");
|
|
|
+ assertTrue(hasMethod(uni, "updateModule"), "[admin.knowledgeBase.manage] "
|
|
|
+ + "UnifiedKnowledgeBaseService 应提供 updateModule(写库+热重载)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-6:知识库相关表落地 schema.sql(§2.2~2.5 设计)")
|
|
|
+ void schemaSql_hasKnowledgeTables() {
|
|
|
+ String schema = readSchemaSql();
|
|
|
+ for (String table : KB_TABLES) {
|
|
|
+ assertTrue(schema.contains(table),
|
|
|
+ "[admin.knowledgeBase.manage] schema.sql 缺少 " + table + " 建表。");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 边界流:同步失败重试(契约 SC-A-08/A-22 要求,当前未实现 → RED)
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-1:知识库同步路径应支持失败重试(SC-A-08/A-22 边界,未实现 → 红)")
|
|
|
+ void syncFailureRetry_syncPathShouldSupportRetry() {
|
|
|
+ // 契约 AC:知识条目 CRUD → 向量同步,边界「同步失败重试」。
|
|
|
+ // 探测同步路径(LanggraphSyncService / KnowledgeBaseVectorController / DanKnowledgeBaseService)
|
|
|
+ // 是否暴露重试能力:syncWithRetry 方法 / maxRetries、retryCount 字段 / @Retryable 注解。
|
|
|
+ assertTrue(hasRetryCapability(SYNC_SERVICE, KB_VECTOR_CONTROLLER, KB_SERVICE),
|
|
|
+ "[admin.knowledgeBase.manage] 同步路径无任何失败重试能力(syncWithRetry / maxRetries / @Retryable):"
|
|
|
+ + "契约 SC-A-08/A-22 边界要求「同步失败重试」,当前未实现。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-2:向量同步端点应接受重试配置参数(SC-A-08 边界,未实现 → 红)")
|
|
|
+ void syncFailureRetry_vectorSyncShouldAcceptRetryParam() {
|
|
|
+ Class<?> c = loadClass(KB_VECTOR_CONTROLLER);
|
|
|
+ if (c == null) {
|
|
|
+ fail("[admin.knowledgeBase.manage] KnowledgeBaseVectorController 类不存在,无法验证重试参数。");
|
|
|
+ }
|
|
|
+ Method sync = findMethod(c, "triggerSync");
|
|
|
+ if (sync == null) {
|
|
|
+ fail("[admin.knowledgeBase.manage] KnowledgeBaseVectorController 缺少 triggerSync(/sync)端点。");
|
|
|
+ }
|
|
|
+ // 契约要求同步可传重试/批量参数;现无参(triggerSync())→ 红
|
|
|
+ boolean hasRetryParam = sync.getParameterCount() > 0
|
|
|
+ && (sync.getParameterTypes()[0] == java.util.Map.class);
|
|
|
+ assertTrue(hasRetryParam,
|
|
|
+ "[admin.knowledgeBase.manage] /api/admin/knowledge-base/vector/sync 应接受重试配置参数"
|
|
|
+ + "(如 maxRetries),支持同步失败重试(SC-A-08 边界);当前为无参 triggerSync()。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 边界流:大批量分批(契约 SC-A-08 要求,当前未实现 → RED)
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-3:知识库同步/回填应支持大批量分批(SC-A-08 边界,未实现 → 红)")
|
|
|
+ void largeBatch_syncAndBackfillShouldSupportBatching() {
|
|
|
+ // 契约 AC:边界「大批量分批」。探测 DanKnowledgeBaseService / LanggraphSyncService 是否暴露
|
|
|
+ // 分批能力:syncBatch / batchSync 方法 或 BATCH_SIZE 常量(默认分批阈值)。
|
|
|
+ assertTrue(hasBatchCapability(KB_SERVICE, SYNC_SERVICE),
|
|
|
+ "[admin.knowledgeBase.manage] 同步/回填路径无任何分批能力(syncBatch / BATCH_SIZE):"
|
|
|
+ + "契约 SC-A-08 边界要求「大批量分批」,当前未实现。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-4:backfill-tags 应分批回填并返回批处理统计(SC-A-08 边界,未实现 → 红)")
|
|
|
+ void largeBatch_backfillShouldReturnBatchStats() {
|
|
|
+ Class<?> svc = loadClass(KB_SERVICE);
|
|
|
+ if (svc == null) {
|
|
|
+ fail("[admin.knowledgeBase.manage] DanKnowledgeBaseService 类不存在,无法验证分批回填。");
|
|
|
+ }
|
|
|
+ Method backfill = findMethod(svc, "backfillTags");
|
|
|
+ if (backfill == null) {
|
|
|
+ fail("[admin.knowledgeBase.manage] DanKnowledgeBaseService 缺少 backfillTags。");
|
|
|
+ }
|
|
|
+ // 契约要求回填按批处理并返回批统计(如 total / processed / batchSize);
|
|
|
+ // 现返回 Map 但无 batchSize 语义探测点(返回类型为 Map<String,Object>,无法静态断言)——
|
|
|
+ // 以是否存在 BATCH_SIZE 常量 / batchSize 字段作为分批哨兵。
|
|
|
+ boolean hasBatchSentinel = false;
|
|
|
+ for (Field f : svc.getDeclaredFields()) {
|
|
|
+ if (f.getName().equalsIgnoreCase("BATCH_SIZE") || f.getName().toLowerCase().contains("batch")) {
|
|
|
+ hasBatchSentinel = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(hasBatchSentinel,
|
|
|
+ "[admin.knowledgeBase.manage] DanKnowledgeBaseService 无 BATCH_SIZE/batch 分批哨兵:"
|
|
|
+ + "契约 SC-A-08 边界要求「大批量分批」回填,当前未实现。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 权限流:知识库管理应受平台角色门禁(SC-A-08 权限粒度)
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流-1:统一知识库端点已实现平台角色门禁(allowed,已落地 → 绿)")
|
|
|
+ void permissionFlow_unifiedEnforcesRole() {
|
|
|
+ Class<?> c = loadClass(KB_UNIFIED_CONTROLLER);
|
|
|
+ if (c == null) {
|
|
|
+ fail("[admin.knowledgeBase.manage] AdminUnifiedKnowledgeController 类不存在。");
|
|
|
+ }
|
|
|
+ // 已落地:allowed(request) 门禁 + ALLOWED_ROLES 集合(admin/operator/finance/knowledge_admin/butler)
|
|
|
+ assertTrue(hasMethod(c, "allowed"),
|
|
|
+ "[admin.knowledgeBase.manage] AdminUnifiedKnowledgeController 应含 allowed(request) 角色门禁。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流-2:知识库 CRUD 写入端点应实现平台角色门禁(SC-A-08 权限粒度,未实现 → 红)")
|
|
|
+ void permissionFlow_kbWriteEndpointsShouldEnforceRole() {
|
|
|
+ Class<?> c = loadClass(KB_CONTROLLER);
|
|
|
+ if (c == null) {
|
|
|
+ fail("[admin.knowledgeBase.manage] KnowledgeBaseController 类不存在。");
|
|
|
+ }
|
|
|
+ // 契约(§4.60.8 + 2026-11-07-admin-ops-contract §3#5「写入端点权限粒度留待测试侧核验」):
|
|
|
+ // 知识库写入(save/delete/bind-tags/bind-dimensions/backfill-tags)应受平台/knowledge_admin 角色门禁,
|
|
|
+ // 实现方式为控制器内 @RequestAttribute("role") 校验或 allowed/roles 门禁。
|
|
|
+ boolean hasRoleGate = hasMethod(c, "allowed") || hasMethod(c, "roles")
|
|
|
+ || hasParameterAnnotation(c, "javax.servlet.http.HttpServletRequest");
|
|
|
+ assertTrue(hasRoleGate,
|
|
|
+ "[admin.knowledgeBase.manage] KnowledgeBaseController 写入端点无平台角色门禁"
|
|
|
+ + "(无 allowed/roles 门禁、无 @RequestAttribute(\"role\")):"
|
|
|
+ + "知识库写入权限粒度应核验(2026-11-07-admin-ops-contract §3#5)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流-3:向量库端点应实现平台角色门禁(SC-A-08 权限粒度,未实现 → 红)")
|
|
|
+ void permissionFlow_vectorEndpointsShouldEnforceRole() {
|
|
|
+ Class<?> c = loadClass(KB_VECTOR_CONTROLLER);
|
|
|
+ if (c == null) {
|
|
|
+ fail("[admin.knowledgeBase.manage] KnowledgeBaseVectorController 类不存在。");
|
|
|
+ }
|
|
|
+ boolean hasRoleGate = hasMethod(c, "allowed") || hasMethod(c, "roles")
|
|
|
+ || hasParameterAnnotation(c, "javax.servlet.http.HttpServletRequest");
|
|
|
+ assertTrue(hasRoleGate,
|
|
|
+ "[admin.knowledgeBase.manage] KnowledgeBaseVectorController 无平台角色门禁"
|
|
|
+ + "(向量库管理应受平台角色保护,与统一知识库一致)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 反射辅助
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ private void assertEndpoints(Class<?> c, String[] endpoints, String label, String mappingPrefix) {
|
|
|
+ for (String ep : endpoints) {
|
|
|
+ boolean found = false;
|
|
|
+ for (Method m : c.getDeclaredMethods()) {
|
|
|
+ if (m.isAnnotationPresent(org.springframework.web.bind.annotation.PostMapping.class)) {
|
|
|
+ org.springframework.web.bind.annotation.PostMapping pm =
|
|
|
+ m.getAnnotation(org.springframework.web.bind.annotation.PostMapping.class);
|
|
|
+ String[] vals = pm.value();
|
|
|
+ for (String v : vals) {
|
|
|
+ if (v.equals(ep)) {
|
|
|
+ found = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (found) break;
|
|
|
+ }
|
|
|
+ assertTrue(found,
|
|
|
+ "[admin.knowledgeBase.manage] " + label + " 缺少端点 " + mappingPrefix + ep
|
|
|
+ + "(§4.60.8)。");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasRetryCapability(String... classNames) {
|
|
|
+ for (String cn : classNames) {
|
|
|
+ Class<?> c = loadClass(cn);
|
|
|
+ if (c == null) continue;
|
|
|
+ for (Method m : c.getDeclaredMethods()) {
|
|
|
+ if (m.getName().toLowerCase().contains("retry")) return true;
|
|
|
+ }
|
|
|
+ for (Field f : c.getDeclaredFields()) {
|
|
|
+ if (f.getName().toLowerCase().contains("retry")) return true;
|
|
|
+ }
|
|
|
+ if (hasAnnotationByName(c, "org.springframework.retry.annotation.Retryable")) return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasBatchCapability(String... classNames) {
|
|
|
+ for (String cn : classNames) {
|
|
|
+ Class<?> c = loadClass(cn);
|
|
|
+ if (c == null) continue;
|
|
|
+ for (Method m : c.getDeclaredMethods()) {
|
|
|
+ if (m.getName().toLowerCase().contains("batch")) return true;
|
|
|
+ }
|
|
|
+ for (Field f : c.getDeclaredFields()) {
|
|
|
+ if (f.getName().equalsIgnoreCase("BATCH_SIZE")
|
|
|
+ || f.getName().toLowerCase().contains("batch")) return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasMethod(Class<?> c, String name) {
|
|
|
+ for (Method m : c.getDeclaredMethods()) {
|
|
|
+ if (m.getName().equals(name)) return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Method findMethod(Class<?> c, String name) {
|
|
|
+ for (Method m : c.getDeclaredMethods()) {
|
|
|
+ if (m.getName().equals(name)) return m;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasParameterAnnotation(Class<?> c, String annotationName) {
|
|
|
+ for (Method m : c.getDeclaredMethods()) {
|
|
|
+ for (java.lang.annotation.Annotation[] paramAnns : m.getParameterAnnotations()) {
|
|
|
+ for (java.lang.annotation.Annotation a : paramAnns) {
|
|
|
+ if (a.annotationType().getName().equals(annotationName)) return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasAnnotationByName(Class<?> c, String annotationName) {
|
|
|
+ for (java.lang.annotation.Annotation a : c.getDeclaredAnnotations()) {
|
|
|
+ if (a.annotationType().getName().equals(annotationName)) return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Class<?> loadClass(String name) {
|
|
|
+ try {
|
|
|
+ return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
|
|
|
+ } catch (ClassNotFoundException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String readSchemaSql() {
|
|
|
+ try (InputStream is = getClass().getClassLoader().getResourceAsStream("schema.sql")) {
|
|
|
+ if (is == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ try (Scanner scanner = new Scanner(is, StandardCharsets.UTF_8.name())) {
|
|
|
+ scanner.useDelimiter("\\A");
|
|
|
+ return scanner.hasNext() ? scanner.next() : "";
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|