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}