001/*
002 * Copyright 2015-2022 Transmogrify LLC, 2022-2026 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 org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.NotThreadSafe;
023import javax.annotation.concurrent.ThreadSafe;
024import java.time.ZoneId;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.List;
029import java.util.Objects;
030import java.util.Optional;
031import java.util.Queue;
032import java.util.concurrent.ConcurrentLinkedQueue;
033import java.util.function.Supplier;
034import java.util.stream.Collectors;
035
036import static java.lang.String.format;
037import static java.util.Objects.requireNonNull;
038
039/**
040 * Data that represents a SQL statement.
041 *
042 * @author <a href="https://www.revetkn.com">Mark Allen</a>
043 * @since 2.0.0
044 */
045@ThreadSafe
046public final class StatementContext<T> {
047        @NonNull
048        private final Statement statement;
049        @NonNull
050        private final List<@Nullable Object> parameters;
051        @Nullable
052        private final Class<T> resultSetRowType;
053        @NonNull
054        private final Supplier<@NonNull DatabaseType> databaseTypeSupplier;
055        @NonNull
056        private final Supplier<@NonNull DatabaseDialect> databaseDialectSupplier;
057        @Nullable
058        private volatile DatabaseDialect databaseDialect;
059        @NonNull
060        private final Supplier<@NonNull DatabaseType> diagnosticDatabaseTypeSupplier;
061        @NonNull
062        private final ZoneId timeZone;
063        @NonNull
064        private final AmbiguousTimestampBindingStrategy ambiguousTimestampBindingStrategy;
065        @NonNull
066        private final ParameterRedactor parameterRedactor;
067        private final boolean batchParameterGroups;
068        @NonNull
069        private final Queue<@NonNull AutoCloseable> cleanupOperations;
070
071        protected StatementContext(@NonNull Builder builder) {
072                requireNonNull(builder);
073
074                this.statement = builder.statement;
075                this.parameters = builder.parameters == null
076                                ? List.of()
077                                : Collections.unmodifiableList(new ArrayList<>(builder.parameters));
078                this.resultSetRowType = builder.resultSetRowType;
079                this.databaseTypeSupplier = builder.databaseTypeSupplier;
080                this.databaseDialectSupplier = builder.databaseDialectSupplier;
081                this.diagnosticDatabaseTypeSupplier = builder.diagnosticDatabaseTypeSupplier;
082                this.timeZone = builder.timeZone;
083                this.ambiguousTimestampBindingStrategy = builder.ambiguousTimestampBindingStrategy;
084                this.parameterRedactor = builder.parameterRedactor;
085                this.batchParameterGroups = builder.batchParameterGroups;
086                this.cleanupOperations = new ConcurrentLinkedQueue<>();
087        }
088
089        @Override
090        public int hashCode() {
091                return Objects.hash(getStatement(), getParameters(), getResultSetRowType(), getDiagnosticDatabaseType(), getTimeZone(), getAmbiguousTimestampBindingStrategy());
092        }
093
094        @Override
095        public boolean equals(Object object) {
096                if (this == object)
097                        return true;
098
099                if (!(object instanceof StatementContext))
100                        return false;
101
102                StatementContext statementContext = (StatementContext) object;
103
104                return Objects.equals(statementContext.getStatement(), getStatement())
105                                && Objects.equals(statementContext.getParameters(), getParameters())
106                                && Objects.equals(statementContext.getResultSetRowType(), getResultSetRowType())
107                                && Objects.equals(statementContext.getDiagnosticDatabaseType(), getDiagnosticDatabaseType())
108                                && Objects.equals(statementContext.getTimeZone(), getTimeZone())
109                                && Objects.equals(statementContext.getAmbiguousTimestampBindingStrategy(), getAmbiguousTimestampBindingStrategy());
110        }
111
112        @Override
113        public String toString() {
114                List<String> components = new ArrayList<>(3);
115
116                components.add(format("statement=%s", getStatement()));
117
118                if (getParameters().size() > 0)
119                        components.add(format("parameters=%s", getRedactedParameters()));
120
121                Class<T> resultSetRowType = getResultSetRowType().orElse(null);
122
123                if (resultSetRowType != null)
124                        components.add(format("resultSetRowType=%s", resultSetRowType));
125
126                components.add(format("databaseType=%s", getDiagnosticDatabaseType().name()));
127                components.add(format("timeZone=%s", getTimeZone().getId()));
128                components.add(format("ambiguousTimestampBindingStrategy=%s", getAmbiguousTimestampBindingStrategy().name()));
129
130                return format("%s{%s}", getClass().getSimpleName(), components.stream().collect(Collectors.joining(", ")));
131        }
132
133        @NonNull
134        public Statement getStatement() {
135                return this.statement;
136        }
137
138        @NonNull
139        public List<@Nullable Object> getParameters() {
140                return this.parameters;
141        }
142
143        /**
144         * Gets this statement's parameters rendered for diagnostics.
145         * <p>
146         * {@link SecureParameter} values render as their masks. Other non-batch values are rendered through the configured
147         * {@link ParameterRedactor}. Batch executions render a bounded summary instead of individual group values.
148         *
149         * @return parameters rendered for diagnostics
150         * @since 4.4.0
151         */
152        @NonNull
153        public List<@Nullable Object> getRedactedParameters() {
154                if (this.batchParameterGroups)
155                        return List.of(batchParameterSummary());
156
157                List<@Nullable Object> redactedParameters = new ArrayList<>(getParameters().size());
158
159                for (int i = 0; i < getParameters().size(); ++i) {
160                        Object parameter = getParameters().get(i);
161                        SecureParameter secureParameter = SecureParameterSupport.displaySecureParameter(parameter);
162
163                        if (secureParameter != null)
164                                redactedParameters.add(SecureParameterSupport.maskOf(secureParameter));
165                        else
166                                redactedParameters.add(this.parameterRedactor.redactParameter(this, i, parameter));
167                }
168
169                return Collections.unmodifiableList(redactedParameters);
170        }
171
172        @NonNull
173        private String batchParameterSummary() {
174                List<@Nullable Object> parameters = getParameters();
175                int groupCount = parameters.size();
176
177                if (groupCount == 0)
178                        return "<batch: 0 groups x 0 parameters>";
179
180                Integer expectedParameterCount = null;
181                boolean mixedParameterCounts = false;
182
183                for (Object parameterGroup : parameters) {
184                        int parameterCount = parameterGroup instanceof List<?> list ? list.size() : 0;
185
186                        if (expectedParameterCount == null) {
187                                expectedParameterCount = parameterCount;
188                        } else if (expectedParameterCount != parameterCount) {
189                                mixedParameterCounts = true;
190                                break;
191                        }
192                }
193
194                if (mixedParameterCounts)
195                        return format("<batch: %s groups x mixed parameter counts>", groupCount);
196
197                return format("<batch: %s groups x %s parameters>", groupCount, expectedParameterCount);
198        }
199
200        @NonNull
201        public Optional<Class<T>> getResultSetRowType() {
202                return Optional.ofNullable(this.resultSetRowType);
203        }
204
205        /**
206         * Gets the database type for this statement.
207         * <p>
208         * If automatic database type detection is enabled and the type has not already been detected, this method may acquire a
209         * connection and inspect {@link java.sql.DatabaseMetaData}. Diagnostic methods such as {@link #toString()},
210         * {@link #equals(Object)}, and {@link #hashCode()} use a non-detecting database-type value instead.
211         *
212         * @return the database type
213         * @throws DatabaseException if automatic database type detection fails
214         * @since 3.0.0
215         */
216        @NonNull
217        public DatabaseType getDatabaseType() {
218                return this.databaseTypeSupplier.get();
219        }
220
221        @NonNull
222        DatabaseDialect getDatabaseDialect() {
223                DatabaseDialect cachedDatabaseDialect = this.databaseDialect;
224
225                if (cachedDatabaseDialect != null)
226                        return cachedDatabaseDialect;
227
228                DatabaseDialect databaseDialect = this.databaseDialectSupplier.get();
229                this.databaseDialect = databaseDialect;
230                return databaseDialect;
231        }
232
233        @NonNull
234        private DatabaseType getDiagnosticDatabaseType() {
235                return this.diagnosticDatabaseTypeSupplier.get();
236        }
237
238        @NonNull
239        public ZoneId getTimeZone() {
240                return this.timeZone;
241        }
242
243        /**
244         * How should Pyranid bind {@link java.time.Instant} and {@link java.time.OffsetDateTime} parameters when JDBC
245         * parameter metadata cannot identify whether the target is {@code TIMESTAMP} or {@code TIMESTAMP WITH TIME ZONE}?
246         *
247         * @return behavior to use when timestamp target metadata is unavailable or non-identifying
248         * @since 4.2.0
249         */
250        @NonNull
251        public AmbiguousTimestampBindingStrategy getAmbiguousTimestampBindingStrategy() {
252                return this.ambiguousTimestampBindingStrategy;
253        }
254
255        void addCleanupOperation(@NonNull AutoCloseable cleanupOperation) {
256                requireNonNull(cleanupOperation);
257                this.cleanupOperations.add(cleanupOperation);
258        }
259
260        @NonNull
261        Queue<@NonNull AutoCloseable> getCleanupOperations() {
262                return this.cleanupOperations;
263        }
264
265        @NonNull
266        public static <T> Builder<T> with(@NonNull Statement statement,
267                                                                                                                                                @NonNull Database database) {
268                requireNonNull(statement);
269                requireNonNull(database);
270
271                return new Builder<>(statement, database);
272        }
273
274        /**
275         * Builder used to construct instances of {@link StatementContext}.
276         * <p>
277         * This class is intended for use by a single thread.
278         *
279         * @author <a href="https://www.revetkn.com">Mark Allen</a>
280         * @since 2.0.0
281         */
282        @NotThreadSafe
283        public static class Builder<T> {
284                @NonNull
285                private final Statement statement;
286                @NonNull
287                private final Supplier<@NonNull DatabaseType> databaseTypeSupplier;
288                @NonNull
289                private final Supplier<@NonNull DatabaseDialect> databaseDialectSupplier;
290                @NonNull
291                private final Supplier<@NonNull DatabaseType> diagnosticDatabaseTypeSupplier;
292                @NonNull
293                private final ZoneId timeZone;
294                @NonNull
295                private final AmbiguousTimestampBindingStrategy ambiguousTimestampBindingStrategy;
296                @NonNull
297                private final ParameterRedactor parameterRedactor;
298                @Nullable
299                private List<@Nullable Object> parameters;
300                @Nullable
301                private Class<T> resultSetRowType;
302                private boolean batchParameterGroups;
303
304                private Builder(@NonNull Statement statement,
305                                                                                @NonNull Database database) {
306                        requireNonNull(statement);
307                        requireNonNull(database);
308
309                        this.statement = statement;
310                        this.databaseTypeSupplier = database::getDatabaseType;
311                        this.databaseDialectSupplier = database::getDatabaseDialect;
312                        this.diagnosticDatabaseTypeSupplier = database::peekDatabaseType;
313                        this.timeZone = database.getTimeZone();
314                        this.ambiguousTimestampBindingStrategy = database.getAmbiguousTimestampBindingStrategy();
315                        this.parameterRedactor = database.getParameterRedactor();
316                }
317
318                @NonNull
319                public Builder parameters(@Nullable List<@Nullable Object> parameters) {
320                        this.parameters = parameters;
321                        return this;
322                }
323
324                @NonNull
325                public Builder parameters(Object @Nullable ... parameters) {
326                        this.parameters = parameters == null ? null : Arrays.asList(parameters);
327                        return this;
328                }
329
330                @NonNull
331                public Builder resultSetRowType(Class<T> resultSetRowType) {
332                        this.resultSetRowType = resultSetRowType;
333                        return this;
334                }
335
336                @NonNull
337                Builder batchParameterGroups(boolean batchParameterGroups) {
338                        this.batchParameterGroups = batchParameterGroups;
339                        return this;
340                }
341
342                @NonNull
343                public StatementContext build() {
344                        return new StatementContext<>(this);
345                }
346        }
347}