Skip to content

Commit ea11fc4

Browse files
committed
fix #437 - fix support for Date range queries, with unit test and javadocs; upgrade to latest Jackson
(cherry picked from commit b096a85)
1 parent 190eb4a commit ea11fc4

File tree

7 files changed

+186
-55
lines changed

7 files changed

+186
-55
lines changed

pom.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,17 +288,17 @@
288288
<dependency>
289289
<groupId>com.fasterxml.jackson.core</groupId>
290290
<artifactId>jackson-core</artifactId>
291-
<version>2.4.1</version>
291+
<version>2.8.3</version>
292292
</dependency>
293293
<dependency>
294294
<groupId>com.fasterxml.jackson.core</groupId>
295295
<artifactId>jackson-annotations</artifactId>
296-
<version>2.4.1</version>
296+
<version>2.8.3</version>
297297
</dependency>
298298
<dependency>
299299
<groupId>com.fasterxml.jackson.core</groupId>
300300
<artifactId>jackson-databind</artifactId>
301-
<version>2.4.1</version>
301+
<version>2.8.3</version>
302302
</dependency>
303303
<!-- test dependencies -->
304304
<dependency>
@@ -359,13 +359,13 @@
359359
<dependency>
360360
<groupId>com.fasterxml.jackson.dataformat</groupId>
361361
<artifactId>jackson-dataformat-xml</artifactId>
362-
<version>2.4.1</version>
362+
<version>2.8.3</version>
363363
<scope>test</scope>
364364
</dependency>
365365
<dependency>
366366
<groupId>com.fasterxml.jackson.dataformat</groupId>
367367
<artifactId>jackson-dataformat-csv</artifactId>
368-
<version>2.4.1</version>
368+
<version>2.8.3</version>
369369
<scope>test</scope>
370370
</dependency>
371371
</dependencies>

src/main/java/com/marklogic/client/impl/PojoQueryBuilderImpl.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.lang.reflect.Method;
1919
import java.lang.reflect.Modifier;
20+
import java.util.Calendar;
2021
import java.util.Date;
2122
import java.util.HashMap;
2223

@@ -245,6 +246,8 @@ public String getRangeIndexType(String propertyName) {
245246
type = "xs:decimal";
246247
} else if ( Date.class.isAssignableFrom(propertyClass) ) {
247248
type = "xs:dateTime";
249+
} else if ( Calendar.class.isAssignableFrom(propertyClass) ) {
250+
type = "xs:dateTime";
248251
}
249252
if ( type == null ) {
250253
throw new IllegalArgumentException("Property " + propertyName + " is not a native Java type");

src/main/java/com/marklogic/client/pojo/PojoRepository.java

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@
2525

2626
import java.io.Serializable;
2727

28-
/** PojoRepository is the central class for the Pojo Facade. It supports CRUD operations
28+
/** <p>PojoRepository is the central class for the Pojo Facade. It supports CRUD operations
2929
* and search. Each PojoRepository instance operates on only one pojo class. Create new
30-
* PojoRepository instances based on your custom pojo type like so:
30+
* PojoRepository instances based on your custom pojo type like so:</p>
31+
*
3132
* <pre> public class MyClass {
32-
* {@literal @}Id
33+
* {@literal @}Id
3334
* public Integer getMyId() {
3435
* ...
3536
* }
@@ -43,47 +44,74 @@
4344
* PojoRepository&lt;MyClass, Integer&gt; myClassRepo =
4445
* client.newPojoRepository(MyClass.class, Integer.class);</pre>
4546
*
46-
* Where MyClass is your custom pojo type, and myId is the bean property of type Integer
47+
* <p>Where MyClass is your custom pojo type, and myId is the bean property of type Integer
4748
* marked with the
4849
* {@literal @}{@link Id Id annotation}. The
4950
* {@literal @}Id annotation can be attached to a public field or a public getter or a
5051
* public setter. The bean property marked with {@literal @}Id must be a native type or
5152
* {@link java.io.Serializable} class and must contain an
5253
* identifier value unique across all persisted instances of that
53-
* type or the instance will overwrite the persisted instance with the same identifier.
54+
* type or the instance will overwrite the persisted instance with the same identifier.</p>
55+
*
56+
* <p>The current implementation of the Pojo Facade uses
57+
* <a href="https://github.com/FasterXML/jackson-databind/">Jackson databind</a>
58+
* for serialization and deserialization to and from json. Thus only classes which
59+
* can be serialized and deserialized directly by Jackson can be serialized by the
60+
* Pojo Facade. Every bean property including the one marked with {@literal @}Id
61+
* must either expose a public field or both a public getter and a public
62+
* setter. To test if your class can be directly serialized and deserialized
63+
* by Jackson, perform the following:</p>
5464
*
55-
* The current implementation of the Pojo Facade uses
56-
* <a href="https://github.com/FasterXML/jackson-databind/">Jackson databind</a> for serialization
57-
* and deserialization to json. Thus only classes which can be serialized and deserialized
58-
* directly by Jackson can be serialized by the Pojo Facade. Every bean property
59-
* including the one marked with {@literal @}Id must either expose a public field or both a public
60-
* getter and a public setter. To test if your class can be directly serialized and
61-
* deserialized by Jackson, perform the following:
6265
* <pre> ObjectMapper objectMapper = new ObjectMapper();
6366
* String value = objectMapper.writeValueAsString(myObjectIn);
6467
* MyClass myObjectOut = objectMapper.readValue(value, MyClass.class);</pre>
6568
*
66-
* If that works but the configured objectMapper in the Pojo Facade is different and not
67-
* working, you can troubleshoot by directly accessing the objectMapper used by the Pojo
68-
* Facade using an unsupported internal method attached to the current implementation:
69-
* <a
69+
* <p>If that works but the configured objectMapper in the Pojo Facade is different and not
70+
* working, you can troubleshoot by directly accessing the objectMapper used by the Pojo
71+
* Facade using an unsupported internal method attached to the current implementation:
72+
* <a
7073
* href="https://github.com/marklogic/java-client-api/blob/master/src/main/java/com/marklogic/client/impl/PojoRepositoryImpl.java"
71-
* >com.marklogic.client.impl.PojoRepositoryImpl</a>.
74+
* >com.marklogic.client.impl.PojoRepositoryImpl</a>.</p>
75+
*
7276
* <pre> ObjectMapper objectMapper = ((PojoRepositoryImpl) myClassRepo).getObjectMapper();</pre>
7377
*
74-
* If your class has properties which are classes (non-native types) they will be automatically
75-
* serialized and deserialized, but cannot be written, read, or searched directly. If you
76-
* wish to directly write, read, or search another class, create a new instance of
77-
* PojoRepository specific to that class.
78+
* <p>Special handling is provided for bean properties of type
79+
* {@link java.util.Date Date} and {@link java.util.Calendar Calendar}. The
80+
* current implementation of Jackson ObjectMapper changes the timezone to UTC.
81+
* The serialized format is ISO 8601 format which is compatible with
82+
* xs:dateTime format and can therefore be indexed in the server by a path
83+
* range index of type dateTime. However, if you wish to query using a range
84+
* index, it is recommended that you modify serialization to remove the class
85+
* wrapper by adding a JsonTypeInfo annotation like so:</p>
86+
*
87+
* <pre> {@literal @}JsonTypeInfo(use=JsonTypeInfo.Id.NONE, include=JsonTypeInfo.As.EXTERNAL_PROPERTY)
88+
* public Calendar getMyDateTime();</pre>
89+
*
90+
* <p>That way you can query the field directly without needing to traverse the
91+
* java.util.GregorianCalendar type wrapper. By removing that wrapper, you can
92+
* create a query like this:</p>
93+
*
94+
* <pre> PojoQueryBuilder qb = pojoRepository.getQueryBuilder();
95+
* PojoQueryDefinition query = qb.range("myDateTime", Operator.LT, Calendar.getInstance());</pre>
96+
*
97+
* <p>If your class has properties which are classes (non-native types) they
98+
* will be automatically serialized and deserialized, but can only be written,
99+
* read, or searched as properties of the parent instance. If you wish to
100+
* directly write, read, or search instances of another class, create and use
101+
* an instance of PojoRepository specific to that class.</p>
78102
*
79-
* Since PojoRepository stores in JSON format, which limits number precision to 15
80-
* significant digits (IEEE754 double precision), you will lose precision on numbers
81-
* longer than 15 significant digits. If you desire larger numbers with no loss of
82-
* precision, use Strings to persist those numbers.
103+
* <p>Since PojoRepository stores in JSON format, which limits number precision
104+
* to 15 significant digits (IEEE754 double precision), you will lose precision
105+
* on numbers longer than 15 significant digits. If you desire larger numbers
106+
* with no loss of precision, use Strings to persist those numbers.</p>
83107
*/
84108
public interface PojoRepository<T, ID extends Serializable> {
85-
/** Get the value of the id field (the field marked with the {@literal @}Id
109+
/** Get the value of the id field (the field marked with the {@literal @}Id
86110
* annotation).
111+
*
112+
* @param entity the entity instance from which you want to get the id value
113+
*
114+
* @return the id value for this entity of type ID
87115
*/
88116
public ID getId(T entity);
89117

src/main/java/com/marklogic/client/query/StructuredQueryBuilder.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.io.StringReader;
2222
import java.io.StringWriter;
2323
import java.util.Calendar;
24+
import java.util.Date;
2425
import java.util.HashMap;
2526
import java.util.Map;
2627

@@ -1844,9 +1845,35 @@ class RangeQuery
18441845
this.values = new String[values.length];
18451846
for (int i=0; i < values.length; i++) {
18461847
Object value = values[i];
1847-
this.values[i] = (value instanceof String) ?
1848-
(String) value : value.toString();
1849-
}
1848+
this.values[i] = formatValue(value, type);
1849+
}
1850+
}
1851+
1852+
String formatValue(Object value, String type) {
1853+
if ( value == null ) {
1854+
return "null";
1855+
}
1856+
Class<?> valClass = value.getClass();
1857+
if ( String.class.isAssignableFrom(valClass) ) {
1858+
return (String) value;
1859+
} else if ( type != null &&
1860+
( type.endsWith("date") || type.endsWith("dateTime") || type.endsWith("time") ) &&
1861+
( Date.class.isAssignableFrom(valClass) || Calendar.class.isAssignableFrom(valClass) ) )
1862+
{
1863+
if ( Date.class.isAssignableFrom(valClass) ) {
1864+
Calendar cal = Calendar.getInstance();
1865+
cal.setTime((Date) value);
1866+
value = cal;
1867+
}
1868+
if ( type.endsWith("date") ) {
1869+
return DatatypeConverter.printDate((Calendar) value);
1870+
} else if ( type.endsWith("dateTime") ) {
1871+
return DatatypeConverter.printDateTime((Calendar) value);
1872+
} else if ( type.endsWith("time") ) {
1873+
return DatatypeConverter.printTime((Calendar) value);
1874+
}
1875+
}
1876+
return value.toString();
18501877
}
18511878
@Override
18521879
public void innerSerialize(XMLStreamWriter serializer) throws Exception {

src/test/java/com/marklogic/client/test/PojoFacadeTest.java

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
import static org.junit.Assert.assertNotNull;
2121
import static org.junit.Assert.assertTrue;
2222

23+
import java.text.SimpleDateFormat;
2324
import java.util.Arrays;
2425
import java.util.Calendar;
26+
import java.util.Date;
2527
import java.util.GregorianCalendar;
2628
import java.util.TimeZone;
2729

@@ -566,39 +568,75 @@ public void testE_IndexNumberAsString() throws Exception {
566568
}
567569
}
568570

569-
public static class TimeTest {
570-
@Id public String id;
571-
public Calendar timeTest;
572-
573-
public TimeTest() {}
574-
public TimeTest(String id, Calendar timeTest) {
575-
this.id = id;
576-
this.timeTest = timeTest;
577-
}
578-
}
579-
580571
@Test
581572
public void testF_DateTime() {
582573
PojoRepository<TimeTest, String> times = Common.client.newPojoRepository(TimeTest.class, String.class);
583574

584-
GregorianCalendar septFirst = new GregorianCalendar(TimeZone.getTimeZone("CET"));
585-
septFirst.set(2014, Calendar.SEPTEMBER, 1, 12, 0, 0);
575+
GregorianCalendar septFirstUTC = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
576+
septFirstUTC.set(2014, Calendar.SEPTEMBER, 1, 12, 0, 0);
586577

587-
TimeTest timeTest1 = new TimeTest("1", septFirst);
578+
TimeTest timeTest1 = new TimeTest("1", septFirstUTC);
588579
times.write(timeTest1);
589580

590581
TimeTest timeTest1FromDb = times.read("1");
591-
assertEquals("Times should be equal", timeTest1.timeTest.getTime().getTime(),
592-
timeTest1FromDb.timeTest.getTime().getTime());
582+
assertEquals("Calendar objs should be equal", timeTest1.calendarTest,
583+
timeTest1FromDb.calendarTest);
584+
assertEquals("Date objs should be equal", timeTest1.dateTest,
585+
timeTest1FromDb.dateTest);
586+
assertEquals("Epoch time should be equal", timeTest1.dateTest.getTime(),
587+
timeTest1FromDb.dateTest.getTime());
593588

594-
GregorianCalendar septFirstGMT = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
595-
septFirstGMT.set(2014, Calendar.SEPTEMBER, 1, 12, 0, 0);
589+
GregorianCalendar septThirdCET = new GregorianCalendar(TimeZone.getTimeZone("CET"));
590+
septThirdCET.set(2014, Calendar.SEPTEMBER, 3, 12, 0, 0);
596591

597-
TimeTest timeTest2 = new TimeTest("2", septFirstGMT);
592+
TimeTest timeTest2 = new TimeTest("2", septThirdCET);
598593
times.write(timeTest2);
599594

600595
TimeTest timeTest2FromDb = times.read("2");
601-
assertEquals("Times should be equal", timeTest2.timeTest, timeTest2FromDb.timeTest);
596+
/* Jackson 2.8.3 converts all Date/Calendar to UTC, so we can't get the object in the correct timezone
597+
assertEquals("Calendar objs should be equal", timeTest2.calendarTest,
598+
timeTest2FromDb.calendarTestCet);
599+
*/
600+
assertEquals("Calendar objs timestamps should be equal", timeTest2.calendarTest.getTime().getTime(),
601+
timeTest2FromDb.calendarTestCet.getTime().getTime());
602+
assertEquals("Date objs should be equal", timeTest2.dateTest,
603+
timeTest2FromDb.dateTest);
604+
assertEquals("Epoch time should be equal", timeTest2.calendarTest.getTime().getTime(),
605+
timeTest2FromDb.calendarTest.getTime().getTime());
606+
607+
// let's try to test serializing back to CET time zone
608+
/* nevermind, it turns out Jackson 2.8.3 doesn't yet support this--it converts all dates to UTC
609+
610+
// start with the ISO 8601 format compatible with xs:dateTime and thus MarkLogic
611+
SimpleDateFormat cetDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
612+
cetDateFormat.setTimeZone(TimeZone.getTimeZone("CET"));
613+
// modify the objectMapper for this PojoRepository instance
614+
((PojoRepositoryImpl) times).getObjectMapper().setDateFormat(cetDateFormat);
615+
// re-read the object with the modified objectMapper
616+
timeTest2FromDb = times.read("2");
617+
// now validate that the object has everything including the time zone equal
618+
assertEquals("Calendar objs should be equal", timeTest2.calendarTest,
619+
timeTest2FromDb.calendarTest);
620+
*/
621+
622+
// let's test a range query that should only match record "2"
623+
GregorianCalendar septSecondUTC = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
624+
septSecondUTC.set(2014, Calendar.SEPTEMBER, 2, 12, 0, 0);
625+
626+
PojoQueryBuilder<TimeTest> qb = times.getQueryBuilder();
627+
for ( String jsonProperty : new String[] {"calendarTest", "calendarTestCet", "dateTest"} ) {
628+
PojoQueryDefinition query = qb.range(jsonProperty, Operator.GT, septSecondUTC);
629+
try ( PojoPage<TimeTest> page = times.search(query, 1) ) {
630+
int numRead = 0;
631+
for ( TimeTest time : page ) {
632+
numRead++;
633+
assertEquals("Should find the right TimeTest id", "2", time.id);
634+
}
635+
assertEquals("Failed to find number of records expected", 1, numRead);
636+
assertEquals("PojoPage failed to report number of records expected", numRead, page.size());
637+
}
638+
}
639+
602640
}
603641

604642
/* TODO: uncomment when we have a fix for https://github.com/marklogic/java-client-api/issues/383
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.marklogic.client.test;
2+
3+
import java.util.Calendar;
4+
import java.util.Date;
5+
6+
import com.fasterxml.jackson.annotation.JsonFormat;
7+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
8+
9+
import com.marklogic.client.pojo.annotation.Id;
10+
11+
public class TimeTest {
12+
@Id public String id;
13+
14+
@JsonTypeInfo(use=JsonTypeInfo.Id.NONE, include=JsonTypeInfo.As.EXTERNAL_PROPERTY)
15+
public Calendar calendarTest;
16+
17+
/* The timezone below works for serializing but not for deserializing */
18+
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone="CET")
19+
@JsonTypeInfo(use=JsonTypeInfo.Id.NONE, include=JsonTypeInfo.As.EXTERNAL_PROPERTY)
20+
public Calendar calendarTestCet;
21+
22+
@JsonTypeInfo(use=JsonTypeInfo.Id.NONE, include=JsonTypeInfo.As.EXTERNAL_PROPERTY)
23+
public Date dateTest;
24+
25+
public TimeTest() {}
26+
public TimeTest(String id, Calendar timestamp) {
27+
this.id = id;
28+
this.calendarTest = timestamp;
29+
this.calendarTestCet = timestamp;
30+
this.dateTest = timestamp.getTime();
31+
}
32+
}

src/test/resources/bootstrap.xqy

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,10 @@ declare function bootstrap:create-path-range-indexes(
402402
let $new-idx := (
403403
"long", "com.marklogic.client.test.City/population",
404404
"string", "com.marklogic.client.test.City/alternateNames",
405-
"string", "com.marklogic.client.test.Country/continent"
405+
"string", "com.marklogic.client.test.Country/continent",
406+
"dateTime", "com.marklogic.client.test.TimeTest/calendarTest",
407+
"dateTime", "com.marklogic.client.test.TimeTest/calendarTestCet",
408+
"dateTime", "com.marklogic.client.test.TimeTest/dateTest"
406409
)
407410
for $i in 1 to (count($new-idx) idiv 2)
408411
let $offset := ($i * 2) - 1

0 commit comments

Comments
 (0)