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 }