View Javadoc
1   /*
2    * Copyright (c) 2002-2022, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.util.sql;
35  
36  import fr.paris.lutece.portal.service.database.AppConnectionService;
37  import fr.paris.lutece.portal.service.database.PluginConnectionService;
38  import fr.paris.lutece.portal.service.plugin.Plugin;
39  import fr.paris.lutece.portal.service.util.AppException;
40  
41  import org.apache.commons.lang3.StringUtils;
42  import org.apache.logging.log4j.LogManager;
43  import org.apache.logging.log4j.Logger;
44  
45  import java.sql.Connection;
46  import java.sql.PreparedStatement;
47  import java.sql.SQLException;
48  
49  /**
50   * Transaction
51   */
52  public class Transaction
53  {
54      /**
55       * Status for opened transactions
56       */
57      public static final int OPENED = -1;
58  
59      /**
60       * Status for committed transactions
61       */
62      public static final int COMMITTED = 0;
63  
64      /**
65       * Status for roll backed transactions
66       */
67      public static final int ROLLEDBACK = 1;
68      private static final String MESSAGE_PLUGIN = "Plugin : '";
69      private static final String DEFAULT_MODULE_NAME = "core";
70      private static final String LOGGER_DEBUG_SQL = "lutece.debug.sql.";
71  
72      /**
73       * The last SQL query executed by this transaction
74       */
75      private String _strSQL = StringUtils.EMPTY;
76  
77      /** JDBC Connection */
78      private Connection _connection;
79  
80      /** Connection Service providing connection from a defined pool */
81      private PluginConnectionService _connectionService;
82  
83      /** Plugin name */
84      private String _strPluginName;
85  
86      /** The debug logger */
87      private Logger _logger;
88      private PreparedStatement _statement;
89      private int _nStatus = OPENED;
90      private boolean _bAutoCommit;
91  
92      /**
93       * Constructor
94       */
95      public Transaction( )
96      {
97          beginTransaction( null );
98      }
99  
100     /**
101      * Constructor
102      * 
103      * @param plugin
104      *            The plugin owner of the transaction
105      */
106     public Transaction( Plugin plugin )
107     {
108         beginTransaction( plugin );
109     }
110 
111     /**
112      * Gets a prepared statement
113      * 
114      * @param strSQL
115      *            The SQL statement
116      * @return The prepared statement
117      * @throws SQLException
118      *             If an SQL error occurs
119      */
120     public PreparedStatement prepareStatement( String strSQL ) throws SQLException
121     {
122         return prepareStatement( strSQL, null, true );
123     }
124 
125     /**
126      * Gets a prepared statement
127      * 
128      * @param strSQL
129      *            The SQL statement
130      * @return The prepared statement
131      * @throws SQLException
132      *             If an SQL error occurs
133      */
134     public PreparedStatement prepareStatement( String strSQL, Integer autoGeneratedKeys ) throws SQLException
135     {
136         return prepareStatement( strSQL, autoGeneratedKeys, true );
137     }
138 
139     /**
140      * Gets a prepared statement
141      * 
142      * @param strSQL
143      *            The SQL statement
144      * @return The prepared statement
145      * @throws SQLException
146      *             If an SQL error occurs
147      */
148     public PreparedStatement prepareStatement( String strSQL, boolean bLogQueries ) throws SQLException
149     {
150         return prepareStatement( strSQL, null, bLogQueries );
151     }
152 
153     /**
154      * Gets a prepared statement
155      *
156      * @since 6.0.0
157      * @param strSQL
158      *            The SQL statement
159      * @param autoGeneratedKeys
160      *            a flag indicating whether auto-generated keys should be returned; For example one of <code>Statement.RETURN_GENERATED_KEYS</code> or
161      *            <code>Statement.NO_GENERATED_KEYS</code>. See {@link PreparedStatement#prepareStatement(String, int)}
162      * @return The prepared statement
163      * @throws SQLException
164      *             If an SQL error occurs
165      */
166     public PreparedStatement prepareStatement( String strSQL, Integer autoGeneratedKeys, boolean bLogQueries ) throws SQLException
167     {
168         // Close the previous statement if exists
169         if ( _statement != null )
170         {
171             _statement.close( );
172         }
173 
174         // Get a new statement
175         _strSQL = strSQL;
176 
177         if ( _connection == null )
178         {
179             throw new SQLException( MESSAGE_PLUGIN + _strPluginName + "' - Connection has been closed. The new prepared statement can not be created : "
180                     + ( bLogQueries ? _strSQL : "(query log disabled)" ) );
181         }
182 
183         if ( autoGeneratedKeys != null )
184         {
185             _statement = _connection.prepareStatement( _strSQL, autoGeneratedKeys );
186         }
187         else
188         {
189             _statement = _connection.prepareStatement( _strSQL );
190         }
191 
192         return _statement;
193     }
194 
195     /**
196      * The current prepared statement
197      * 
198      * @return The current statement
199      */
200     public PreparedStatement getStatement( )
201     {
202         return _statement;
203     }
204 
205     /**
206      * Execute the current statement
207      * 
208      * @throws SQLException
209      *             If an SQL error occurs
210      */
211     public void executeStatement( ) throws SQLException
212     {
213         _logger.debug( "{} {}' - EXECUTE STATEMENT : {}", MESSAGE_PLUGIN, _strPluginName, _strSQL );
214         _statement.executeUpdate( );
215     }
216 
217     /**
218      * Commit the transaction
219      */
220     public void commit( )
221     {
222         try
223         {
224             if ( _connection == null )
225             {
226                 throw new SQLException( MESSAGE_PLUGIN + _strPluginName + "' - Transaction has already been closed and can not be committed" );
227             }
228 
229             _connection.commit( );
230             _logger.debug( "{} {}' - COMMIT TRANSACTION", MESSAGE_PLUGIN, _strPluginName );
231             closeTransaction( COMMITTED );
232         }
233         catch( SQLException e )
234         {
235             rollback( e );
236         }
237     }
238 
239     /**
240      * Rollback the transaction
241      */
242     public void rollback( )
243     {
244         rollback( null );
245     }
246 
247     /**
248      * Rollback the transaction
249      * 
250      * @param e
251      *            The exception that cause the rollback
252      */
253     public void rollback( Exception e )
254     {
255         if ( e != null )
256         {
257             _logger.error( "Transaction Error - Rollback in progress {}", e.getMessage( ), e.getCause( ) );
258         }
259 
260         try
261         {
262             if ( _connection != null )
263             {
264                 _connection.rollback( );
265                 _logger.debug( "{} {}' - ROLLBACK TRANSACTION", MESSAGE_PLUGIN, _strPluginName );
266             }
267             else
268             {
269                 _logger.debug( "{} {}' - TRANSACTION HAS ALREADY BEEN ROLLED BACK", MESSAGE_PLUGIN, _strPluginName );
270             }
271         }
272         catch( SQLException ex )
273         {
274             _logger.error( "Transaction Error - Rollback error : {}", ex.getMessage( ), ex );
275         }
276         finally
277         {
278             closeTransaction( ROLLEDBACK );
279         }
280     }
281 
282     /**
283      * Return the transaction status
284      * 
285      * @return The transaction status
286      */
287     public int getStatus( )
288     {
289         return _nStatus;
290     }
291 
292     /**
293      * Get the underlying connection.
294      * 
295      * @return The connection of this transaction. If the transaction has not begin, then return null.
296      */
297     protected Connection getConnection( )
298     {
299         return _connection;
300     }
301 
302     /**
303      * Begin a transaction
304      * 
305      * @param plugin
306      *            The plugin owner of the transaction
307      */
308     protected void beginTransaction( Plugin plugin )
309     {
310         if ( plugin != null )
311         {
312             _strPluginName = plugin.getName( );
313             _connectionService = plugin.getConnectionService( );
314         }
315         else
316         {
317             _strPluginName = DEFAULT_MODULE_NAME;
318             _connectionService = AppConnectionService.getDefaultConnectionService( );
319         }
320 
321         if ( _connectionService == null )
322         {
323             throw new AppException( "Database access error. Please check component installations and db.properties." );
324         }
325 
326         _logger = LogManager.getLogger( LOGGER_DEBUG_SQL + _strPluginName );
327         _logger.debug( "{}{}' - BEGIN TRANSACTION", MESSAGE_PLUGIN, _strPluginName );
328 
329         try
330         {
331             _connection = _connectionService.getConnection( );
332 
333             // Save the autocommit configuration of the connection
334             _bAutoCommit = _connection.getAutoCommit( );
335             _connection.setAutoCommit( false );
336         }
337         catch( SQLException e )
338         {
339             rollback( e );
340         }
341     }
342 
343     /**
344      * Close the transaction
345      * 
346      * @param nStatus
347      *            The status of the transaction
348      */
349     private void closeTransaction( int nStatus )
350     {
351         _nStatus = nStatus;
352 
353         try
354         {
355             if ( _statement != null )
356             {
357                 _statement.close( );
358             }
359 
360             // Restore the autocommit configuration of the connection
361             if ( _connection != null )
362             {
363                 _connection.setAutoCommit( _bAutoCommit );
364             }
365         }
366         catch( SQLException ex )
367         {
368             _logger.error( "Transaction Error - Unable to close transaction {}", ex.getMessage( ), ex );
369         }
370         finally
371         {
372             _connectionService.freeConnection( _connection );
373             _connection = null;
374         }
375     }
376 
377     /**
378      * Checks that the transaction has been committed (or rolled back) before being destroyed and release all transaction resources (statement, connection, ...)
379      * if not. {@inheritDoc }
380      */
381     @Override
382     protected void finalize( ) throws Throwable
383     {
384         if ( _nStatus == OPENED )
385         {
386             _logger.error( "The transaction has not been commited" );
387             closeTransaction( OPENED );
388         }
389 
390         super.finalize( );
391     }
392 }