001/*
002 * ============================================================================
003 *  Copyright © 2002-2023 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.UnexpectedExceptionError;
043import org.tquadrat.foundation.stream.MapStream;
044
045/**
046 *  The implementation for
047 *  {@link org.tquadrat.foundation.sql.EnhancedPreparedStatement}.
048 *
049 *  @version $Id: EnhancedPreparedStatementImpl.java 1100 2024-02-16 23:33:45Z tquadrat $
050 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
051 *  @UMLGraph.link
052 *  @since 0.1.0
053 */
054@ClassVersion( sourceVersion = "$Id: EnhancedPreparedStatementImpl.java 1100 2024-02-16 23:33:45Z tquadrat $" )
055@API( status = INTERNAL, since = "0.1.0" )
056public final class EnhancedPreparedStatementImpl extends EnhancedPreparedStatementBase
057{
058        /*------------*\
059    ====** Attributes **=======================================================
060        \*------------*/
061    /**
062     *  Flag that controls whether a stack trace should be added to the log
063     *  output.
064     */
065    private static boolean m_AddStacktrace = false;
066
067    /**
068     *  The log check method.
069     */
070    private static BooleanSupplier m_LogCheck = () -> false;
071
072    /**
073     *  The logger method.
074     */
075    private static StatementLogger m_Logger = null;
076
077        /*------------------------*\
078    ====** Static Initialisations **===========================================
079        \*------------------------*/
080    /**
081     *  The pattern that is used to identify a variable in a SQL statement
082     *  text.
083     */
084    private static final Pattern m_VariablePattern;
085
086    static
087    {
088        try
089        {
090            m_VariablePattern = compile( VARIABLE_PATTERN );
091        }
092        catch( final PatternSyntaxException e )
093        {
094            /*
095             * The provided pattern is a constant and should be properly
096             * tested. Therefore, a PatternSyntaxException is unlikely.
097             */
098            throw new UnexpectedExceptionError( e );
099        }
100    }
101
102        /*--------------*\
103    ====** Constructors **=====================================================
104        \*--------------*/
105    /**
106     *  Creates a new instance of {@code EnhancedPreparedStatementImpl}.
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    @API( status = INTERNAL, since = "0.1.0" )
138    public static final Map<String,int []> convertIndexBufferToParameterIndex( final Map<String, ? extends List<Integer>> indexBuffer )
139    {
140        final var retValue = MapStream.of( requireNonNullArgument( indexBuffer, "indexBuffer" ) )
141            .mapValues( v -> v.stream().mapToInt( Integer::intValue ).toArray() )
142            .collect();
143
144        //---* Done *----------------------------------------------------------
145        return retValue;
146    }   //  convertIndexBufferToParameterIndex()
147
148    /**
149     *  Creates a new instance of {@code EnhancedPreparedStatementImpl}.
150     *
151     *  @param  connection  The connection to the database.
152     *  @param  sql The text of the SQL statement with the placeholders.
153     *  @return The new statement.
154     *  @throws SQLException    Unable to create an instance of an
155     *      {@link org.tquadrat.foundation.sql.EnhancedPreparedStatement}.
156     */
157    @API( status = MAINTAINED, since = "0.1.0" )
158    public static final EnhancedPreparedStatementImpl create( final Connection connection, final String sql ) throws SQLException
159    {
160        final Map<String, List<Integer>> indexBuffer = new HashMap<>();
161        final var preparedStatement = requireNonNullArgument( connection, "connection" ).prepareStatement( parseSQL( sql, indexBuffer ) );
162        final var retValue = new EnhancedPreparedStatementImpl( sql, preparedStatement, convertIndexBufferToParameterIndex( indexBuffer ) );
163
164        //---* Done *----------------------------------------------------------
165        return retValue;
166    }   //  create()
167
168    /**
169     *  {@inheritDoc}
170     */
171    @Override
172    protected final void doLogging( final String operation, final StackTraceElement [] stackTrace )
173    {
174        final var values = getCurrentValues()
175            .stream()
176            .map( v -> format( "%1$s [%2$s]: %3$s", v.parameterName(), (isNull( v.type() ) ? NULL_STRING : v.type().getName()), v.value() ) )
177            .sorted()
178            .toList();
179        m_Logger.log( operation, getSourceStatement(), values, stackTrace );
180    }   //  doLogging()
181
182    /**
183     *  <p>{@summary Enables the logging output for the
184     *  {@code EnhancedPreparedStatement} instances.}</p>
185     *  <p>The {@code logger} method takes three arguments:</p>
186     *  <ol>
187     *      <li>{@code operation} – The name of the operation that logs.</li>
188     *      <li>{@code statement} – The source of the prepared statement.</li>
189     *      <li>{@code values} – A list of the values in the format
190     *      <pre><code>&lt;<i>name</i>&gt;<b> [</b>&lt;<i>type</i>&gt;<b>]:&lt;<i>value</i>&gt;</b></code></pre>
191     *      A type of {@code NULL} indicates an unknown type; for large values (like
192     *      {@link java.sql.Blob}
193     *      or
194     *      {@link java.io.Reader})
195     *      only the class is given instead of the real value.</li>
196     *      <li>{@code stacktrace} – The stacktrace; will be {@code null} if
197     *      {@code addStacktrace} is {@code false}.</li>
198     *  </ol>
199     *  <p>The {@code logCheck} method returns {@code true} only when logging
200     *  should be done. No information is collected while it returns
201     *  {@code false}. As the method is called for nearly any operation, its
202     *  implementation should be as efficient as possible.</p>
203     *
204     *  @param  logger  The method that takes the logging information.
205     *  @param  logCheck    The method that returns a flag whether log output
206     *      is desired.
207     *  @param  addStacktrace   {@code true} if the stacktrace should be added
208     *      to the log output.
209     */
210    @SuppressWarnings( "MethodOverridesStaticMethodOfSuperclass" )
211    public static final void enableLogging( final StatementLogger logger, final BooleanSupplier logCheck, final boolean addStacktrace )
212    {
213        m_Logger = requireNonNullArgument( logger, "logger" );
214        m_LogCheck = requireNonNullArgument( logCheck, "logCheck" );
215        m_AddStacktrace = addStacktrace;
216    }   //  enableLogging()
217
218    /**
219     *  {@inheritDoc}
220     */
221    @Override
222    public final boolean isLoggingEnabled() { return m_LogCheck.getAsBoolean(); }
223
224    /**
225     *  Parses the given SQL statement with the named placeholders and returns
226     *  the text for a call to
227     *  {@link java.sql.Connection#prepareStatement(String)}.
228     *
229     *  @param  sql The source text for the SQL statement.
230     *  @param  indexBuffer The mapping from the names to the indexes.
231     *  @return The target SQL text.
232     *
233     *  @note   The method is public to allow simpler Unit tests.
234     */
235    @API( status = INTERNAL, since = "0.1.0" )
236    public static final String parseSQL( final String sql, final Map<? super String, List<Integer>> indexBuffer )
237    {
238        requireNonNullArgument( indexBuffer, "indexBuffer" );
239
240        //---* Parse the statement text *--------------------------------------
241        var index = 0;
242        final var buffer = new StringBuilder();
243        final var matcher = m_VariablePattern.matcher( requireNotBlankArgument( sql, "sql" ) );
244        while( matcher.find() )
245        {
246            final var variableName = matcher.group( 1 );
247            indexBuffer.computeIfAbsent( variableName, $ -> new ArrayList<>() ).add( Integer.valueOf( ++index ) );
248            matcher.appendReplacement( buffer, " ?" );
249        }
250        matcher.appendTail( buffer );
251
252        //---* Save the text of the prepared statement *-----------------------
253        final var retValue = buffer.toString();
254
255        //---* Done *----------------------------------------------------------
256        return retValue;
257    }   //  parseSQL()
258}
259//  class EnhancedPreparedStatementImpl
260
261/*
262 *  End of File
263 */