Skip to content

Commit

Permalink
Solve #80 (jackson-modules-java8): Support case-insensitive date/time (
Browse files Browse the repository at this point in the history
…#83)

Fix #80 (jackson-modules-java8): support case-insensitive dates
  • Loading branch information
pards authored and cowtowncoder committed Sep 22, 2019
1 parent d29cf4f commit 2cdd107
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

import java.io.IOException;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Locale;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Feature;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.util.ClassUtil;

Expand Down Expand Up @@ -79,11 +82,16 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
if (format.hasPattern()) {
final String pattern = format.getPattern();
final Locale locale = format.hasLocale() ? format.getLocale() : ctxt.getLocale();
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
if (acceptCaseInsensitiveValues(ctxt, format)) {
builder.parseCaseInsensitive();
}
builder.appendPattern(pattern);
DateTimeFormatter df;
if (locale == null) {
df = DateTimeFormatter.ofPattern(pattern);
df = builder.toFormatter();
} else {
df = DateTimeFormatter.ofPattern(pattern, locale);
df = builder.toFormatter(locale);
}
//Issue #69: For instant serializers/deserializers we need to configure the formatter with
//a time zone picked up from JsonFormat annotation, otherwise serialization might not work
Expand Down Expand Up @@ -112,6 +120,15 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
protected boolean isLenient() {
return _isLenient;
}

private boolean acceptCaseInsensitiveValues(DeserializationContext ctxt, JsonFormat.Value format)
{
Boolean enabled = format.getFeature( Feature.ACCEPT_CASE_INSENSITIVE_VALUES);
if( enabled == null) {
enabled = ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES);
}
return enabled;
}

protected void _throwNoNumericTimestampNeedTimeZone(JsonParser p, DeserializationContext ctxt)
throws IOException
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
package com.fasterxml.jackson.datatype.jsr310.deser;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.time.temporal.Temporal;
import java.util.Map;

import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Feature;
import com.fasterxml.jackson.annotation.JsonFormat.Value;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
Expand Down Expand Up @@ -221,7 +227,7 @@ public void testStrictDeserializFromEmptyString() throws Exception {
public void testDeserializationAsArrayDisabled() throws Throwable
{
try {
READER.readValue("[\"2000-01-01\"]");
read(READER, "[\"2000-01-01\"]");
fail("expected MismatchedInputException");
} catch (MismatchedInputException e) {
verifyException(e, "Unexpected token (VALUE_STRING) within Array");
Expand All @@ -232,9 +238,57 @@ public void testDeserializationAsArrayDisabled() throws Throwable
public void testDeserializationAsEmptyArrayDisabled() throws Throwable
{
// works even without the feature enabled
assertNull(READER.readValue("[]"));
assertNull(read(READER, "[]"));
}

@Test
public void testDeserializationCaseInsensitiveEnabledOnValue() throws Throwable
{
ObjectMapper mapper = newMapper();
Value format = JsonFormat.Value
.forPattern("dd-MMM-yyyy")
.withFeature(Feature.ACCEPT_CASE_INSENSITIVE_VALUES);
mapper.configOverride(LocalDate.class).setFormat(format);
ObjectReader reader = mapper.readerFor(LocalDate.class);
String[] jsons = new String[] {"'01-Jan-2000'","'01-JAN-2000'", "'01-jan-2000'"};
for(String json : jsons) {
expectSuccess(reader, LocalDate.of(2000, Month.JANUARY, 1), json);
}
}

@Test
public void testDeserializationCaseInsensitiveEnabled() throws Throwable
{
ObjectMapper mapper = newMapper().configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, true);
mapper.configOverride(LocalDate.class).setFormat(JsonFormat.Value.forPattern("dd-MMM-yyyy"));
ObjectReader reader = mapper.readerFor(LocalDate.class);
String[] jsons = new String[] {"'01-Jan-2000'","'01-JAN-2000'", "'01-jan-2000'"};
for(String json : jsons) {
expectSuccess(reader, LocalDate.of(2000, Month.JANUARY, 1), json);
}
}

@Test
public void testDeserializationCaseInsensitiveDisabled() throws Throwable
{
ObjectMapper mapper = newMapper().configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, false);
mapper.configOverride(LocalDate.class).setFormat(JsonFormat.Value.forPattern("dd-MMM-yyyy"));
ObjectReader reader = mapper.readerFor(LocalDate.class);
expectSuccess(reader, LocalDate.of(2000, Month.JANUARY, 1), "'01-Jan-2000'");
}

@Test
public void testDeserializationCaseInsensitiveDisabled_InvalidDate() throws Throwable
{
ObjectMapper mapper = newMapper().configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, false);
mapper.configOverride(LocalDate.class).setFormat(JsonFormat.Value.forPattern("dd-MMM-yyyy"));
ObjectReader reader = mapper.readerFor(LocalDate.class);
String[] jsons = new String[] {"'01-JAN-2000'", "'01-jan-2000'"};
for(String json : jsons) {
expectFailure(reader, json);
}
}

@Test
public void testDeserializationAsArrayEnabled() throws Throwable
{
Expand All @@ -253,12 +307,40 @@ public void testDeserializationAsEmptyArrayEnabled() throws Throwable
.readValue("[]");
assertNull(value);
}

private void expectFailure(ObjectReader reader, String json) throws Throwable {
try {
read(reader, json);
fail("expected DateTimeParseException");
} catch (JsonProcessingException e) {
if (e.getCause() == null) {
throw e;
}
if (!(e.getCause() instanceof DateTimeParseException)) {
throw e.getCause();
}
} catch (IOException e) {
throw e;
}
}

/*
/**********************************************************
/* Polymorphic deser
/**********************************************************
*/
private void expectSuccess(ObjectReader reader, Object exp, String json) throws IOException {
final LocalDate value = read(reader, json);
notNull(value);
expect(exp, value);
}

private LocalDate read(ObjectReader reader, final String json) throws IOException {
return reader.readValue(aposToQuotes(json));
}

private static void notNull(Object value) {
assertNotNull("The value should not be null.", value);
}

private static void expect(Object exp, Object value) {
assertEquals("The value is not correct.", exp, value);
}

@Test
public void testDeserializationWithTypeInfo01() throws Exception
Expand All @@ -285,7 +367,9 @@ public void testCustomFormat() throws Exception
{
Wrapper w = MAPPER.readValue("{\"value\":\"2015_07_28T13:53+0300\"}", Wrapper.class);
LocalDate date = w.value;
assertNotNull(date);
assertEquals(28, date.getDayOfMonth());
}
}



Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,33 @@

package com.fasterxml.jackson.datatype.jsr310.deser;

import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneOffset;
import java.time.temporal.Temporal;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Feature;
import com.fasterxml.jackson.annotation.JsonFormat.Value;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;

import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;

import org.junit.Test;

import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.time.temporal.Temporal;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;

import static org.junit.Assert.*;

public class LocalDateTimeDeserTest
Expand Down Expand Up @@ -401,4 +402,87 @@ public void testDeserilizeFromSimpleTimestamp() throws Exception
verifyException(e, "raw timestamp (1235) not allowed for `java.time.LocalDateTime`");
}
}

// [modules-java8#80]: handle case-insensitive date/time
@Test
public void testDeserializationCaseInsensitiveEnabledOnValue() throws Throwable
{
ObjectMapper mapper = newMapper();
Value format = JsonFormat.Value
.forPattern("dd-MMM-yyyy HH:mm")
.withFeature(Feature.ACCEPT_CASE_INSENSITIVE_VALUES);
mapper.configOverride(LocalDateTime.class).setFormat(format);
ObjectReader reader = mapper.readerFor(LocalDateTime.class);
String[] jsons = new String[] {"'01-Jan-2000 13:14'","'01-JAN-2000 13:14'", "'01-jan-2000 13:14'"};
for(String json : jsons) {
expectSuccess(reader, LocalDateTime.of(2000, Month.JANUARY, 1, 13, 14), json);
}
}

@Test
public void testDeserializationCaseInsensitiveEnabled() throws Throwable
{
ObjectMapper mapper = newMapper().configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, true);
mapper.configOverride(LocalDateTime.class).setFormat(JsonFormat.Value.forPattern("dd-MMM-yyyy HH:mm"));
ObjectReader reader = mapper.readerFor(LocalDateTime.class);
String[] jsons = new String[] {"'01-Jan-2000 13:45'","'01-JAN-2000 13:45'", "'01-jan-2000 13:45'"};
for(String json : jsons) {
expectSuccess(reader, LocalDateTime.of(2000, Month.JANUARY, 1, 13, 45), json);
}
}

@Test
public void testDeserializationCaseInsensitiveDisabled() throws Throwable
{
ObjectMapper mapper = newMapper().configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, false);
mapper.configOverride(LocalDateTime.class).setFormat(JsonFormat.Value.forPattern("dd-MMM-yyyy HH:mm"));
ObjectReader reader = mapper.readerFor(LocalDateTime.class);
expectSuccess(reader, LocalDateTime.of(2000, Month.JANUARY, 1, 13, 45), "'01-Jan-2000 13:45'");
}

@Test
public void testDeserializationCaseInsensitiveDisabled_InvalidDate() throws Throwable
{
ObjectMapper mapper = newMapper().configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, false);
mapper.configOverride(LocalDateTime.class).setFormat(JsonFormat.Value.forPattern("dd-MMM-yyyy"));
ObjectReader reader = mapper.readerFor(LocalDateTime.class);
String[] jsons = new String[] {"'01-JAN-2000'", "'01-jan-2000'"};
for(String json : jsons) {
expectFailure(reader, json);
}
}

private void expectSuccess(ObjectReader reader, Object exp, String json) throws IOException {
final LocalDateTime value = read(reader, json);
notNull(value);
expect(exp, value);
}

private void expectFailure(ObjectReader reader, String json) throws Throwable {
try {
read(reader, json);
fail("expected DateTimeParseException");
} catch (JsonProcessingException e) {
if (e.getCause() == null) {
throw e;
}
if (!(e.getCause() instanceof DateTimeParseException)) {
throw e.getCause();
}
} catch (IOException e) {
throw e;
}
}

private LocalDateTime read(ObjectReader reader, final String json) throws IOException {
return reader.readValue(aposToQuotes(json));
}

private void notNull(Object value) {
assertNotNull("The value should not be null.", value);
}

private void expect(Object exp, Object value) {
assertEquals("The value is not correct.", exp, value);
}
}

0 comments on commit 2cdd107

Please sign in to comment.