diff --git a/src/main/java/io/cdap/plugin/servicenow/connector/ServiceNowRecordConverter.java b/src/main/java/io/cdap/plugin/servicenow/connector/ServiceNowRecordConverter.java index 9ff2dccc..c7425a49 100644 --- a/src/main/java/io/cdap/plugin/servicenow/connector/ServiceNowRecordConverter.java +++ b/src/main/java/io/cdap/plugin/servicenow/connector/ServiceNowRecordConverter.java @@ -27,6 +27,9 @@ import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -34,9 +37,50 @@ * Utility class for converting the record from ServiceNow data type to CDAP schema data types */ public class ServiceNowRecordConverter { - private static final String DATE_PATTERN = "yyyy-MM-dd"; - private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; - private static final String TIME_PATTERN = "HH:mm:ss"; + + // supported date and time formats. + // ref: https://www.servicenow.com/docs/bundle/yokohama-api-reference/page/app-store/ + // dev_portal/API_reference/GlideDateTime/concept/c_GlideDateTimeAPI.html + // Instead of using DateTimeFormatterBuilder.appendOptional(...), we maintain a List + // and attempt to parse the input with each formatter in order. This approach is more reliable because + // DateTimeFormatterBuilder processes patterns sequentially and may partially match an incorrect format. + // For example, if "MM/dd/yyyy HH:mm:ss" is tried before "dd/MM/yyyy HH:mm:ss", an input like + // "14/06/2025 13:00:00" would fail, as 14 is not a valid month. + // By explicitly trying each formatter, we avoid such ambiguities and ensure correct parsing of + // date formats with overlapping patterns. + private static final List DATE_TIME_FORMATTER = Collections.unmodifiableList( + Arrays.asList( + DateTimeFormatter.ofPattern("dd.MM.yyyy hh:mm:ss a"), + DateTimeFormatter.ofPattern("dd.MM.yyyy hh.mm.ss a"), + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"), + DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"), + DateTimeFormatter.ofPattern("dd.MM.yyyy HH.mm.ss"), + DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss"), + DateTimeFormatter.ofPattern("dd-MM-yyyy HH.mm.ss"), + DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss"), + DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"), + DateTimeFormatter.ofPattern("MM-dd-yyyy HH:mm:ss"), + DateTimeFormatter.ofPattern("dd-MM-yy HH.mm.ss"), + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"), + DateTimeFormatter.ofPattern("MM-dd-yyyy HH:mm"), + DateTimeFormatter.ofPattern("dd-MM-yyyy HH.mm") + )); + + private static final List DATE_FORMATTER = Collections.unmodifiableList( + Arrays.asList( + DateTimeFormatter.ofPattern("yyyy-MM-dd"), + DateTimeFormatter.ofPattern("dd.MM.yyyy"), + DateTimeFormatter.ofPattern("dd-MM-yyyy"), + DateTimeFormatter.ofPattern("dd/MM/yyyy"), + DateTimeFormatter.ofPattern("MM/dd/yyyy"), + DateTimeFormatter.ofPattern("MM-dd-yyyy") + )); + + private static final List TIME_FORMATTER = Collections.unmodifiableList( + Arrays.asList( + DateTimeFormatter.ofPattern("HH:mm:ss"), + DateTimeFormatter.ofPattern("HH:mm") + )); public static void convertToValue(String fieldName, Schema fieldSchema, Map record, StructuredRecord.Builder recordBuilder) { @@ -53,34 +97,16 @@ public static void convertToValue(String fieldName, Schema fieldSchema, Map dateTimeValues = Arrays.asList( + "2025-05-14 13:45:30", + "2025-05-14 13:45", + "14-04-2025 13.45.32", + "14-04-25 13.45.34", + "14/05/2025 13:45:30", + "14.05.2025 01.45.30 PM", + "14.05.2025 01:45:30 PM", + "28/10/2017 01:00:01" + ); + + for (String value : dateTimeValues) { + Map inputMap = new HashMap<>(); + inputMap.put("DateTimeField", value); + + StructuredRecord.Builder recordBuilder = StructuredRecord.builder(recordSchema); + try { + ServiceNowRecordConverter.convertToValue("DateTimeField", fieldSchema, inputMap, recordBuilder); + StructuredRecord record = recordBuilder.build(); + Assert.assertNotNull("Parsed datetime should not be null for input: " + value, + record.get("DateTimeField")); + } catch (UnexpectedFormatException e) { + Assert.fail("Failed to parse valid datetime format: " + value + " - " + e.getMessage()); + } + } + } + + @Test + public void testConvertToDateValue() { + Schema recordSchema = Schema.recordOf( + "record", + Schema.Field.of("DateField", Schema.of(Schema.LogicalType.DATE)) + ); + Schema fieldSchema = recordSchema.getField("DateField").getSchema(); + + List dateValues = Arrays.asList( + "2025-05-14", + "14/05/2025", + "14.05.2025", + "02/10/2017" + ); + + for (String value : dateValues) { + Map inputMap = new HashMap<>(); + inputMap.put("DateField", value); + + StructuredRecord.Builder recordBuilder = StructuredRecord.builder(recordSchema); + try { + ServiceNowRecordConverter.convertToValue("DateField", fieldSchema, inputMap, recordBuilder); + StructuredRecord record = recordBuilder.build(); + Assert.assertNotNull("Parsed date should not be null for input: " + value, + record.get("DateField")); + } catch (UnexpectedFormatException e) { + Assert.fail("Failed to parse valid date format: " + value + " - " + e.getMessage()); + } + } + } + + @Test + public void testConvertToTimeValue() { + Schema recordSchema = Schema.recordOf( + "record", + Schema.Field.of("TimeField", Schema.of(Schema.LogicalType.TIME_MICROS)) + ); + Schema fieldSchema = recordSchema.getField("TimeField").getSchema(); + + List timeValues = Arrays.asList( + "13:45:30", + "13:45" + ); + + for (String value : timeValues) { + Map inputMap = new HashMap<>(); + inputMap.put("TimeField", value); + + StructuredRecord.Builder recordBuilder = StructuredRecord.builder(recordSchema); + try { + ServiceNowRecordConverter.convertToValue("TimeField", fieldSchema, inputMap, recordBuilder); + StructuredRecord record = recordBuilder.build(); + Assert.assertNotNull("Parsed date should not be null for input: " + value, + record.get("TimeField")); + } catch (UnexpectedFormatException e) { + Assert.fail("Failed to parse valid date format: " + value + " - " + e.getMessage()); + } + } + } + @Test public void testConvertToDoubleValue() throws ParseException { Assert.assertEquals(42.0, ServiceNowRecordConverter.convertToDoubleValue("42"),