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. } }