001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2024 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.Currency; 029import java.util.Date; 030import java.util.List; 031import java.util.Locale; 032import java.util.Optional; 033import java.util.TimeZone; 034import java.util.UUID; 035 036import static java.util.Objects.requireNonNull; 037 038/** 039 * Basic implementation of {@link PreparedStatementBinder}. 040 * 041 * @author <a href="https://www.revetkn.com">Mark Allen</a> 042 * @since 1.0.0 043 */ 044@ThreadSafe 045public class DefaultPreparedStatementBinder implements PreparedStatementBinder { 046 @Nonnull 047 private final DatabaseType databaseType; 048 @Nonnull 049 private final ZoneId timeZone; 050 @Nonnull 051 private final Calendar timeZoneCalendar; 052 053 /** 054 * Creates a {@code PreparedStatementBinder}. 055 */ 056 public DefaultPreparedStatementBinder() { 057 this(null, null); 058 } 059 060 /** 061 * Creates a {@code PreparedStatementBinder} for the given {@code databaseType}. 062 * 063 * @param databaseType the type of database we're working with 064 */ 065 public DefaultPreparedStatementBinder(@Nullable DatabaseType databaseType) { 066 this(null, null); 067 } 068 069 /** 070 * Creates a {@code PreparedStatementBinder} for the given {@code timeZone}. 071 * 072 * @param timeZone the timezone to use when working with {@link java.sql.Timestamp} and similar values 073 */ 074 public DefaultPreparedStatementBinder(@Nullable ZoneId timeZone) { 075 this(null, timeZone); 076 } 077 078 /** 079 * Creates a {@code PreparedStatementBinder} for the given {@code databaseType}. 080 * 081 * @param databaseType the type of database we're working with 082 * @param timeZone the timezone to use when working with {@link java.sql.Timestamp} and similar values 083 * @since 1.0.15 084 */ 085 public DefaultPreparedStatementBinder(@Nullable DatabaseType databaseType, 086 @Nullable ZoneId timeZone) { 087 this.databaseType = databaseType == null ? DatabaseType.GENERIC : databaseType; 088 this.timeZone = timeZone == null ? ZoneId.systemDefault() : timeZone; 089 this.timeZoneCalendar = Calendar.getInstance(TimeZone.getTimeZone(this.timeZone)); 090 } 091 092 @Override 093 public <T> void bind(@Nonnull StatementContext<T> statementContext, 094 @Nonnull PreparedStatement preparedStatement, 095 @Nonnull List<Object> parameters) { 096 requireNonNull(statementContext); 097 requireNonNull(preparedStatement); 098 requireNonNull(parameters); 099 100 try { 101 for (int i = 0; i < parameters.size(); ++i) { 102 Object parameter = parameters.get(i); 103 104 if (parameter != null) { 105 Object normalizedParameter = normalizeParameter(parameter).orElse(null); 106 107 if (normalizedParameter instanceof java.sql.Timestamp) { 108 java.sql.Timestamp timestamp = (java.sql.Timestamp) normalizedParameter; 109 preparedStatement.setTimestamp(i + 1, timestamp, getTimeZoneCalendar()); 110 } else if (normalizedParameter instanceof java.sql.Date) { 111 java.sql.Date date = (java.sql.Date) normalizedParameter; 112 preparedStatement.setDate(i + 1, date, getTimeZoneCalendar()); 113 } else if (normalizedParameter instanceof java.sql.Time) { 114 java.sql.Time time = (java.sql.Time) normalizedParameter; 115 preparedStatement.setTime(i + 1, time, getTimeZoneCalendar()); 116 } else { 117 preparedStatement.setObject(i + 1, normalizedParameter); 118 } 119 } else { 120 preparedStatement.setObject(i + 1, parameter); 121 } 122 } 123 } catch (Exception e) { 124 throw new DatabaseException(e); 125 } 126 } 127 128 /** 129 * Massages a parameter into a JDBC-friendly format if needed. 130 * <p> 131 * For example, we need to do special work to prepare a {@link UUID} for Oracle. 132 * 133 * @param parameter the parameter to (possibly) massage 134 * @return the result of the massaging process 135 */ 136 @Nonnull 137 protected Optional<Object> normalizeParameter(@Nullable Object parameter) { 138 if (parameter == null) 139 return Optional.empty(); 140 141 if (parameter instanceof Date) 142 return Optional.of(new Timestamp(((Date) parameter).getTime())); 143 if (parameter instanceof Instant) 144 return Optional.of(new Timestamp(((Instant) parameter).toEpochMilli())); 145 if (parameter instanceof Locale) 146 return Optional.of(((Locale) parameter).toLanguageTag()); 147 if (parameter instanceof Currency) 148 return Optional.of(((Currency) parameter).getCurrencyCode()); 149 if (parameter instanceof Enum) 150 return Optional.of(((Enum<?>) parameter).name()); 151 // Java 11 uses internal implementation java.time.ZoneRegion, which Postgres JDBC driver does not support. 152 // Force ZoneId to use its ID here 153 if (parameter instanceof ZoneId) 154 return Optional.of(((ZoneId) parameter).getId()); 155 156 // Special handling for Oracle 157 if (databaseType() == DatabaseType.ORACLE) { 158 if (parameter instanceof java.util.UUID) { 159 ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); 160 byteBuffer.putLong(((UUID) parameter).getMostSignificantBits()); 161 byteBuffer.putLong(((UUID) parameter).getLeastSignificantBits()); 162 return Optional.of(byteBuffer.array()); 163 } 164 165 // Other massaging here if needed... 166 } 167 168 return Optional.ofNullable(parameter); 169 } 170 171 @Nonnull 172 protected DatabaseType databaseType() { 173 return this.databaseType; 174 } 175 176 @Nonnull 177 protected ZoneId getTimeZone() { 178 return timeZone; 179 } 180 181 @Nonnull 182 protected Calendar getTimeZoneCalendar() { 183 return timeZoneCalendar; 184 } 185}