Skip to content

Instantly share code, notes, and snippets.

@gregopet
Last active September 5, 2017 09:24
Show Gist options
  • Save gregopet/8d0feb4fe4075a8525c1175243ee38b0 to your computer and use it in GitHub Desktop.
Save gregopet/8d0feb4fe4075a8525c1175243ee38b0 to your computer and use it in GitHub Desktop.
timestamptz mapping to custom Java type in jOOQ
import org.jooq.*;
import org.jooq.impl.DSL;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
/**
* Maps Postgres' TimestampTZ ranges as Java objects.
*/
public class InstantRangeConverter implements Binding<Object, Interval> {
private static final String DATE_OR_EMPTY = "(?:(?:"([^"]+)")?)";
private static final Pattern PATTERN = Pattern.compile("[(\\[(]" + DATE_OR_EMPTY + "," + DATE_OR_EMPTY + "[\\])]");
private static final DateTimeFormatter PG_TIME_FORMATTER = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.append(ISO_LOCAL_DATE)
.appendLiteral(' ')
.append(ISO_LOCAL_TIME)
.appendOffset("+HHmm", "+00")
.toFormatter();
@Override
public Converter<Object, Interval> converter() {
return new Converter<Object, Interval>() {
@Override
public Interval from(Object t) {
if (t == null) return null;
if ("empty".equals(t)) return Interval.EMPTY;
Matcher m = PATTERN.matcher("" + t);
if (m.find()) {
String fromStr = m.group(1);
String toStr = m.group(2);
Instant from = (fromStr == null || "".equals(fromStr)) ? null : PG_TIME_FORMATTER.parse(fromStr, OffsetDateTime::from).toInstant();
Instant to = (toStr == null || "".equals(toStr)) ? null : PG_TIME_FORMATTER.parse(toStr, OffsetDateTime::from).toInstant();
return new Interval(from, to);
} else {
throw new IllegalArgumentException("Unsupported range : " + t);
}
}
@Override
public Object to(Interval u) {
if (u == null) return null;
if (u == EMPTY) return "empty";
if (u.from == null && u.to == null) return "(,)";
String fromStr = u.from != null ? Timestamp.from(u.from).toString() : "";
String toStr = u.to != null ? Timestamp.from(u.to).toString() : "";
return "[" + fromStr + "," + toStr + "]";
}
@Override
public Class<Object> fromType() {
return Object.class;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Class<Interval> toType() {
return Interval.class;
}
};
}
@Override
public void register(BindingRegisterContext<Interval> ctx) throws SQLException {
ctx.statement().registerOutParameter(ctx.index(), Types.VARCHAR);
}
@Override
public void sql(BindingSQLContext<Interval> ctx) throws SQLException {
ctx.render()
.visit(DSL.val(ctx.convert(converter()).value()))
.sql("::tstzrange");
}
@Override
public void get(BindingGetResultSetContext<Interval> ctx) throws SQLException {
ctx.convert(converter()).value(ctx.resultSet().getString(ctx.index()));
}
// Getting a String value from a JDBC CallableStatement and converting that to a String
@Override
public void get(BindingGetStatementContext<Interval> ctx) throws SQLException {
ctx.convert(converter()).value(ctx.statement().getString(ctx.index()));
}
@Override
public void set(BindingSetStatementContext<Interval> ctx) throws SQLException {
ctx.statement().setString(ctx.index(), Objects.toString(ctx.convert(converter()).value(), null));
}
// Setting a value on a JDBC SQLOutput (useful for Oracle OBJECT types)
@Override
public void set(BindingSetSQLOutputContext<Interval> ctx) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
// Getting a value from a JDBC SQLInput (useful for Oracle OBJECT types)
@Override
public void get(BindingGetSQLInputContext<Interval> ctx) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
}
/**
* Represents an interval between two Instants. From/to can be null meaning an unbounded side; if both are null the
* interval is completely unbounded and every time falls within it (unless it is the EMPTY singleton!).
*/
public class Interval {
/** This instance is the only instance of an empty interval */
public static final Interval EMPTY = new Interval(null, null);
public final Instant from;
public final Instant to;
public Interval(Instant from, Instant to) {
this.from = from;
this.to = to;
}
/** Is this interval empty? */
public boolean isEmpty() {
return this == EMPTY;
}
@Override
public String toString() {
if (isEmpty()) return "(empty)";
if (from == null && to == null) return "(unbounded)";
return "[" + (from != null ? from : "") + "," + (to != null ? to : "") + "]";
}
}
@gregopet
Copy link
Author

gregopet commented Sep 5, 2017

...initial tests pass, didn't need to differentiate between open/closed intervals

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment