Skip to content

Commit

Permalink
HSEARCH-2269 Added ES support for offset/zoned date/time
Browse files Browse the repository at this point in the history
  • Loading branch information
yrodiere committed Oct 5, 2016
1 parent c1c6673 commit 4d54787
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 2 deletions.
Expand Up @@ -24,7 +24,7 @@
*/
public class ElasticsearchLocalDateTimeBridge extends ElasticsearchTemporalAccessorStringBridge<LocalDateTime> {

private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
.append( ElasticsearchLocalDateBridge.FORMATTER )
.appendLiteral( 'T' )
.append( ElasticsearchLocalTimeBridge.FORMATTER )
Expand Down
@@ -0,0 +1,43 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.elasticsearch.bridge.builtin.time.impl;

import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.util.Locale;

/**
* Converts a {@link OffsetDateTime} to a {@link String} in ISO-8601 extended format (9 digits for the year instead of 4).
*
* <p>Be aware that this format is <strong>not</strong> the same as {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME}
* (mainly because of the second fraction field, which is at least 3 characters long), nor as Elasticsearch's
* "strict_date_time" format (since years with more than 4 digits are allowed).
*
* @author Yoann Rodiere
*/
public class ElasticsearchOffsetDateTimeBridge extends ElasticsearchTemporalAccessorStringBridge<OffsetDateTime> {

static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
.append( ElasticsearchLocalDateTimeBridge.FORMATTER )
.appendOffsetId()
.toFormatter( Locale.ROOT )
.withResolverStyle( ResolverStyle.STRICT );

public static final ElasticsearchOffsetDateTimeBridge INSTANCE = new ElasticsearchOffsetDateTimeBridge();

private ElasticsearchOffsetDateTimeBridge() {
super( FORMATTER, OffsetDateTime.class );
}

@Override
OffsetDateTime parse(DateTimeFormatter formatter, String stringValue) throws DateTimeParseException {
return OffsetDateTime.parse( stringValue, formatter );
}
}
@@ -0,0 +1,42 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.elasticsearch.bridge.builtin.time.impl;

import java.time.OffsetTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.util.Locale;

/**
* Converts a {@link OffsetTime} to a {@link String} in Elasticsearch's "strict_time" format.
*
* <p>Be aware that this format is <strong>not</strong> the same as {@link DateTimeFormatter#ISO_OFFSET_TIME}
* (mainly because of the second fraction field, which is at least 3 characters long).
*
* @author Yoann Rodiere
*/
public class ElasticsearchOffsetTimeBridge extends ElasticsearchTemporalAccessorStringBridge<OffsetTime> {

private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
.append( ElasticsearchLocalTimeBridge.FORMATTER )
.appendOffsetId()
.toFormatter( Locale.ROOT )
.withResolverStyle( ResolverStyle.STRICT );

public static final ElasticsearchOffsetTimeBridge INSTANCE = new ElasticsearchOffsetTimeBridge();

private ElasticsearchOffsetTimeBridge() {
super( FORMATTER, OffsetTime.class );
}

@Override
OffsetTime parse(DateTimeFormatter formatter, String stringValue) throws DateTimeParseException {
return OffsetTime.parse( stringValue, formatter );
}
}
@@ -0,0 +1,48 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.elasticsearch.bridge.builtin.time.impl;

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.util.Locale;

import org.hibernate.search.util.impl.TimeHelper;

/**
* Converts a {@link ZonedDateTime} to a {@link String} in ISO-8601 extended format (9 digits for the year instead of 4).
*
* <p>Be aware that this format is <strong>not</strong> the same as {@link DateTimeFormatter#ISO_ZONED_DATE_TIME}
* (mainly because of the second fraction field, which is at least 3 characters long), nor as Elasticsearch's
* "strict_date_time" format (since years with more than 4 digits are allowed, and both the zone ID and offset are displayed).
*
* @author Yoann Rodiere
*/
public class ElasticsearchZonedDateTimeBridge extends ElasticsearchTemporalAccessorStringBridge<ZonedDateTime> {

private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
.append( ElasticsearchOffsetDateTimeBridge.FORMATTER )
.appendLiteral( '[' )
.parseCaseSensitive()
.appendZoneRegionId()
.appendLiteral( ']' )
.toFormatter( Locale.ROOT )
.withResolverStyle( ResolverStyle.STRICT );

public static final ElasticsearchZonedDateTimeBridge INSTANCE = new ElasticsearchZonedDateTimeBridge();

private ElasticsearchZonedDateTimeBridge() {
super( FORMATTER, ZonedDateTime.class );
}

@Override
ZonedDateTime parse(DateTimeFormatter formatter, String stringValue) throws DateTimeParseException {
return TimeHelper.parseZoneDateTime( stringValue, formatter );
}
}
Expand Up @@ -12,8 +12,11 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -26,8 +29,11 @@
import org.hibernate.search.elasticsearch.bridge.builtin.time.impl.ElasticsearchLocalDateTimeBridge;
import org.hibernate.search.elasticsearch.bridge.builtin.time.impl.ElasticsearchLocalTimeBridge;
import org.hibernate.search.elasticsearch.bridge.builtin.time.impl.ElasticsearchMonthDayBridge;
import org.hibernate.search.elasticsearch.bridge.builtin.time.impl.ElasticsearchOffsetDateTimeBridge;
import org.hibernate.search.elasticsearch.bridge.builtin.time.impl.ElasticsearchOffsetTimeBridge;
import org.hibernate.search.elasticsearch.bridge.builtin.time.impl.ElasticsearchYearBridge;
import org.hibernate.search.elasticsearch.bridge.builtin.time.impl.ElasticsearchYearMonthBridge;
import org.hibernate.search.elasticsearch.bridge.builtin.time.impl.ElasticsearchZonedDateTimeBridge;
import org.hibernate.search.elasticsearch.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

Expand Down Expand Up @@ -65,6 +71,9 @@ private static Map<String, FieldBridge> populateBridgeMap() {
bridges.put( LocalDate.class.getName(), new TwoWayString2FieldBridgeIgnoreAnalyzerAdaptor( ElasticsearchLocalDateBridge.INSTANCE ) );
bridges.put( LocalTime.class.getName(), new TwoWayString2FieldBridgeIgnoreAnalyzerAdaptor( ElasticsearchLocalTimeBridge.INSTANCE ) );
bridges.put( Instant.class.getName(), new TwoWayString2FieldBridgeIgnoreAnalyzerAdaptor( ElasticsearchInstantBridge.INSTANCE ) );
bridges.put( OffsetDateTime.class.getName(), new TwoWayString2FieldBridgeIgnoreAnalyzerAdaptor( ElasticsearchOffsetDateTimeBridge.INSTANCE ) );
bridges.put( OffsetTime.class.getName(), new TwoWayString2FieldBridgeIgnoreAnalyzerAdaptor( ElasticsearchOffsetTimeBridge.INSTANCE ) );
bridges.put( ZonedDateTime.class.getName(), new TwoWayString2FieldBridgeIgnoreAnalyzerAdaptor( ElasticsearchZonedDateTimeBridge.INSTANCE ) );

/*
* Use the default Lucene bridges for ZoneOffset, ZoneId, Period and Duration
Expand Down
Expand Up @@ -563,6 +563,20 @@ private ElasticsearchFieldType addTypeOptions(String fieldName, JsonObject field
elasticsearchType = ElasticsearchFieldType.DATE;
formats.add( "strict_hour_minute_second_fraction" );
break;
case OFFSET_DATE_TIME:
elasticsearchType = ElasticsearchFieldType.DATE;
formats.add( "strict_date_time" );
formats.add( "yyyyyyyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSZ" );
break;
case OFFSET_TIME:
elasticsearchType = ElasticsearchFieldType.DATE;
formats.add( "strict_time" );
break;
case ZONED_DATE_TIME:
elasticsearchType = ElasticsearchFieldType.DATE;
formats.add( "yyyy-MM-dd'T'HH:mm:ss.SSSZZ'['ZZZ']'" );
formats.add( "yyyyyyyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSZZ'['ZZZ']'" );
break;
case YEAR:
elasticsearchType = ElasticsearchFieldType.DATE;
formats.add( "strict_year" );
Expand Down
Expand Up @@ -49,6 +49,9 @@ public enum ExtendedFieldType {
LOCAL_DATE,
LOCAL_TIME,
LOCAL_DATE_TIME,
OFFSET_DATE_TIME,
OFFSET_TIME,
ZONED_DATE_TIME,
YEAR,
YEAR_MONTH,
MONTH_DAY,
Expand Down Expand Up @@ -166,6 +169,15 @@ else if ( "java.time.LocalTime".equals( propertyClass.getName() ) ) {
else if ( "java.time.LocalDateTime".equals( propertyClass.getName() ) ) {
return ExtendedFieldType.LOCAL_DATE_TIME;
}
else if ( "java.time.OffsetDateTime".equals( propertyClass.getName() ) ) {
return ExtendedFieldType.OFFSET_DATE_TIME;
}
else if ( "java.time.OffsetTime".equals( propertyClass.getName() ) ) {
return ExtendedFieldType.OFFSET_TIME;
}
else if ( "java.time.ZonedDateTime".equals( propertyClass.getName() ) ) {
return ExtendedFieldType.ZONED_DATE_TIME;
}
else if ( "java.time.Year".equals( propertyClass.getName() ) ) {
return ExtendedFieldType.YEAR;
}
Expand Down
Expand Up @@ -22,6 +22,7 @@
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;

import javax.persistence.Column;
import javax.persistence.Entity;
Expand Down Expand Up @@ -129,6 +130,97 @@ public void testInstant() throws Exception {
assertThatFieldIsFormatted( sample, "instant", "1998-02-12T13:05:33.005Z" );
}

@Test
public void testOffsetDateTimeMilliseconds() throws Exception {
OffsetDateTime value = OffsetDateTime.of(
221998, Month.FEBRUARY.getValue(), 12,
13, 05, 33, 7_000_000,
ZoneOffset.of( "+01:00" )
);

Sample sample = new Sample( 1L, "OffsetDateTime example" );
sample.offsetDateTime = value;

// The "fields" attribute only ever contains UTC date/times
assertThatFieldIsFormatted( sample, "offsetDateTime", "+221998-02-12T13:05:33.007+01:00", "221998-02-12T12:05:33.007Z" );
}

@Test
public void testOffsetDateTimeNanoseconds() throws Exception {
OffsetDateTime value = OffsetDateTime.of(
221998, Month.FEBRUARY.getValue(), 12,
13, 05, 33, 7,
ZoneOffset.of( "+01:00" )
);

Sample sample = new Sample( 1L, "OffsetDateTime example" );
sample.offsetDateTime = value;

// Elasticsearch only has millisecond-precision, so the "fields" value is missing the nanoseconds
// Also, the "fields" attribute only ever contains UTC date/times
assertThatFieldIsFormatted( sample, "offsetDateTime", "+221998-02-12T13:05:33.000000007+01:00", "221998-02-12T12:05:33.000Z" );
}

@Test
public void testOffsetTimeMilliseconds() throws Exception {
OffsetTime value = OffsetTime.of(
13, 05, 33, 7_000_000,
ZoneOffset.of( "+01:00" )
);

Sample sample = new Sample( 1L, "OffsetTime example" );
sample.offsetTime = value;

// The "fields" attribute only ever contains UTC date/times
assertThatFieldIsFormatted( sample, "offsetTime", "13:05:33.007+01:00", "12:05:33.007Z" );
}

@Test
public void testOffsetTimeNanoseconds() throws Exception {
OffsetTime value = OffsetTime.of(
13, 05, 33, 7,
ZoneOffset.of( "+01:00" )
);

Sample sample = new Sample( 1L, "OffsetTime example" );
sample.offsetTime = value;

// Elasticsearch only has millisecond-precision, so the "fields" value is missing the nanoseconds
// Also, the "fields" attribute only ever contains UTC date/times
assertThatFieldIsFormatted( sample, "offsetTime", "13:05:33.000000007+01:00", "12:05:33.000Z" );
}

@Test
public void testZonedDateTimeMilliseconds() throws Exception {
// CET DST rolls back at 2011-10-30 2:59:59 (+02) to 2011-10-30 2:00:00 (+01)
// Credit: user leonbloy at http://stackoverflow.com/a/18794412/6692043
LocalDateTime localDateTime = LocalDateTime.of( 2011, 10, 30, 2, 50, 0, 7_000_000 );

ZonedDateTime value = localDateTime.atZone( ZoneId.of( "CET" ) ).withLaterOffsetAtOverlap();

Sample sample = new Sample( 1L, "ZonedDateTime example" );
sample.zonedDateTime = value;

// The "fields" attribute only ever contains UTC date/times
assertThatFieldIsFormatted( sample, "zonedDateTime", "2011-10-30T02:50:00.007+01:00[CET]", "2011-10-30T01:50:00.007+00:00[UTC]" );
}

@Test
public void testZonedDateTimeNanoseconds() throws Exception {
// CET DST rolls back at 2011-10-30 2:59:59 (+02) to 2011-10-30 2:00:00 (+01)
// Credit: user leonbloy at http://stackoverflow.com/a/18794412/6692043
LocalDateTime localDateTime = LocalDateTime.of( 2011, 10, 30, 2, 50, 0, 7 );

ZonedDateTime value = localDateTime.atZone( ZoneId.of( "CET" ) ).withLaterOffsetAtOverlap();

Sample sample = new Sample( 1L, "ZonedDateTime example" );
sample.zonedDateTime = value;

// Elasticsearch only has millisecond-precision, so the "fields" value is missing the nanoseconds
// Also, the "fields" attribute only ever contains UTC date/times
assertThatFieldIsFormatted( sample, "zonedDateTime", "2011-10-30T02:50:00.000000007+01:00[CET]", "2011-10-30T01:50:00.000+00:00[UTC]" );
}

@Test
public void testYear() throws Exception {
/* Elasticsearch only accepts years in the range [-292275054,292278993]
Expand Down Expand Up @@ -240,6 +332,9 @@ public Sample(long id, String description) {
@Field(analyze = Analyze.NO, store = Store.YES)
private OffsetDateTime offsetDateTime;

@Field(analyze = Analyze.NO, store = Store.YES)
private ZonedDateTime zonedDateTime;

@Field(analyze = Analyze.NO, store = Store.YES)
private OffsetTime offsetTime;

Expand Down
Expand Up @@ -144,7 +144,13 @@ public void testZoneId() throws Exception {

@Test
public void testOffsetDateTime() throws Exception {
OffsetDateTime value = OffsetDateTime.MIN;
/* Elasticsearch only accepts years in the range [-292275054,292278993]
*/
OffsetDateTime value = OffsetDateTime.of(
221998, Month.FEBRUARY.getValue(), 12,
13, 05, 33, 7,
ZoneOffset.of( "+01:00" )
);

Sample sample = new Sample( 1L, "OffsetDateTime example" );
sample.offsetDateTime = value;
Expand Down

0 comments on commit 4d54787

Please sign in to comment.