View Javadoc
1   /*
2    * Copyright (c) 2002-2014, Mairie de 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.pool.service;
35  
36  import fr.paris.lutece.portal.service.util.AppException;
37  
38  import org.apache.log4j.Logger;
39  
40  import java.io.PrintWriter;
41  
42  import java.sql.Connection;
43  import java.sql.DriverManager;
44  import java.sql.SQLException;
45  import java.sql.Statement;
46  
47  import java.util.ArrayList;
48  import java.util.List;
49  
50  import javax.sql.DataSource;
51  
52  
53  /**
54   * This class manages a database connection pool.
55   * <br>
56   * Connections are wrapped by a {@link LuteceConnection}
57   * to avoid explicit calls to {@link Connection#close()}. Connections
58   * are released to this pool when {@link Connection#close()} is called, and
59   * are not actually closed until {@link #release()} call.
60   * @see LuteceConnection
61   */
62  public class ConnectionPool implements DataSource
63  {
64      private static final String DEFAULT_CHECK_VALID_CONNECTION_SQL = "SELECT 1";
65      private String _strUrl;
66      private String _strUser;
67      private String _strPassword;
68      private int _nMaxConns;
69      private int _nTimeOut;
70      private Logger _logger;
71      private int _nCheckedOut;
72      private List<Connection> _freeConnections = new ArrayList<Connection>(  );
73      private String _strCheckValidConnectionSql; // Added in v1.4
74      private PrintWriter _logWriter;
75  
76      /**
77       * Constructor.
78       *
79       * @param strName Nom du pool
80       * @param strUrl JDBC Data source URL
81       * @param strUser SQL User
82       * @param strPassword SQL Password
83       * @param nMaxConns Max connections
84       * @param nInitConns Initials connections
85       * @param nTimeOut Timeout to get a connection
86       * @param logger the Logger object
87       * @param strCheckValidConnectionSql The SQL syntax used for check connexion validatation
88       */
89      public ConnectionPool( String strName, String strUrl, String strUser, String strPassword, int nMaxConns,
90          int nInitConns, int nTimeOut, Logger logger, String strCheckValidConnectionSql )
91      {
92          _strUrl = strUrl;
93          _strUser = strUser;
94          _strPassword = strPassword;
95          _nMaxConns = nMaxConns;
96          _nTimeOut = ( nTimeOut > 0 ) ? nTimeOut : 5;
97          _logger = logger;
98          initPool( nInitConns );
99          _logger.info( "New pool created : " + strName );
100 
101         _strCheckValidConnectionSql = ( ( strCheckValidConnectionSql != null ) &&
102             !strCheckValidConnectionSql.equals( "" ) ) ? strCheckValidConnectionSql : DEFAULT_CHECK_VALID_CONNECTION_SQL;
103 
104         String lf = System.getProperty( "line.separator" );
105         _logger.debug( lf + " url=" + strUrl + lf + " user=" + _strUser + lf + " password=" + _strPassword + lf +
106             " initconns=" + nInitConns + lf + " maxconns=" + _nMaxConns + lf + " logintimeout=" + _nTimeOut );
107         _logger.debug( getStats(  ) );
108     }
109 
110     /**
111      * Initializes the pool
112      *
113      * @param initConns Number of connections to create at the initialisation
114      */
115     private void initPool( int initConns )
116     {
117         for ( int i = 0; i < initConns; i++ )
118         {
119             try
120             {
121                 Connection pc = newConnection(  );
122                 _freeConnections.add( pc );
123             }
124             catch ( SQLException e )
125             {
126                 throw new AppException( "SQL Error executing command : " + e.toString(  ) );
127             }
128         }
129     }
130 
131     /**
132      * Returns a connection from the pool.
133      *
134      * @return An open connection
135      * @throws SQLException The SQL exception
136      */
137     @Override
138     public Connection getConnection(  ) throws SQLException
139     {
140         _logger.debug( "Request for connection received" );
141 
142         try
143         {
144             return getConnection( _nTimeOut * 1000L );
145         }
146         catch ( SQLException e )
147         {
148             _logger.error( "Exception getting connection", e );
149             throw e;
150         }
151     }
152 
153     /**
154      * Returns a connection from the pool.
155      *
156      * @param timeout The maximum time to wait for a connection
157      * @return An open connection
158      * @throws SQLException The SQL exception
159      */
160     private synchronized Connection getConnection( long timeout )
161         throws SQLException
162     {
163         // Get a pooled Connection from the cache or a new one.
164         // Wait if all are checked out and the max limit has
165         // been reached.
166         long startTime = System.currentTimeMillis(  );
167         long remaining = timeout;
168         Connection conn = null;
169 
170         while ( ( conn = getPooledConnection(  ) ) == null )
171         {
172             try
173             {
174                 _logger.debug( "Waiting for connection. Timeout=" + remaining );
175 
176                 wait( remaining );
177             }
178             catch ( InterruptedException e )
179             {
180                 _logger.debug( "A connection has been released by another thread." );
181             }
182 
183             remaining = timeout - ( System.currentTimeMillis(  ) - startTime );
184 
185             if ( remaining <= 0 )
186             {
187                 // Timeout has expired
188                 _logger.debug( "Time-out while waiting for connection" );
189                 throw new SQLException( "getConnection() timed-out" );
190             }
191         }
192 
193         // Check if the Connection is still OK
194         if ( !isConnectionOK( conn ) )
195         {
196             // It was bad. Try again with the remaining timeout
197             _logger.error( "Removed selected bad connection from pool" );
198 
199             return getConnection( remaining );
200         }
201 
202         _nCheckedOut++;
203         _logger.debug( "Delivered connection from pool" );
204         _logger.debug( getStats(  ) );
205 
206         return conn;
207     }
208 
209     /**
210      * Checks a connection to see if it's still alive
211      *
212      * @param conn The connection to check
213      * @return true if the connection is OK, otherwise false.
214      */
215     private boolean isConnectionOK( Connection conn )
216     {
217         Statement testStmt = null;
218 
219         try
220         {
221             if ( !conn.isClosed(  ) )
222             {
223                 // Try to createStatement to see if it's really alive
224                 testStmt = conn.createStatement(  );
225                 testStmt.executeQuery( _strCheckValidConnectionSql );
226                 testStmt.close(  );
227             }
228             else
229             {
230                 return false;
231             }
232         }
233         catch ( SQLException e )
234         {
235             if ( testStmt != null )
236             {
237                 try
238                 {
239                     testStmt.close(  );
240                 }
241                 catch ( SQLException se )
242                 {
243                     throw new AppException( "ConnectionService : SQL Error executing command : " + se.toString(  ) );
244                 }
245             }
246 
247             _logger.error( "Pooled Connection was not okay", e );
248 
249             return false;
250         }
251 
252         return true;
253     }
254 
255     /**
256      * Gets a connection from the pool
257      *
258      * @return An opened connection
259      * @throws SQLException The exception
260      */
261     private Connection getPooledConnection(  ) throws SQLException
262     {
263         Connection conn = null;
264 
265         if ( _freeConnections.size(  ) > 0 )
266         {
267             // Pick the first Connection in the Vector
268             // to get round-robin usage
269             conn = _freeConnections.get( 0 );
270             _freeConnections.remove( 0 );
271         }
272         else if ( ( _nMaxConns == 0 ) || ( _nCheckedOut < _nMaxConns ) )
273         {
274             conn = newConnection(  );
275         }
276 
277         return conn;
278     }
279 
280     /**
281      * Creates a new connection.
282      * <br>
283      * The connection is wrapped by {@link LuteceConnection}
284      *
285      * @return The new created connection
286      * @throws SQLException The SQL exception
287      */
288     private Connection newConnection(  ) throws SQLException
289     {
290         Connection conn;
291 
292         if ( _strUser == null )
293         {
294             conn = DriverManager.getConnection( _strUrl );
295         }
296         else
297         {
298             conn = DriverManager.getConnection( _strUrl, _strUser, _strPassword );
299         }
300 
301         // wrap connection so this connection pool is used when conn.close() is called
302         conn = LuteceConnectionFactory.newInstance( this, conn );
303 
304         _logger.info( "New connection created. Connections count is : " + ( getConnectionCount(  ) + 1 ) );
305 
306         return conn;
307     }
308 
309     /**
310      * Returns a connection to pool.
311      *
312      * @param conn The released connection to return to pool
313      */
314     public synchronized void freeConnection( Connection conn )
315     {
316         // Put the connection at the end of the Vector
317         _freeConnections.add( conn );
318         _nCheckedOut--;
319         notifyAll(  );
320         _logger.debug( "Returned connection to pool" );
321         _logger.debug( getStats(  ) );
322     }
323 
324     /**
325      * Releases the pool by closing all its connections.
326      */
327     public synchronized void release(  )
328     {
329         for ( Connection connection : _freeConnections )
330         {
331             try
332             {
333                 if ( connection instanceof LuteceConnection )
334                 {
335                     ( (LuteceConnection) connection ).closeConnection(  );
336                 }
337                 else
338                 {
339                     connection.close(  );
340                 }
341 
342                 _logger.debug( "Closed connection" );
343             }
344             catch ( SQLException e )
345             {
346                 _logger.error( "Couldn't close connection", e );
347             }
348         }
349 
350         _freeConnections.clear(  );
351     }
352 
353     /**
354      * Returns stats on pool's connections
355      *
356      * @return Stats as String.
357      */
358     private String getStats(  )
359     {
360         return "Total connections: " + getConnectionCount(  ) + " Available: " + getFreeConnectionCount(  ) +
361         " Checked-out: " + getBusyConnectionCount(  );
362     }
363 
364     /**
365      * Returns the number of connections opened by the pool (available or busy)
366      * @return A connection count
367      */
368     public int getConnectionCount(  )
369     {
370         return getFreeConnectionCount(  ) + getBusyConnectionCount(  );
371     }
372 
373     /**
374      * Returns the number of free connections of the pool (available or busy)
375      * @return A connection count
376      */
377     public int getFreeConnectionCount(  )
378     {
379         return _freeConnections.size(  );
380     }
381 
382     /**
383      * Returns the number of busy connections of the pool (available or busy)
384      * @return A connection count
385      */
386     public int getBusyConnectionCount(  )
387     {
388         return _nCheckedOut;
389     }
390 
391     /**
392      * Returns the maximum number of connections of the pool
393      * @return A connection count
394      */
395     public int getMaxConnectionCount(  )
396     {
397         return _nMaxConns;
398     }
399 
400     /**
401      * Returns the connection of the pool.
402      *
403      * @param username the username
404      * @param password the password
405      * @return A connection
406      * @throws SQLException the sQL exception
407      */
408     @Override
409     public Connection getConnection( String username, String password )
410         throws SQLException
411     {
412         return getConnection(  );
413     }
414 
415     /**
416      * Get the log.
417      *
418      * @return A log writer
419      * @throws SQLException the sQL exception
420      */
421     @Override
422     public PrintWriter getLogWriter(  ) throws SQLException
423     {
424         _logger.debug( "ConnectionPool : DataSource getLogWriter called" );
425 
426         return _logWriter;
427     }
428 
429     /**
430      * Set the log.
431      *
432      * @param out the new log writer
433      * @throws SQLException the sQL exception
434      */
435     @Override
436     public void setLogWriter( PrintWriter out ) throws SQLException
437     {
438         _logger.debug( "ConnectionPool : DataSource setLogWriter called" );
439         _logWriter = out;
440     }
441 
442     /**
443      * Set Login Timeout.
444      *
445      * @param seconds the new login timeout
446      * @throws SQLException the sQL exception
447      */
448     @Override
449     public void setLoginTimeout( int seconds ) throws SQLException
450     {
451     }
452 
453     /**
454      * Get loging timeout.
455      *
456      * @return A time out
457      * @throws SQLException the sQL exception
458      */
459     @Override
460     public int getLoginTimeout(  ) throws SQLException
461     {
462         return _nTimeOut;
463     }
464 
465     /**
466      * Get the unwrap.
467      *
468      * @param <T> the generic type
469      * @param iface the iface
470      * @return null
471      * @throws SQLException the sQL exception
472      */
473     @Override
474     public <T> T unwrap( Class<T> iface ) throws SQLException
475     {
476         return null;
477     }
478 
479     /**
480      * Get the wrapper.
481      *
482      * @param iface the iface
483      * @return false
484      * @throws SQLException the sQL exception
485      */
486     @Override
487     public boolean isWrapperFor( Class<?> iface ) throws SQLException
488     {
489         return false;
490     }
491 
492     /**
493      * Implementation of JDBC 4.1's getParentLogger method (Java 7)
494      *
495      * @return the parent logger
496      */
497     public java.util.logging.Logger getParentLogger(  )
498     {
499         return java.util.logging.Logger.getLogger( java.util.logging.Logger.GLOBAL_LOGGER_NAME );
500     }
501 }