Add native support for range field types by using a range object

Original Pull Request #1863
Closes #1862
This commit is contained in:
Sascha Woo 2021-07-12 20:15:35 +02:00 committed by GitHub
parent 66d13444aa
commit 271e1eee0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1435 additions and 97 deletions

View File

@ -0,0 +1,444 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core;
import java.util.Optional;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Simple value object to work with ranges and boundaries.
*
* @author Sascha Woo
* @since 4.3
*/
public class Range<T> {
private final static Range<?> UNBOUNDED = Range.of(Bound.unbounded(), Bound.UNBOUNDED);
/**
* The lower bound of the range.
*/
private Bound<T> lowerBound;
/**
* The upper bound of the range.
*/
private Bound<T> upperBound;
/**
* Creates a new {@link Range} with inclusive bounds for both values.
*
* @param <T>
* @param from must not be {@literal null}.
* @param to must not be {@literal null}.
* @return
*/
public static <T> Range<T> closed(T from, T to) {
return new Range<>(Bound.inclusive(from), Bound.inclusive(to));
}
/**
* Creates a new Range with the given value as sole member.
*
* @param <T>
* @param value must not be {@literal null}.
* @return
* @see Range#closed(T, T)
*/
public static <T> Range<T> just(T value) {
return Range.closed(value, value);
}
/**
* Creates a new left-open {@link Range}, i.e. left exclusive, right inclusive.
*
* @param <T>
* @param from must not be {@literal null}.
* @param to must not be {@literal null}.
* @return
*/
public static <T> Range<T> leftOpen(T from, T to) {
return new Range<>(Bound.exclusive(from), Bound.inclusive(to));
}
/**
* Creates a left-unbounded {@link Range} (the left bound set to {@link Bound#unbounded()}) with the given right
* bound.
*
* @param <T>
* @param to the right {@link Bound}, must not be {@literal null}.
* @return
*/
public static <T> Range<T> leftUnbounded(Bound<T> to) {
return new Range<>(Bound.unbounded(), to);
}
/**
* Creates a new {@link Range} with the given lower and upper bound. Prefer {@link #from(Bound)} for a more builder
* style API.
*
* @param lowerBound must not be {@literal null}.
* @param upperBound must not be {@literal null}.
* @see #from(Bound)
*/
public static <T> Range<T> of(Bound<T> lowerBound, Bound<T> upperBound) {
return new Range<>(lowerBound, upperBound);
}
/**
* Creates a new {@link Range} with exclusive bounds for both values.
*
* @param <T>
* @param from must not be {@literal null}.
* @param to must not be {@literal null}.
* @return
*/
public static <T> Range<T> open(T from, T to) {
return new Range<>(Bound.exclusive(from), Bound.exclusive(to));
}
/**
* Creates a new right-open {@link Range}, i.e. left inclusive, right exclusive.
*
* @param <T>
* @param from must not be {@literal null}.
* @param to must not be {@literal null}.
* @return
*/
public static <T> Range<T> rightOpen(T from, T to) {
return new Range<>(Bound.inclusive(from), Bound.exclusive(to));
}
/**
* Creates a right-unbounded {@link Range} (the right bound set to {@link Bound#unbounded()}) with the given left
* bound.
*
* @param <T>
* @param from the left {@link Bound}, must not be {@literal null}.
* @return
*/
public static <T> Range<T> rightUnbounded(Bound<T> from) {
return new Range<>(from, Bound.unbounded());
}
/**
* Returns an unbounded {@link Range}.
*
* @return
*/
@SuppressWarnings("unchecked")
public static <T> Range<T> unbounded() {
return (Range<T>) UNBOUNDED;
}
private Range() {
}
private Range(Bound<T> lowerBound, Bound<T> upperBound) {
Assert.notNull(lowerBound, "Lower bound must not be null!");
Assert.notNull(upperBound, "Upper bound must not be null!");
this.lowerBound = lowerBound;
this.upperBound = upperBound;
}
/**
* Returns whether the {@link Range} contains the given value.
*
* @param value must not be {@literal null}.
* @return
*/
public boolean contains(T value) {
Assert.notNull(value, "Reference value must not be null!");
Assert.isInstanceOf(Comparable.class, value, "value must implements Comparable!");
boolean greaterThanLowerBound = lowerBound.getValue() //
.map(it -> lowerBound.isInclusive() ? ((Comparable<? super T>) it).compareTo(value) <= 0
: ((Comparable<? super T>) it).compareTo(value) < 0) //
.orElse(true);
boolean lessThanUpperBound = upperBound.getValue() //
.map(it -> upperBound.isInclusive() ? ((Comparable<? super T>) it).compareTo(value) >= 0
: ((Comparable<? super T>) it).compareTo(value) > 0) //
.orElse(true);
return greaterThanLowerBound && lessThanUpperBound;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Range)) {
return false;
}
Range<?> range = (Range<?>) o;
if (!ObjectUtils.nullSafeEquals(lowerBound, range.lowerBound)) {
return false;
}
return ObjectUtils.nullSafeEquals(upperBound, range.upperBound);
}
public Range.Bound<T> getLowerBound() {
return this.lowerBound;
}
public Range.Bound<T> getUpperBound() {
return this.upperBound;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(lowerBound);
result = 31 * result + ObjectUtils.nullSafeHashCode(upperBound);
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return String.format("%s-%s", lowerBound.toPrefixString(), upperBound.toSuffixString());
}
/**
* Value object representing a boundary. A boundary can either be {@link #unbounded() unbounded}, {@link #inclusive(T)
* including its value} or {@link #exclusive(T) its value}.
*/
public static final class Bound<T> {
@SuppressWarnings({ "rawtypes", "unchecked" }) //
private static final Bound<?> UNBOUNDED = new Bound(Optional.empty(), true);
private final Optional<T> value;
private final boolean inclusive;
/**
* Creates a boundary excluding {@code value}.
*
* @param value must not be {@literal null}.
* @return
*/
public static Bound<Double> exclusive(double value) {
return exclusive((Double) value);
}
/**
* Creates a boundary excluding {@code value}.
*
* @param value must not be {@literal null}.
* @return
*/
public static Bound<Float> exclusive(float value) {
return exclusive((Float) value);
}
/**
* Creates a boundary excluding {@code value}.
*
* @param value must not be {@literal null}.
* @return
*/
public static Bound<Integer> exclusive(int value) {
return exclusive((Integer) value);
}
/**
* Creates a boundary excluding {@code value}.
*
* @param value must not be {@literal null}.
* @return
*/
public static Bound<Long> exclusive(long value) {
return exclusive((Long) value);
}
/**
* Creates a boundary excluding {@code value}.
*
* @param value must not be {@literal null}.
* @return
*/
public static <T> Bound<T> exclusive(T value) {
Assert.notNull(value, "Value must not be null!");
Assert.isInstanceOf(Comparable.class, value, "value must implements Comparable!");
return new Bound<>(Optional.of(value), false);
}
/**
* Creates a boundary including {@code value}.
*
* @param value must not be {@literal null}.
* @return
*/
public static Bound<Double> inclusive(double value) {
return inclusive((Double) value);
}
/**
* Creates a boundary including {@code value}.
*
* @param value must not be {@literal null}.
* @return
*/
public static Bound<Float> inclusive(float value) {
return inclusive((Float) value);
}
/**
* Creates a boundary including {@code value}.
*
* @param value must not be {@literal null}.
* @return
*/
public static Bound<Integer> inclusive(int value) {
return inclusive((Integer) value);
}
/**
* Creates a boundary including {@code value}.
*
* @param value must not be {@literal null}.
* @return
*/
public static Bound<Long> inclusive(long value) {
return inclusive((Long) value);
}
/**
* Creates a boundary including {@code value}.
*
* @param value must not be {@literal null}.
* @return
*/
public static <T> Bound<T> inclusive(T value) {
Assert.notNull(value, "Value must not be null!");
Assert.isInstanceOf(Comparable.class, value, "value must implements Comparable!");
return new Bound<>(Optional.of(value), true);
}
/**
* Creates an unbounded {@link Bound}.
*/
@SuppressWarnings("unchecked")
public static <T> Bound<T> unbounded() {
return (Bound<T>) UNBOUNDED;
}
private Bound(Optional<T> value, boolean inclusive) {
this.value = value;
this.inclusive = inclusive;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Bound)) {
return false;
}
Bound<?> bound = (Bound<?>) o;
if (inclusive != bound.inclusive)
return false;
return ObjectUtils.nullSafeEquals(value, bound.value);
}
public Optional<T> getValue() {
return this.value;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(value);
result = 31 * result + (inclusive ? 1 : 0);
return result;
}
/**
* Returns whether this boundary is bounded.
*
* @return
*/
public boolean isBounded() {
return value.isPresent();
}
public boolean isInclusive() {
return this.inclusive;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return value.map(Object::toString).orElse("unbounded");
}
String toPrefixString() {
return getValue() //
.map(Object::toString) //
.map(it -> isInclusive() ? "[".concat(it) : "(".concat(it)) //
.orElse("unbounded");
}
String toSuffixString() {
return getValue() //
.map(Object::toString) //
.map(it -> isInclusive() ? it.concat("]") : it.concat(")")) //
.orElse("unbounded");
}
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.convert;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentPropertyConverter;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.util.Assert;
/**
* @author Sascha Woo
* @since 4.3
*/
public abstract class AbstractPersistentPropertyConverter implements ElasticsearchPersistentPropertyConverter {
private final PersistentProperty<?> property;
public AbstractPersistentPropertyConverter(PersistentProperty<?> property) {
Assert.notNull(property, "property must not be null.");
this.property = property;
}
protected PersistentProperty<?> getProperty() {
return property;
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.convert;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.data.elasticsearch.core.Range;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.util.Assert;
/**
* @author Sascha Woo
* @since 4.3
*/
public abstract class AbstractRangePersistentPropertyConverter<T> extends AbstractPersistentPropertyConverter {
protected static final String LT_FIELD = "lt";
protected static final String LTE_FIELD = "lte";
protected static final String GT_FIELD = "gt";
protected static final String GTE_FIELD = "gte";
public AbstractRangePersistentPropertyConverter(PersistentProperty<?> property) {
super(property);
}
@Override
public Object read(Object value) {
Assert.notNull(value, "value must not be null.");
Assert.isInstanceOf(Map.class, value, "value must be instance of Map.");
try {
Map<String, Object> source = (Map<String, Object>) value;
Range.Bound<T> lowerBound;
Range.Bound<T> upperBound;
if (source.containsKey(GTE_FIELD)) {
lowerBound = Range.Bound.inclusive(parse((String) source.get(GTE_FIELD)));
} else if (source.containsKey(GT_FIELD)) {
lowerBound = Range.Bound.exclusive(parse((String) source.get(GT_FIELD)));
} else {
lowerBound = Range.Bound.unbounded();
}
if (source.containsKey(LTE_FIELD)) {
upperBound = Range.Bound.inclusive(parse((String) source.get(LTE_FIELD)));
} else if (source.containsKey(LT_FIELD)) {
upperBound = Range.Bound.exclusive(parse((String) source.get(LT_FIELD)));
} else {
upperBound = Range.Bound.unbounded();
}
return Range.of(lowerBound, upperBound);
} catch (Exception e) {
throw new ConversionException(
String.format("Unable to convert value '%s' of property '%s'", value, getProperty().getName()), e);
}
}
@Override
public Object write(Object value) {
Assert.notNull(value, "value must not be null.");
Assert.isInstanceOf(Range.class, value, "value must be instance of Range.");
try {
Range<T> range = (Range<T>) value;
Range.Bound<T> lowerBound = range.getLowerBound();
Range.Bound<T> upperBound = range.getUpperBound();
Map<String, Object> target = new LinkedHashMap<>();
if (lowerBound.isBounded()) {
String lowerBoundValue = format(lowerBound.getValue().get());
if (lowerBound.isInclusive()) {
target.put(GTE_FIELD, lowerBoundValue);
} else {
target.put(GT_FIELD, lowerBoundValue);
}
}
if (upperBound.isBounded()) {
String upperBoundValue = format(upperBound.getValue().get());
if (upperBound.isInclusive()) {
target.put(LTE_FIELD, upperBoundValue);
} else {
target.put(LT_FIELD, upperBoundValue);
}
}
return target;
} catch (Exception e) {
throw new ConversionException(
String.format("Unable to convert value '%s' of property '%s'", value, getProperty().getName()), e);
}
}
protected abstract String format(T value);
protected Class<?> getGenericType() {
return getProperty().getTypeInformation().getTypeArguments().get(0).getType();
}
protected abstract T parse(String value);
}

View File

@ -0,0 +1,70 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.convert;
import java.util.Date;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mapping.PersistentProperty;
/**
* @author Sascha Woo
* @since 4.3
*/
public class DatePersistentPropertyConverter extends AbstractPersistentPropertyConverter {
private static final Logger LOGGER = LoggerFactory.getLogger(DatePersistentPropertyConverter.class);
private final List<ElasticsearchDateConverter> dateConverters;
public DatePersistentPropertyConverter(PersistentProperty<?> property,
List<ElasticsearchDateConverter> dateConverters) {
super(property);
this.dateConverters = dateConverters;
}
@Override
public Object read(Object value) {
String s = value.toString();
for (ElasticsearchDateConverter dateConverter : dateConverters) {
try {
return dateConverter.parse(s);
} catch (Exception e) {
LOGGER.trace(e.getMessage(), e);
}
}
throw new ConversionException(String.format("Unable to convert value '%s' to %s for property '%s'", s,
getProperty().getActualType().getTypeName(), getProperty().getName()));
}
@Override
public Object write(Object value) {
try {
return dateConverters.get(0).format((Date) value);
} catch (Exception e) {
throw new ConversionException(
String.format("Unable to convert value '%s' of property '%s'", value, getProperty().getName()), e);
}
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.convert;
import java.util.Date;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mapping.PersistentProperty;
/**
* @author Sascha Woo
* @since 4.3
*/
public class DateRangePersistentPropertyConverter extends AbstractRangePersistentPropertyConverter<Date> {
private static final Logger LOGGER = LoggerFactory.getLogger(DateRangePersistentPropertyConverter.class);
private final List<ElasticsearchDateConverter> dateConverters;
public DateRangePersistentPropertyConverter(PersistentProperty<?> property,
List<ElasticsearchDateConverter> dateConverters) {
super(property);
this.dateConverters = dateConverters;
}
@Override
protected String format(Date value) {
return dateConverters.get(0).format(value);
}
@Override
protected Date parse(String value) {
for (ElasticsearchDateConverter converters : dateConverters) {
try {
return converters.parse(value);
} catch (Exception e) {
LOGGER.trace(e.getMessage(), e);
}
}
throw new ConversionException(String.format("Unable to convert value '%s' to %s for property '%s'", value,
getGenericType().getTypeName(), getProperty().getName()));
}
}

View File

@ -482,10 +482,7 @@ public class MappingElasticsearchConverter
}
private Object convertOnRead(ElasticsearchPersistentPropertyConverter propertyConverter, Object source) {
if (String.class.isAssignableFrom(source.getClass())) {
source = propertyConverter.read((String) source);
}
return source;
return propertyConverter.read(source);
}
/**

View File

@ -0,0 +1,53 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.convert;
import org.springframework.data.mapping.PersistentProperty;
/**
* @author Sascha Woo
* @since 4.3
*/
public class NumberRangePersistentPropertyConverter extends AbstractRangePersistentPropertyConverter<Number> {
public NumberRangePersistentPropertyConverter(PersistentProperty<?> property) {
super(property);
}
@Override
protected String format(Number number) {
return String.valueOf(number);
}
@Override
protected Number parse(String value) {
Class<?> type = getGenericType();
if (Integer.class.isAssignableFrom(type)) {
return Integer.valueOf(value);
} else if (Float.class.isAssignableFrom(type)) {
return Float.valueOf(value);
} else if (Long.class.isAssignableFrom(type)) {
return Long.valueOf(value);
} else if (Double.class.isAssignableFrom(type)) {
return Double.valueOf(value);
}
throw new ConversionException(String.format("Unable to convert value '%s' to %s for property '%s'", value,
type.getTypeName(), getProperty().getName()));
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.convert;
import java.time.temporal.TemporalAccessor;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mapping.PersistentProperty;
/**
* @author Sascha Woo
* @since 4.3
*/
public class TemporalPersistentPropertyConverter extends AbstractPersistentPropertyConverter {
private static final Logger LOGGER = LoggerFactory.getLogger(TemporalPersistentPropertyConverter.class);
private final List<ElasticsearchDateConverter> dateConverters;
public TemporalPersistentPropertyConverter(PersistentProperty<?> property,
List<ElasticsearchDateConverter> dateConverters) {
super(property);
this.dateConverters = dateConverters;
}
@SuppressWarnings("unchecked")
@Override
public Object read(Object value) {
String s = value.toString();
Class<?> actualType = getProperty().getActualType();
for (ElasticsearchDateConverter dateConverter : dateConverters) {
try {
return dateConverter.parse(s, (Class<? extends TemporalAccessor>) actualType);
} catch (Exception e) {
LOGGER.trace(e.getMessage(), e);
}
}
throw new ConversionException(String.format("Unable to convert value '%s' to %s for property '%s'", s,
getProperty().getActualType().getTypeName(), getProperty().getName()));
}
@Override
public Object write(Object value) {
try {
return dateConverters.get(0).format((TemporalAccessor) value);
} catch (Exception e) {
throw new ConversionException(
String.format("Unable to convert value '%s' of property '%s'", value, getProperty().getName()), e);
}
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.convert;
import java.time.temporal.TemporalAccessor;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.util.Assert;
/**
* @author Sascha Woo
* @since 4.3
*/
public class TemporalRangePersistentPropertyConverter
extends AbstractRangePersistentPropertyConverter<TemporalAccessor> {
private static final Logger LOGGER = LoggerFactory.getLogger(TemporalRangePersistentPropertyConverter.class);
private final List<ElasticsearchDateConverter> dateConverters;
public TemporalRangePersistentPropertyConverter(PersistentProperty<?> property,
List<ElasticsearchDateConverter> dateConverters) {
super(property);
Assert.notEmpty(dateConverters, "dateConverters must not be empty.");
this.dateConverters = dateConverters;
}
@Override
protected String format(TemporalAccessor temporal) {
return dateConverters.get(0).format(temporal);
}
@Override
protected TemporalAccessor parse(String value) {
Class<?> type = getGenericType();
for (ElasticsearchDateConverter converters : dateConverters) {
try {
return converters.parse(value, (Class<? extends TemporalAccessor>) type);
} catch (Exception e) {
LOGGER.trace(e.getMessage(), e);
}
}
throw new ConversionException(String.format("Unable to convert value '%s' to %s for property '%s'", value,
type.getTypeName(), getProperty().getName()));
}
}

View File

@ -16,25 +16,26 @@
package org.springframework.data.elasticsearch.core.mapping;
/**
* Interface defining methods to convert a property value to a String and back.
* Interface defining methods to convert a persistent property value to an elasticsearch property value and back.
*
* @author Peter-Josef Meisch
* @author Sascha Woo
*/
public interface ElasticsearchPersistentPropertyConverter {
/**
* converts the property value to a String.
* Converts a persistent property value to an elasticsearch property value.
*
* @param property the property value to convert, must not be {@literal null}
* @return String representation.
* @param value the persistent property value to convert, must not be {@literal null}
* @return The elasticsearch property value.
*/
String write(Object property);
Object write(Object value);
/**
* converts a property value from a String.
* Converts an elasticsearch property value to a persistent property value.
*
* @param s the property to convert, must not be {@literal null}
* @return property value
* @param value the elasticsearch property value to convert, must not be {@literal null}
* @return The persistent property value.
*/
Object read(String s);
Object read(Object value);
}

View File

@ -29,9 +29,14 @@ import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.GeoPointField;
import org.springframework.data.elasticsearch.annotations.GeoShapeField;
import org.springframework.data.elasticsearch.annotations.MultiField;
import org.springframework.data.elasticsearch.core.Range;
import org.springframework.data.elasticsearch.core.completion.Completion;
import org.springframework.data.elasticsearch.core.convert.ConversionException;
import org.springframework.data.elasticsearch.core.convert.DatePersistentPropertyConverter;
import org.springframework.data.elasticsearch.core.convert.DateRangePersistentPropertyConverter;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchDateConverter;
import org.springframework.data.elasticsearch.core.convert.NumberRangePersistentPropertyConverter;
import org.springframework.data.elasticsearch.core.convert.TemporalPersistentPropertyConverter;
import org.springframework.data.elasticsearch.core.convert.TemporalRangePersistentPropertyConverter;
import org.springframework.data.elasticsearch.core.geo.GeoJson;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.join.JoinField;
@ -39,6 +44,7 @@ import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.Property;
@ -92,7 +98,7 @@ public class SimpleElasticsearchPersistentProperty extends
throw new MappingException("@Field annotation must not be used on a @MultiField property.");
}
initDateConverter();
initPropertyConverter();
storeNullValue = isField && getRequiredAnnotation(Field.class).storeNullValue();
}
@ -128,102 +134,127 @@ public class SimpleElasticsearchPersistentProperty extends
}
/**
* Initializes an {@link ElasticsearchPersistentPropertyConverter} if this property is annotated as a Field with type
* {@link FieldType#Date}, has a {@link DateFormat} set and if the type of the property is one of the Java8 temporal
* classes or java.util.Date.
* Initializes the property converter for this {@link PersistentProperty}, if any.
*/
private void initDateConverter() {
Field field = findAnnotation(Field.class);
private void initPropertyConverter() {
Class<?> actualType = getActualTypeOrNull();
if (actualType == null) {
return;
}
boolean isTemporalAccessor = TemporalAccessor.class.isAssignableFrom(actualType);
boolean isDate = Date.class.isAssignableFrom(actualType);
Field field = findAnnotation(Field.class);
if (field == null) {
return;
}
if (field != null && (field.type() == FieldType.Date || field.type() == FieldType.Date_Nanos)
&& (isTemporalAccessor || isDate)) {
DateFormat[] dateFormats = field.format();
String[] dateFormatPatterns = field.pattern();
String property = getOwner().getType().getSimpleName() + "." + getName();
if (dateFormats.length == 0 && dateFormatPatterns.length == 0) {
LOGGER.warn(
"Property '{}' has @Field type '{}' but has no built-in format or custom date pattern defined. Make sure you have a converter registered for type {}.",
property, field.type().name(), actualType.getSimpleName());
return;
}
List<ElasticsearchDateConverter> converters = new ArrayList<>();
// register converters for built-in formats
for (DateFormat dateFormat : dateFormats) {
switch (dateFormat) {
case none:
case custom:
break;
case weekyear:
case weekyear_week:
case weekyear_week_day:
LOGGER.warn("No default converter available for '{}' and date format '{}'. Use a custom converter instead.",
actualType.getName(), dateFormat.name());
break;
default:
converters.add(ElasticsearchDateConverter.of(dateFormat));
break;
switch (field.type()) {
case Date:
case Date_Nanos: {
List<ElasticsearchDateConverter> dateConverters = getDateConverters(field, actualType);
if (dateConverters.isEmpty()) {
LOGGER.warn("No date formatters configured for property '{}'.", getName());
return;
}
}
// register converters for custom formats
for (String dateFormatPattern : dateFormatPatterns) {
if (!StringUtils.hasText(dateFormatPattern)) {
throw new MappingException(String.format("Date pattern of property '%s' must not be empty", property));
if (TemporalAccessor.class.isAssignableFrom(actualType)) {
propertyConverter = new TemporalPersistentPropertyConverter(this, dateConverters);
} else if (Date.class.isAssignableFrom(actualType)) {
propertyConverter = new DatePersistentPropertyConverter(this, dateConverters);
} else {
LOGGER.warn("Unsupported type '{}' for date property '{}'.", actualType, getName());
}
converters.add(ElasticsearchDateConverter.of(dateFormatPattern));
break;
}
case Date_Range: {
if (!Range.class.isAssignableFrom(actualType)) {
return;
}
if (!converters.isEmpty()) {
propertyConverter = new ElasticsearchPersistentPropertyConverter() {
final List<ElasticsearchDateConverter> dateConverters = converters;
List<ElasticsearchDateConverter> dateConverters = getDateConverters(field, actualType);
if (dateConverters.isEmpty()) {
LOGGER.warn("No date formatters configured for property '{}'.", getName());
return;
}
@SuppressWarnings("unchecked")
@Override
public Object read(String s) {
for (ElasticsearchDateConverter dateConverter : dateConverters) {
try {
if (isTemporalAccessor) {
return dateConverter.parse(s, (Class<? extends TemporalAccessor>) actualType);
} else { // must be date
return dateConverter.parse(s);
}
} catch (Exception e) {
LOGGER.trace(e.getMessage(), e);
}
}
Class<?> genericType = getTypeInformation().getTypeArguments().get(0).getType();
if (TemporalAccessor.class.isAssignableFrom(genericType)) {
propertyConverter = new TemporalRangePersistentPropertyConverter(this, dateConverters);
} else if (Date.class.isAssignableFrom(genericType)) {
propertyConverter = new DateRangePersistentPropertyConverter(this, dateConverters);
} else {
LOGGER.warn("Unsupported generic type '{}' for date range property '{}'.", genericType, getName());
}
break;
}
case Integer_Range:
case Float_Range:
case Long_Range:
case Double_Range: {
if (!Range.class.isAssignableFrom(actualType)) {
return;
}
throw new ConversionException(String
.format("Unable to parse date value '%s' of property '%s' with configured converters", s, property));
}
Class<?> genericType = getTypeInformation().getTypeArguments().get(0).getType();
if ((field.type() == FieldType.Integer_Range && !Integer.class.isAssignableFrom(genericType))
|| (field.type() == FieldType.Float_Range && !Float.class.isAssignableFrom(genericType))
|| (field.type() == FieldType.Long_Range && !Long.class.isAssignableFrom(genericType))
|| (field.type() == FieldType.Double_Range && !Double.class.isAssignableFrom(genericType))) {
LOGGER.warn("Unsupported generic type '{}' for range field type '{}' of property '{}'.", genericType,
field.type(), getName());
return;
}
@Override
public String write(Object property) {
ElasticsearchDateConverter dateConverter = dateConverters.get(0);
if (isTemporalAccessor && TemporalAccessor.class.isAssignableFrom(property.getClass())) {
return dateConverter.format((TemporalAccessor) property);
} else if (isDate && Date.class.isAssignableFrom(property.getClass())) {
return dateConverter.format((Date) property);
} else {
return property.toString();
}
}
};
propertyConverter = new NumberRangePersistentPropertyConverter(this);
break;
}
case Ip_Range: {
// TODO currently unsupported, needs a library like https://seancfoley.github.io/IPAddress/
}
default:
break;
}
}
private List<ElasticsearchDateConverter> getDateConverters(Field field, Class<?> actualType) {
DateFormat[] dateFormats = field.format();
String[] dateFormatPatterns = field.pattern();
List<ElasticsearchDateConverter> converters = new ArrayList<>();
if (dateFormats.length == 0 && dateFormatPatterns.length == 0) {
LOGGER.warn(
"Property '{}' has @Field type '{}' but has no built-in format or custom date pattern defined. Make sure you have a converter registered for type {}.",
getName(), field.type().name(), actualType.getSimpleName());
return converters;
}
// register converters for built-in formats
for (DateFormat dateFormat : dateFormats) {
switch (dateFormat) {
case none:
case custom:
break;
case weekyear:
case weekyear_week:
case weekyear_week_day:
LOGGER.warn("No default converter available for '{}' and date format '{}'. Use a custom converter instead.",
actualType.getName(), dateFormat.name());
break;
default:
converters.add(ElasticsearchDateConverter.of(dateFormat));
break;
}
}
for (String dateFormatPattern : dateFormatPatterns) {
if (!StringUtils.hasText(dateFormatPattern)) {
throw new MappingException(String.format("Date pattern of property '%s' must not be empty", getName()));
}
converters.add(ElasticsearchDateConverter.of(dateFormatPattern));
}
return converters;
}
@SuppressWarnings("ConstantConditions")
@ -303,4 +334,5 @@ public class SimpleElasticsearchPersistentProperty extends
public boolean isCompletionProperty() {
return getActualType() == Completion.class;
}
}

View File

@ -196,7 +196,7 @@ public class CriteriaQueryMappingUnitTests {
CriteriaQuery criteriaQuery = new CriteriaQuery( //
Criteria.or().subCriteria(Criteria.where("birthDate") //
.between(LocalDate.of(1989, 11, 9), LocalDate.of(1990, 11, 9))) //
.subCriteria(Criteria.where("createdDate").is(383745721653L)) //
.subCriteria(Criteria.where("createdDate").is(new Date(383745721653L))) //
);
// mapped field name and converted parameter

View File

@ -0,0 +1,177 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core;
import static org.assertj.core.api.Assertions.*;
import java.time.LocalDate;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
/**
* @author Sascha Woo
* @since 4.3
*/
public class RangeTests {
@Test
public void shouldContainsLocalDate() {
// given
// when
// then
assertThat(Range.open(LocalDate.of(2021, 1, 1), LocalDate.of(2021, 2, 1)).contains(LocalDate.of(2021, 1, 10)))
.isTrue();
}
@Test
public void shouldEqualToSameRange() {
// given
Range<LocalDate> range1 = Range.open(LocalDate.of(2021, 1, 1), LocalDate.of(2021, 2, 1));
Range<LocalDate> range2 = Range.open(LocalDate.of(2021, 1, 1), LocalDate.of(2021, 2, 1));
// when
// then
assertThat(range1).isEqualTo(range2);
}
@Test
public void shouldHaveClosedBoundaries() {
// given
Range<Integer> range = Range.closed(1, 3);
// when
// then
assertThat(range.contains(1)).isTrue();
assertThat(range.contains(2)).isTrue();
assertThat(range.contains(3)).isTrue();
}
@Test
public void shouldHaveJustOneValue() {
// given
Range<Integer> range = Range.just(2);
// when
// then
assertThat(range.contains(1)).isFalse();
assertThat(range.contains(2)).isTrue();
assertThat(range.contains(3)).isFalse();
}
@Test
public void shouldHaveLeftOpenBoundary() {
// given
Range<Integer> range = Range.leftOpen(1, 3);
// when
// then
assertThat(range.contains(1)).isFalse();
assertThat(range.contains(2)).isTrue();
assertThat(range.contains(3)).isTrue();
}
@Test
public void shouldHaveLeftUnboundedAndRightExclusive() {
// given
Range<Integer> range = Range.leftUnbounded(Range.Bound.exclusive(3));
// when
// then
assertThat(range.contains(0)).isTrue();
assertThat(range.contains(1)).isTrue();
assertThat(range.contains(2)).isTrue();
assertThat(range.contains(3)).isFalse();
}
@Test
public void shouldHaveLeftUnboundedAndRightInclusive() {
// given
Range<Integer> range = Range.leftUnbounded(Range.Bound.inclusive(3));
// when
// then
assertThat(range.contains(0)).isTrue();
assertThat(range.contains(1)).isTrue();
assertThat(range.contains(2)).isTrue();
assertThat(range.contains(3)).isTrue();
}
@Test
public void shouldHaveOpenBoundaries() {
// given
Range<Integer> range = Range.open(1, 3);
// when
// then
assertThat(range.contains(1)).isFalse();
assertThat(range.contains(2)).isTrue();
assertThat(range.contains(3)).isFalse();
}
@Test
public void shouldHaveRightOpenBoundary() {
// given
Range<Integer> range = Range.rightOpen(1, 3);
// when
// then
assertThat(range.contains(1)).isTrue();
assertThat(range.contains(2)).isTrue();
assertThat(range.contains(3)).isFalse();
}
@Test
public void shouldHaveRightUnboundedAndLeftExclusive() {
// given
Range<Integer> range = Range.rightUnbounded(Range.Bound.exclusive(1));
// when
// then
assertThat(range.contains(1)).isFalse();
assertThat(range.contains(2)).isTrue();
assertThat(range.contains(3)).isTrue();
assertThat(range.contains(4)).isTrue();
}
@Test
public void shouldHaveRightUnboundedAndLeftInclusive() {
// given
Range<Integer> range = Range.rightUnbounded(Range.Bound.inclusive(1));
// when
// then
assertThat(range.contains(1)).isTrue();
assertThat(range.contains(2)).isTrue();
assertThat(range.contains(3)).isTrue();
assertThat(range.contains(4)).isTrue();
}
@Test
public void shouldThrowExceptionIfNotComparable() {
// given
// when
Throwable thrown = catchThrowable(() -> {
Range.just(Arrays.asList("test"));
});
// then
assertThat(thrown).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("value must implements Comparable!");
}
}

View File

@ -20,9 +20,15 @@ import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
@ -46,6 +52,7 @@ import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.GeoPointField;
import org.springframework.data.elasticsearch.core.Range;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.geo.GeoJsonEntity;
import org.springframework.data.elasticsearch.core.geo.GeoJsonGeometryCollection;
@ -882,6 +889,200 @@ public class MappingElasticsearchConverterUnitTests {
assertEquals(expected, document.toJson(), false);
}
@Nested
class RangeTests {
static final String JSON = "{"
+ "\"_class\":\"org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverterUnitTests$RangeTests$RangeEntity\","
+ "\"integerRange\":{\"gt\":\"1\",\"lt\":\"10\"}," //
+ "\"floatRange\":{\"gte\":\"1.2\",\"lte\":\"2.5\"}," //
+ "\"longRange\":{\"gt\":\"2\",\"lte\":\"5\"}," //
+ "\"doubleRange\":{\"gte\":\"3.2\",\"lt\":\"7.4\"}," //
+ "\"dateRange\":{\"gte\":\"1970-01-01T00:00:00.000Z\",\"lte\":\"1970-01-01T01:00:00.000Z\"}," //
+ "\"localDateRange\":{\"gte\":\"2021-07-06\"}," //
+ "\"localTimeRange\":{\"gte\":\"00:30:00.000\",\"lt\":\"02:30:00.000\"}," //
+ "\"localDateTimeRange\":{\"gt\":\"2021-01-01T00:30:00.000\",\"lt\":\"2021-01-01T02:30:00.000\"}," //
+ "\"offsetTimeRange\":{\"gte\":\"00:30:00.000+02:00\",\"lt\":\"02:30:00.000+02:00\"}," //
+ "\"zonedDateTimeRange\":{\"gte\":\"2021-01-01T00:30:00.000+02:00\",\"lte\":\"2021-01-01T00:30:00.000+02:00\"}," //
+ "\"nullRange\":null}";
@Test
public void shouldReadRanges() throws JSONException {
// given
Document source = Document.parse(JSON);
// when
RangeEntity entity = mappingElasticsearchConverter.read(RangeEntity.class, source);
// then
assertThat(entity) //
.isNotNull() //
.satisfies(e -> {
assertThat(e.getIntegerRange()).isEqualTo(Range.open(1, 10));
assertThat(e.getFloatRange()).isEqualTo(Range.closed(1.2f, 2.5f));
assertThat(e.getLongRange()).isEqualTo(Range.leftOpen(2l, 5l));
assertThat(e.getDoubleRange()).isEqualTo(Range.rightOpen(3.2d, 7.4d));
assertThat(e.getDateRange()).isEqualTo(Range.closed(new Date(0), new Date(60 * 60 * 1000)));
assertThat(e.getLocalDateRange())
.isEqualTo(Range.rightUnbounded(Range.Bound.inclusive(LocalDate.of(2021, 7, 6))));
assertThat(e.getLocalTimeRange()).isEqualTo(Range.rightOpen(LocalTime.of(0, 30), LocalTime.of(2, 30)));
assertThat(e.getLocalDateTimeRange())
.isEqualTo(Range.open(LocalDateTime.of(2021, 1, 1, 0, 30), LocalDateTime.of(2021, 1, 1, 2, 30)));
assertThat(e.getOffsetTimeRange())
.isEqualTo(Range.rightOpen(OffsetTime.of(LocalTime.of(0, 30), ZoneOffset.ofHours(2)),
OffsetTime.of(LocalTime.of(2, 30), ZoneOffset.ofHours(2))));
assertThat(e.getZonedDateTimeRange()).isEqualTo(
Range.just(ZonedDateTime.of(LocalDate.of(2021, 1, 1), LocalTime.of(0, 30), ZoneOffset.ofHours(2))));
assertThat(e.getNullRange()).isNull();
});
}
@Test
public void shouldWriteRanges() throws JSONException {
// given
Document source = Document.parse(JSON);
RangeEntity entity = new RangeEntity();
entity.setIntegerRange(Range.open(1, 10));
entity.setFloatRange(Range.closed(1.2f, 2.5f));
entity.setLongRange(Range.leftOpen(2l, 5l));
entity.setDoubleRange(Range.rightOpen(3.2d, 7.4d));
entity.setDateRange(Range.closed(new Date(0), new Date(60 * 60 * 1000)));
entity.setLocalDateRange(Range.rightUnbounded(Range.Bound.inclusive(LocalDate.of(2021, 7, 6))));
entity.setLocalTimeRange(Range.rightOpen(LocalTime.of(0, 30), LocalTime.of(2, 30)));
entity
.setLocalDateTimeRange(Range.open(LocalDateTime.of(2021, 1, 1, 0, 30), LocalDateTime.of(2021, 1, 1, 2, 30)));
entity.setOffsetTimeRange(Range.rightOpen(OffsetTime.of(LocalTime.of(0, 30), ZoneOffset.ofHours(2)),
OffsetTime.of(LocalTime.of(2, 30), ZoneOffset.ofHours(2))));
entity.setZonedDateTimeRange(
Range.just(ZonedDateTime.of(LocalDate.of(2021, 1, 1), LocalTime.of(0, 30), ZoneOffset.ofHours(2))));
entity.setNullRange(null);
// when
Document document = mappingElasticsearchConverter.mapObject(entity);
// then
assertThat(document).isEqualTo(source);
}
@org.springframework.data.elasticsearch.annotations.Document(indexName = "test-index-range-entity-mapper")
class RangeEntity {
@Id private String id;
@Field(type = FieldType.Integer_Range) private Range<Integer> integerRange;
@Field(type = FieldType.Float_Range) private Range<Float> floatRange;
@Field(type = FieldType.Long_Range) private Range<Long> longRange;
@Field(type = FieldType.Double_Range) private Range<Double> doubleRange;
@Field(type = FieldType.Date_Range) private Range<Date> dateRange;
@Field(type = FieldType.Date_Range, format = DateFormat.year_month_day) private Range<LocalDate> localDateRange;
@Field(type = FieldType.Date_Range,
format = DateFormat.hour_minute_second_millis) private Range<LocalTime> localTimeRange;
@Field(type = FieldType.Date_Range,
format = DateFormat.date_hour_minute_second_millis) private Range<LocalDateTime> localDateTimeRange;
@Field(type = FieldType.Date_Range, format = DateFormat.time) private Range<OffsetTime> offsetTimeRange;
@Field(type = FieldType.Date_Range) private Range<ZonedDateTime> zonedDateTimeRange;
@Field(type = FieldType.Date_Range, storeNullValue = true) private Range<ZonedDateTime> nullRange;
public String getId() {
return id;
}
public Range<Integer> getIntegerRange() {
return integerRange;
}
public Range<Float> getFloatRange() {
return floatRange;
}
public Range<Long> getLongRange() {
return longRange;
}
public Range<Double> getDoubleRange() {
return doubleRange;
}
public Range<Date> getDateRange() {
return dateRange;
}
public Range<LocalDate> getLocalDateRange() {
return localDateRange;
}
public Range<LocalTime> getLocalTimeRange() {
return localTimeRange;
}
public Range<LocalDateTime> getLocalDateTimeRange() {
return localDateTimeRange;
}
public Range<OffsetTime> getOffsetTimeRange() {
return offsetTimeRange;
}
public Range<ZonedDateTime> getZonedDateTimeRange() {
return zonedDateTimeRange;
}
public Range<ZonedDateTime> getNullRange() {
return nullRange;
}
public void setId(String id) {
this.id = id;
}
public void setIntegerRange(Range<Integer> integerRange) {
this.integerRange = integerRange;
}
public void setFloatRange(Range<Float> floatRange) {
this.floatRange = floatRange;
}
public void setLongRange(Range<Long> longRange) {
this.longRange = longRange;
}
public void setDoubleRange(Range<Double> doubleRange) {
this.doubleRange = doubleRange;
}
public void setDateRange(Range<Date> dateRange) {
this.dateRange = dateRange;
}
public void setLocalDateRange(Range<LocalDate> localDateRange) {
this.localDateRange = localDateRange;
}
public void setLocalTimeRange(Range<LocalTime> localTimeRange) {
this.localTimeRange = localTimeRange;
}
public void setLocalDateTimeRange(Range<LocalDateTime> localDateTimeRange) {
this.localDateTimeRange = localDateTimeRange;
}
public void setOffsetTimeRange(Range<OffsetTime> offsetTimeRange) {
this.offsetTimeRange = offsetTimeRange;
}
public void setZonedDateTimeRange(Range<ZonedDateTime> zonedDateTimeRange) {
this.zonedDateTimeRange = zonedDateTimeRange;
}
public void setNullRange(Range<ZonedDateTime> nullRange) {
this.nullRange = nullRange;
}
}
}
@Nested
class GeoJsonUnitTests {
private GeoJsonEntity entity;
@ -2074,7 +2275,8 @@ public class MappingElasticsearchConverterUnitTests {
}
}
private static class ElectricCar extends Car {}
private static class ElectricCar extends Car {
}
private static class PersonWithCars {
@Id @Nullable String id;

View File

@ -114,7 +114,7 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getRequiredPersistentProperty("localDate");
LocalDate localDate = LocalDate.of(2019, 12, 27);
String converted = persistentProperty.getPropertyConverter().write(localDate);
String converted = persistentProperty.getPropertyConverter().write(localDate).toString();
assertThat(converted).isEqualTo("27.12.2019");
}
@ -138,7 +138,7 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
.from(ZonedDateTime.of(LocalDateTime.of(2020, 4, 19, 19, 44), ZoneId.of("UTC")));
Date legacyDate = calendar.getTime();
String converted = persistentProperty.getPropertyConverter().write(legacyDate);
String converted = persistentProperty.getPropertyConverter().write(legacyDate).toString();
assertThat(converted).isEqualTo("20200419T194400.000Z");
}