All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vladmihalcea.hibernate.type.range.guava.PostgreSQLGuavaRangeType Maven / Gradle / Ivy

There is a newer version: 2.21.1
Show newest version
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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy