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>&lt;<i>name</i>&gt;<b> [</b>&lt;<i>type</i>&gt;<b>]:&lt;<i>value</i>&gt;</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 */