001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2023 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.pyranid; 018 019import javax.annotation.Nonnull; 020import javax.annotation.Nullable; 021import javax.annotation.concurrent.ThreadSafe; 022import java.nio.ByteBuffer; 023import java.sql.PreparedStatement; 024import java.sql.Timestamp; 025import java.time.Instant; 026import java.time.ZoneId; 027import java.util.Calendar; 028import java.util.Date; 029import java.util.List; 030import java.util.Locale; 031import java.util.Optional; 032import java.util.TimeZone; 033import java.util.UUID; 034 035import static java.util.Objects.requireNonNull; 036 037/** 038 * Basic implementation of {@link PreparedStatementBinder}. 039 * 040 * @author <a href="https://www.revetkn.com">Mark Allen</a> 041 * @since 1.0.0 042 */ 043@ThreadSafe 044public class DefaultPreparedStatementBinder implements PreparedStatementBinder { 045 @Nonnull 046 private final DatabaseType databaseType; 047 @Nonnull 048 private final ZoneId timeZone; 049 @Nonnull 050 private final Calendar timeZoneCalendar; 051 052 /** 053 * Creates a {@code PreparedStatementBinder}. 054 */ 055 public DefaultPreparedStatementBinder() { 056 this(null, null); 057 } 058 059 /** 060 * Creates a {@code PreparedStatementBinder} for the given {@code databaseType}. 061 * 062 * @param databaseType the type of database we're working with 063 */ 064 public DefaultPreparedStatementBinder(@Nullable DatabaseType databaseType) { 065 this(null, null); 066 } 067 068 /** 069 * Creates a {@code PreparedStatementBinder} for the given {@code timeZone}. 070 * 071 * @param timeZone the timezone to use when working with {@link java.sql.Timestamp} and similar values 072 */ 073 public DefaultPreparedStatementBinder(@Nullable ZoneId timeZone) { 074 this(null, timeZone); 075 } 076 077 /** 078 * Creates a {@code PreparedStatementBinder} for the given {@code databaseType}. 079 * 080 * @param databaseType the type of database we're working with 081 * @param timeZone the timezone to use when working with {@link java.sql.Timestamp} and similar values 082 * @since 1.0.15 083 */ 084 public DefaultPreparedStatementBinder(@Nullable DatabaseType databaseType, 085 @Nullable ZoneId timeZone) { 086 this.databaseType = databaseType == null ? DatabaseType.GENERIC : databaseType; 087 this.timeZone = timeZone == null ? ZoneId.systemDefault() : timeZone; 088 this.timeZoneCalendar = Calendar.getInstance(TimeZone.getTimeZone(this.timeZone)); 089 } 090 091 @Override 092 public <T> void bind(@Nonnull StatementContext<T> statementContext, 093 @Nonnull PreparedStatement preparedStatement, 094 @Nonnull List<Object> parameters) { 095 requireNonNull(statementContext); 096 requireNonNull(preparedStatement); 097 requireNonNull(parameters); 098 099 try { 100 for (int i = 0; i < parameters.size(); ++i) { 101 Object parameter = parameters.get(i); 102 103 if (parameter != null) { 104 Object normalizedParameter = normalizeParameter(parameter).orElse(null); 105 106 if (normalizedParameter instanceof java.sql.Timestamp) { 107 java.sql.Timestamp timestamp = (java.sql.Timestamp) normalizedParameter; 108 preparedStatement.setTimestamp(i + 1, timestamp, getTimeZoneCalendar()); 109 } else if (normalizedParameter instanceof java.sql.Date) { 110 java.sql.Date date = (java.sql.Date) normalizedParameter; 111 preparedStatement.setDate(i + 1, date, getTimeZoneCalendar()); 112 } else if (normalizedParameter instanceof java.sql.Time) { 113 java.sql.Time time = (java.sql.Time) normalizedParameter; 114 preparedStatement.setTime(i + 1, time, getTimeZoneCalendar()); 115 } else { 116 preparedStatement.setObject(i + 1, normalizedParameter); 117 } 118 } else { 119 preparedStatement.setObject(i + 1, parameter); 120 } 121 } 122 } catch (Exception e) { 123 throw new DatabaseException(e); 124 } 125 } 126 127 /** 128 * Massages a parameter into a JDBC-friendly format if needed. 129 * <p> 130 * For example, we need to do special work to prepare a {@link UUID} for Oracle. 131 * 132 * @param parameter the parameter to (possibly) massage 133 * @return the result of the massaging process 134 */ 135 @Nonnull 136 protected Optional<Object> normalizeParameter(@Nullable Object parameter) { 137 if (parameter == null) 138 return Optional.empty(); 139 140 if (parameter instanceof Date) 141 return Optional.of(new Timestamp(((Date) parameter).getTime())); 142 if (parameter instanceof Instant) 143 return Optional.of(new Timestamp(((Instant) parameter).toEpochMilli())); 144 if (parameter instanceof Locale) 145 return Optional.of(((Locale) parameter).toLanguageTag()); 146 if (parameter instanceof Enum) 147 return Optional.of(((Enum<?>) parameter).name()); 148 // Java 11 uses internal implementation java.time.ZoneRegion, which Postgres JDBC driver does not support. 149 // Force ZoneId to use its ID here 150 if (parameter instanceof ZoneId) 151 return Optional.of(((ZoneId) parameter).getId()); 152 153 // Special handling for Oracle 154 if (databaseType() == DatabaseType.ORACLE) { 155 if (parameter instanceof java.util.UUID) { 156 ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); 157 byteBuffer.putLong(((UUID) parameter).getMostSignificantBits()); 158 byteBuffer.putLong(((UUID) parameter).getLeastSignificantBits()); 159 return Optional.of(byteBuffer.array()); 160 } 161 162 // Other massaging here if needed... 163 } 164 165 return Optional.ofNullable(parameter); 166 } 167 168 @Nonnull 169 protected DatabaseType databaseType() { 170 return this.databaseType; 171 } 172 173 @Nonnull 174 protected ZoneId getTimeZone() { 175 return timeZone; 176 } 177 178 @Nonnull 179 protected Calendar getTimeZoneCalendar() { 180 return timeZoneCalendar; 181 } 182}