Parse multiple patterns using org.threeten.bp.format.DateTimeFormatter

1.1k views Asked by At

Simple version:

I need to be able to parse two types of Timestamp strings using only one org.threeten.bp.format.DateTimeFormatter object.

Pattern 1 ("YYYY-MM-DD HH:mm:ss.SSSSSS" -- this code works):

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("YYYY-MM-DD HH:mm:ss.SSSSSS");
System.out.println(dtf.parse("2020-06-30 20:20:42.871216"));

Pattern 2 ("YYYY-MM-DD'T'HH:mm:ss.SSS'Z'" -- this code also works):

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("YYYY-MM-DD'T'HH:mm:ss.SSS'Z'");
System.out.println(dtf.parse("2020-06-30T20:20:42.871Z"));

But I need the single object to parse both (this doesn't work, obviously lol):

DateTimeFormatter dtf = DateTimeFormatter
        .ofPattern("YYYY-MM-DD HH:mm:ss.SSSSSS")
        .andThisPattern("YYYY-MM-DD'T'HH:mm:ss.SSS'Z'");
System.out.println(dtf.parse("2020-06-30 20:20:42.871216"));
System.out.println(dtf.parse("2020-06-30T20:20:42.871Z"));

I've attempted several things, this is the latest attempt:

DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder();
DateTimeFormatter dtf = dtfb
        .appendPattern("YYYY-MM-DD HH:mm:ss.SSSSSS")
        .appendPattern("YYYY-MM-DD'T'HH:mm:ss.SSS'Z'")
        .toFormatter();

System.out.println(dtf.parse("2020-06-30 20:20:42.871216"));
System.out.println(dtf.parse("2020-06-30T20:20:42.871Z"));

But this didn't work. Nothing I do seems to allow the single object to parse two types.

Is there a way to accomplish this?

Larger context (Swagger codegen):

I have an application that interacts with a web service using Java Swagger codegen. The JSON response from the web service contains two timestamp formats (see above). At a certain point in my application, I call JSON#deserialize which attempts to use the (configurable) DateTimeFormatter object. However, it's impossible to know before you make the call what timestamp format you'll have.

2020-07-06 18:53:45 ERROR PartyContactMethodsControllerEmail:352 - org.threeten.bp.format.DateTimeParseException: Text '2020-07-06 18:53:45.449445' could not be parsed at index 10
    at org.threeten.bp.format.DateTimeFormatter.parseToBuilder(DateTimeFormatter.java:1587)
    at org.threeten.bp.format.DateTimeFormatter.parse(DateTimeFormatter.java:1491)
    at org.threeten.bp.OffsetDateTime.parse(OffsetDateTime.java:359)
    at webservice.com.webapp.invoker.JSON$OffsetDateTimeTypeAdapter.read(JSON.java:183)
    at webservice.com.webapp.invoker.JSON$OffsetDateTimeTypeAdapter.read(JSON.java:1)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220)
    at com.google.gson.Gson.fromJson(Gson.java:887)
    at com.google.gson.Gson.fromJson(Gson.java:852)
    at com.google.gson.Gson.fromJson(Gson.java:801)
    at webservice.com.webapp.invoker.JSON.deserialize(JSON.java:133)
    at webservice.com.webapp.invoker.ApiClient.deserialize(ApiClient.java:711)
    at webservice.com.webapp.invoker.ApiClient.handleResponse(ApiClient.java:914)
    at webservice.com.webapp.invoker.ApiClient.execute(ApiClient.java:841)
    ...

So the desearialize call is unable to successfully parse the JSON response when it finds a timestamp format it doesn't expect.

Original question again: how do I configure the DateTimeFormatter to (before calling deserialize) not choke on the timestamp format? Is there a way I can configure / interface with Swagger codegen to accomodate the server's response?

1

There are 1 answers

0
Anonymous On BEST ANSWER

The basic challenge in what you are asking is that the two formats don’t convey compatible information. 2020-06-30 20:20:42.871216 is a date and time of day without time zone or UTC offset. It probably denotes some point in time, but we are unable to interpret it correctly as such unless we know which time zone is correct for doing so. On the other hand the trailing Z in 2020-06-30T20:20:42.871Z is an offset of zero form UTC, so here we have the information given to us.

In the following code snippet I am assuming that also the format without UTC offset is to be understood in UTC.

    DateTimeFormatter singleFormatter = new DateTimeFormatterBuilder()
            .append(DateTimeFormatter.ISO_LOCAL_DATE)
            .appendPattern("[ ]['T']")
            .append(DateTimeFormatter.ISO_LOCAL_TIME)
            .appendPattern("[X]")
            .parseDefaulting(ChronoField.OFFSET_SECONDS, 0)
            .toFormatter();

    OffsetDateTime odt1 = OffsetDateTime.parse("2020-06-30 20:20:42.871216", singleFormatter);
    System.out.println(odt1);
    
    OffsetDateTime odt2 = OffsetDateTime.parse("2020-06-30T20:20:42.871Z", singleFormatter);
    System.out.println(odt2);

Output is:

2020-06-30T20:20:42.871216Z
2020-06-30T20:20:42.871Z

The difference between having a space and a T between date and time I handle through optional elements in the format pattern string. Anything enclosed in square brackets is considered optional, so [ ]['T'] parses an optional space followed by an optional literal T. The variation in the number of decimals is easy enough since the built-in DateTimeFormatter.ISO_LOCAL_TIME handles this. This was my main reason why I wanted to use a builder: it allows me to build other formatters into the formatter I am building. Finally the optional offset is handled through a new set of square brackets. Uppercase X parses an offset that may be given as Z for zero. Since Z is an offset, you must never use a hardcoded literal Z for parsing it since this throws the offset information away. Parsing the offset allows me to parse the entire string into an OffsetDateTime. Only how is that possible when the other format hasn’t got an offset? The call to parseDefaulting() specifies the offset to use when none is parsed.