From c0f6dea7854e030b11512ce7ba204e0303718b93 Mon Sep 17 00:00:00 2001 From: sehjotsinghunthinkable Date: Thu, 9 Jul 2026 17:15:10 +0530 Subject: [PATCH 1/3] Tuned DB connection pools and enable response compression (#172) --- src/main/resources/application.properties | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 12be1b44..fccbd57c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,10 @@ +# Response compression — shrinks large JSON responses over the wire without changing the API +# contract; honors the Accept-Encoding: gzip clients already send. +server.compression.enabled=true +server.compression.mime-types=application/json,application/xml,text/html,text/plain,text/css,application/javascript +server.compression.min-response-size=1024 + + spring.main.banner-mode=off spring.data.jpa.repositories.enabled=true spring.jpa.hibernate.ddl-auto=none @@ -16,7 +23,8 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect # Connection Pool Size - CRITICAL for handling multiple concurrent requests # Rule: (max-pool-size) = (number of CPUs * 2) + effectiveSpindles # For 8 CPUs: 8*2 + 4 = 20-30 is optimal -spring.datasource.hikari.maximum-pool-size=40 +spring.datasource.hikari.maximum-pool-size=100 +#spring.datasource.hikari.maximum-pool-size=100 spring.datasource.hikari.minimum-idle=20 # Connection Timeouts - REDUCED for faster failure detection From 76a678a6a63ac11e954f62aebcf73d267e531352 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Sat, 18 Jul 2026 17:07:41 +0530 Subject: [PATCH 2/3] Revert "Tuned DB connection pools and enable response compression (#172)" This reverts commit c0f6dea7854e030b11512ce7ba204e0303718b93. --- src/main/resources/application.properties | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index fccbd57c..12be1b44 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,10 +1,3 @@ -# Response compression — shrinks large JSON responses over the wire without changing the API -# contract; honors the Accept-Encoding: gzip clients already send. -server.compression.enabled=true -server.compression.mime-types=application/json,application/xml,text/html,text/plain,text/css,application/javascript -server.compression.min-response-size=1024 - - spring.main.banner-mode=off spring.data.jpa.repositories.enabled=true spring.jpa.hibernate.ddl-auto=none @@ -23,8 +16,7 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect # Connection Pool Size - CRITICAL for handling multiple concurrent requests # Rule: (max-pool-size) = (number of CPUs * 2) + effectiveSpindles # For 8 CPUs: 8*2 + 4 = 20-30 is optimal -spring.datasource.hikari.maximum-pool-size=100 -#spring.datasource.hikari.maximum-pool-size=100 +spring.datasource.hikari.maximum-pool-size=40 spring.datasource.hikari.minimum-idle=20 # Connection Timeouts - REDUCED for faster failure detection From 8e586fe9bdaee54d2bbdc60bf41d93d606b8fe16 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Sat, 18 Jul 2026 17:54:10 +0530 Subject: [PATCH 3/3] fix(dob): stop global Timestamp adapter from nulling dob on GPS-enabled endpoints The GPS feature (STOP-148) registered a custom Gson TypeAdapter globally on InputMapper's GsonBuilder to parse the new gpsTimestamp field. Because it was global, it also intercepted dob, silently returning null whenever the incoming date string didn't match one of its four hardcoded formats. - Add GpsTimestampAdapter, attached only via @JsonAdapter on the gpsTimestamp field (Address, RMNCHBeneficiaryDetailsRmnch, RMNCHHouseHoldDetails), so it can't affect any other Timestamp field. - Remove the global registerTypeAdapter(Timestamp.class, ...) from both InputMapper.java copies, restoring Gson's default Timestamp parsing for dob and everything else (matching vb/stoptb). - Revert createIdentity()'s parser back to a bare new Gson(), matching Common-API's RegisterBenificiaryServiceImpl, which also serializes the outgoing identity payload with a bare new Gson(). InputMapper's setDateFormat is incompatible with that wire format (non-zero-padded day), which is why dob was nulling specifically for beneficiaries born on the 1st-9th of a month once createIdentity switched to InputMapper's Gson. Co-Authored-By: Claude Sonnet 5 --- .../controller/IdentityController.java | 7 +- .../rmnch/RMNCHBeneficiaryDetailsRmnch.java | 3 + .../data/rmnch/RMNCHHouseHoldDetails.java | 3 + .../iemr/common/identity/domain/Address.java | 4 + .../identity/mapper/GpsTimestampAdapter.java | 116 ++++++++++++++++++ .../common/identity/mapper/InputMapper.java | 47 +------ .../identity/utils/mapper/InputMapper.java | 40 +----- 7 files changed, 142 insertions(+), 78 deletions(-) create mode 100644 src/main/java/com/iemr/common/identity/mapper/GpsTimestampAdapter.java diff --git a/src/main/java/com/iemr/common/identity/controller/IdentityController.java b/src/main/java/com/iemr/common/identity/controller/IdentityController.java index de5e551e..948d38b6 100644 --- a/src/main/java/com/iemr/common/identity/controller/IdentityController.java +++ b/src/main/java/com/iemr/common/identity/controller/IdentityController.java @@ -594,7 +594,12 @@ public String createIdentity(@Param(value = "{\r\n" + " \"eventTypeName\": \"St + " \"createdDate\": \"Timestamp\"\r\n" + " \"faceEmbedding\": [\"Float\"]\r\n" + "}") @RequestBody String identityData) throws IEMRException { logger.info("IdentityController.createIdentity - start"); - IdentityDTO identity = InputMapper.getInstance().gson().fromJson(identityData, IdentityDTO.class); + // Bare Gson matches Common-API's RegisterBenificiaryServiceImpl, which also + // serializes the outgoing identity payload with a bare new Gson(). dob relies + // on this symmetric default format; gpsTimestamp is still parsed correctly via + // its field-level @JsonAdapter(GpsTimestampAdapter.class) on Address, which + // works regardless of which Gson instance performs the parse. + IdentityDTO identity = new Gson().fromJson(identityData, IdentityDTO.class); logger.info("identity hit: " + identity); BeneficiaryCreateResp map; map = svc.createIdentity(identity); diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java index f5ad60b5..49030bdd 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java @@ -34,6 +34,8 @@ import jakarta.persistence.Transient; import com.google.gson.annotations.Expose; +import com.google.gson.annotations.JsonAdapter; +import com.iemr.common.identity.mapper.GpsTimestampAdapter; import lombok.Data; @@ -567,6 +569,7 @@ public class RMNCHBeneficiaryDetailsRmnch { @Expose @Column(name = "gpsTimestamp") + @JsonAdapter(GpsTimestampAdapter.class) private Timestamp gpsTimestamp; @Expose diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java index b73f9b00..7000f6d0 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java @@ -31,7 +31,9 @@ import jakarta.persistence.Table; import com.google.gson.annotations.Expose; +import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; +import com.iemr.common.identity.mapper.GpsTimestampAdapter; import lombok.Data; @@ -376,6 +378,7 @@ public class RMNCHHouseHoldDetails { @Expose @Column(name = "gpsTimestamp") + @JsonAdapter(GpsTimestampAdapter.class) private Timestamp gpsTimestamp; @Expose diff --git a/src/main/java/com/iemr/common/identity/domain/Address.java b/src/main/java/com/iemr/common/identity/domain/Address.java index dfce1672..f355cbc8 100644 --- a/src/main/java/com/iemr/common/identity/domain/Address.java +++ b/src/main/java/com/iemr/common/identity/domain/Address.java @@ -25,6 +25,9 @@ import java.sql.Timestamp; +import com.google.gson.annotations.JsonAdapter; +import com.iemr.common.identity.mapper.GpsTimestampAdapter; + public @Data class Address { private String addrLine1; private String addrLine2; @@ -56,6 +59,7 @@ private Double gpsLatitude; private Double gpsLongitude; private String digipin; + @JsonAdapter(GpsTimestampAdapter.class) private Timestamp gpsTimestamp; private Boolean isGpsUnavailable; private String gpsUnavailableReason; diff --git a/src/main/java/com/iemr/common/identity/mapper/GpsTimestampAdapter.java b/src/main/java/com/iemr/common/identity/mapper/GpsTimestampAdapter.java new file mode 100644 index 00000000..1f1fe2ff --- /dev/null +++ b/src/main/java/com/iemr/common/identity/mapper/GpsTimestampAdapter.java @@ -0,0 +1,116 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.identity.mapper; + +import java.io.IOException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Parses the GPS capture timestamp (epoch millis, ISO-8601 with/without a literal + * 'Z', or the mobile client's "MMM dd, yyyy, h:mm:ss a" format). + * + * Attach with {@code @JsonAdapter(GpsTimestampAdapter.class)} directly on a + * gpsTimestamp field only. Do NOT register this globally on a GsonBuilder for + * Timestamp.class — that previously intercepted every Timestamp field in the + * request (including dob) and silently nulled it out whenever the client's + * format didn't match one of the patterns below. + */ +public class GpsTimestampAdapter extends TypeAdapter { + + private static final Logger logger = LoggerFactory.getLogger(GpsTimestampAdapter.class); + + private static final DateTimeFormatter ISO_WITH_Z = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + private static final DateTimeFormatter ISO_NO_TZ = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); + private static final DateTimeFormatter CLIENT_DATE_FORMAT = + DateTimeFormatter.ofPattern("MMM dd, yyyy, h:mm:ss a", Locale.ENGLISH); + + @Override + public void write(JsonWriter out, Timestamp value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.getTime()); + } + } + + @Override + public Timestamp read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + if (in.peek() == JsonToken.NUMBER) { + return new Timestamp(in.nextLong()); + } + + String s = in.nextString(); + + // epoch millis as string + try { + return new Timestamp(Long.parseLong(s)); + } catch (NumberFormatException ignored) { + // not epoch millis, try the date formats below + } + // ISO 8601 with Z, e.g. "2021-06-18T00:00:00.000Z" + try { + return Timestamp.from(Instant.parse(s)); + } catch (Exception ignored) { + // not this format + } + // Mobile client format, e.g. "Jun 18, 2021, 5:30:00 AM" + try { + return Timestamp.valueOf(LocalDateTime.parse(s, CLIENT_DATE_FORMAT)); + } catch (Exception ignored) { + // not this format + } + // ISO with literal 'Z' pattern, e.g. "2021-06-18T00:00:00.000Z" parsed as local + try { + return Timestamp.valueOf(LocalDateTime.parse(s, ISO_WITH_Z)); + } catch (Exception ignored) { + // not this format + } + // ISO without timezone, e.g. "2021-06-18T00:00:00.000" (assume UTC) + try { + return Timestamp.from(LocalDateTime.parse(s, ISO_NO_TZ).toInstant(ZoneOffset.UTC)); + } catch (Exception ignored) { + // not this format + } + + logger.warn("GpsTimestampAdapter: unable to parse gpsTimestamp value '{}' with any known format; storing as null", s); + return null; + } +} diff --git a/src/main/java/com/iemr/common/identity/mapper/InputMapper.java b/src/main/java/com/iemr/common/identity/mapper/InputMapper.java index 704fe671..3549efc2 100644 --- a/src/main/java/com/iemr/common/identity/mapper/InputMapper.java +++ b/src/main/java/com/iemr/common/identity/mapper/InputMapper.java @@ -21,13 +21,6 @@ */ package com.iemr.common.identity.mapper; -import java.io.IOException; -import java.sql.Timestamp; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Locale; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,10 +33,6 @@ import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.LongSerializationPolicy; -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; import com.iemr.common.identity.exception.IEMRException; public class InputMapper @@ -53,41 +42,15 @@ public class InputMapper private static GsonBuilder builder; private static InputMapper instance = null; - private static final DateTimeFormatter ISO_WITH_Z = - DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); - private static final DateTimeFormatter CLIENT_DATE_FORMAT = - DateTimeFormatter.ofPattern("MMM dd, yyyy, h:mm:ss a", Locale.ENGLISH); - - private static final TypeAdapter TIMESTAMP_ADAPTER = new TypeAdapter() { - @Override - public void write(JsonWriter out, Timestamp value) throws IOException { - if (value == null) out.nullValue(); - else out.value(value.getTime()); - } - - @Override - public Timestamp read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } - if (in.peek() == JsonToken.NUMBER) return new Timestamp(in.nextLong()); - String s = in.nextString(); - // epoch millis as string - try { return new Timestamp(Long.parseLong(s)); } catch (NumberFormatException ignored) {} - // ISO 8601 with Z e.g. "2021-06-18T00:00:00.000Z" - try { return Timestamp.from(Instant.parse(s)); } catch (Exception ignored) {} - // Mobile client format e.g. "Jun 18, 2021, 5:30:00 AM" - try { return Timestamp.valueOf(LocalDateTime.parse(s, CLIENT_DATE_FORMAT)); } catch (Exception ignored) {} - // Fallback: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" literal Z - try { return Timestamp.valueOf(LocalDateTime.parse(s, ISO_WITH_Z)); } catch (Exception ignored) {} - return null; - } - }; - private InputMapper() { + // Timestamp fields (including dob) use Gson's default parsing here, same as + // on vb/stoptb. The gpsTimestamp field on Address/RMNCH entities is parsed by + // GpsTimestampAdapter via a field-level @JsonAdapter annotation instead of a + // global registration, so it can't affect any other Timestamp field. builder = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") // .excludeFieldsWithoutExposeAnnotation() - .serializeNulls().setLongSerializationPolicy(LongSerializationPolicy.STRING) - .registerTypeAdapter(Timestamp.class, TIMESTAMP_ADAPTER); + .serializeNulls().setLongSerializationPolicy(LongSerializationPolicy.STRING); } public static InputMapper getInstance() diff --git a/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java b/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java index fb419105..6dbc0301 100644 --- a/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java +++ b/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java @@ -21,13 +21,6 @@ */ package com.iemr.common.identity.utils.mapper; -import java.io.IOException; -import java.sql.Timestamp; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -35,10 +28,6 @@ import com.google.gson.ExclusionStrategy; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; import com.iemr.common.identity.utils.exception.IEMRException; /** @@ -59,30 +48,11 @@ public InputMapper() { if (builder == null) { builder = new GsonBuilder(); builder.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); - builder.registerTypeAdapter(Timestamp.class, new TypeAdapter() { - private final DateTimeFormatter isoWithZ = DateTimeFormatter.ISO_INSTANT; - private final DateTimeFormatter isoNoTz = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); - - @Override - public void write(JsonWriter out, Timestamp value) throws IOException { - if (value == null) out.nullValue(); - else out.value(value.getTime()); - } - - @Override - public Timestamp read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } - if (in.peek() == JsonToken.NUMBER) return new Timestamp(in.nextLong()); - String s = in.nextString(); - // epoch millis as string - try { return new Timestamp(Long.parseLong(s)); } catch (NumberFormatException ignored) {} - // ISO 8601 with timezone (e.g. "2026-05-28T03:24:35.000Z") - try { return Timestamp.from(Instant.parse(s)); } catch (Exception ignored) {} - // ISO without timezone (e.g. "2026-05-28T03:24:35.000") - try { return Timestamp.from(LocalDateTime.parse(s, isoNoTz).toInstant(ZoneOffset.UTC)); } catch (Exception ignored) {} - return null; - } - }); + // Timestamp fields (including dob) use Gson's default parsing here, same as + // on vb/stoptb. The gpsTimestamp field on RMNCH entities is parsed by + // com.iemr.common.identity.mapper.GpsTimestampAdapter via a field-level + // @JsonAdapter annotation instead of a global registration, so it can't + // affect any other Timestamp field. } }