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}