001/* 002 * ============================================================================ 003 * Copyright © 2002-2026 by Thomas Thrien. 004 * All Rights Reserved. 005 * ============================================================================ 006 * Licensed to the public under the agreements of the GNU Lesser General Public 007 * License, version 3.0 (the "License"). You may obtain a copy of the License at 008 * 009 * http://www.gnu.org/licenses/lgpl.html 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 014 * License for the specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package org.tquadrat.foundation.sql.internal; 019 020import static java.lang.String.format; 021import static java.util.regex.Pattern.compile; 022import static org.apiguardian.api.API.Status.INTERNAL; 023import static org.apiguardian.api.API.Status.MAINTAINED; 024import static org.tquadrat.foundation.lang.CommonConstants.NULL_STRING; 025import static org.tquadrat.foundation.lang.Objects.isNull; 026import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument; 027import static org.tquadrat.foundation.lang.Objects.requireNotBlankArgument; 028 029import java.sql.Connection; 030import java.sql.PreparedStatement; 031import java.sql.SQLException; 032import java.util.ArrayList; 033import java.util.HashMap; 034import java.util.List; 035import java.util.Map; 036import java.util.function.BooleanSupplier; 037import java.util.regex.Pattern; 038import java.util.regex.PatternSyntaxException; 039 040import org.apiguardian.api.API; 041import org.tquadrat.foundation.annotation.ClassVersion; 042import org.tquadrat.foundation.exception.UnlikelyExceptionError; 043import org.tquadrat.foundation.sql.EnhancedPreparedStatement; 044import org.tquadrat.foundation.stream.MapStream; 045 046/** 047 * <p>{@summary The implementation for 048 * {@link EnhancedPreparedStatement}.}</p> 049 * 050 * @version $Id: EnhancedPreparedStatementImpl.java 1258 2026-06-04 18:33:06Z tquadrat $ 051 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 052 * @UMLGraph.link 053 * @since 0.1.0 054 */ 055@ClassVersion( sourceVersion = "$Id: EnhancedPreparedStatementImpl.java 1258 2026-06-04 18:33:06Z tquadrat $" ) 056@API( status = INTERNAL, since = "0.1.0" ) 057public final class EnhancedPreparedStatementImpl extends EnhancedPreparedStatementBase 058{ 059 /*------------*\ 060 ====** Attributes **======================================================= 061 \*------------*/ 062 /** 063 * Flag that controls whether a stack trace should be added to the log 064 * output. 065 */ 066 private static boolean m_AddStacktrace = false; 067 068 /** 069 * The log check method. 070 */ 071 private static BooleanSupplier m_LogCheck = () -> false; 072 073 /** 074 * The logger method. 075 */ 076 private static StatementLogger m_Logger = null; 077 078 /*------------------------*\ 079 ====** Static Initialisations **=========================================== 080 \*------------------------*/ 081 /** 082 * The pattern that is used to identify a variable in a SQL statement 083 * text. 084 */ 085 private static final Pattern m_VariablePattern; 086 087 static 088 { 089 try 090 { 091 m_VariablePattern = compile( VARIABLE_PATTERN ); 092 } 093 catch( final PatternSyntaxException e ) 094 { 095 throw new UnlikelyExceptionError( """ 096 The provided pattern "%s" is a constant and should be properly tested. 097 Therefore, a PatternSyntaxException is unlikely.\ 098 """.formatted( VARIABLE_PATTERN ), e ); 099 } 100 } 101 102 /*--------------*\ 103 ====** Constructors **===================================================== 104 \*--------------*/ 105 /** 106 * <p>{@summary {@constructor}}</p> 107 * 108 * @param sourceStatement The original SQL statement with the 109 * placeholders; mainly used for logging purposes. 110 * @param preparedStatement The wrapped instance of 111 * {@link PreparedStatement}. 112 * @param parameterIndex The mapping for the named placeholders to the 113 * position based placeholders. 114 */ 115 public EnhancedPreparedStatementImpl( final String sourceStatement, final PreparedStatement preparedStatement, final Map<String,int[]> parameterIndex ) 116 { 117 super( sourceStatement, preparedStatement, parameterIndex ); 118 } // EnhancedPreparedStatementImpl() 119 120 /*---------*\ 121 ====** Methods **========================================================== 122 \*---------*/ 123 /** 124 * {@inheritDoc} 125 */ 126 @Override 127 protected final boolean addStacktrace() { return m_AddStacktrace; } 128 129 /** 130 * Converts the index buffer to a parameter index. 131 * 132 * @param indexBuffer The index buffer. 133 * @return The parameter index. 134 * 135 * @note The method is public to allow simpler Unit tests. 136 */ 137 @SuppressWarnings( "NewMethodNamingConvention" ) 138 @API( status = INTERNAL, since = "0.1.0" ) 139 public static final Map<String,int []> convertIndexBufferToParameterIndex( final Map<String, ? extends List<Integer>> indexBuffer ) 140 { 141 final var retValue = MapStream.of( requireNonNullArgument( indexBuffer, "indexBuffer" ) ) 142 .mapValues( v -> v.stream().mapToInt( Integer::intValue ).toArray() ) 143 .collect(); 144 145 //---* Done *---------------------------------------------------------- 146 return retValue; 147 } // convertIndexBufferToParameterIndex() 148 149 /** 150 * Creates a new instance of {@code EnhancedPreparedStatementImpl}. 151 * 152 * @param connection The connection to the database. 153 * @param sql The text of the SQL statement with the placeholders. 154 * @return The new statement. 155 * @throws SQLException Unable to create an instance of an 156 * {@link org.tquadrat.foundation.sql.EnhancedPreparedStatement}. 157 */ 158 @API( status = MAINTAINED, since = "0.1.0" ) 159 public static final EnhancedPreparedStatementImpl create( final Connection connection, final String sql ) throws SQLException 160 { 161 final Map<String, List<Integer>> indexBuffer = new HashMap<>(); 162 @SuppressWarnings( "JDBCPrepareStatementWithNonConstantString" ) 163 final var preparedStatement = requireNonNullArgument( connection, "connection" ).prepareStatement( parseSQL( sql, indexBuffer ) ); 164 final var retValue = new EnhancedPreparedStatementImpl( sql, preparedStatement, convertIndexBufferToParameterIndex( indexBuffer ) ); 165 166 //---* Done *---------------------------------------------------------- 167 return retValue; 168 } // create() 169 170 /** 171 * {@inheritDoc} 172 */ 173 @Override 174 protected final void doLogging( final String operation, final StackTraceElement [] stackTrace ) 175 { 176 final var values = getCurrentValues() 177 .stream() 178 .map( v -> format( "%1$s [%2$s]: %3$s", v.parameterName(), (isNull( v.type() ) ? NULL_STRING : v.type().getName()), v.value() ) ) 179 .sorted() 180 .toList(); 181 m_Logger.log( operation, getSourceStatement(), values, stackTrace ); 182 } // doLogging() 183 184 /** 185 * <p>{@summary Enables the logging output for the 186 * {@code EnhancedPreparedStatement} instances.}</p> 187 * <p>The {@code logger} method takes three arguments:</p> 188 * <ol> 189 * <li>{@code operation} – The name of the operation that logs.</li> 190 * <li>{@code statement} – The source of the prepared statement.</li> 191 * <li>{@code values} – A list of the values in the format 192 * <pre><code><<i>name</i>><b> [</b><<i>type</i>><b>]:<<i>value</i>></b></code></pre> 193 * A type of {@code NULL} indicates an unknown type; for large values (like 194 * {@link java.sql.Blob} 195 * or 196 * {@link java.io.Reader}) 197 * only the class is given instead of the real value.</li> 198 * <li>{@code stacktrace} – The stacktrace; will be {@null} if 199 * {@code addStacktrace} is {@false}.</li> 200 * </ol> 201 * <p>The {@code logCheck} method returns {@true} only when logging 202 * should be done. No information is collected while it returns 203 * {@false}. As the method is called for nearly any operation, its 204 * implementation should be as efficient as possible.</p> 205 * 206 * @param logger The method that takes the logging information. 207 * @param logCheck The method that returns a flag whether log output 208 * is desired. 209 * @param addStacktrace {@true} if the stacktrace should be added 210 * to the log output. 211 */ 212 @SuppressWarnings( "MethodOverridesStaticMethodOfSuperclass" ) 213 public static final void enableLogging( final StatementLogger logger, final BooleanSupplier logCheck, final boolean addStacktrace ) 214 { 215 m_Logger = requireNonNullArgument( logger, "logger" ); 216 m_LogCheck = requireNonNullArgument( logCheck, "logCheck" ); 217 m_AddStacktrace = addStacktrace; 218 } // enableLogging() 219 220 /** 221 * {@inheritDoc} 222 */ 223 @Override 224 public final boolean isLoggingEnabled() { return m_LogCheck.getAsBoolean(); } 225 226 /** 227 * Parses the given SQL statement with the named placeholders and returns 228 * the text for a call to 229 * {@link java.sql.Connection#prepareStatement(String)}. 230 * 231 * @param sql The source text for the SQL statement. 232 * @param indexBuffer The mapping from the names to the indexes. 233 * @return The target SQL text. 234 * 235 * @note The method is public to allow simpler Unit tests. 236 */ 237 @API( status = INTERNAL, since = "0.1.0" ) 238 public static final String parseSQL( final String sql, final Map<? super String, List<Integer>> indexBuffer ) 239 { 240 requireNonNullArgument( indexBuffer, "indexBuffer" ); 241 242 //---* Parse the statement text *-------------------------------------- 243 var index = 0; 244 final var buffer = new StringBuilder(); 245 final var matcher = m_VariablePattern.matcher( requireNotBlankArgument( sql, "sql" ) ); 246 while( matcher.find() ) 247 { 248 final var variableName = matcher.group( 1 ); 249 indexBuffer.computeIfAbsent( variableName, _ -> new ArrayList<>() ).add( Integer.valueOf( ++index ) ); 250 matcher.appendReplacement( buffer, " ?" ); 251 } 252 matcher.appendTail( buffer ); 253 254 //---* Save the text of the prepared statement *----------------------- 255 final var retValue = buffer.toString(); 256 257 //---* Done *---------------------------------------------------------- 258 return retValue; 259 } // parseSQL() 260} 261// class EnhancedPreparedStatementImpl 262 263/* 264 * End of File 265 */