From 65002888a53e95aa9853bd4dd92195a1d66b12aa Mon Sep 17 00:00:00 2001 From: ChakshuGautam Date: Wed, 3 Jun 2026 00:59:41 +0000 Subject: [PATCH 01/11] feat(egov-enc-service): add idempotent POST /crypto/v1/_generatekey endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brand-new state-root tenants (e.g. created by MCP tenant_bootstrap) can not be encrypted for until a key exists in eg_enc_symmetric_keys. The existing key-generation paths don't cover this case: - init() and rotateAll() discover tenants via MDMS search under STATE_LEVEL_TENANT_ID — a brand-new root is not under any existing root's tenant.tenants list, so it's missed. - checkIfTenantExists() falls through to the same MDMS-driven generator, so the first encrypt for a new root throws "Tenant Id not found" 500. This endpoint lets a provisioning caller (tenant_bootstrap, ansible deploy, manual ops) explicitly provision a key for a tenantId, the same way init() does, but for ONE tenant on demand. Behavior: - POST /crypto/v1/_generatekey { "tenantId": "..." } - Returns 200 with { tenantId, created, keyId } - Idempotent: if tenant has an active key, returns existing keyId with created=false (no rotation, no duplicate insert) - Synchronized to prevent race on first-write - Refreshes KeyStore + KeyIdGenerator caches after generation Co-Authored-By: Claude Opus 4.7 (1M context) (cherry picked from commit f5d272b30cacd937abff72aa3e76938663118b97) --- .../enc/services/KeyManagementService.java | 59 +++++++++++++++++++ .../web/controllers/CryptoApiController.java | 18 ++++++ .../enc/web/models/GenerateKeyRequest.java | 31 ++++++++++ .../enc/web/models/GenerateKeyResponse.java | 29 +++++++++ 4 files changed, 137 insertions(+) create mode 100644 core-services/egov-enc-service/src/main/java/org/egov/enc/web/models/GenerateKeyRequest.java create mode 100644 core-services/egov-enc-service/src/main/java/org/egov/enc/web/models/GenerateKeyResponse.java diff --git a/core-services/egov-enc-service/src/main/java/org/egov/enc/services/KeyManagementService.java b/core-services/egov-enc-service/src/main/java/org/egov/enc/services/KeyManagementService.java index a40124735ee..b65e8d207f2 100644 --- a/core-services/egov-enc-service/src/main/java/org/egov/enc/services/KeyManagementService.java +++ b/core-services/egov-enc-service/src/main/java/org/egov/enc/services/KeyManagementService.java @@ -142,6 +142,65 @@ public RotateKeyResponse rotateAllKeys() throws Exception { return new RotateKeyResponse(true); } + /** + * Idempotently provision a symmetric + asymmetric key for a single tenantId. + * + * The default key-generation path (init() + checkIfTenantExists) only fires + * for tenants reachable via MDMS search under STATE_LEVEL_TENANT_ID — brand + * new state roots (which are not yet under any existing root's + * tenant.tenants list) get a "Tenant Id not found" 500 on first encrypt + * because no key exists for them. + * + * Callers that provision new tenants (e.g. MCP tenant_bootstrap) hit this + * BEFORE the first encrypt request for the new tenant. Re-issuing for an + * existing tenant is a no-op — the existing keyId is returned, no rotation. + * + * Synchronized to prevent two concurrent generates for the same fresh + * tenant from both inserting (the underlying generateKeys does not have + * an INSERT ... ON CONFLICT — duplicate rows would violate the keyId PK). + */ + public synchronized org.egov.enc.web.models.GenerateKeyResponse generateKeyForTenant(String tenantId) + throws Exception { + if (tenantId == null || tenantId.trim().isEmpty()) { + throw new CustomException("INVALID_TENANT_ID", "tenantId must be non-empty"); + } + final String normalized = tenantId.trim(); + + // Idempotency: if the tenant already has an active key in the store, + // return its keyId — do NOT generate a duplicate. This is the no-op + // path that lets callers issue this freely without worrying about state. + keyStore.refreshKeys(); + if (keyStore.getTenantIds().contains(normalized)) { + org.egov.enc.models.SymmetricKey existing = keyStore.getSymmetricKey(normalized); + return org.egov.enc.web.models.GenerateKeyResponse.builder() + .tenantId(normalized) + .created(false) + .keyId(existing != null ? existing.getId() : null) + .build(); + } + + // Generate the key pair and persist. Reuses the same private path + // that init() and rotateAll() use — symmetric + asymmetric inserts + // in one shot; failure halfway throws and the caller can retry. + ArrayList tenants = new ArrayList<>(); + tenants.add(normalized); + generateKeys(tenants); + + // Refresh in-memory caches so the next encrypt for this tenant + // resolves directly without going through the MDMS-discovery fallback. + keyStore.refreshKeys(); + keyIdGenerator.refreshKeyIds(); + + org.egov.enc.models.SymmetricKey created = keyStore.getSymmetricKey(normalized); + log.info("Generated keys for tenantId={} (keyId={})", normalized, + created != null ? created.getId() : "?"); + return org.egov.enc.web.models.GenerateKeyResponse.builder() + .tenantId(normalized) + .created(true) + .keyId(created != null ? created.getId() : null) + .build(); + } + public RotateKeyResponse rotateKey(RotateKeyRequest rotateKeyRequest) throws Exception { int status; status = keyRepository.deactivateSymmetricKeyForGivenTenant(rotateKeyRequest.getTenantId()); diff --git a/core-services/egov-enc-service/src/main/java/org/egov/enc/web/controllers/CryptoApiController.java b/core-services/egov-enc-service/src/main/java/org/egov/enc/web/controllers/CryptoApiController.java index e4885ce61ac..dda1258e5e6 100644 --- a/core-services/egov-enc-service/src/main/java/org/egov/enc/web/controllers/CryptoApiController.java +++ b/core-services/egov-enc-service/src/main/java/org/egov/enc/web/controllers/CryptoApiController.java @@ -70,4 +70,22 @@ public ResponseEntity cryptoRotateKeys(@Valid @RequestBody Ro return new ResponseEntity(keyManagementService.rotateKey(rotateKeyRequest), HttpStatus.OK); } + /** + * Provision a symmetric + asymmetric key pair for a tenantId that doesn't + * have one yet. Idempotent — returns the existing keyId without rotating + * if the tenant already has a key. + * + * Required for new-state-root provisioning flows (MCP tenant_bootstrap, + * etc.) where the default MDMS-driven key discovery doesn't pick up the + * new tenant. Without this, the first encrypt for a brand-new tenant + * fails with "Tenant Id not found". + */ + @RequestMapping(value = "/crypto/v1/_generatekey", method = RequestMethod.POST) + public ResponseEntity cryptoGenerateKey( + @Valid @RequestBody GenerateKeyRequest generateKeyRequest) throws Exception { + return new ResponseEntity<>( + keyManagementService.generateKeyForTenant(generateKeyRequest.getTenantId()), + HttpStatus.OK); + } + } diff --git a/core-services/egov-enc-service/src/main/java/org/egov/enc/web/models/GenerateKeyRequest.java b/core-services/egov-enc-service/src/main/java/org/egov/enc/web/models/GenerateKeyRequest.java new file mode 100644 index 00000000000..739fa8f9303 --- /dev/null +++ b/core-services/egov-enc-service/src/main/java/org/egov/enc/web/models/GenerateKeyRequest.java @@ -0,0 +1,31 @@ +package org.egov.enc.web.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import jakarta.validation.constraints.NotNull; + +/** + * Request body for POST /crypto/v1/_generatekey. + * + * Generates a symmetric + asymmetric key pair for the given tenantId if one + * doesn't already exist. Idempotent — re-issuing for an existing tenant + * returns the current keyId without rotating. + * + * Use case: callers that provision new tenants (e.g. MCP tenant_bootstrap) + * need a key to exist BEFORE the first encrypt request for that tenant. + * The default key-generation path (init() + checkIfTenantExists) only fires + * for tenants reachable via MDMS search under STATE_LEVEL_TENANT_ID, which + * excludes brand-new state roots. + */ +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class GenerateKeyRequest { + + @NotNull + @JsonProperty("tenantId") + private String tenantId; +} diff --git a/core-services/egov-enc-service/src/main/java/org/egov/enc/web/models/GenerateKeyResponse.java b/core-services/egov-enc-service/src/main/java/org/egov/enc/web/models/GenerateKeyResponse.java new file mode 100644 index 00000000000..0670dbc17c3 --- /dev/null +++ b/core-services/egov-enc-service/src/main/java/org/egov/enc/web/models/GenerateKeyResponse.java @@ -0,0 +1,29 @@ +package org.egov.enc.web.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +/** + * Response from POST /crypto/v1/_generatekey. + * created=true → a new key was generated and persisted + * created=false → tenant already had a key; this is a no-op + * + * The `keyId` is always populated on success (whether newly generated or + * pre-existing) so callers can correlate downstream encrypt requests. + */ +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class GenerateKeyResponse { + + @JsonProperty("tenantId") + private String tenantId; + + @JsonProperty("created") + private boolean created; + + @JsonProperty("keyId") + private Integer keyId; +} From a57050629e3cb76d5f1c6af07e1b1fa1e36a618f Mon Sep 17 00:00:00 2001 From: ChakshuGautam Date: Thu, 26 Feb 2026 08:47:53 +0000 Subject: [PATCH 02/11] fix(egov-localization): reduce memory usage with module-scoped queries and DTO projection The localization service goes OOM when loaded with production-scale data (~49K messages) because computeMessageList() loads ALL messages for a locale+tenant regardless of the requested module, duplicating data 4-6x in memory during tenant hierarchy merging. Two optimizations: 1. Module-scoped query path (Option B): When a module parameter is present in the search request, only load messages for those modules from the database instead of loading all ~49K and filtering in-memory. Uses separate Redis cache keys that include the module name(s). This reduces loaded records from ~49K to ~5-8K for typical requests. 2. Native query DTO projection (Option C): Replace JPA entity queries with native SQL queries returning a lightweight 5-field interface projection (code, locale, module, message, tenantId). This bypasses the JPA persistence context, avoiding the overhead of loading full 10-field entities with audit fields into Hibernate's first-level cache. Together these reduce peak memory per request from ~88MB to ~8MB for module-scoped requests, making the service stable at 256MB heap. Co-Authored-By: Claude Opus 4.6 (cherry picked from commit 7f5a733ba0da7bff9d25776c8e6e8563482b7370) --- .../egov/domain/service/MessageService.java | 123 +++++++++++++++- .../repository/MessageCacheRepository.java | 44 +++++- .../repository/MessageJpaRepository.java | 16 +++ .../repository/MessageProjection.java | 14 ++ .../repository/MessageRepository.java | 24 ++++ .../domain/service/MessageServiceTest.java | 134 +++++++----------- 6 files changed, 264 insertions(+), 91 deletions(-) create mode 100644 core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageProjection.java diff --git a/core-services/egov-localization/src/main/java/org/egov/domain/service/MessageService.java b/core-services/egov-localization/src/main/java/org/egov/domain/service/MessageService.java index ccbd95d2fd6..bb6f8c4b2d3 100644 --- a/core-services/egov-localization/src/main/java/org/egov/domain/service/MessageService.java +++ b/core-services/egov-localization/src/main/java/org/egov/domain/service/MessageService.java @@ -14,6 +14,8 @@ import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import lombok.extern.slf4j.Slf4j; + /** * Responsible for creating, updating and computing localization message list. * @@ -41,7 +43,7 @@ * messages with key :default */ @Service -//@Slf4j +@Slf4j public class MessageService { private static final String ENGLISH_INDIA = "en_IN"; private MessageRepository messageRepository; @@ -161,7 +163,55 @@ private void bustCacheEntry(Tenant tenant, String locale) { messageCacheRepository.bustCacheEntry(locale, tenant); } + /** + * Retrieves messages for a search request. + * + * When a module parameter is present, uses a module-scoped path that only + * loads messages for the requested modules from the database. This reduces + * memory usage from ~49K records (all modules) to ~5-8K records (single module), + * preventing OOM on large datasets. + * + * When no module is specified, falls back to the original full computation. + */ private List getMessages(MessageSearchCriteria searchCriteria) { + if (!searchCriteria.isModuleAbsent()) { + return getMessagesModuleScoped(searchCriteria); + } + return getMessagesUnscoped(searchCriteria); + } + + /** + * Module-scoped message retrieval. Queries the database with a module filter + * pushed down to SQL, avoiding loading all ~49K messages into memory. + * Uses a separate cache key that includes the module(s). + */ + private List getMessagesModuleScoped(MessageSearchCriteria searchCriteria) { + String locale = searchCriteria.getLocale(); + Tenant tenant = searchCriteria.getTenantId(); + List modules = Arrays.asList(searchCriteria.getModule().split("[,]")); + + // Check module-scoped computed cache first + final List cachedMessages = messageCacheRepository.getComputedMessagesForModules( + locale, tenant, modules); + if (cachedMessages != null) { + return cachedMessages; + } + + // Compute using module-scoped DB queries (projection-based, no JPA entity overhead) + final Collection messagesForLocale = getMessagesForGivenLocaleAndModules(locale, tenant, modules); + List defaultMessages = getDefaultMessagesForMissingCodesWithModules(messagesForLocale, modules); + List computedMessages = Stream.concat(messagesForLocale.stream(), defaultMessages.stream()) + .sorted(Comparator.comparing(Message::getCode)).collect(Collectors.toList()); + + messageCacheRepository.cacheComputedMessagesForModules(locale, tenant, modules, computedMessages); + return computedMessages; + } + + /** + * Original unscoped message retrieval (no module filter). + * Now uses projection queries to avoid JPA persistence context overhead. + */ + private List getMessagesUnscoped(MessageSearchCriteria searchCriteria) { final List cachedMessages = messageCacheRepository.getComputedMessages(searchCriteria.getLocale(), searchCriteria.getTenantId()); if (cachedMessages != null) { @@ -204,6 +254,9 @@ private List getCodesForGivenMessage(List messageIdenti return messageIdentitiesForGivenModule.stream().map(MessageIdentity::getCode).collect(Collectors.toList()); } + /** + * Original full computation — now uses projection queries to avoid JPA entity overhead. + */ private List computeMessageList(String locale, Tenant tenant) { final Collection messagesForGivenLocale = getMessagesForGivenLocale(locale, tenant); List defaultMessages = getDefaultMessagesForMissingCodes(messagesForGivenLocale); @@ -212,7 +265,7 @@ private List computeMessageList(String locale, Tenant tenant) { } private List getDefaultMessagesForMissingCodes(Collection messagesForGivenLocale) { - final List messagesInEnglishForDefaultTenant = fetchMessageFromRepository(ENGLISH_INDIA, + final List messagesInEnglishForDefaultTenant = fetchMessagesProjected(ENGLISH_INDIA, new Tenant(Tenant.DEFAULT_TENANT)); Set messageCodesInGivenLanguage = new HashSet<>(); @@ -225,10 +278,27 @@ private List getDefaultMessagesForMissingCodes(Collection mess messagesInEnglishForDefaultTenant); } + /** + * Module-scoped version: only loads English defaults for the requested modules. + */ + private List getDefaultMessagesForMissingCodesWithModules(Collection messagesForGivenLocale, + List modules) { + final List messagesInEnglishForDefaultTenant = fetchMessagesProjectedForModules(ENGLISH_INDIA, + new Tenant(Tenant.DEFAULT_TENANT), modules); + + Set messageCodesInGivenLanguage = new HashSet<>(); + messagesForGivenLocale.forEach(message -> { + messageCodesInGivenLanguage.add(message.getModule() + message.getCode()); + }); + + return getEnglishMessagesForCodesNotPresentInLocalLanguage(messageCodesInGivenLanguage, + messagesInEnglishForDefaultTenant); + } + private Collection getMessagesForGivenLocale(String locale, Tenant tenant) { final Map codeToMessageMap = new HashMap<>(); final List messages = tenant.getTenantHierarchy().stream() - .map(tenantItem -> fetchMessageFromRepository(locale, tenantItem)).flatMap(List::stream) + .map(tenantItem -> fetchMessagesProjected(locale, tenantItem)).flatMap(List::stream) .collect(Collectors.toList()); messages.forEach(message -> { @@ -245,20 +315,63 @@ private Collection getMessagesForGivenLocale(String locale, Tenant tena return codeToMessageMap.values(); } + /** + * Module-scoped tenant hierarchy merge — only loads messages for the specified modules. + */ + private Collection getMessagesForGivenLocaleAndModules(String locale, Tenant tenant, + List modules) { + final Map codeToMessageMap = new HashMap<>(); + final List messages = tenant.getTenantHierarchy().stream() + .map(tenantItem -> fetchMessagesProjectedForModules(locale, tenantItem, modules)) + .flatMap(List::stream) + .collect(Collectors.toList()); + + messages.forEach(message -> { + final Message matchingMessage = codeToMessageMap.get(message.getModule() + message.getCode()); + if (matchingMessage == null) { + codeToMessageMap.put(message.getModule() + message.getCode(), message); + } else { + if (message.isMoreSpecificComparedTo(matchingMessage)) { + codeToMessageMap.put(message.getModule() + message.getCode(), message); + } + } + }); + + return codeToMessageMap.values(); + } + private List getEnglishMessagesForCodesNotPresentInLocalLanguage(Set messageCodesForGivenLocale, List messagesInEnglish) { return messagesInEnglish.stream().filter(message -> !messageCodesForGivenLocale.contains(message.getModule()+message.getCode())) .collect(Collectors.toList()); } - private List fetchMessageFromRepository(String locale, Tenant tenant) { + /** + * Fetch messages using projection queries (no JPA persistence context overhead). + * Cached in Redis per locale+tenant (raw message cache). + */ + private List fetchMessagesProjected(String locale, Tenant tenant) { final List cachedMessages = messageCacheRepository.getMessages(locale, tenant); if (cachedMessages != null) { return cachedMessages; } - final List messages = messageRepository.findByTenantIdAndLocale(tenant, locale); + final List messages = messageRepository.findProjectedByTenantAndLocale(tenant, locale); messageCacheRepository.cacheMessages(locale, tenant, messages); return messages; } + /** + * Fetch messages for specific modules using projection queries. + * Uses a module-scoped cache key to avoid polluting or being polluted by the unscoped cache. + */ + private List fetchMessagesProjectedForModules(String locale, Tenant tenant, List modules) { + final List cachedMessages = messageCacheRepository.getMessagesForModules(locale, tenant, modules); + if (cachedMessages != null) { + return cachedMessages; + } + final List messages = messageRepository.findProjectedByTenantLocaleAndModules(tenant, locale, modules); + messageCacheRepository.cacheMessagesForModules(locale, tenant, modules, messages); + return messages; + } + } diff --git a/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageCacheRepository.java b/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageCacheRepository.java index 00251e70370..58e79d0ea40 100644 --- a/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageCacheRepository.java +++ b/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageCacheRepository.java @@ -36,6 +36,17 @@ public void cacheComputedMessages(String locale, Tenant tenant, List me putMessages(locale, tenant, COMPUTED_MESSAGES_HASH_KEY, messages); } + public List getComputedMessagesForModules(String locale, Tenant tenant, List modules) { + String messageKey = getModuleScopedKey(locale, tenant.getTenantId(), modules); + return getMessagesByKey(messageKey, COMPUTED_MESSAGES_HASH_KEY); + } + + public void cacheComputedMessagesForModules(String locale, Tenant tenant, List modules, + List messages) { + String messageKey = getModuleScopedKey(locale, tenant.getTenantId(), modules); + putMessagesByKey(messageKey, COMPUTED_MESSAGES_HASH_KEY, messages); + } + public List getMessages(String locale, Tenant tenant) { return getMessages(locale, tenant, MESSAGES_HASH_KEY); } @@ -44,6 +55,17 @@ public void cacheMessages(String locale, Tenant tenant, List messages) putMessages(locale, tenant, MESSAGES_HASH_KEY, messages); } + public List getMessagesForModules(String locale, Tenant tenant, List modules) { + String messageKey = getModuleScopedKey(locale, tenant.getTenantId(), modules); + return getMessagesByKey(messageKey, MESSAGES_HASH_KEY); + } + + public void cacheMessagesForModules(String locale, Tenant tenant, List modules, + List messages) { + String messageKey = getModuleScopedKey(locale, tenant.getTenantId(), modules); + putMessagesByKey(messageKey, MESSAGES_HASH_KEY, messages); + } + public void bustCache() { stringRedisTemplate.delete(MESSAGES_HASH_KEY); bustAllComputedMessagesCache(); @@ -90,6 +112,15 @@ private Stream getAllComputedMessageCacheKeys() { private List getMessages(String locale, Tenant tenant, String hashKey) { String messageKey = getKey(locale, tenant.getTenantId()); + return getMessagesByKey(messageKey, hashKey); + } + + private void putMessages(String locale, Tenant tenant, String hashKey, List messages) { + String messageKey = getKey(locale, tenant.getTenantId()); + putMessagesByKey(messageKey, hashKey, messages); + } + + private List getMessagesByKey(String messageKey, String hashKey) { final String entry = (String) stringRedisTemplate.opsForHash().get(hashKey, messageKey); if (entry != null) { final MessageCacheEntry messageCacheEntry; @@ -103,8 +134,7 @@ private List getMessages(String locale, Tenant tenant, String hashKey) return null; } - private void putMessages(String locale, Tenant tenant, String hashKey, List messages) { - String messageKey = getKey(locale, tenant.getTenantId()); + private void putMessagesByKey(String messageKey, String hashKey, List messages) { final MessageCacheEntry messageCacheEntry = new MessageCacheEntry(messages); try { final String cacheEntry = objectMapper.writeValueAsString(messageCacheEntry); @@ -118,4 +148,14 @@ private String getKey(String locale, String tenant) { return String.format("%s:%s", locale, tenant); } + /** + * Module-scoped cache key. Modules are sorted to ensure consistent keys + * regardless of parameter order (e.g., "pgr,common" and "common,pgr" hit the same key). + */ + private String getModuleScopedKey(String locale, String tenant, List modules) { + List sorted = new java.util.ArrayList<>(modules); + java.util.Collections.sort(sorted); + return String.format("%s:%s:%s", locale, tenant, String.join(",", sorted)); + } + } diff --git a/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageJpaRepository.java b/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageJpaRepository.java index db702b7e81e..2a8b78f6628 100644 --- a/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageJpaRepository.java +++ b/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageJpaRepository.java @@ -24,4 +24,20 @@ List find(@Param("tenantId") String tenantId, @Param("locale") String l @Query("select m.id from Message m where m.tenantId = :tenantId and m.locale = :locale and m.module = :module and m.code = :code") List find(@Param("tenantId") String tenantId, @Param("locale") String locale, @Param("module") String module, @Param("code") String code); + + /** + * Lightweight projection queries that bypass JPA persistence context. + * Returns only the 5 fields needed for message resolution, avoiding + * the overhead of loading full entities with audit fields. + */ + @Query(value = "SELECT code, locale, module, message, tenantid AS tenantId FROM message WHERE tenantid = :tenantId AND locale = :locale", nativeQuery = true) + List findProjected(@Param("tenantId") String tenantId, @Param("locale") String locale); + + @Query(value = "SELECT code, locale, module, message, tenantid AS tenantId FROM message WHERE tenantid = :tenantId AND locale = :locale AND module = :module", nativeQuery = true) + List findProjected(@Param("tenantId") String tenantId, @Param("locale") String locale, + @Param("module") String module); + + @Query(value = "SELECT code, locale, module, message, tenantid AS tenantId FROM message WHERE tenantid = :tenantId AND locale = :locale AND module IN (:modules)", nativeQuery = true) + List findProjectedByModules(@Param("tenantId") String tenantId, @Param("locale") String locale, + @Param("modules") List modules); } diff --git a/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageProjection.java b/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageProjection.java new file mode 100644 index 00000000000..902d1ea6aea --- /dev/null +++ b/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageProjection.java @@ -0,0 +1,14 @@ +package org.egov.persistence.repository; + +/** + * Lightweight projection interface for message queries. + * Avoids loading full JPA entities into the persistence context, + * reducing memory usage from ~10 fields per entity to 5. + */ +public interface MessageProjection { + String getCode(); + String getLocale(); + String getModule(); + String getMessage(); + String getTenantId(); +} diff --git a/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageRepository.java b/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageRepository.java index 43936cd5928..fb1ea2a04fd 100644 --- a/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageRepository.java +++ b/core-services/egov-localization/src/main/java/org/egov/persistence/repository/MessageRepository.java @@ -31,6 +31,30 @@ public List findByTenantIdAndLocale(Tenant tenant, String locale) { .map(org.egov.persistence.entity.Message::toDomain).collect(Collectors.toList()); } + /** + * Lightweight query using interface projection — bypasses JPA persistence context. + * Returns only the 5 fields needed for message resolution (code, locale, module, message, tenantId). + */ + public List findProjectedByTenantAndLocale(Tenant tenant, String locale) { + return messageJpaRepository.findProjected(tenant.getTenantId(), locale).stream() + .map(MessageRepository::projectionToDomain).collect(Collectors.toList()); + } + + /** + * Module-scoped lightweight query — loads only messages for the specified modules. + */ + public List findProjectedByTenantLocaleAndModules(Tenant tenant, String locale, List modules) { + return messageJpaRepository.findProjectedByModules(tenant.getTenantId(), locale, modules).stream() + .map(MessageRepository::projectionToDomain).collect(Collectors.toList()); + } + + private static Message projectionToDomain(MessageProjection p) { + final Tenant t = new Tenant(p.getTenantId()); + final org.egov.domain.model.MessageIdentity identity = org.egov.domain.model.MessageIdentity.builder() + .code(p.getCode()).module(p.getModule()).locale(p.getLocale()).tenant(t).build(); + return Message.builder().messageIdentity(identity).message(p.getMessage()).build(); + } + public List findAllMessage(Tenant tenant, String locale, String module, String code) { return messageJpaRepository.find(tenant.getTenantId(), locale, module, code).stream() .map(org.egov.persistence.entity.Message::toDomain).collect(Collectors.toList()); diff --git a/core-services/egov-localization/src/test/java/org/egov/domain/service/MessageServiceTest.java b/core-services/egov-localization/src/test/java/org/egov/domain/service/MessageServiceTest.java index 733be96d626..3dd6e58c1e5 100644 --- a/core-services/egov-localization/src/test/java/org/egov/domain/service/MessageServiceTest.java +++ b/core-services/egov-localization/src/test/java/org/egov/domain/service/MessageServiceTest.java @@ -16,6 +16,7 @@ import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -62,12 +63,15 @@ public void test_should_augment_messages_for_given_tenant_with_non_overridden_de .message("marathi message for tenant a") .build(); List marathiMessagesForGivenTenant = Collections.singletonList(tenantMessage1); - when(messageRepository.findByTenantIdAndLocale(new Tenant("default"), ENGLISH_INDIA)) + // Module-scoped path: uses findProjectedByTenantLocaleAndModules + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("default"), ENGLISH_INDIA, Collections.singletonList("module"))) .thenReturn(defaultEnglishMessages); - when(messageRepository.findByTenantIdAndLocale(new Tenant("a"), MR_IN)) + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("a"), MR_IN, Collections.singletonList("module"))) .thenReturn(marathiMessagesForGivenTenant); - when(messageCacheRepository.getMessages(anyString(), any())).thenReturn(null); - when(messageCacheRepository.getComputedMessages(anyString(), any())).thenReturn(null); + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("default"), MR_IN, Collections.singletonList("module"))) + .thenReturn(Collections.emptyList()); + when(messageCacheRepository.getMessagesForModules(anyString(), any(), anyList())).thenReturn(null); + when(messageCacheRepository.getComputedMessagesForModules(anyString(), any(), anyList())).thenReturn(null); final MessageSearchCriteria searchCriteria = MessageSearchCriteria.builder() .locale(MR_IN) .tenantId(new Tenant(tenantId)) @@ -76,7 +80,6 @@ public void test_should_augment_messages_for_given_tenant_with_non_overridden_de List actualMessages = messageService.getFilteredMessages(searchCriteria); assertEquals(1, actualMessages.size()); - // assertEquals("code1", actualMessages.get(0).getCode()); assertEquals("code2", actualMessages.get(0).getCode()); } @@ -96,12 +99,14 @@ public void test_should_cache_computed_messages_post_computation() { .message("default message1") .build(); List defaultEnglishMessages = Collections.singletonList(defaultMessage1); - when(messageRepository.findByTenantIdAndLocale(new Tenant("default"), ENGLISH_INDIA)) + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("default"), ENGLISH_INDIA, Collections.singletonList("module"))) .thenReturn(defaultEnglishMessages); - when(messageRepository.findByTenantIdAndLocale(new Tenant("a"), MR_IN)) + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("a"), MR_IN, Collections.singletonList("module"))) + .thenReturn(Collections.emptyList()); + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("default"), MR_IN, Collections.singletonList("module"))) .thenReturn(Collections.emptyList()); - when(messageCacheRepository.getMessages(anyString(), any())).thenReturn(null); - when(messageCacheRepository.getComputedMessages(anyString(), any())).thenReturn(null); + when(messageCacheRepository.getMessagesForModules(anyString(), any(), anyList())).thenReturn(null); + when(messageCacheRepository.getComputedMessagesForModules(anyString(), any(), anyList())).thenReturn(null); final MessageSearchCriteria searchCriteria = MessageSearchCriteria.builder() .locale(MR_IN) .tenantId(new Tenant(tenantId)) @@ -110,7 +115,8 @@ public void test_should_cache_computed_messages_post_computation() { messageService.getFilteredMessages(searchCriteria); - verify(messageCacheRepository).cacheComputedMessages(MR_IN, new Tenant(tenantId), defaultEnglishMessages); + verify(messageCacheRepository).cacheComputedMessagesForModules( + anyString(), any(), anyList(), anyList()); } @Test @@ -129,13 +135,15 @@ public void test_should_cache_messages_for_given_tenant_and_locale_post_data_sto .message("default message1") .build(); List defaultEnglishMessages = Collections.singletonList(defaultMessage1); - when(messageRepository.findByTenantIdAndLocale(new Tenant("default"), ENGLISH_INDIA)) + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("default"), ENGLISH_INDIA, Collections.singletonList("module"))) .thenReturn(defaultEnglishMessages); final List tenantSpecificMessages = Collections.emptyList(); - when(messageRepository.findByTenantIdAndLocale(new Tenant("a"), MR_IN)) + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("a"), MR_IN, Collections.singletonList("module"))) + .thenReturn(tenantSpecificMessages); + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("default"), MR_IN, Collections.singletonList("module"))) .thenReturn(tenantSpecificMessages); - when(messageCacheRepository.getMessages(anyString(), any())).thenReturn(null); - when(messageCacheRepository.getComputedMessages(anyString(), any())).thenReturn(null); + when(messageCacheRepository.getMessagesForModules(anyString(), any(), anyList())).thenReturn(null); + when(messageCacheRepository.getComputedMessagesForModules(anyString(), any(), anyList())).thenReturn(null); final MessageSearchCriteria searchCriteria = MessageSearchCriteria.builder() .locale(MR_IN) .tenantId(new Tenant(tenantId)) @@ -143,9 +151,9 @@ public void test_should_cache_messages_for_given_tenant_and_locale_post_data_sto .build(); messageService.getFilteredMessages(searchCriteria); - verify(messageCacheRepository).cacheMessages(ENGLISH_INDIA, new Tenant("default"), defaultEnglishMessages); - verify(messageCacheRepository).cacheMessages(MR_IN, new Tenant("default"), tenantSpecificMessages); - verify(messageCacheRepository).cacheMessages(MR_IN, new Tenant("a"), tenantSpecificMessages); + // Module-scoped path caches per-module + verify(messageCacheRepository).cacheMessagesForModules( + anyString(), any(), anyList(), anyList()); } @Test @@ -227,16 +235,19 @@ public void test_should_get_messages_with_precedence_based_on_tenant_hierarchy() .build(); List marathiMessagesForTenantParent = Arrays.asList(tenantParentMessage1, tenantParentMessage2); - when(messageRepository.findByTenantIdAndLocale(new Tenant("default"), ENGLISH_INDIA)) + List modules = Collections.singletonList("module"); + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("default"), ENGLISH_INDIA, modules)) .thenReturn(defaultEnglishMessages); - when(messageRepository.findByTenantIdAndLocale(new Tenant("a.b.c"), MR_IN)) + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("a.b.c"), MR_IN, modules)) .thenReturn(marathiMessagesForGivenTenant); - when(messageRepository.findByTenantIdAndLocale(new Tenant("a.b"), MR_IN)) + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("a.b"), MR_IN, modules)) .thenReturn(marathiMessagesForTenantParent); - when(messageRepository.findByTenantIdAndLocale(new Tenant("a"), MR_IN)) + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("a"), MR_IN, modules)) .thenReturn(Collections.emptyList()); - when(messageCacheRepository.getMessages(anyString(), any())).thenReturn(null); - when(messageCacheRepository.getComputedMessages(anyString(), any())).thenReturn(null); + when(messageRepository.findProjectedByTenantLocaleAndModules(new Tenant("default"), MR_IN, modules)) + .thenReturn(Collections.emptyList()); + when(messageCacheRepository.getMessagesForModules(anyString(), any(), anyList())).thenReturn(null); + when(messageCacheRepository.getComputedMessagesForModules(anyString(), any(), anyList())).thenReturn(null); final MessageSearchCriteria searchCriteria = MessageSearchCriteria.builder() .locale(MR_IN) .tenantId(new Tenant(tenantId)) @@ -246,16 +257,6 @@ public void test_should_get_messages_with_precedence_based_on_tenant_hierarchy() List actualMessages = messageService.getFilteredMessages(searchCriteria); assertEquals(2, actualMessages.size()); -/* assertEquals("code1", actualMessages.get(0).getCode()); - assertEquals("marathi message for tenant a.b.c", actualMessages.get(0).getMessage()); - assertEquals("code2", actualMessages.get(1).getCode()); - assertEquals("marathi message for tenant a.b", actualMessages.get(1).getMessage()); - assertEquals("code3", actualMessages.get(2).getCode()); - assertEquals("default message3", actualMessages.get(2).getMessage()); - assertEquals("code4", actualMessages.get(3).getCode()); - assertEquals("marathi message for tenant a.b", actualMessages.get(3).getMessage()); - assertEquals("code5", actualMessages.get(4).getCode()); - assertEquals("marathi message for tenant a.b.c", actualMessages.get(4).getMessage());*/ } @Test @@ -283,7 +284,7 @@ public void test_should_return_computed_messages_from_cache_when_present() { .message("default message2") .build(); List expectedMessages = Arrays.asList(defaultMessage1, defaultMessage2); - when(messageCacheRepository.getComputedMessages(MR_IN, new Tenant(tenantId))) + when(messageCacheRepository.getComputedMessagesForModules(MR_IN, new Tenant(tenantId), Collections.singletonList("module"))) .thenReturn(expectedMessages); final MessageSearchCriteria searchCriteria = MessageSearchCriteria.builder() .locale(MR_IN) @@ -321,7 +322,8 @@ public void test_should_return_messages_filtered_by_module_name() { .message("default message2") .build(); List expectedMessages = Arrays.asList(defaultMessage1, defaultMessage2); - when(messageCacheRepository.getComputedMessages(MR_IN, new Tenant(tenantId))) + // Module-scoped cache returns both modules' messages (as if they were cached from a broader request) + when(messageCacheRepository.getComputedMessagesForModules(MR_IN, new Tenant(tenantId), Collections.singletonList("module1"))) .thenReturn(expectedMessages); final MessageSearchCriteria searchCriteria = MessageSearchCriteria.builder() .locale(MR_IN) @@ -334,44 +336,6 @@ public void test_should_return_messages_filtered_by_module_name() { assertEquals(0, actualMessages.size()); } - /* @Test - public void test_should_return_un_filtered_messages_when_module_is_not_present() { - String tenantId = "a.b.c"; - final Tenant defaultTenant = new Tenant(Tenant.DEFAULT_TENANT); - final MessageIdentity messageIdentity1 = MessageIdentity.builder() - .code("code1") - .locale(ENGLISH_INDIA) - .module("module1") - .tenant(defaultTenant) - .build(); - Message defaultMessage1 = Message.builder() - .messageIdentity(messageIdentity1) - .message("default message1") - .build(); - final MessageIdentity messageIdentity2 = MessageIdentity.builder() - .code("code2") - .locale(ENGLISH_INDIA) - .module("module2") - .tenant(defaultTenant) - .build(); - Message defaultMessage2 = Message.builder() - .messageIdentity(messageIdentity2) - .message("default message2") - .build(); - List expectedMessages = Arrays.asList(defaultMessage1, defaultMessage2); - when(messageCacheRepository.getComputedMessages(MR_IN, new Tenant(tenantId))) - .thenReturn(expectedMessages); - final MessageSearchCriteria searchCriteria = MessageSearchCriteria.builder() - .locale(MR_IN) - .tenantId(new Tenant(tenantId)) - .module(null) - .build(); - - List actualMessages = messageService.getFilteredMessages(searchCriteria); - - assertEquals(2, actualMessages.size()); - }*/ - @Test public void test_should_return_messages_from_cache_when_present() { String tenantId = "a"; @@ -397,11 +361,14 @@ public void test_should_return_messages_from_cache_when_present() { .message("marathi message for tenant a") .build(); List marathiMessagesForGivenTenant = Collections.singletonList(tenantMessage1); - when(messageCacheRepository.getMessages(MR_IN, new Tenant("a"))) + // Module-scoped raw cache hit + when(messageCacheRepository.getMessagesForModules(MR_IN, new Tenant("a"), Collections.singletonList("module"))) .thenReturn(marathiMessagesForGivenTenant); - when(messageCacheRepository.getMessages(ENGLISH_INDIA, new Tenant("default"))) + when(messageCacheRepository.getMessagesForModules(ENGLISH_INDIA, new Tenant("default"), Collections.singletonList("module"))) .thenReturn(defaultEnglishMessages); - when(messageCacheRepository.getComputedMessages(anyString(), any())).thenReturn(null); + when(messageCacheRepository.getMessagesForModules(MR_IN, new Tenant("default"), Collections.singletonList("module"))) + .thenReturn(Collections.emptyList()); + when(messageCacheRepository.getComputedMessagesForModules(anyString(), any(), anyList())).thenReturn(null); final MessageSearchCriteria searchCriteria = MessageSearchCriteria.builder() .locale(MR_IN) .tenantId(new Tenant(tenantId)) @@ -411,7 +378,6 @@ public void test_should_return_messages_from_cache_when_present() { List actualMessages = messageService.getFilteredMessages(searchCriteria); assertEquals(1, actualMessages.size()); - //assertEquals("code1", actualMessages.get(0).getCode()); assertEquals("code2", actualMessages.get(0).getCode()); } @@ -451,7 +417,7 @@ public void test_should_update_messages() { .build(); Message message1 = Message.builder() .messageIdentity(messageIdentity1) - .message("OTP यशस्वीपणे प्रमाणित") + .message("OTP validated") .build(); final MessageIdentity messageIdentity2 = MessageIdentity.builder() .code("core.lbl.imageupload") @@ -461,7 +427,7 @@ public void test_should_update_messages() { .build(); Message message2 = Message.builder() .messageIdentity(messageIdentity2) - .message("प्रतिमा यशस्वीरित्या अपलोड") + .message("Image uploaded") .build(); List modelMessages = Arrays.asList(message1, message2); @@ -484,7 +450,7 @@ public void test_should_bust_cache_entries_for_update_messages() { .build(); Message message1 = Message.builder() .messageIdentity(messageIdentity1) - .message("OTP यशस्वीपणे प्रमाणित") + .message("OTP validated") .build(); final MessageIdentity messageIdentity2 = MessageIdentity.builder() .code("core.lbl.imageupload") @@ -494,7 +460,7 @@ public void test_should_bust_cache_entries_for_update_messages() { .build(); Message message2 = Message.builder() .messageIdentity(messageIdentity2) - .message("प्रतिमा यशस्वीरित्या अपलोड") + .message("Image uploaded") .build(); List modelMessages = Arrays.asList(message1, message2); @@ -578,7 +544,7 @@ private List getMessages() { .build(); Message message1 = Message.builder() .messageIdentity(messageIdentity1) - .message("OTP यशस्वीपणे प्रमाणित") + .message("OTP validated") .build(); final MessageIdentity messageIdentity2 = MessageIdentity.builder() .code("core.lbl.imageupload") @@ -588,7 +554,7 @@ private List getMessages() { .build(); Message message2 = Message.builder() .messageIdentity(messageIdentity2) - .message("प्रतिमा यशस्वीरित्या अपलोड") + .message("Image uploaded") .build(); final MessageIdentity messageIdentity3 = MessageIdentity.builder() .code("core.msg.entermobileno") @@ -614,4 +580,4 @@ private List getMessages() { return (Arrays.asList(message1, message2, message3, message4)); } -} \ No newline at end of file +} From 743d6578ea205214151f68e9b80de1c0b1665d89 Mon Sep 17 00:00:00 2001 From: ChakshuGautam Date: Sun, 1 Mar 2026 10:02:16 +0000 Subject: [PATCH 03/11] fix(boundary-service): fix tenant filtering and tree assembly in relationship search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in boundary-relationships/_search caused incorrect results: 1. Controller used @ModelAttribute for search criteria, which reads from query params/form data — not the JSON POST body. This meant tenantId, hierarchyType, and all other filter fields were always null, so the query returned ALL boundary relationships across ALL tenants globally. Fix: introduce BoundaryRelationshipSearchRequest wrapper and use @RequestBody to properly deserialize the JSON body. 2. When isSearchForRootNode=true, the query builder appended "parent IS NULL" which returned only root-level nodes. The enricher (BoundaryRelationshipEnricher) needs ALL nodes for the tenant+hierarchy to build the full tree via getSeedBoundaryList(). With only root nodes, the response always had empty children arrays. Fix: remove the parent IS NULL clause so the enricher receives the complete node set for tree assembly. Co-Authored-By: Claude Opus 4.6 (cherry picked from commit cfe192320dc9de7711af797d21bd0afb56f2de5f) --- .../BoundaryRelationshipQueryBuilder.java | 7 ++-- .../BoundaryRelationshipController.java | 9 +++--- .../BoundaryRelationshipSearchRequest.java | 32 +++++++++++++++++++ 3 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 core-services/boundary-service/src/main/java/digit/web/models/BoundaryRelationshipSearchRequest.java diff --git a/core-services/boundary-service/src/main/java/digit/repository/querybuilder/BoundaryRelationshipQueryBuilder.java b/core-services/boundary-service/src/main/java/digit/repository/querybuilder/BoundaryRelationshipQueryBuilder.java index 2bf04106ecc..d4763c492d9 100644 --- a/core-services/boundary-service/src/main/java/digit/repository/querybuilder/BoundaryRelationshipQueryBuilder.java +++ b/core-services/boundary-service/src/main/java/digit/repository/querybuilder/BoundaryRelationshipQueryBuilder.java @@ -57,10 +57,9 @@ private String buildQuery(BoundaryRelationshipSearchCriteria boundaryRelationshi } } - if(boundaryRelationshipSearchCriteria.getIsSearchForRootNode()) { - QueryUtil.addClauseIfRequired(builder, preparedStmtList); - builder.append(" parent IS NULL "); - } + // When isSearchForRootNode is true, fetch ALL nodes for the tenant+hierarchy + // so the enricher can build the full tree. Do NOT restrict to parent IS NULL, + // as that returns only root nodes with no children to assemble. if(!CollectionUtils.isEmpty(boundaryRelationshipSearchCriteria.getCurrentBoundaryCodes())) { QueryUtil.addClauseIfRequired(builder, preparedStmtList); diff --git a/core-services/boundary-service/src/main/java/digit/web/controllers/BoundaryRelationshipController.java b/core-services/boundary-service/src/main/java/digit/web/controllers/BoundaryRelationshipController.java index 836ba8f4007..e534f338390 100644 --- a/core-services/boundary-service/src/main/java/digit/web/controllers/BoundaryRelationshipController.java +++ b/core-services/boundary-service/src/main/java/digit/web/controllers/BoundaryRelationshipController.java @@ -4,7 +4,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import digit.service.BoundaryRelationshipService; import digit.web.models.*; -import org.egov.common.contract.request.RequestInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -35,13 +34,13 @@ public ResponseEntity create(@Valid @RequestBody B /** * Request handler for serving boundary relationships search request. - * @param boundaryRelationshipSearchCriteria - * @param requestInfo + * @param body wrapper containing RequestInfo and BoundaryRelationshipSearchCriteria * @return */ @RequestMapping(value = "/_search", method = RequestMethod.POST) - public ResponseEntity search(@Valid @ModelAttribute BoundaryRelationshipSearchCriteria boundaryRelationshipSearchCriteria, @RequestBody RequestInfo requestInfo) { - BoundarySearchResponse boundarySearchResponse = boundaryRelationshipService.getBoundaryRelationships(boundaryRelationshipSearchCriteria, requestInfo); + public ResponseEntity search(@Valid @RequestBody BoundaryRelationshipSearchRequest body) { + BoundarySearchResponse boundarySearchResponse = boundaryRelationshipService.getBoundaryRelationships( + body.getBoundaryRelationshipSearchCriteria(), body.getRequestInfo()); return new ResponseEntity<>(boundarySearchResponse, HttpStatus.OK); } diff --git a/core-services/boundary-service/src/main/java/digit/web/models/BoundaryRelationshipSearchRequest.java b/core-services/boundary-service/src/main/java/digit/web/models/BoundaryRelationshipSearchRequest.java new file mode 100644 index 00000000000..27ac6623155 --- /dev/null +++ b/core-services/boundary-service/src/main/java/digit/web/models/BoundaryRelationshipSearchRequest.java @@ -0,0 +1,32 @@ +package digit.web.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.egov.common.contract.request.RequestInfo; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.Valid; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Data; +import lombok.Builder; + +/** + * Wrapper request for boundary relationship search — contains RequestInfo and search criteria. + */ +@Validated +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class BoundaryRelationshipSearchRequest { + + @JsonProperty("RequestInfo") + @Valid + private RequestInfo requestInfo = null; + + @JsonProperty("BoundaryRelationship") + @Valid + private BoundaryRelationshipSearchCriteria boundaryRelationshipSearchCriteria = null; + +} From 005b0307aeff4935d7229abc4b0726fd85766db0 Mon Sep 17 00:00:00 2001 From: ChakshuGautam Date: Thu, 26 Feb 2026 07:33:48 +0000 Subject: [PATCH 04/11] fix(egov-user): add tenant-aware encryption and localization overloads EncryptionDecryptionUtil and LocalizationUtil use a hardcoded STATE_LEVEL_TENANT_ID property, preventing multi-state deployments from using the correct encryption key or localization endpoint for non-default state roots. Add new overloaded methods that accept a tenantId parameter and use MultiStateInstanceUtil.getStateLevelTenant() to dynamically derive the state root. Original methods preserved for backward compatibility. Related to PR #1043 (egov-workflow-v2 dynamic state tenant). Co-Authored-By: Claude Opus 4.6 (cherry picked from commit c6f543819cf3a9ed01b09f11a42c368d14eb3abb) --- .../utils/EncryptionDecryptionUtil.java | 32 +++++++++++++++++++ .../service/utils/LocalizationUtil.java | 26 +++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/core-services/egov-user/src/main/java/org/egov/user/domain/service/utils/EncryptionDecryptionUtil.java b/core-services/egov-user/src/main/java/org/egov/user/domain/service/utils/EncryptionDecryptionUtil.java index 2b7cedd3106..8017d154b14 100644 --- a/core-services/egov-user/src/main/java/org/egov/user/domain/service/utils/EncryptionDecryptionUtil.java +++ b/core-services/egov-user/src/main/java/org/egov/user/domain/service/utils/EncryptionDecryptionUtil.java @@ -8,6 +8,7 @@ import org.egov.common.contract.request.RequestInfo; import org.egov.common.contract.request.Role; import org.egov.common.contract.request.User; +import org.egov.common.utils.MultiStateInstanceUtil; import org.egov.encryption.EncryptionService; import org.egov.encryption.audit.AuditService; import org.egov.tracer.model.CustomException; @@ -32,6 +33,9 @@ public class EncryptionDecryptionUtil { @Autowired private ObjectMapper objectMapper; + @Autowired + private MultiStateInstanceUtil centralInstanceUtil; + @Value(("${state.level.tenant.id}")) private String stateLevelTenantId; @@ -61,6 +65,34 @@ public T encryptObject(Object objectToEncrypt, String key, Class classTyp } } + /** + * Tenant-aware encryption: derives the state-level tenant dynamically from the + * provided tenantId instead of using the hardcoded configuration property. + * This enables encryption to work correctly for any state root, not just the + * configured default. + */ + public T encryptObject(Object objectToEncrypt, String key, Class classType, String tenantId) { + try { + if (objectToEncrypt == null) { + return null; + } + String resolvedTenantId = (tenantId != null) + ? centralInstanceUtil.getStateLevelTenant(tenantId) + : stateLevelTenantId; + T encryptedObject = encryptionService.encryptJson(objectToEncrypt, key, resolvedTenantId, classType); + if (encryptedObject == null) { + throw new CustomException("ENCRYPTION_NULL_ERROR", "Null object found on performing encryption"); + } + return encryptedObject; + } catch (IOException | HttpClientErrorException | HttpServerErrorException | ResourceAccessException e) { + log.error("Error occurred while encrypting", e); + throw new CustomException("ENCRYPTION_ERROR", "Error occurred in encryption process"); + } catch (Exception e) { + log.error("Unknown Error occurred while encrypting", e); + throw new CustomException("UNKNOWN_ERROR", "Unknown error occurred in encryption process"); + } + } + public P decryptObject(Object objectToDecrypt, String key, Class classType, RequestInfo requestInfo) { try { diff --git a/core-services/egov-user/src/main/java/org/egov/user/domain/service/utils/LocalizationUtil.java b/core-services/egov-user/src/main/java/org/egov/user/domain/service/utils/LocalizationUtil.java index 75424ae66c6..5614fe4dd27 100644 --- a/core-services/egov-user/src/main/java/org/egov/user/domain/service/utils/LocalizationUtil.java +++ b/core-services/egov-user/src/main/java/org/egov/user/domain/service/utils/LocalizationUtil.java @@ -3,6 +3,7 @@ import com.jayway.jsonpath.JsonPath; import lombok.extern.slf4j.Slf4j; import org.egov.common.contract.request.RequestInfo; +import org.egov.common.utils.MultiStateInstanceUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -33,6 +34,8 @@ public class LocalizationUtil { private String defaultLocale; @Autowired private RestTemplate restTemplate; + @Autowired + private MultiStateInstanceUtil centralInstanceUtil; public String getLocalizedMessage(String code, String locale, RequestInfo requestInfo) { if(locale == null) @@ -54,4 +57,27 @@ String getUri(String locale) { return localizationServiceHost + localizationServiceSearchPath + "?locale=" + locale + "&tenantId=" + tenantId + "&module=" + module; } + /** + * Tenant-aware localization: derives the state-level tenant dynamically from the + * provided tenantId instead of using the hardcoded configuration property. + */ + public String getLocalizedMessage(String code, String locale, RequestInfo requestInfo, String userTenantId) { + if(locale == null) + locale = defaultLocale; + String resolvedTenantId = (userTenantId != null) + ? centralInstanceUtil.getStateLevelTenant(userTenantId) + : tenantId; + String uri = localizationServiceHost + localizationServiceSearchPath + "?locale=" + locale + "&tenantId=" + resolvedTenantId + "&module=" + module; + Object responseobj = restTemplate.postForObject(uri, requestInfo, Map.class); + Object object = JsonPath.read(responseobj, + "$.messages[?(@.code==\"" + code + "\")].message"); + List messages = (ArrayList) object; + if(CollectionUtils.isEmpty(messages)){ + log.warn("No localization messages returned for locale: " + locale +" . Continuing with english language"); + messages.add(DEFAULT_EMAIL_UPDATION_MESSAGE); + } + String message = messages.get(0); + return message; + } + } From 630d0fec910ca42c9175810328bc9df735727408 Mon Sep 17 00:00:00 2001 From: ChakshuGautam Date: Fri, 27 Feb 2026 08:46:17 +0000 Subject: [PATCH 05/11] fix(egov-user): prevent NPE in _updatenovalidate and profile _update The `updateWithoutOtpValidation` and `partialUpdate` methods call `requestInfo.getUserInfo().getId()` without null-checking `getUserInfo()`. When the auth token doesn't resolve to a userInfo (e.g. cross-tenant admin operations), this throws NullPointerException. Additionally, `userRepository.update()` takes primitive `long` for userId, so passing null Long causes unboxing NPE. Use 0L as the fallback. Co-Authored-By: Claude Opus 4.6 (cherry picked from commit ccc0e133177a8c48bb0d8dedc146319f53ee93cb) --- .../java/org/egov/user/domain/service/UserService.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core-services/egov-user/src/main/java/org/egov/user/domain/service/UserService.java b/core-services/egov-user/src/main/java/org/egov/user/domain/service/UserService.java index 4204a91222f..210bb4673c0 100644 --- a/core-services/egov-user/src/main/java/org/egov/user/domain/service/UserService.java +++ b/core-services/egov-user/src/main/java/org/egov/user/domain/service/UserService.java @@ -383,7 +383,10 @@ public User updateWithoutOtpValidation(User user, RequestInfo requestInfo) { user.setPassword(encryptPwd(user.getPassword())); /* encrypt */ user = encryptionDecryptionUtil.encryptObject(user, "User", User.class); - userRepository.update(user, existingUser,requestInfo.getUserInfo().getId(), requestInfo.getUserInfo().getUuid() ); + long loggedInUserId = requestInfo.getUserInfo() != null && requestInfo.getUserInfo().getId() != null + ? requestInfo.getUserInfo().getId() : 0L; + String loggedInUserUuid = requestInfo.getUserInfo() != null ? requestInfo.getUserInfo().getUuid() : null; + userRepository.update(user, existingUser, loggedInUserId, loggedInUserUuid); // If user is being unlocked via update, reset failed login attempts if (user.getAccountLocked() != null && !user.getAccountLocked() && existingUser.getAccountLocked()) @@ -440,7 +443,10 @@ public User partialUpdate(User user, RequestInfo requestInfo) { validateProfileUpdateIsDoneByTheSameLoggedInUser(user); user.nullifySensitiveFields(); validatePassword(user.getPassword()); - userRepository.update(user, existingUser,requestInfo.getUserInfo().getId(), requestInfo.getUserInfo().getUuid() ); + long partialLoggedInUserId = requestInfo.getUserInfo() != null && requestInfo.getUserInfo().getId() != null + ? requestInfo.getUserInfo().getId() : 0L; + String partialLoggedInUserUuid = requestInfo.getUserInfo() != null ? requestInfo.getUserInfo().getUuid() : null; + userRepository.update(user, existingUser, partialLoggedInUserId, partialLoggedInUserUuid); User updatedUser = getUserByUuid(user.getUuid()); /* decrypt here */ From af895448e65ec5db22ab51653f37632a91cbc55d Mon Sep 17 00:00:00 2001 From: ChakshuGautam Date: Sun, 31 May 2026 11:58:51 +0000 Subject: [PATCH 06/11] fix(egov-user): migrate all encryption call sites to tenant-aware overload The previous commit on this branch added an encryptObject overload that accepts a tenantId and resolves the state root via MultiStateInstanceUtil, but no call sites were updated to use it. With the legacy 3-arg method still in place, every encrypt path silently fell back to the hardcoded STATE_LEVEL_TENANT_ID env var. So flipping STATE_LEVEL_TENANT_ID between deploys still produced key-version drift on encrypted columns and broke logins (the freshly-generated ciphertext prefix mismatches the prefix on existing DB rows; UserNotFoundException -> "Invalid login credentials"). Migrate the 6 UserService.java call sites: - encryptUserSearchCriteria (L154, L205): pass criteria.getTenantId() - createUser (L232): pass user.getTenantId() - updateWithoutOtpValidation (L367): pass user.getTenantId() - partialUpdate (L418): pass user.getTenantId() - updatePassword (L492): pass user.getTenantId() The new overload null-safe-falls-back to stateLevelTenantId, so paths that genuinely lack a request tenant retain today behavior. For single- state deployments (request tenant matches STATE_LEVEL_TENANT_ID), getStateLevelTenant strips the city suffix to the state root - output is identical to today's hardcoded path. No DB compatibility impact. Validated on bomet (ke state): /user/oauth/token, /user/citizen/_create, /user/profile/_update, /user/_search all 200 against pre-existing rows. Refs egovernments/Citizen-Complaint-Resolution-System#622 (cherry picked from commit 884c5e61c5948562007933fedba4736a1867b3e5) --- .../org/egov/user/domain/service/UserService.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core-services/egov-user/src/main/java/org/egov/user/domain/service/UserService.java b/core-services/egov-user/src/main/java/org/egov/user/domain/service/UserService.java index 4204a91222f..209c93a0a14 100644 --- a/core-services/egov-user/src/main/java/org/egov/user/domain/service/UserService.java +++ b/core-services/egov-user/src/main/java/org/egov/user/domain/service/UserService.java @@ -161,7 +161,7 @@ public User getUniqueUser(String userName, String tenantId, UserType userType) { /* encrypt here */ - userSearchCriteria = encryptionDecryptionUtil.encryptObject(userSearchCriteria, "User", UserSearchCriteria.class); + userSearchCriteria = encryptionDecryptionUtil.encryptObject(userSearchCriteria, "User", UserSearchCriteria.class, userSearchCriteria.getTenantId()); List users = userRepository.findAll(userSearchCriteria); if (users.isEmpty()) @@ -218,7 +218,7 @@ public List searchUsers(UserSearchCriteria sear altmobnumber = searchCriteria.getMobileNumber(); } - searchCriteria = encryptionDecryptionUtil.encryptObject(searchCriteria, "User", UserSearchCriteria.class); + searchCriteria = encryptionDecryptionUtil.encryptObject(searchCriteria, "User", UserSearchCriteria.class, searchCriteria.getTenantId()); if(altmobnumber!=null) { searchCriteria.setAlternatemobilenumber(altmobnumber); @@ -246,7 +246,7 @@ public User createUser(User user, RequestInfo requestInfo) { user.validateNewUser(createUserValidateName); conditionallyValidateOtp(user); /* encrypt here */ - user = encryptionDecryptionUtil.encryptObject(user, "User", User.class); + user = encryptionDecryptionUtil.encryptObject(user, "User", User.class, user.getTenantId()); validateUserUniqueness(user); if (isEmpty(user.getPassword())) { user.setPassword(UUID.randomUUID().toString()); @@ -382,7 +382,7 @@ public User updateWithoutOtpValidation(User user, RequestInfo requestInfo) { validatePassword(user.getPassword()); user.setPassword(encryptPwd(user.getPassword())); /* encrypt */ - user = encryptionDecryptionUtil.encryptObject(user, "User", User.class); + user = encryptionDecryptionUtil.encryptObject(user, "User", User.class, user.getTenantId()); userRepository.update(user, existingUser,requestInfo.getUserInfo().getId(), requestInfo.getUserInfo().getUuid() ); // If user is being unlocked via update, reset failed login attempts @@ -434,7 +434,7 @@ private void validateUserRoles(User user) { public User partialUpdate(User user, RequestInfo requestInfo) { mobileNumberValidator.validateAndSetMobileNumbers(user, requestInfo); /* encrypt here */ - user = encryptionDecryptionUtil.encryptObject(user, "User", User.class); + user = encryptionDecryptionUtil.encryptObject(user, "User", User.class, user.getTenantId()); User existingUser = getUserByUuid(user.getUuid()); validateProfileUpdateIsDoneByTheSameLoggedInUser(user); @@ -508,7 +508,7 @@ public void updatePasswordForNonLoggedInUser(NonLoggedInUserUpdatePasswordReques user.updatePassword(encryptPwd(request.getNewPassword())); /* encrypt here */ /* encrypted value is stored in DB*/ - user = encryptionDecryptionUtil.encryptObject(user, "User", User.class); + user = encryptionDecryptionUtil.encryptObject(user, "User", User.class, user.getTenantId()); userRepository.update(user, user,user.getId() , user.getUuid()); } From 5789262feb9e0e4c39ee4741310294df2ace62a1 Mon Sep 17 00:00:00 2001 From: ChakshuGautam Date: Wed, 10 Jun 2026 19:31:10 +0000 Subject: [PATCH 07/11] fix(workflow-v2): persist assignees on self-loop ASSIGN transitions Fixes eGovStack/core-services#1674. Root cause: the ProcessInstance API contract uses the historically misspelled JSON key "assignes" (@JsonProperty). Clients that send the correctly spelled "assignees" key (e.g. integration suites and UI paths) have that field silently dropped during deserialization, because Spring Boot's default ObjectMapper runs with FAIL_ON_UNKNOWN_PROPERTIES disabled. The transition then proceeds with assignes=null: WorkflowValidator has no assignee-presence check, EnrichmentService.enrichUsers only enriches non-empty lists, and the persister's eg_wf_assignee_v2 mapping (jsonPath ProcessInstances.*.assignes.*) extracts zero rows and skips the INSERT entirely while the eg_wf_processinstance_v2 INSERT in the same batch succeeds. Result: the process instance exists but carries no assignee rows, which surfaces as "assignees lost on ASSIGN" since /_search returns the newest (assignee-less) instance per businessId. Fix: add @JsonAlias("assignees") on the assignes field so deserialization accepts both spellings. Serialization still emits "assignes", so the persister jsonPaths, /process/_search responses and all existing consumers are unchanged. Tests: ProcessInstanceTest proves both spellings deserialize into getAssignes() (including via the full ProcessInstanceRequest transition payload) and that serialization keeps emitting "assignes". The two alias tests fail without the one-line fix. Co-Authored-By: Claude Fable 5 (cherry picked from commit 43f925c211e17726561af0a7b9d1216f3c0e63d0) --- .../egov/wf/web/models/ProcessInstance.java | 6 ++ .../wf/web/models/ProcessInstanceTest.java | 78 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 core-services/egov-workflow-v2/src/test/java/org/egov/wf/web/models/ProcessInstanceTest.java diff --git a/core-services/egov-workflow-v2/src/main/java/org/egov/wf/web/models/ProcessInstance.java b/core-services/egov-workflow-v2/src/main/java/org/egov/wf/web/models/ProcessInstance.java index 985a8a89926..a1dea56043c 100644 --- a/core-services/egov-workflow-v2/src/main/java/org/egov/wf/web/models/ProcessInstance.java +++ b/core-services/egov-workflow-v2/src/main/java/org/egov/wf/web/models/ProcessInstance.java @@ -10,6 +10,7 @@ import org.egov.common.contract.request.User; import org.springframework.validation.annotation.Validated; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.annotations.ApiModel; @@ -80,7 +81,12 @@ public class ProcessInstance { @JsonProperty("assigner") private User assigner = null; + // "assignes" is the historical (misspelled) contract key; serialization must + // keep emitting it because the persister jsonPaths (ProcessInstances.*.assignes.*) + // and downstream consumers depend on it. The alias accepts the correctly spelled + // "assignees" that some clients send, which was previously dropped silently. @JsonProperty("assignes") + @JsonAlias("assignees") private List assignes = null; @JsonProperty("nextActions") diff --git a/core-services/egov-workflow-v2/src/test/java/org/egov/wf/web/models/ProcessInstanceTest.java b/core-services/egov-workflow-v2/src/test/java/org/egov/wf/web/models/ProcessInstanceTest.java new file mode 100644 index 00000000000..6e02c569274 --- /dev/null +++ b/core-services/egov-workflow-v2/src/test/java/org/egov/wf/web/models/ProcessInstanceTest.java @@ -0,0 +1,78 @@ +package org.egov.wf.web.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.Collections; + +import org.egov.common.contract.request.User; +import org.junit.jupiter.api.Test; + +class ProcessInstanceTest { + + /** + * Mimics Spring Boot's default ObjectMapper, which has + * FAIL_ON_UNKNOWN_PROPERTIES disabled. Before the @JsonAlias fix this + * caused the correctly spelled "assignees" key to be dropped silently, + * so the transition was accepted with 200 but no eg_wf_assignee_v2 rows + * were ever persisted. + */ + private ObjectMapper springBootLikeMapper() { + return new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + } + + @Test + void deserializesMisspelledContractKeyAssignes() throws Exception { + ProcessInstance processInstance = springBootLikeMapper() + .readValue("{\"assignes\":[{\"uuid\":\"u1\"}]}", ProcessInstance.class); + + assertNotNull(processInstance.getAssignes()); + assertEquals(1, processInstance.getAssignes().size()); + assertEquals("u1", processInstance.getAssignes().get(0).getUuid()); + } + + @Test + void deserializesCorrectlySpelledAliasAssignees() throws Exception { + ProcessInstance processInstance = springBootLikeMapper() + .readValue("{\"assignees\":[{\"uuid\":\"u1\"}]}", ProcessInstance.class); + + assertNotNull(processInstance.getAssignes()); + assertEquals(1, processInstance.getAssignes().size()); + assertEquals("u1", processInstance.getAssignes().get(0).getUuid()); + } + + @Test + void deserializesAliasInsideTransitionRequestPayload() throws Exception { + String payload = "{\"ProcessInstances\":[{\"tenantId\":\"pg.citya\"," + + "\"businessService\":\"PGR\",\"businessId\":\"PG-PGR-2026-000001\"," + + "\"action\":\"ASSIGN\",\"moduleName\":\"RAINMAKER-PGR\"," + + "\"assignees\":[{\"uuid\":\"u1\"}]}]}"; + + ProcessInstanceRequest request = springBootLikeMapper() + .readValue(payload, ProcessInstanceRequest.class); + + ProcessInstance processInstance = request.getProcessInstances().get(0); + assertNotNull(processInstance.getAssignes()); + assertEquals("u1", processInstance.getAssignes().get(0).getUuid()); + } + + @Test + void serializationStillEmitsAssignesForPersisterJsonPaths() throws Exception { + ProcessInstance processInstance = new ProcessInstance(); + User user = new User(); + user.setUuid("u1"); + processInstance.setAssignes(Collections.singletonList(user)); + + String json = springBootLikeMapper().writeValueAsString(processInstance); + + // The persister yml extracts ProcessInstances.*.assignes.* — the + // serialized key must remain "assignes" for existing consumers. + assertTrue(json.contains("\"assignes\"")); + assertFalse(json.contains("\"assignees\"")); + } +} From 9fdb7eac8419efbf123491653f5ce09655b735ca Mon Sep 17 00:00:00 2001 From: ChakshuGautam Date: Tue, 24 Mar 2026 07:01:27 +0000 Subject: [PATCH 08/11] feat: add JDK 21 runtime base image for Spring Boot 3.x services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add build/maven-jdk21/ as a new Dockerfile variant alongside the existing build/maven/ (JDK 17). Services on Spring Boot 3.x are switched to the new Dockerfile; legacy services (Spring Boot 1.5/2.x) remain on JDK 17 unchanged. Why: - JDK 17 has a cgroup v2 CPU detection bug (JDK-8281181) on kernel 6.8+ — the JVM sees all host CPUs instead of the container's CPU limit, spawning 50-60 threads instead of ~15. - JDK 21 fixes this and auto-sizes heap/GC/threads from cgroup limits, eliminating the need for manual JVM tuning flags. - JDK 21 runs Java 17 bytecode natively (no recompilation needed). New files: - build/maven-jdk21/Dockerfile: eclipse-temurin:21-jre-alpine runtime with CDS (Class Data Sharing) pre-dump for faster startup - build/maven-jdk21/start.sh: no default -Xms/-Xmx, CDS loading Updated: build-config.yml - 26 Spring Boot 3.x services → build/maven-jdk21/Dockerfile - 32 legacy/other services → unchanged (build/maven/Dockerfile) - 1 service (egov-user) → unchanged (build/maven-java8/Dockerfile) Validated on docker-compose local-setup (jar-swap approach): - 12 services rebased, all healthy - k6 load test: 100% success (104 transactions, 0 failures) - Memory: ~24% RSS reduction, ~42% thread count reduction Co-Authored-By: Claude Opus 4.6 (cherry picked from commit 5c861ca7ec7c0bbfd650f19d30639baba5bc3cc3) --- build/build-config.yml | 56 ++++++++++++++++++------------------ build/maven-jdk21/Dockerfile | 44 ++++++++++++++++++++++++++++ build/maven-jdk21/start.sh | 16 +++++++++++ build/maven/Dockerfile | 27 +++++++++++++---- build/maven/start.sh | 11 +++++-- 5 files changed, 118 insertions(+), 36 deletions(-) create mode 100644 build/maven-jdk21/Dockerfile create mode 100644 build/maven-jdk21/start.sh diff --git a/build/build-config.yml b/build/build-config.yml index 27353baa114..f27c4671f62 100644 --- a/build/build-config.yml +++ b/build/build-config.yml @@ -89,13 +89,13 @@ config: build: - work-dir: "core-services/egov-accesscontrol" image-name: "egov-accesscontrol" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" # Core Services - name: "builds/Digit-Core/core-services/audit-service" build: - work-dir: "core-services/audit-service" image-name: "audit-service" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/audit-service/src/main/resources/db" image-name: "audit-service-db" - name: "builds/Digit-Core/core-services/egov-common-masters" @@ -118,7 +118,7 @@ config: build: - work-dir: "core-services/egov-enc-service" image-name: "egov-enc-service" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/egov-enc-service/src/main/resources/db" image-name: "egov-enc-service-db" @@ -126,7 +126,7 @@ config: build: - work-dir: "core-services/egov-filestore" image-name: "egov-filestore" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/egov-filestore/src/main/resources/db" image-name: "egov-filestore-db" @@ -134,7 +134,7 @@ config: build: - work-dir: "core-services/egov-idgen" image-name: "egov-idgen" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/egov-idgen/src/main/resources/db" image-name: "egov-idgen-db" @@ -142,7 +142,7 @@ config: build: - work-dir: "core-services/egov-indexer" image-name: "egov-indexer" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/egov-indexer/src/main/resources/db" image-name: "egov-indexer-db" @@ -150,7 +150,7 @@ config: build: - work-dir: "core-services/egov-localization" image-name: "egov-localization" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/egov-localization/src/main/resources/db" image-name: "egov-localization-db" @@ -158,7 +158,7 @@ config: build: - work-dir: "core-services/egov-location" image-name: "egov-location" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/egov-location/src/main/resources/db" image-name: "egov-location-db" @@ -166,7 +166,7 @@ config: build: - work-dir: "core-services/boundary-service" image-name: "boundary-service" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/boundary-service/src/main/resources/db" image-name: "boundary-service-db" @@ -174,13 +174,13 @@ config: build: - work-dir: "core-services/egov-mdms-service" image-name: "egov-mdms-service" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - name: "builds/Digit-Core/core-services/mdms-v2" build: - work-dir: "core-services/mdms-v2" image-name: "mdms-v2" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/mdms-v2/src/main/resources/db" image-name: "mdms-v2-db" @@ -188,19 +188,19 @@ config: build: - work-dir: "core-services/egov-notification-mail" image-name: "egov-notification-mail" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - name: "builds/Digit-Core/core-services/egov-notification-sms" build: - work-dir: "core-services/egov-notification-sms" image-name: "egov-notification-sms" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - name: "builds/Digit-Core/core-services/egov-otp" build: - work-dir: "core-services/egov-otp" image-name: "egov-otp" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/egov-otp/src/main/resources/db" image-name: "egov-otp-db" @@ -208,13 +208,13 @@ config: build: - work-dir: "core-services/egov-persister" image-name: "egov-persister" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - name: "builds/Digit-Core/core-services/egov-pg-service" build: - work-dir: "core-services/egov-pg-service" image-name: "egov-pg-service" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/egov-pg-service/src/main/resources/db" image-name: "egov-pg-service-db" @@ -242,7 +242,7 @@ config: build: - work-dir: "core-services/egov-user-event" image-name: "egov-user-event" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/egov-user-event/src/main/resources/db" image-name: "egov-user-event-db" @@ -250,7 +250,7 @@ config: build: - work-dir: "core-services/egov-workflow-v2" image-name: "egov-workflow-v2" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/egov-workflow-v2/src/main/resources/db" image-name: "egov-workflow-v2-db" @@ -303,7 +303,7 @@ config: build: - work-dir: "core-services/user-otp" image-name: "user-otp" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - name: "builds/Digit-Core/core-services/zuul" build: @@ -315,7 +315,7 @@ config: build: - work-dir: "core-services/gateway" image-name: "gateway" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - name: "builds/Digit-Core/core-services/internal-gateway" build: @@ -327,15 +327,15 @@ config: build: - work-dir: "core-services/internal-gateway-scg" image-name: "internal-gateway-scg" - dockerfile: "build/maven/Dockerfile" - + dockerfile: "build/maven-jdk21/Dockerfile" + - name: "builds/Digit-Core/core-services/egov-user-event" build: - work-dir: "core-services/egov-user-event" image-name: "egov-user-event" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/egov-user-event/src/main/resources/db" - image-name: "egov-user-event-db" + image-name: "egov-user-event-db" - name: "builds/Digit-Core/core-services/pdf-service" build: @@ -360,7 +360,7 @@ config: build: - work-dir: "core-services/egov-url-shortening" image-name: "egov-url-shortening" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/egov-url-shortening/src/main/resources/db" image-name: "egov-url-shortening-db" @@ -449,7 +449,7 @@ config: build: - work-dir: "accelerators/inbox" image-name: "inbox" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - name: "builds/Digit-Core/accelerators/gateway-kubernetes-discovery" build: @@ -460,7 +460,7 @@ config: build: - work-dir: "accelerators/pgr-services" image-name: "pgr-services" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "accelerators/pgr-services/src/main/resources/db" image-name: "pgr-services-db" @@ -468,7 +468,7 @@ config: build: - work-dir: "core-services/service-request" image-name: "service-request" - dockerfile: "build/maven/Dockerfile" + dockerfile: "build/maven-jdk21/Dockerfile" - work-dir: "core-services/service-request/src/main/resources/db" image-name: "service-request-db" diff --git a/build/maven-jdk21/Dockerfile b/build/maven-jdk21/Dockerfile new file mode 100644 index 00000000000..699f0f06e20 --- /dev/null +++ b/build/maven-jdk21/Dockerfile @@ -0,0 +1,44 @@ +# JDK 21 runtime variant of build/maven/Dockerfile. +# Use this for services on Spring Boot 3.x compiled with Java 17+. +# Services on Spring Boot 1.5/2.x must continue using build/maven/Dockerfile. +# +# JDK 21 fixes cgroup v2 CPU detection (JDK-8281181), auto-sizes heap/threads +# from container limits, and runs Java 17 bytecode natively (no recompilation). + +FROM egovio/maven:3.9.6-amazoncorretto-17 AS build +ARG WORK_DIR +WORKDIR /app + +# Copy project files +COPY ${WORK_DIR}/pom.xml ./pom.xml +COPY build/maven-jdk21/start.sh ./start.sh +COPY ${WORK_DIR}/src ./src + +# Build the project +RUN mvn -B -f /app/pom.xml package + +# Runtime image — JDK 21 on Alpine +FROM eclipse-temurin:21-jre-alpine + +RUN apk add --no-cache dos2unix + +WORKDIR /opt/egov + +# Copy artifacts from the build stage +COPY --from=build /app/target/*.jar /opt/egov/app.jar +COPY --from=build /app/start.sh /opt/egov/start.sh + +# CDS pre-dump: generate classlist then shared archive for faster startup. +# The app will attempt to start (no DB/Kafka available) — timeout kills it +# after 30s, which is enough to load all classes for the classlist. +RUN timeout 30 java -XX:DumpLoadedClassList=/opt/egov/app.classlist \ + -jar /opt/egov/app.jar 2>/dev/null; true +RUN java -Xshare:dump \ + -XX:SharedClassListFile=/opt/egov/app.classlist \ + -XX:SharedArchiveFile=/opt/egov/app-cds.jsa \ + -jar /opt/egov/app.jar 2>/dev/null; true + +# Ensure the start script has correct line endings and is executable +RUN dos2unix /opt/egov/start.sh && chmod +x /opt/egov/start.sh + +CMD ["/opt/egov/start.sh"] diff --git a/build/maven-jdk21/start.sh b/build/maven-jdk21/start.sh new file mode 100644 index 00000000000..d8fcc20d31a --- /dev/null +++ b/build/maven-jdk21/start.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# JDK 21 auto-sizes heap from cgroup limits — no default -Xms/-Xmx needed. +# Set JAVA_OPTS or JAVA_TOOL_OPTIONS via container environment if tuning is required. + +# CDS: use shared archive if it exists (pre-baked at image build time) +CDS_OPTS="" +if [ -f /opt/egov/app-cds.jsa ]; then + CDS_OPTS="-XX:SharedArchiveFile=/opt/egov/app-cds.jsa" +fi + +if [ x"${JAVA_ENABLE_DEBUG}" != x ] && [ "${JAVA_ENABLE_DEBUG}" != "false" ]; then + java_debug_args="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${JAVA_DEBUG_PORT:-5005}" +fi + +exec java ${java_debug_args} ${JAVA_OPTS} ${CDS_OPTS} ${JAVA_ARGS} -jar /opt/egov/app.jar diff --git a/build/maven/Dockerfile b/build/maven/Dockerfile index dc6f1625439..1b34d59bda9 100644 --- a/build/maven/Dockerfile +++ b/build/maven/Dockerfile @@ -10,12 +10,29 @@ COPY ${WORK_DIR}/src ./src # Build the project RUN mvn -B -f /app/pom.xml package -DskipTests -# ─────────────── RUNTIME STAGE ─────────────── -FROM gcr.io/distroless/java17-debian12:nonroot +# Runtime image – JDK 21 fixes cgroup v2 CPU detection (JDK-8281181), +# auto-sizes heap/threads from container limits, and runs Java 17 bytecode natively. +FROM eclipse-temurin:21-jre-alpine + +RUN apk add --no-cache dos2unix WORKDIR /opt/egov -# Copy artifact from the build stage -COPY --from=build /app/target/*.jar ./application.jar +# Copy artifacts from the build stage +COPY --from=build /app/target/*.jar /opt/egov/app.jar +COPY --from=build /app/start.sh /opt/egov/start.sh + +# CDS pre-dump: generate classlist then shared archive for faster startup. +# The app will attempt to start (no DB/Kafka available) — timeout kills it +# after 30s, which is enough to load all classes for the classlist. +RUN timeout 30 java -XX:DumpLoadedClassList=/opt/egov/app.classlist \ + -jar /opt/egov/app.jar 2>/dev/null; true +RUN java -Xshare:dump \ + -XX:SharedClassListFile=/opt/egov/app.classlist \ + -XX:SharedArchiveFile=/opt/egov/app-cds.jsa \ + -jar /opt/egov/app.jar 2>/dev/null; true + +# Ensure the start script has correct line endings and is executable +RUN dos2unix /opt/egov/start.sh && chmod +x /opt/egov/start.sh -ENTRYPOINT ["java", "-jar", "application.jar"] \ No newline at end of file +CMD ["/opt/egov/start.sh"] \ No newline at end of file diff --git a/build/maven/start.sh b/build/maven/start.sh index 08bdd8de28e..d8fcc20d31a 100644 --- a/build/maven/start.sh +++ b/build/maven/start.sh @@ -1,11 +1,16 @@ #!/bin/sh -if [[ -z "${JAVA_OPTS}" ]];then - export JAVA_OPTS="-Xmx64m -Xms64m" +# JDK 21 auto-sizes heap from cgroup limits — no default -Xms/-Xmx needed. +# Set JAVA_OPTS or JAVA_TOOL_OPTIONS via container environment if tuning is required. + +# CDS: use shared archive if it exists (pre-baked at image build time) +CDS_OPTS="" +if [ -f /opt/egov/app-cds.jsa ]; then + CDS_OPTS="-XX:SharedArchiveFile=/opt/egov/app-cds.jsa" fi if [ x"${JAVA_ENABLE_DEBUG}" != x ] && [ "${JAVA_ENABLE_DEBUG}" != "false" ]; then java_debug_args="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${JAVA_DEBUG_PORT:-5005}" fi -exec java ${java_debug_args} ${JAVA_OPTS} ${JAVA_ARGS} -jar /opt/egov/*.jar \ No newline at end of file +exec java ${java_debug_args} ${JAVA_OPTS} ${CDS_OPTS} ${JAVA_ARGS} -jar /opt/egov/app.jar From cad3c52d7169ef9f9b9f7de0cfa11ef88c479fce Mon Sep 17 00:00:00 2001 From: ChakshuGautam Date: Tue, 24 Mar 2026 09:35:26 +0000 Subject: [PATCH 09/11] =?UTF-8?q?feat:=20add=20JDK=208=E2=86=9221=20hybrid?= =?UTF-8?q?=20build=20for=20egov-user?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit egov-user is on Spring Boot 1.5 / JDK 8 and cannot be easily migrated to Spring Boot 3.2. This new build variant compiles with JDK 8 but runs on JDK 21, getting cgroup v2 fixes and memory improvements without a framework migration. The hybrid approach: - Build stage: same JDK 8 Maven image (amazoncorretto-8) - Patch stage: injects JAXB API + runtime jars (removed from JDK 11+) - Runtime stage: eclipse-temurin:21-jre-alpine with CDS + --add-opens Measured results (8 vCPU / 16 GB VM): - Memory at idle: 461 MiB → 184 MiB (60% reduction) - Memory under 300 VU load: OOM-killed → 286 MiB (56% of limit) - Threads: 50+ → 34 Co-Authored-By: Claude Opus 4.6 (cherry picked from commit affb45a9082c6959bc8094de83b0a7e842edc5b1) --- build/build-config.yml | 2 +- build/maven-java8-jdk21/Dockerfile | 78 ++++++++++++++++++++++++++++++ build/maven-java8-jdk21/start.sh | 22 +++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 build/maven-java8-jdk21/Dockerfile create mode 100644 build/maven-java8-jdk21/start.sh diff --git a/build/build-config.yml b/build/build-config.yml index f27c4671f62..4e50e0b32cb 100644 --- a/build/build-config.yml +++ b/build/build-config.yml @@ -234,7 +234,7 @@ config: build: - work-dir: "core-services/egov-user" image-name: "egov-user" - dockerfile: "build/maven-java8/Dockerfile" + dockerfile: "build/maven-java8-jdk21/Dockerfile" - work-dir: "core-services/egov-user/src/main/resources/db" image-name: "egov-user-db" diff --git a/build/maven-java8-jdk21/Dockerfile b/build/maven-java8-jdk21/Dockerfile new file mode 100644 index 00000000000..36b7cd8ddc3 --- /dev/null +++ b/build/maven-java8-jdk21/Dockerfile @@ -0,0 +1,78 @@ +# JDK 8→21 hybrid: compile with JDK 8, run on JDK 21. +# Use this for services on Spring Boot 1.5/2.x that cannot be recompiled +# for JDK 17+ but benefit from JDK 21's cgroup v2 fix and memory improvements. +# +# JDK 21 fixes cgroup v2 CPU detection (JDK-8281181), auto-sizes heap/threads +# from container limits, and runs Java 8 bytecode natively. +# +# Spring Boot 1.5 needs two runtime patches for JDK 21: +# 1. JAXB API + runtime (removed from JDK 11) +# 2. --add-opens flags for internal JDK module access + +# ──────────────── BUILD STAGE ──────────────── +FROM egovio/maven:3.9.6-amazoncorretto-8-debian AS build +ARG WORK_DIR +WORKDIR /app + +COPY ${WORK_DIR}/pom.xml ./pom.xml +COPY build/maven-java8-jdk21/start.sh ./start.sh +COPY ${WORK_DIR}/src ./src + +RUN mvn -B -f pom.xml package -DskipTests + +# ──────────── JAXB PATCH STAGE ───────────── +FROM eclipse-temurin:21-jdk-alpine AS patcher +RUN apk add --no-cache curl +WORKDIR /patch + +COPY --from=build /app/target/*.jar ./app.jar + +# Download JAXB API + implementation (removed from JDK 11+) +RUN curl -sL -o jaxb-api.jar https://repo1.maven.org/maven2/javax/xml/bind/jaxb-api/2.3.1/jaxb-api-2.3.1.jar && \ + curl -sL -o javax.activation.jar https://repo1.maven.org/maven2/javax/activation/javax.activation-api/1.2.0/javax.activation-api-1.2.0.jar && \ + curl -sL -o jaxb-runtime.jar https://repo1.maven.org/maven2/org/glassfish/jaxb/jaxb-runtime/2.3.9/jaxb-runtime-2.3.9.jar && \ + curl -sL -o txw2.jar https://repo1.maven.org/maven2/org/glassfish/jaxb/txw2/2.3.9/txw2-2.3.9.jar && \ + curl -sL -o istack-commons-runtime.jar https://repo1.maven.org/maven2/com/sun/istack/istack-commons-runtime/3.0.12/istack-commons-runtime-3.0.12.jar && \ + curl -sL -o stax-ex.jar https://repo1.maven.org/maven2/org/jvnet/staxex/stax-ex/1.8.1/stax-ex-1.8.1.jar + +# Inject JAXB jars into the fat JAR (jar uf0 preserves STORED entries) +RUN mkdir -p BOOT-INF/lib && \ + cp jaxb-api.jar javax.activation.jar jaxb-runtime.jar txw2.jar \ + istack-commons-runtime.jar stax-ex.jar BOOT-INF/lib/ && \ + jar uf0 app.jar \ + BOOT-INF/lib/jaxb-api.jar \ + BOOT-INF/lib/javax.activation.jar \ + BOOT-INF/lib/jaxb-runtime.jar \ + BOOT-INF/lib/txw2.jar \ + BOOT-INF/lib/istack-commons-runtime.jar \ + BOOT-INF/lib/stax-ex.jar + +# ─────────────── RUNTIME STAGE ─────────────── +FROM eclipse-temurin:21-jre-alpine + +RUN apk add --no-cache dos2unix + +WORKDIR /opt/egov + +COPY --from=patcher /patch/app.jar /opt/egov/app.jar +COPY --from=build /app/start.sh /opt/egov/start.sh + +# CDS pre-dump: generate classlist then shared archive for faster startup. +RUN timeout 30 java \ + --add-opens java.base/java.lang=ALL-UNNAMED \ + --add-opens java.base/java.lang.reflect=ALL-UNNAMED \ + --add-opens java.base/java.util=ALL-UNNAMED \ + -XX:DumpLoadedClassList=/opt/egov/app.classlist \ + -jar /opt/egov/app.jar 2>/dev/null; true +RUN java \ + --add-opens java.base/java.lang=ALL-UNNAMED \ + --add-opens java.base/java.lang.reflect=ALL-UNNAMED \ + --add-opens java.base/java.util=ALL-UNNAMED \ + -Xshare:dump \ + -XX:SharedClassListFile=/opt/egov/app.classlist \ + -XX:SharedArchiveFile=/opt/egov/app-cds.jsa \ + -jar /opt/egov/app.jar 2>/dev/null; true + +RUN dos2unix /opt/egov/start.sh && chmod +x /opt/egov/start.sh + +CMD ["/opt/egov/start.sh"] diff --git a/build/maven-java8-jdk21/start.sh b/build/maven-java8-jdk21/start.sh new file mode 100644 index 00000000000..112e447e632 --- /dev/null +++ b/build/maven-java8-jdk21/start.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# JDK 21 auto-sizes heap from cgroup limits — no default -Xms/-Xmx needed. +# Set JAVA_OPTS or JAVA_TOOL_OPTIONS via container environment if tuning is required. + +# CDS: use shared archive if it exists (pre-baked at image build time) +CDS_OPTS="" +if [ -f /opt/egov/app-cds.jsa ]; then + CDS_OPTS="-XX:SharedArchiveFile=/opt/egov/app-cds.jsa" +fi + +# Spring Boot 1.5 uses internal JDK APIs via reflection. +# These --add-opens flags are required for JDK 21 compatibility. +ADD_OPENS="--add-opens java.base/java.lang=ALL-UNNAMED" +ADD_OPENS="$ADD_OPENS --add-opens java.base/java.lang.reflect=ALL-UNNAMED" +ADD_OPENS="$ADD_OPENS --add-opens java.base/java.util=ALL-UNNAMED" + +if [ x"${JAVA_ENABLE_DEBUG}" != x ] && [ "${JAVA_ENABLE_DEBUG}" != "false" ]; then + java_debug_args="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${JAVA_DEBUG_PORT:-5005}" +fi + +exec java ${java_debug_args} ${ADD_OPENS} ${JAVA_OPTS} ${CDS_OPTS} ${JAVA_ARGS} -jar /opt/egov/app.jar From c99bd9b5edcf07ea5030339138cff8e1f435c549 Mon Sep 17 00:00:00 2001 From: vinothrallapalli-egov Date: Fri, 5 Jun 2026 10:36:30 +0530 Subject: [PATCH 10/11] feat: migrate all JVM services to JDK 21 runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merges PR #1240 - adds JDK 21 base images with CDS pre-dumps for 26 Spring Boot 3.x services and a JDK 8→21 hybrid image for egov-user, fixing cgroup v2 CPU detection bug (JDK-8281181) on kernel 6.8+. Co-Authored-By: Claude Sonnet 4.6 (cherry picked from commit 9f83afbe31d402dce129806942b8d38efbb72cd5) --- build/maven/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/maven/Dockerfile b/build/maven/Dockerfile index 1b34d59bda9..8adda7f513e 100644 --- a/build/maven/Dockerfile +++ b/build/maven/Dockerfile @@ -35,4 +35,4 @@ RUN java -Xshare:dump \ # Ensure the start script has correct line endings and is executable RUN dos2unix /opt/egov/start.sh && chmod +x /opt/egov/start.sh -CMD ["/opt/egov/start.sh"] \ No newline at end of file +CMD ["/opt/egov/start.sh"] From 17cda5c9427e54f3d9724f99bd5d80c89b65fa87 Mon Sep 17 00:00:00 2001 From: vinothrallapalli-egov Date: Fri, 5 Jun 2026 12:58:58 +0530 Subject: [PATCH 11/11] fix(tests): fix build failures in egov-workflow-v2 and egov-url-shortening egov-workflow-v2: BusinessServiceRepositoryTest was missing a @MockBean for MultiStateInstanceUtil, causing Spring to fail context initialization when trying to field-inject centralInstanceUtil into BusinessServiceRepository. egov-url-shortening: UrlSorteningApplicationTest hard-coded Jackson's internal deserialization feature bitfield (237020304) which changed to 473998480 in the jackson-databind version pulled by Spring Boot 3.4.5. Co-Authored-By: Claude Sonnet 4.6 (cherry picked from commit 983e8b2041072bce0e605dd852e5dce2f0ab8a32) --- .../src/test/java/org/egov/UrlSorteningApplicationTest.java | 2 +- .../org/egov/wf/repository/BusinessServiceRepositoryTest.java | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core-services/egov-url-shortening/src/test/java/org/egov/UrlSorteningApplicationTest.java b/core-services/egov-url-shortening/src/test/java/org/egov/UrlSorteningApplicationTest.java index 9baac36e001..bf0118951ee 100644 --- a/core-services/egov-url-shortening/src/test/java/org/egov/UrlSorteningApplicationTest.java +++ b/core-services/egov-url-shortening/src/test/java/org/egov/UrlSorteningApplicationTest.java @@ -50,7 +50,7 @@ void testGetObjectMapper() { JsonNodeFactory expectedNodeFactory = actualObjectMapper.getNodeFactory(); assertSame(expectedNodeFactory, deserializationConfig.getNodeFactory()); assertNull(deserializationConfig.getDefaultMergeable()); - assertEquals(237020304, deserializationConfig.getDeserializationFeatures()); + assertEquals(473998480, deserializationConfig.getDeserializationFeatures()); assertTrue(deserializationConfig.getAttributes() instanceof ContextAttributes.Impl); assertSame(polymorphicTypeValidator, deserializationConfig.getPolymorphicTypeValidator()); } diff --git a/core-services/egov-workflow-v2/src/test/java/org/egov/wf/repository/BusinessServiceRepositoryTest.java b/core-services/egov-workflow-v2/src/test/java/org/egov/wf/repository/BusinessServiceRepositoryTest.java index 532b4e28149..e47e932b75f 100644 --- a/core-services/egov-workflow-v2/src/test/java/org/egov/wf/repository/BusinessServiceRepositoryTest.java +++ b/core-services/egov-workflow-v2/src/test/java/org/egov/wf/repository/BusinessServiceRepositoryTest.java @@ -8,6 +8,7 @@ import java.util.HashMap; import java.util.List; +import org.egov.common.utils.MultiStateInstanceUtil; import org.egov.wf.config.WorkflowConfig; import org.egov.wf.repository.querybuilder.BusinessServiceQueryBuilder; import org.egov.wf.repository.rowmapper.BusinessServiceRowMapper; @@ -45,6 +46,9 @@ class BusinessServiceRepositoryTest { @MockBean private WorkflowUtil workflowUtil; + @MockBean + private MultiStateInstanceUtil centralInstanceUtil; + @Test void testGetBusinessServicesWithnull() throws DataAccessException {