001package com.hfg.sql.jdbc;
002
003import java.sql.Connection;
004import java.sql.DatabaseMetaData;
005import java.sql.SQLException;
006import java.util.ArrayList;
007import java.util.Date;
008import java.util.HashSet;
009import java.util.List;
010import java.util.Set;
011import java.util.logging.Level;
012import java.util.logging.Logger;
013
014import com.hfg.datetime.DateUtil;
015import com.hfg.security.LoginCredentials;
016import com.hfg.sql.SQLUtil;
017import com.hfg.util.collection.CollectionUtil;
018import com.hfg.util.StringBuilderPlus;
019import com.hfg.util.StringUtil;
020
021//------------------------------------------------------------------------------
022/**
023 A JDBC-compatible connection pool.
024 <div>
025 @author J. Alex Taylor, hairyfatguy.com
026 </div>
027 */
028//------------------------------------------------------------------------------
029// com.hfg XML/HTML Coding Library
030//
031// This library is free software; you can redistribute it and/or
032// modify it under the terms of the GNU Lesser General Public
033// License as published by the Free Software Foundation; either
034// version 2.1 of the License, or (at your option) any later version.
035//
036// This library is distributed in the hope that it will be useful,
037// but WITHOUT ANY WARRANTY; without even the implied warranty of
038// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
039// Lesser General Public License for more details.
040//
041// You should have received a copy of the GNU Lesser General Public
042// License along with this library; if not, write to the Free Software
043// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
044//
045// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
046// jataylor@hairyfatguy.com
047//------------------------------------------------------------------------------
048
049public abstract class JDBCConnectionPool<T extends JDBCConnectionPoolSettings> extends JDBCDataSource<T>
050{
051   private List<PooledJDBCConnection> mPooledConnections;
052   private Thread mIdleConnReaperThread;
053   private int mNumConnectionRequests = 0;
054   private boolean mIsActive = true;
055   private Set<String> mBorrowerClassesToIgnore;
056
057   private Integer mDatabaseMajorVersion;
058   private Integer mDatabaseMinorVersion;
059   private String  mDatabaseVersionString;
060   
061   private final static Logger LOGGER = Logger.getLogger(JDBCConnectionPool.class.getName());
062
063   static
064   {
065      LOGGER.setLevel(Level.INFO);
066   }
067
068   //###########################################################################
069   // CONSTRUCTORS
070   //###########################################################################
071
072   //---------------------------------------------------------------------------
073   public JDBCConnectionPool(JDBCServer inServer, String inDatabaseName)
074   {
075      super(inServer, inDatabaseName);
076   }
077
078   //---------------------------------------------------------------------------
079   public JDBCConnectionPool(JDBCServer inServer, String inDatabaseName, LoginCredentials inCredentials)
080   {
081      super(inServer, inDatabaseName);
082      setCredentials(inCredentials);
083   }
084
085
086   //###########################################################################
087   // PUBLIC METHODS
088   //###########################################################################
089
090   //---------------------------------------------------------------------------
091   public synchronized JDBCConnection getConnection()
092      throws SQLException, NoAvailableConnectionException
093   {
094      JDBCConnection conn = null;
095
096      long startTime = System.currentTimeMillis();
097
098      StackTraceElement borrower = getBorrower();
099
100      while (null == conn)
101      {
102         try
103         {
104            conn = getPoolConnection(borrower);
105         }
106         catch (NoAvailableConnectionException e)
107         {
108            if (getSettings().getAvailabilityTimeoutSec() != null
109                  && System.currentTimeMillis() - startTime > (getSettings().getAvailabilityTimeoutSec() * 1000))
110            {
111               throw e;
112            }
113            else
114            {
115               try
116               {
117                  Thread.sleep(250);
118               }
119               catch (InterruptedException e2)
120               {
121                  // Ignore
122               }
123            }
124         }
125      }
126
127      mNumConnectionRequests++;
128
129      return conn;
130   }
131
132   //---------------------------------------------------------------------------
133   public synchronized void destroy()
134      throws SQLException
135   {
136      mIsActive = false;
137
138      if (CollectionUtil.hasValues(mPooledConnections))
139      {
140         for (PooledJDBCConnection conn : mPooledConnections)
141         {
142            conn.destroy();
143         }
144
145         mPooledConnections.clear();
146      }
147
148      mIdleConnReaperThread.interrupt();
149   }
150
151   //---------------------------------------------------------------------------
152   /**
153    Specifies a class to be ignored when tracking connection lending. When examining
154    the stacktrace to see who is requesting a connection, any elements from this class
155    will be skipped and a subsequent element will be used to identify the borrower.
156    * @param inValue a class to be ignored when tracking connection lending
157    * @return this connection pool object to enable method chaining
158    */
159   public JDBCConnectionPool addIgnorableBorrowerClass(Class inValue)
160   {
161      if (inValue != null)
162      {
163         if (null == mBorrowerClassesToIgnore)
164         {
165            mBorrowerClassesToIgnore = new HashSet<>(5);
166         }
167
168         mBorrowerClassesToIgnore.add(inValue.getName());
169      }
170
171      return this;
172   }
173
174   //---------------------------------------------------------------------------
175   public int getDatabaseMajorVersion()
176         throws SQLException
177   {
178      if (null == mDatabaseMajorVersion)
179      {
180         Connection conn = null;
181         try
182         {
183            conn = getConnection();
184
185            DatabaseMetaData meta = conn.getMetaData();
186            // Oracle (and some other vendors) do not support
187            // some the following methods; therefore, we need
188            // to use try-catch block.
189            try 
190            {
191               mDatabaseMajorVersion = meta.getDatabaseMajorVersion();
192            } 
193            catch (Exception e) 
194            {
195               System.err.println("Database major version: unsupported feature");
196            }
197         }
198         finally
199         {
200            SQLUtil.close(conn);
201         }
202
203      }
204
205      return mDatabaseMajorVersion;
206   }
207
208   //---------------------------------------------------------------------------
209   public int getDatabaseMinorVersion()
210         throws SQLException
211   {
212      if (null == mDatabaseMinorVersion)
213      {
214         Connection conn = null;
215         try
216         {
217            conn = getConnection();
218
219            DatabaseMetaData meta = conn.getMetaData();
220            // Oracle (and some other vendors) do not support
221            // some the following methods; therefore, we need
222            // to use try-catch block.
223            try 
224            {
225               mDatabaseMinorVersion = meta.getDatabaseMinorVersion();
226            } 
227            catch (Exception e) 
228            {
229               System.err.println("Database minor version: unsupported feature");
230            }
231         }
232         finally
233         {
234            SQLUtil.close(conn);
235         }
236
237      }
238
239      return mDatabaseMajorVersion;
240   }
241
242   //---------------------------------------------------------------------------
243   public String getDatabaseVersionString()
244   {
245      if (null == mDatabaseVersionString)
246      {
247         Connection conn = null;
248         try
249         {
250            conn = getConnection();
251
252            DatabaseMetaData meta = conn.getMetaData();
253            mDatabaseVersionString = meta.getDatabaseProductVersion();
254         }
255         catch (SQLException e)
256         {
257            throw new JDBCException("Error retrieving database version string!", e);
258         }
259         finally
260         {
261            SQLUtil.close(conn);
262         }
263
264      }
265
266      return mDatabaseVersionString;
267   }
268   
269   //---------------------------------------------------------------------------
270   public String getStatusReport()
271   {
272      StringBuilderPlus buffer = new StringBuilderPlus()
273            .appendln("Connection Pool Status Report " + DateUtil.getYYYY_MM_DD_HH_mm_ss(new Date()))
274            .appendln()
275            .appendln(String.format("  %s %s", getServer().getRDBMS().name(), getDatabaseVersionString()))
276            .appendln("  Server: " + getServer().getConnectString(getDatabaseName()))
277            .appendln("  Num Connection Requests: " + mNumConnectionRequests)
278            .appendln(String.format("  Connections (Min/Max/Avail.): %d / %d / %d",
279                      getSettings().getMinConnections(), getSettings().getMaxConnections(), getNumAvailableConnections()));
280
281      StringBuilderPlus activeConnBuffer = new StringBuilderPlus();
282      if (CollectionUtil.hasValues(mPooledConnections))
283      {
284         for (int i = 0; i < mPooledConnections.size(); i++)
285         {
286            PooledJDBCConnection poolConn = mPooledConnections.get(i);
287
288            if (! poolConn.isAvailable())
289            {
290               activeConnBuffer.appendln(String.format("    %2d. %s [%s]", (i + 1), poolConn.getBorrower().toString(), DateUtil.generateElapsedTimeString(poolConn.getCheckoutTime())));
291            }
292         }
293      }
294      
295      if (activeConnBuffer.length() > 0)
296      {
297         buffer.appendln("  Active Connections:")
298               .append(activeConnBuffer);
299      }
300      else 
301      {
302         buffer.appendln("  No Active Connections.");
303      }
304
305      return buffer.toString();
306   }
307
308   //---------------------------------------------------------------------------
309   public static Logger getLogger()
310   {
311      return LOGGER;
312   }
313
314   //###########################################################################
315   // PROTECTED METHODS
316   //###########################################################################
317
318   //---------------------------------------------------------------------------
319   protected JDBCConnection getPoolConnection(StackTraceElement inBorrower)
320         throws SQLException, NoAvailableConnectionException
321   {
322      PooledJDBCConnection conn = null;
323
324      if (null == mPooledConnections)
325      {
326         initializePool();
327      }
328
329      for (int i = 0; i < mPooledConnections.size(); i++)
330      {
331         PooledJDBCConnection poolConn = mPooledConnections.get(i);
332
333         if (poolConn.isAvailable())
334         {
335            try
336            {
337               conn = poolConn.checkout(inBorrower);
338               if (null == conn)
339               {  // The connection must not be valid anymore
340                  poolConn.destroy();
341                  mPooledConnections.remove(i--);
342               }
343               else
344               {
345                  // Connection is good to go
346                  break;
347               }
348            }
349            catch (Exception e)
350            {
351               e.printStackTrace();
352               poolConn.destroy();
353               mPooledConnections.remove(i--);
354            }
355         }
356      }
357
358      if (null == conn)
359      {
360         // No available connections were found in the pool. Can we create a new one?
361         if (mPooledConnections.size() < getSettings().getMaxConnections())
362         {
363            conn = new PooledJDBCConnection(createConnection());
364            LOGGER.fine("Creating new connection: " + conn.name());
365            conn = conn.checkout(inBorrower);
366            if (conn != null)
367            {
368               mPooledConnections.add(conn);
369            }
370         }
371         else
372         {
373            throw new NoAvailableConnectionException();
374         }
375      }
376
377      return conn;
378   }
379
380   //---------------------------------------------------------------------------
381   protected T initSettings()
382   {
383      return (T) new JDBCConnectionPoolSettings();
384   }
385
386   //---------------------------------------------------------------------------
387   protected abstract JDBCConnection createConnection();
388
389   //###########################################################################
390   // PRIVATE METHODS
391   //###########################################################################
392
393   //---------------------------------------------------------------------------
394   private void initializePool()
395   {
396      mPooledConnections = new ArrayList<>(getSettings().getMaxConnections());
397
398      for (int i = 0; i < getSettings().getMinConnections(); i++)
399      {
400         JDBCConnection conn = createConnection();
401
402         mPooledConnections.add(new PooledJDBCConnection(conn));
403      }
404
405      mIdleConnReaperThread = new Thread(new IdleConnReaper());
406      mIdleConnReaperThread.setDaemon(true);
407      mIdleConnReaperThread.setPriority(Thread.MIN_PRIORITY);
408      mIdleConnReaperThread.setName(generateIdleRepearThreadName());
409      mIdleConnReaperThread.start();
410   }
411
412
413   //---------------------------------------------------------------------------
414   private String generateIdleRepearThreadName()
415   {
416      StringBuilderPlus name = new StringBuilderPlus().setDelimiter(" ");
417      if (StringUtil.isSet(name()))
418      {
419         name.append(name());
420      }
421
422      if (StringUtil.isSet(getServer().name()))
423      {
424         name.delimitedAppend(getServer().name());
425      }
426
427      if (StringUtil.isSet(getDatabaseName()))
428      {
429         name.delimitedAppend(getDatabaseName());
430      }
431
432      name.delimitedAppend(" Idle Conn Reaper");
433
434      return name.toString();
435   }
436
437   //---------------------------------------------------------------------------
438   private StackTraceElement getBorrower()
439   {
440      StackTraceElement borrower = null;
441
442      StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
443      for (int i = 3; i < stackTrace.length; i++)
444      {
445         StackTraceElement stackTraceElement = stackTrace[i];
446         if (! CollectionUtil.hasValues(mBorrowerClassesToIgnore)
447             || ! mBorrowerClassesToIgnore.contains(stackTraceElement.getClassName()))
448         {
449            borrower = stackTraceElement;
450            break;
451         }
452      }
453
454      return borrower;
455   }
456
457   //---------------------------------------------------------------------------
458   private synchronized void removeConnectionFromPool(PooledJDBCConnection inConn)
459   {
460      mPooledConnections.remove(inConn);
461   }
462
463   //---------------------------------------------------------------------------
464   private synchronized void removeIdledConnections()
465      throws SQLException
466   {
467      if (poolContainsMoreThanTheMinNumOfConnections())
468      {
469         for (int i = 0; i < getSettings().getMinConnections(); i++)
470         {
471            PooledJDBCConnection poolConn = mPooledConnections.get(i);
472            if (poolConn.isAvailable()
473                && poolConn.getIdleTimeSec() > getSettings().getMaxIdleTimeSec())
474            {
475               poolConn.destroy();
476               mPooledConnections.remove(i--);
477
478               if (! poolContainsMoreThanTheMinNumOfConnections())
479               {
480                  break;
481               }
482            }
483         }
484      }
485   }
486
487   //---------------------------------------------------------------------------
488   private boolean poolContainsMoreThanTheMinNumOfConnections()
489   {
490      return (mPooledConnections.size() > 0
491              && (null == getSettings().getMinConnections()
492                  || mPooledConnections.size() > getSettings().getMinConnections()));
493   }
494
495   //---------------------------------------------------------------------------
496   private int getNumAvailableConnections()
497   {
498      int numAvailableConns = 0;
499
500      if (mPooledConnections != null)
501      {
502         for (int i = 0; i < mPooledConnections.size(); i++)
503         {
504            PooledJDBCConnection poolConn = mPooledConnections.get(i);
505
506            if (poolConn.isAvailable())
507            {
508               numAvailableConns++;
509            }
510         }
511      }
512
513      return numAvailableConns;
514   }
515
516   //###########################################################################
517   // INNER CLASS
518   //###########################################################################
519
520   private class PooledJDBCConnection extends JDBCConnection
521   {
522      private boolean           mIsAvailable;
523      private Long              mCheckoutTime;
524      private StackTraceElement mBorrower;
525      private Long              mIdleStartTime;
526
527      //------------------------------------------------------------------------
528      public PooledJDBCConnection(JDBCConnection inConn)
529      {
530         super(inConn);
531         initPooledConnection();
532      }
533
534      //------------------------------------------------------------------------
535      private void initPooledConnection()
536      {
537         mCheckoutTime  = null;
538         mBorrower      = null;
539         mIdleStartTime = System.currentTimeMillis();
540         mIsAvailable   = true;
541      }
542
543      //------------------------------------------------------------------------
544      public synchronized boolean isAvailable()
545      {
546         return mIsAvailable;
547      }
548
549      //------------------------------------------------------------------------
550      public synchronized PooledJDBCConnection checkout(StackTraceElement inBorrower)
551         throws SQLException
552      {
553         mIsAvailable   = false;
554         mCheckoutTime  = System.currentTimeMillis();
555         mBorrower      = inBorrower;
556         mIdleStartTime = null;
557
558         if (! getAutoCommit())
559         {
560            setAutoCommit(true);
561         }
562         
563         PooledJDBCConnection conn = this;
564
565         if (getServer().getRDBMS().getConnTestQuery() != null)
566         {
567            try
568            {
569               execute(getServer().getRDBMS().getConnTestQuery().toSQL());
570            }
571            catch (JDBCException e)
572            {
573               conn = null;
574            }
575         }
576
577         if (conn != null)
578         {
579            LOGGER.fine(name() + " checked out to " + inBorrower);
580         }
581
582         return conn;
583      }
584
585      //------------------------------------------------------------------------
586      public Long getCheckoutTime()
587      {
588         return mCheckoutTime;
589      }
590
591      //------------------------------------------------------------------------
592      public StackTraceElement getBorrower()
593      {
594         return mBorrower;
595      }
596
597      //------------------------------------------------------------------------
598      private int getIdleTimeSec()
599      {
600         int idleTimeSec = 0;
601         if (mIdleStartTime != null)
602         {
603            idleTimeSec = (int) (System.currentTimeMillis() - mIdleStartTime)/1000;
604         }
605
606         return idleTimeSec;
607      }
608
609      //------------------------------------------------------------------------
610      // Don't actually close the underlying connection. It goes back into the pool.
611      public void close()
612         throws SQLException
613      {
614         LOGGER.fine(name() + " returned from " + mBorrower);
615
616         if (isClosed())
617         {
618            removeConnectionFromPool(this);
619         }
620         else
621         {
622            clean();
623
624            initPooledConnection();
625         }
626      }
627
628      //------------------------------------------------------------------------
629      public void destroy()
630         throws SQLException
631      {
632         if (! isClosed())
633         {
634            unwrap(Connection.class).close();
635         }
636      }
637
638      //------------------------------------------------------------------------
639      private void clean()
640         throws SQLException
641      {
642         if (! getAutoCommit())
643         {
644            boolean returnedInUncommittedState = false;
645            try
646            {
647               setAutoCommit(true);
648            }
649            catch (SQLException e)
650            {
651               returnedInUncommittedState = true;
652
653               // Try rolling back the transaction.
654               try
655               {
656                  rollback();
657                  setAutoCommit(true);
658               }
659               catch (SQLException e2)
660               {
661                  destroy();
662                  removeConnectionFromPool(this);
663               }
664            }
665
666            if (returnedInUncommittedState)
667            {
668               throw new JDBCException("Connection returned in an uncommitted state!");
669            }
670         }
671      }
672
673
674      //------------------------------------------------------------------------
675      @Override
676      public String getCurrentUser()
677            throws SQLException
678      {
679         return ((JDBCConnection) getInnerConnection()).getCurrentUser();
680      }
681
682   }
683
684   //###########################################################################
685   // INNER CLASS
686   //###########################################################################
687
688   private class IdleConnReaper implements Runnable
689   {
690      private int mExecutionFreqSec = 60;
691
692      //------------------------------------------------------------------------
693      public void run()
694      {
695         while (mIsActive)
696         {
697            try
698            {
699               Thread.sleep(mExecutionFreqSec * 1000);
700            }
701            catch (InterruptedException e2)
702            {
703               // Ignore
704            }
705
706            try
707            {
708               removeIdledConnections();
709            }
710            catch (Exception e)
711            {
712               // Ignore
713               e.printStackTrace();
714            }
715         }
716      }
717
718   }
719}