Skip to content

Commit ce479a6

Browse files
author
Karl Rieb
committed
Support DateTime and Date Babel Timestamp formats in v2.
generator: Fix Date truncation to seconds granularity when date field is optional. generator: Fix deserialization of optional primitives when they are omitted from JSON. Fixes T84863.
1 parent fb19d70 commit ce479a6

File tree

9 files changed

+349
-7
lines changed

9 files changed

+349
-7
lines changed

ChangeLog.txt

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
- Fix bug when deserializing v2 data types with missing optional primitive fields.
2+
- Fix bug where some Date objects were not being properly truncated to seconds granularity in v2
3+
data types.
4+
- Fix v2 timestamp parsing to support DateTime and Date formats.
15
- Add support for Dropbox API app endpoints.
26
- Update upload-file example to include chunked upload example.
37
- Increase default socket read timeout to 2 minutes.

babel

Submodule babel updated from df5cedd to 5ca2764

generator/java.babelg.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -3617,13 +3617,18 @@ def generate_field_assignment(self, field, lhs=None, rhs=None, allow_default=Tru
36173617
lhs = lhs or ('this.%s' % field.java_name)
36183618
rhs = rhs or field.java_name
36193619

3620+
underlying_data_type = field.data_type
3621+
if underlying_data_type.is_nullable:
3622+
underlying_data_type = underlying_data_type.nullable_data_type
3623+
if underlying_data_type.is_list:
3624+
underlying_data_type = underlying_data_type.list_data_type
3625+
36203626
# our timestamp format only allows for second-level granularity (no millis).
36213627
# enforce this.
36223628
#
36233629
# TODO: gotta be a better way than this...
3624-
if is_timestamp_type(field.data_type.as_babel) and rhs != 'null':
3625-
rhs = 'new %s(%s.getTime() - (%s.getTime() %% 1000))' % (
3626-
JavaClass(self.ctx, "java.util.Date"), rhs, rhs)
3630+
if is_timestamp_type(underlying_data_type.as_babel) and rhs != 'null':
3631+
rhs = 'com.dropbox.core.util.LangUtil.truncateMillis(%s)' % rhs
36273632

36283633
if allow_default and field.has_default:
36293634
if rhs == 'null':
@@ -3829,7 +3834,10 @@ def generate_struct_json_reader(self, data_type):
38293834
#
38303835
out('')
38313836
for field in data_type.all_fields:
3832-
out('%s %s = null;' % (field.data_type.java_type(boxed=True), field.java_name))
3837+
if field.has_default:
3838+
out('%s %s = %s;' % (field.java_type(), field.java_name, field.default_value))
3839+
else:
3840+
out('%s %s = null;' % (field.data_type.java_type(boxed=True), field.java_name))
38333841

38343842
out('')
38353843
with self.g.block('while (_p.getCurrentToken() == JsonToken.FIELD_NAME)'):

src/main/java/com/dropbox/core/json/CompositeJsonDeserializer.java

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.fasterxml.jackson.databind.JsonMappingException;
1313

1414
import java.io.IOException;
15+
import java.text.ParseException;
1516
import java.util.ArrayList;
1617
import java.util.HashMap;
1718
import java.util.Map;

src/main/java/com/dropbox/core/json/JsonUtil.java

+60-2
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,26 @@
77
import com.fasterxml.jackson.databind.SerializationFeature;
88

99
import java.text.DateFormat;
10+
import java.text.FieldPosition;
11+
import java.text.ParseException;
12+
import java.text.ParsePosition;
1013
import java.text.SimpleDateFormat;
14+
import java.util.Calendar;
15+
import java.util.Date;
1116
import java.util.TimeZone;
1217

1318
public final class JsonUtil {
1419
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
15-
private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
20+
private static final String DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
21+
private static final String DATE_FORMAT = "yyyy-MM-dd";
1622

1723
private static final ObjectMapper MAPPER = createMapper(false);
1824
private static final ObjectMapper MAPPER_PRETTY = createMapper(true);
1925

2026
private static ObjectMapper createMapper(boolean prettyPrint) {
21-
DateFormat df = new SimpleDateFormat(DATE_FORMAT);
27+
DateFormat df = new BabelDateFormat();
2228
df.setTimeZone(UTC);
29+
df.setLenient(false);
2330

2431
ObjectMapper mapper = new ObjectMapper()
2532
.setTimeZone(UTC)
@@ -52,4 +59,55 @@ public static JavaType createType(TypeReference<?> typeReference) {
5259
public static JavaType createType(Class<?> clazz) {
5360
return MAPPER.getTypeFactory().constructType(clazz);
5461
}
62+
63+
private static final class BabelDateFormat extends SimpleDateFormat {
64+
private static final long serialVersionUID = 0L;
65+
private static final int LONG_FORMAT_LENGTH = DATE_TIME_FORMAT.replace("'", "").length();
66+
private static final int SHORT_FORMAT_LENGTH = DATE_FORMAT.replace("'", "").length();
67+
68+
private DateFormat shortFormat;
69+
70+
public BabelDateFormat() {
71+
super(DATE_TIME_FORMAT);
72+
this.shortFormat = new SimpleDateFormat(DATE_FORMAT);
73+
}
74+
75+
@Override
76+
public Object clone() {
77+
BabelDateFormat other = (BabelDateFormat) super.clone();
78+
other.shortFormat = (DateFormat) shortFormat.clone();
79+
return other;
80+
}
81+
82+
@Override
83+
public Date parse(String source, ParsePosition pos) {
84+
int dateLength = source.length() - pos.getIndex();
85+
if (dateLength == LONG_FORMAT_LENGTH) {
86+
return super.parse(source, pos);
87+
} else if (dateLength == SHORT_FORMAT_LENGTH) {
88+
return shortFormat.parse(source, pos);
89+
} else {
90+
pos.setErrorIndex(Math.min(source.length(), pos.getIndex() + LONG_FORMAT_LENGTH + 1));
91+
return null;
92+
}
93+
}
94+
95+
@Override
96+
public void setCalendar(Calendar newCalendar) {
97+
super.setCalendar(newCalendar);
98+
shortFormat.setCalendar(newCalendar);
99+
}
100+
101+
@Override
102+
public void setLenient(boolean lenient) {
103+
super.setLenient(lenient);
104+
shortFormat.setLenient(lenient);
105+
}
106+
107+
@Override
108+
public void setTimeZone(TimeZone zone) {
109+
super.setTimeZone(zone);
110+
shortFormat.setTimeZone(zone);
111+
}
112+
}
55113
}

src/main/java/com/dropbox/core/util/LangUtil.java

+25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.dropbox.core.util;
22

3+
import java.util.ArrayList;
34
import java.util.Arrays;
5+
import java.util.Date;
6+
import java.util.List;
47

58
/*>>> import checkers.nullness.quals.Nullable; */
69
/*>>> import checkers.nullness.quals.NonNull; */
@@ -50,4 +53,26 @@ public static int nullableHashCode(/*@Nullable*/Object o)
5053
if (o == null) return 0;
5154
return o.hashCode() + 1;
5255
}
56+
57+
public static Date truncateMillis(/*@Nullable*/Date date) {
58+
if (date != null) {
59+
long time = date.getTime();
60+
return new Date(time - (time % 1000L));
61+
} else {
62+
return date;
63+
}
64+
}
65+
66+
public static List<Date> truncateMillis(/*@Nullable*/List<Date> dates) {
67+
if (dates != null) {
68+
List<Date> truncated = new ArrayList<Date>(dates.size());
69+
for (Date date : dates) {
70+
long time = date.getTime();
71+
truncated.add(new Date(time - (time % 1000L)));
72+
}
73+
return truncated;
74+
} else {
75+
return dates;
76+
}
77+
}
5378
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.dropbox.core.json;
2+
3+
import static org.testng.Assert.*;
4+
5+
import com.fasterxml.jackson.core.JsonProcessingException;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
8+
import org.testng.annotations.Test;
9+
10+
import java.text.SimpleDateFormat;
11+
import java.util.Date;
12+
import java.util.TimeZone;
13+
14+
public class JsonUtilTest {
15+
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
16+
private static final String LONG_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
17+
private static final String SHORT_DATE_TIME_FORMAT = "yyyy-MM-dd";
18+
19+
@Test
20+
public void testV2Timestamps() throws Exception {
21+
// v2 allows 2 different formats for Babel Timestamp fields:
22+
//
23+
// DateTime format: Timestamp("%Y-%m-%dT%H:%M:%SZ")
24+
// Date format: Timestamp("%Y-%m-%d")
25+
//
26+
// The SDKs should be able to handle both formats.
27+
ObjectMapper mapper = JsonUtil.getMapper();
28+
29+
// LONG FORMAT DESERIALIZATION
30+
String expectedTimestamp = "2016-03-08T21:38:04Z";
31+
Date expected = fromTimestampString(expectedTimestamp);
32+
Date actual = mapper.readValue(quoted(expectedTimestamp).getBytes("UTF-8"), Date.class);
33+
34+
assertEquals(actual, expected);
35+
36+
// LONG FORMAT SERIALIZATION
37+
String actualTimestamp = mapper.writeValueAsString(expected);
38+
39+
assertEquals(actualTimestamp, quoted(expectedTimestamp));
40+
41+
// SHORT FORMAT DESERIALIZATION
42+
String shortTimestamp = "2011-02-03";
43+
expected = fromTimestampString(shortTimestamp);
44+
actual = mapper.readValue(quoted(shortTimestamp).getBytes("UTF-8"), Date.class);
45+
46+
assertEquals(actual, expected);
47+
48+
// SHORT FORMAT SERIALIZATION
49+
actualTimestamp = mapper.writeValueAsString(expected);
50+
51+
// we always format to long-form
52+
expectedTimestamp = toTimestampString(expected);
53+
assertEquals(actualTimestamp, quoted(expectedTimestamp));
54+
}
55+
56+
@Test(expectedExceptions = JsonProcessingException.class)
57+
public void testV2BadLongTimestamp() throws Exception {
58+
ObjectMapper mapper = JsonUtil.getMapper();
59+
// we don't support milliseconds
60+
mapper.readValue(quoted("2016-03-08T21:38:04.352+0500"), Date.class);
61+
}
62+
63+
@Test(expectedExceptions = JsonProcessingException.class)
64+
public void testV2BadShortTimestamp() throws Exception {
65+
ObjectMapper mapper = JsonUtil.getMapper();
66+
mapper.readValue(quoted("2016/03/08"), Date.class);
67+
}
68+
69+
private static String quoted(String value) {
70+
return "\"" + value + "\"";
71+
}
72+
73+
private static String toTimestampString(Date date) {
74+
SimpleDateFormat df = new SimpleDateFormat(LONG_DATE_TIME_FORMAT);
75+
df.setTimeZone(UTC);
76+
return df.format(date);
77+
}
78+
79+
private static Date fromTimestampString(String timestamp) {
80+
SimpleDateFormat df;
81+
if (timestamp.length() > SHORT_DATE_TIME_FORMAT.length()) {
82+
df = new SimpleDateFormat(LONG_DATE_TIME_FORMAT);
83+
} else {
84+
df = new SimpleDateFormat(SHORT_DATE_TIME_FORMAT);
85+
}
86+
87+
df.setTimeZone(UTC);
88+
try {
89+
return df.parse(timestamp);
90+
} catch (Exception ex) {
91+
fail("invalid timestamp", ex);
92+
return null;
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)