Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -567,6 +569,7 @@ public class RMNCHBeneficiaryDetailsRmnch {

@Expose
@Column(name = "gpsTimestamp")
@JsonAdapter(GpsTimestampAdapter.class)
private Timestamp gpsTimestamp;

@Expose
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -376,6 +378,7 @@ public class RMNCHHouseHoldDetails {

@Expose
@Column(name = "gpsTimestamp")
@JsonAdapter(GpsTimestampAdapter.class)
private Timestamp gpsTimestamp;

@Expose
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/iemr/common/identity/domain/Address.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
116 changes: 116 additions & 0 deletions src/main/java/com/iemr/common/identity/mapper/GpsTimestampAdapter.java
Original file line number Diff line number Diff line change
@@ -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<Timestamp> {

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;
}
}
47 changes: 5 additions & 42 deletions src/main/java/com/iemr/common/identity/mapper/InputMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -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> TIMESTAMP_ADAPTER = new TypeAdapter<Timestamp>() {
@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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,13 @@
*/
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;

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;

/**
Expand All @@ -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<Timestamp>() {
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.
}
}

Expand Down
Loading