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}