Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 1-based Month[De]serializer enabled with JavaTimeFeature.ONE_BASED_MONTHS option #292

Merged
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.time.*;

import com.fasterxml.jackson.core.json.PackageVersion;
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JavaType;
Expand All @@ -36,6 +37,7 @@
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.MonthDayDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.MonthDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.OffsetTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.YearDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.YearMonthDeserializer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ public enum JavaTimeFeature implements JacksonFeature
* stringified numbers are always accepted as timestamps regardless of
* this feature.
*/
ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS(false)
ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS(false),

ONE_BASED_MONTHS(false)
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.time.*;

import com.fasterxml.jackson.core.json.PackageVersion;
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
import com.fasterxml.jackson.core.util.JacksonFeatureSet;

import com.fasterxml.jackson.databind.*;
Expand Down Expand Up @@ -133,6 +134,7 @@ public void setupModule(SetupContext context) {
desers.addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE);
desers.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE);
desers.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE);
desers.addDeserializer(Month.class, MonthDeserializer.INSTANCE.withFeatures(_features));
desers.addDeserializer(MonthDay.class, MonthDayDeserializer.INSTANCE);
desers.addDeserializer(OffsetTime.class, OffsetTimeDeserializer.INSTANCE);
desers.addDeserializer(Period.class, JSR310StringParsableDeserializer.PERIOD);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.fasterxml.jackson.datatype.jsr310.deser;

import java.io.IOException;
import java.time.DateTimeException;
import java.time.Month;
import java.time.MonthDay;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.util.JacksonFeatureSet;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add @since 2.17 in Javadoc here too

/**
* Deserializer for Java 8 temporal {@link MonthDay}s.
*/
public class MonthDeserializer extends JSR310DateTimeDeserializerBase<Month>
{
private static final long serialVersionUID = 1L;

public static final MonthDeserializer INSTANCE = new MonthDeserializer();

private boolean _oneBaseMonths = false;

/**
* NOTE: only {@code public} so that use via annotations (see [modules-java8#202])
* is possible
*
* @since 2.12
*/
public MonthDeserializer() {
this(null);
}

public MonthDeserializer(DateTimeFormatter formatter) {
super(Month.class, formatter);
}

public MonthDeserializer(DateTimeFormatter formatter, boolean _oneBaseMonths) {
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
super(Month.class, formatter);
this._oneBaseMonths = _oneBaseMonths;
}


@Override
public Month deserialize(JsonParser parser, DeserializationContext context) throws IOException
{
if (parser.currentToken() == JsonToken.VALUE_STRING) {
String monthText = parser.getText();
if (monthText.isEmpty()) {
return null;
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
}
for(Month month : Month.values()) {
if (month.name().equalsIgnoreCase(monthText)) {
return month;
}
}
try {
int monthNo = Integer.parseInt(monthText);
if (_oneBaseMonths) {
return Month.of(monthNo);
}
return Month.values()[monthNo];
} catch (NumberFormatException nfe) {
throw new MonthParsingException(new DateTimeParseException("Cannot parse java.time.Month", monthText, 0, nfe));
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
}
}
if(parser.currentToken() == JsonToken.VALUE_NUMBER_INT) {
int monthNo = parser.getIntValue();
if (_oneBaseMonths) {
return Month.of(monthNo);
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
}
return Month.values()[monthNo];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, but more important as we don't want to get ArrayIndexOutOfBundsException...

}
return null;
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
protected JSR310DateTimeDeserializerBase<Month> withDateFormat(DateTimeFormatter dtf) {
return new MonthDeserializer(dtf, _oneBaseMonths);
}

@Override
protected JSR310DateTimeDeserializerBase<Month> withLeniency(Boolean leniency) {
return this;
}

public MonthDeserializer withFeatures(JacksonFeatureSet<JavaTimeFeature> features) {
return new MonthDeserializer(this._formatter, features.isEnabled(JavaTimeFeature.ONE_BASED_MONTHS));
}

static class MonthParsingException extends JsonProcessingException {
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
protected MonthParsingException(Throwable rootCause) {
super(rootCause);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package com.fasterxml.jackson.datatype.jsr310.deser;



import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
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.Month;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import java.util.Map;

import static org.junit.Assert.*;

public class MonthDeserTest extends ModuleTestBase
{

@Test
public void testDeserializationAsString01_feature() throws Exception
{
ObjectReader READER = JsonMapper.builder()
.addModule(new JavaTimeModule().enable(JavaTimeFeature.ONE_BASED_MONTHS))
.build()
.readerFor(Month.class);

assertEquals(Month.JANUARY, READER.readValue("\"01\""));
}


@Test
public void testDeserializationAsString01_default() throws Exception
{
ObjectReader READER = newMapper().readerFor(Month.class);

assertEquals(Month.FEBRUARY, READER.readValue("\"01\""));
}


@Test
public void testDeserializationAsString02_feature() throws Exception
{
ObjectReader READER = JsonMapper.builder()
.addModule(new JavaTimeModule().enable(JavaTimeFeature.ONE_BASED_MONTHS))
.build()
.readerFor(Month.class);

assertEquals(Month.JANUARY, READER.readValue("\"JANUARY\""));
}

@Test
public void testDeserializationAsString02_default() throws Exception
{
ObjectReader READER = newMapper().readerFor(Month.class);

assertEquals(Month.JANUARY, READER.readValue("\"JANUARY\""));
}

@Test
public void testBadDeserializationAsString01() throws Throwable
{
expectFailure("\"notamonth\"");
}

@Test
public void testDeserialization01_default() throws Exception
{
ObjectMapper MAPPER = newMapper();

assertEquals(Month.FEBRUARY, MAPPER.readValue("1", Month.class));
}

@Test
public void testDeserialization01_feature() throws Exception
{
ObjectMapper MAPPER = JsonMapper.builder()
.addModule(new JavaTimeModule().enable(JavaTimeFeature.ONE_BASED_MONTHS))
.build();

assertEquals(Month.JANUARY, MAPPER.readValue("1", Month.class));
}

@Test
public void testDeserialization02_default() throws Exception
{
ObjectMapper MAPPER = newMapper();

assertEquals(Month.SEPTEMBER, MAPPER.readValue("\"08\"", Month.class));
}

@Test
public void testDeserialization02_feature() throws Exception
{
ObjectMapper MAPPER = JsonMapper.builder()
.addModule(new JavaTimeModule().enable(JavaTimeFeature.ONE_BASED_MONTHS))
.build();

assertEquals(Month.AUGUST, MAPPER.readValue("\"08\"", Month.class));
}

@Test
public void testDeserializationWithTypeInfo01_feature() throws Exception
{
ObjectMapper MAPPER = new ObjectMapper()
.registerModule(new JavaTimeModule().enable(JavaTimeFeature.ONE_BASED_MONTHS));
MAPPER.addMixIn(TemporalAccessor.class, MockObjectConfiguration.class);

TemporalAccessor value = MAPPER.readValue("[\"java.time.Month\",\"11\"]", TemporalAccessor.class);
assertEquals(Month.NOVEMBER, value);
}

@Test
public void testDeserializationWithTypeInfo01_default() throws Exception
{
ObjectMapper MAPPER = new ObjectMapper();
MAPPER.addMixIn(TemporalAccessor.class, MockObjectConfiguration.class);

TemporalAccessor value = MAPPER.readValue("[\"java.time.Month\",\"11\"]", TemporalAccessor.class);
assertEquals(Month.DECEMBER, value);
}


@Test
public void _testDeserializationWithTypeInfo01_default() throws Exception
{
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule());
mapper.addMixIn(TemporalAccessor.class, MockObjectConfiguration.class);

TemporalAccessor value = mapper.readValue("[\"java.time.Month\",\"11\"]", TemporalAccessor.class);
assertEquals(Month.DECEMBER, value);
}

@Test
public void _testDeserializationWithTypeInfo01_feature() throws Exception
{
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule().enable(JavaTimeFeature.ONE_BASED_MONTHS));
mapper.addMixIn(TemporalAccessor.class, MockObjectConfiguration.class);

TemporalAccessor value = mapper.readValue("[\"java.time.Month\",\"11\"]", TemporalAccessor.class);
assertEquals(Month.NOVEMBER, value);
}



static class Wrapper {
@JsonFormat(pattern="MM")
public Month value;

public Wrapper(Month v) { value = v; }
}

@Test
public void testFormatAnnotation() throws Exception
{
ObjectMapper MAPPER = newMapper();
String json = newMapper().writeValueAsString(new Wrapper(Month.DECEMBER));
assertEquals("{\"value\":\"12\"}", json);

Wrapper output = MAPPER.readValue(json, Wrapper.class);
assertEquals(new Wrapper(Month.of(12)).value, output.value);
}

/*
/**********************************************************
/* Tests for empty string handling
/**********************************************************
*/

// minor changes in 2.12
@Test
public void testDeserializeFromEmptyString() throws Exception
{
// First: by default, lenient, so empty String fine
TypeReference<Map<String, Month>> MAP_TYPE_REF = new TypeReference<Map<String, Month>>() { };
ObjectReader objectReader = newMapper().registerModule(new JavaTimeModule())
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
.readerFor(MAP_TYPE_REF);

Map<String, Month> map = objectReader.readValue("{\"month\":null}");
assertNull(map.get("month"));

Map<String, Month> map2 = objectReader.readValue("{\"month\":\"\"}");
assertNotNull(map2);

// But can make strict:
ObjectMapper strictMapper = mapperBuilder()
.addModule(new JavaTimeModule())
.build();
strictMapper.configOverride(Month.class)
.setFormat(JsonFormat.Value.forLeniency(false));

try {
strictMapper.readerFor(MAP_TYPE_REF).readValue("{\"date\":\"\"}");
fail("Should not pass");
} catch (MismatchedInputException e) {
verifyException(e, "not allowed because 'strict' mode set for");
}
}

private void expectFailure(String aposJson) throws Throwable {
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
try {
newMapper().registerModule(new JavaTimeModule())
.readerFor(Month.class)
.readValue(aposJson);
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;
}
}

}
Loading