com.vladmihalcea.hibernate.type.range.guava.PostgreSQLGuavaRangeType Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of hibernate-types-60 Show documentation
Show all versions of hibernate-types-60 Show documentation
Hibernate ORM 6.0 extra Types
package com.vladmihalcea.hibernate.type.range.guava;
import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
import com.vladmihalcea.hibernate.type.ImmutableType;
import com.vladmihalcea.hibernate.util.ReflectionUtils;
import org.hibernate.HibernateException;
import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.annotations.common.reflection.java.JavaXMember;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.usertype.DynamicParameterizedType;
import org.postgresql.util.PGobject;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.util.Properties;
import java.util.function.Function;
/**
* Maps a {@link Range} object type to a PostgreSQL range
* column type.
*
* Supported range types:
*
* - int4range
* - int8range
* - numrange
* - tsrange
* - tstzrange
* - daterange
*
*
* @author Edgar Asatryan
* @author Vlad Mihalcea
* @author Jan-Willem Gmelig Meyling
*/
public class PostgreSQLGuavaRangeType extends ImmutableType implements DynamicParameterizedType {
/**
* An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to.
*/
private static final Range EMPTY_INT_RANGE = Range.closedOpen(Integer.MIN_VALUE, Integer.MIN_VALUE);
/**
* An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to.
*/
private static final Range EMPTY_LONG_RANGE = Range.closedOpen(Long.MIN_VALUE, Long.MIN_VALUE);
/**
* An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to.
*/
private static final Range EMPTY_BIGDECIMAL_RANGE = Range.closedOpen(BigDecimal.ZERO, BigDecimal.ZERO);
/**
* An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to.
*/
private static final Range EMPTY_LOCALDATETIME_RANGE = Range.closedOpen(LocalDateTime.MIN, LocalDateTime.MIN);
/**
* An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to.
*/
private static final Range EMPTY_OFFSETDATETIME_RANGE = Range.closedOpen(OffsetDateTime.MIN, OffsetDateTime.MIN);
/**
* An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to.
*/
private static final Range EMPTY_ZONEDDATETIME_RANGE = Range.closedOpen(OffsetDateTime.MIN.toZonedDateTime(), OffsetDateTime.MIN.toZonedDateTime());
/**
* An empty int range that satisfies {@link Range#isEmpty()} to map PostgreSQL's {@code empty} to.
*/
private static final Range EMPTY_DATE_RANGE = Range.closedOpen(LocalDate.MIN, LocalDate.MIN);
private static final DateTimeFormatter LOCAL_DATE_TIME = new DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd HH:mm:ss")
.optionalStart()
.appendPattern(".")
.appendFraction(ChronoField.NANO_OF_SECOND, 1, 6, false)
.optionalEnd()
.toFormatter();
private static final DateTimeFormatter OFFSET_DATE_TIME = new DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd HH:mm:ss")
.optionalStart()
.appendPattern(".")
.appendFraction(ChronoField.NANO_OF_SECOND, 1, 6, false)
.optionalEnd()
.appendPattern("X")
.toFormatter();
public static final PostgreSQLGuavaRangeType INSTANCE = new PostgreSQLGuavaRangeType();
private Type type;
private Class elementType;
public PostgreSQLGuavaRangeType() {
super(Range.class);
}
public PostgreSQLGuavaRangeType(Class elementType) {
super(Range.class);
this.elementType = elementType;
}
@Override
public int getSqlType() {
return Types.OTHER;
}
@Override
protected Range get(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException {
PGobject pgObject = (PGobject) rs.getObject(position);
if (pgObject == null) {
return null;
}
String type = pgObject.getType();
String value = pgObject.getValue();
switch (type) {
case "int4range":
return integerRange(value);
case "int8range":
return longRange(value);
case "numrange":
return bigDecimalRange(value);
case "tsrange":
return localDateTimeRange(value);
case "tstzrange":
return ZonedDateTime.class.equals(elementType) ? zonedDateTimeRange(value) : offsetDateTimeRange(value);
case "daterange":
return localDateRange(value);
default:
throw new HibernateException(
new IllegalStateException("The range type [" + type + "] is not supported!")
);
}
}
@Override
protected void set(PreparedStatement st, Range range, int index, SharedSessionContractImplementor session) throws SQLException {
if (range == null) {
st.setNull(index, Types.OTHER);
} else {
PGobject object = new PGobject();
object.setType(determineRangeType(range));
object.setValue(asString(range));
st.setObject(index, object);
}
}
private String determineRangeType(Range range) {
Type clazz = this.elementType;
if (clazz == null) {
Object anyEndpoint = range.hasLowerBound() ? range.lowerEndpoint() :
range.hasUpperBound() ? range.upperEndpoint() : null;
if (anyEndpoint == null) {
throw new HibernateException(
new IllegalArgumentException("The range " + range + " doesn't have any upper or lower bound!")
);
}
clazz = anyEndpoint.getClass();
}
if (clazz.equals(Integer.class)) {
return "int4range";
} else if (clazz.equals(Long.class)) {
return "int8range";
} else if (clazz.equals(BigDecimal.class)) {
return "numrange";
} else if (clazz.equals(LocalDateTime.class)) {
return "tsrange";
} else if (clazz.equals(ZonedDateTime.class) || clazz.equals(OffsetDateTime.class)) {
return "tstzrange";
} else if (clazz.equals(LocalDate.class)) {
return "daterange";
}
throw new HibernateException(
new IllegalStateException("The class [" + clazz + "] is not supported!")
);
}
public static > Range ofString(String str, Function converter, Class clazz) {
if ("empty".equals(str)) {
if (clazz.equals(Integer.class)) {
return (Range) EMPTY_INT_RANGE;
} else if (clazz.equals(Long.class)) {
return (Range) EMPTY_LONG_RANGE;
} else if (clazz.equals(BigDecimal.class)) {
return (Range) EMPTY_BIGDECIMAL_RANGE;
} else if (clazz.equals(LocalDateTime.class)) {
return (Range) EMPTY_LOCALDATETIME_RANGE;
} else if (clazz.equals(ZonedDateTime.class)) {
return (Range) EMPTY_ZONEDDATETIME_RANGE;
} else if (clazz.equals(OffsetDateTime.class)) {
return (Range) EMPTY_OFFSETDATETIME_RANGE;
} else if (clazz.equals(LocalDate.class)) {
return (Range) EMPTY_DATE_RANGE;
}
throw new HibernateException(
new IllegalStateException("The class [" + clazz.getName() + "] is not supported!")
);
}
BoundType lowerBound = str.charAt(0) == '[' ? BoundType.CLOSED : BoundType.OPEN;
BoundType upperBound = str.charAt(str.length() - 1) == ']' ? BoundType.CLOSED : BoundType.OPEN;
int delim = str.indexOf(',');
if (delim == -1) {
throw new HibernateException(
new IllegalArgumentException("Cannot find comma character")
);
}
String lowerStr = str.substring(1, delim);
String upperStr = str.substring(delim + 1, str.length() - 1);
T lower = null;
T upper = null;
if (lowerStr.length() > 0) {
lower = converter.apply(lowerStr);
}
if (upperStr.length() > 0) {
upper = converter.apply(upperStr);
}
if (lower == null && upper == null && upperBound == BoundType.OPEN && lowerBound == BoundType.OPEN) {
return Range.all();
}
if (lowerStr.length() == 0) {
return upperBound == BoundType.CLOSED ?
Range.atMost(upper) :
Range.lessThan(upper);
} else if (upperStr.length() == 0) {
return lowerBound == BoundType.CLOSED ?
Range.atLeast(lower) :
Range.greaterThan(lower);
} else {
return Range.range(lower, lowerBound, upper, upperBound);
}
}
/**
* Creates the {@code BigDecimal} range from provided string:
* {@code
* Range closed = Range.bigDecimalRange("[0.1,1.1]");
* Range halfOpen = Range.bigDecimalRange("(0.1,1.1]");
* Range open = Range.bigDecimalRange("(0.1,1.1)");
* Range leftUnbounded = Range.bigDecimalRange("(,1.1)");
* }
*
* @param range The range string, for example {@literal "[5.5,7.8]"}.
*
* @return The range of {@code BigDecimal}s.
*
* @throws NumberFormatException when one of the bounds are invalid.
*/
public static Range bigDecimalRange(String range) {
return ofString(range, BigDecimal::new, BigDecimal.class);
}
/**
* Creates the {@code Integer} range from provided string:
* {@code
* Range closed = Range.integerRange("[1,5]");
* Range halfOpen = Range.integerRange("(-1,1]");
* Range open = Range.integerRange("(1,2)");
* Range leftUnbounded = Range.integerRange("(,10)");
* Range unbounded = Range.integerRange("(,)");
* }
*
* @param range The range string, for example {@literal "[5,7]"}.
*
* @return The range of {@code Integer}s.
*
* @throws NumberFormatException when one of the bounds are invalid.
*/
public static Range integerRange(String range) {
return ofString(range, Integer::parseInt, Integer.class);
}
/**
* Creates the {@code Long} range from provided string:
* {@code
* Range closed = Range.longRange("[1,5]");
* Range halfOpen = Range.longRange("(-1,1]");
* Range open = Range.longRange("(1,2)");
* Range leftUnbounded = Range.longRange("(,10)");
* Range unbounded = Range.longRange("(,)");
* }
*
* @param range The range string, for example {@literal "[5,7]"}.
*
* @return The range of {@code Long}s.
*
* @throws NumberFormatException when one of the bounds are invalid.
*/
public static Range longRange(String range) {
return ofString(range, Long::parseLong, Long.class);
}
/**
* Creates the {@code LocalDateTime} range from provided string:
* {@code
* Range closed = Range.localDateTimeRange("[2014-04-28 16:00:49,2015-04-28 16:00:49]");
* Range quoted = Range.localDateTimeRange("[\"2014-04-28 16:00:49\",\"2015-04-28 16:00:49\"]");
* Range iso = Range.localDateTimeRange("[\"2014-04-28T16:00:49.2358\",\"2015-04-28T16:00:49\"]");
* }
*
* The valid formats for bounds are:
*
* - yyyy-MM-dd HH:mm:ss[.SSSSSS]
* - yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]
*
*
* @param range The range string, for example {@literal "[2014-04-28 16:00:49,2015-04-28 16:00:49]"}.
*
* @return The range of {@code LocalDateTime}s.
*
* @throws DateTimeParseException when one of the bounds are invalid.
*/
public static Range localDateTimeRange(String range) {
return ofString(range, parseLocalDateTime().compose(unquote()), LocalDateTime.class);
}
/**
* Creates the {@code LocalDate} range from provided string:
* {@code
* Range closed = Range.localDateRange("[2014-04-28,2015-04-289]");
* Range quoted = Range.localDateRange("[\"2014-04-28\",\"2015-04-28\"]");
* Range iso = Range.localDateRange("[\"2014-04-28\",\"2015-04-28\"]");
* }
*
* The valid formats for bounds are:
*
* - yyyy-MM-dd
* - yyyy-MM-dd
*
*
* @param range The range string, for example {@literal "[2014-04-28,2015-04-28]"}.
*
* @return The range of {@code LocalDate}s.
*
* @throws DateTimeParseException when one of the bounds are invalid.
*/
public static Range localDateRange(String range) {
Function parseLocalDate = LocalDate::parse;
return ofString(range, parseLocalDate.compose(unquote()), LocalDate.class);
}
/**
* Creates the {@code ZonedDateTime} range from provided string:
* {@code
* Range closed = Range.zonedDateTimeRange("[2007-12-03T10:15:30+01:00\",\"2008-12-03T10:15:30+01:00]");
* Range quoted = Range.zonedDateTimeRange("[\"2007-12-03T10:15:30+01:00\",\"2008-12-03T10:15:30+01:00\"]");
* Range iso = Range.zonedDateTimeRange("[2011-12-03T10:15:30+01:00[Europe/Paris], 2012-12-03T10:15:30+01:00[Europe/Paris]]");
* }
*
* The valid formats for bounds are:
*
* - yyyy-MM-dd HH:mm:ss[.SSSSSS]X
* - yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]X
*
*
* @param rangeStr The range string, for example {@literal "[2011-12-03T10:15:30+01:00,2012-12-03T10:15:30+01:00]"}.
*
* @return The range of {@code ZonedDateTime}s.
*
* @throws DateTimeParseException when one of the bounds are invalid.
* @throws IllegalArgumentException when bounds time zones are different.
*/
public static Range zonedDateTimeRange(String rangeStr) {
Range range = ofString(rangeStr, parseZonedDateTime().compose(unquote()), ZonedDateTime.class);
if (range.hasLowerBound() && range.hasUpperBound()) {
ZoneId lowerZone = range.lowerEndpoint().getZone();
ZoneId upperZone = range.upperEndpoint().getZone();
if (!lowerZone.equals(upperZone)) {
Duration lowerDst = ZoneId.systemDefault().getRules().getDaylightSavings(range.lowerEndpoint().toInstant());
Duration upperDst = ZoneId.systemDefault().getRules().getDaylightSavings(range.upperEndpoint().toInstant());
long dstSeconds = upperDst.minus(lowerDst).getSeconds();
if (dstSeconds < 0) {
dstSeconds *= -1;
}
long zoneDriftSeconds = ((ZoneOffset) lowerZone).getTotalSeconds() - ((ZoneOffset) upperZone).getTotalSeconds();
if (zoneDriftSeconds < 0) {
zoneDriftSeconds *= -1;
}
if (dstSeconds != zoneDriftSeconds) {
throw new HibernateException(
new IllegalArgumentException("The upper and lower bounds must be in same time zone!")
);
}
}
}
return range;
}
/**
* Creates the {@code OffsetDateTime} range from provided string:
* {@code
* Range closed = Range.offsetDateTimeRange("[2007-12-03T10:15:30+01:00\",\"2008-12-03T10:15:30+01:00]");
* Range quoted = Range.offsetDateTimeRange("[\"2007-12-03T10:15:30+01:00\",\"2008-12-03T10:15:30+01:00\"]");
* Range iso = Range.offsetDateTimeRange("[2011-12-03T10:15:30+01:00[Europe/Paris], 2012-12-03T10:15:30+01:00[Europe/Paris]]");
* }
*
* The valid formats for bounds are:
*
* - yyyy-MM-dd HH:mm:ss[.SSSSSS]X
* - yyyy-MM-dd'T'HH:mm:ss[.SSSSSS]X
*
*
* @param rangeStr The range string, for example {@literal "[2011-12-03T10:15:30+01:00,2012-12-03T10:15:30+01:00]"}.
*
* @return The range of {@code OffsetDateTime}s.
*
* @throws DateTimeParseException when one of the bounds are invalid.
* @throws IllegalArgumentException when bounds time zones are different.
*/
public static Range offsetDateTimeRange(String rangeStr) {
return ofString(rangeStr, parseOffsetDateTime().compose(unquote()), OffsetDateTime.class);
}
private static Function parseLocalDateTime() {
return str -> {
try {
return LocalDateTime.parse(str, LOCAL_DATE_TIME);
} catch (DateTimeParseException e) {
return LocalDateTime.parse(str);
}
};
}
private static Function parseZonedDateTime() {
return s -> {
try {
return ZonedDateTime.parse(s, OFFSET_DATE_TIME);
} catch (DateTimeParseException e) {
return ZonedDateTime.parse(s);
}
};
}
private static Function parseOffsetDateTime() {
return s -> {
try {
return OffsetDateTime.parse(s, OFFSET_DATE_TIME);
} catch (DateTimeParseException e) {
return OffsetDateTime.parse(s);
}
};
}
private static Function unquote() {
return s -> {
if (s.charAt(0) == '\"' && s.charAt(s.length() - 1) == '\"') {
return s.substring(1, s.length() - 1);
}
return s;
};
}
public String asString(Range range) {
if (range.isEmpty()) {
return "empty";
}
StringBuilder sb = new StringBuilder();
sb.append(range.hasLowerBound() && range.lowerBoundType() == BoundType.CLOSED ? '[' : '(')
.append(range.hasLowerBound() ? asString(range.lowerEndpoint()) : "")
.append(",")
.append(range.hasUpperBound() ? asString(range.upperEndpoint()) : "")
.append(range.hasUpperBound() && range.upperBoundType() == BoundType.CLOSED ? ']' : ')');
return sb.toString();
}
private String asString(Object value) {
if (value instanceof ZonedDateTime) {
return OFFSET_DATE_TIME.format((ZonedDateTime) value);
}
return value.toString();
}
@Override
public void setParameterValues(Properties parameters) {
final XProperty xProperty = (XProperty) parameters.get(DynamicParameterizedType.XPROPERTY);
if (xProperty instanceof JavaXMember) {
type = ReflectionUtils.invokeGetter(xProperty, "javaType");
} else {
type = ((ParameterType) parameters.get(PARAMETER_TYPE)).getReturnedClass();
}
if (type instanceof ParameterizedType) {
elementType = (Class) ((ParameterizedType) type).getActualTypeArguments()[0];
}
}
public Class getElementType() {
return elementType;
}
}