org/sonews/daemon/storage/Database.java
author chris <chris@marvin>
Fri Jun 26 16:48:50 2009 +0200 (2009-06-26)
changeset 1 6fceb66e1ad7
child 2 1090e2141798
permissions -rw-r--r--
Hooray... sonews/0.5.0 final

HG: Enter commit message. Lines beginning with 'HG:' are removed.
HG: Remove all lines to abort the collapse operation.
     1 /*
     2  *   SONEWS News Server
     3  *   see AUTHORS for the list of contributors
     4  *
     5  *   This program is free software: you can redistribute it and/or modify
     6  *   it under the terms of the GNU General Public License as published by
     7  *   the Free Software Foundation, either version 3 of the License, or
     8  *   (at your option) any later version.
     9  *
    10  *   This program is distributed in the hope that it will be useful,
    11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  *   GNU General Public License for more details.
    14  *
    15  *   You should have received a copy of the GNU General Public License
    16  *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  */
    18 
    19 package org.sonews.daemon.storage;
    20 
    21 import java.sql.Connection;
    22 import java.sql.DriverManager;
    23 import java.sql.ResultSet;
    24 import java.sql.SQLException;
    25 import java.sql.Statement;
    26 import java.sql.PreparedStatement;
    27 import java.util.ArrayList;
    28 import java.util.Enumeration;
    29 import java.util.List;
    30 import java.util.Map;
    31 import java.util.concurrent.ConcurrentHashMap;
    32 import javax.mail.Header;
    33 import javax.mail.internet.InternetAddress;
    34 import javax.mail.internet.MimeUtility;
    35 import org.sonews.daemon.BootstrapConfig;
    36 import org.sonews.util.Log;
    37 import org.sonews.feed.Subscription;
    38 import org.sonews.util.Pair;
    39 
    40 /**
    41  * Database facade class.
    42  * @author Christian Lins
    43  * @since sonews/0.5.0
    44  */
    45 // TODO: Refactor this class to reduce size (e.g. ArticleDatabase GroupDatabase)
    46 public class Database
    47 {
    48 
    49   public static final int MAX_RESTARTS = 3;
    50   
    51   private static final Map<Thread, Database> instances 
    52     = new ConcurrentHashMap<Thread, Database>();
    53   
    54   /**
    55    * @return Instance of the current Database backend. Returns null if an error
    56    * has occurred.
    57    */
    58   public static Database getInstance(boolean create)
    59     throws SQLException
    60   {
    61     if(!instances.containsKey(Thread.currentThread()) && create)
    62     {
    63       Database db = new Database();
    64       db.arise();
    65       instances.put(Thread.currentThread(), db);
    66       return db;
    67     }
    68     else
    69     {
    70       return instances.get(Thread.currentThread());
    71     }
    72   }
    73   
    74   public static Database getInstance()
    75     throws SQLException
    76   {
    77     return getInstance(true);
    78   }
    79   
    80   private Connection        conn = null;
    81   private PreparedStatement pstmtAddArticle1 = null;
    82   private PreparedStatement pstmtAddArticle2 = null;
    83   private PreparedStatement pstmtAddArticle3 = null;
    84   private PreparedStatement pstmtAddArticle4 = null;
    85   private PreparedStatement pstmtAddGroup0   = null;
    86   private PreparedStatement pstmtAddEvent = null;
    87   private PreparedStatement pstmtCountArticles = null;
    88   private PreparedStatement pstmtCountGroups   = null;
    89   private PreparedStatement pstmtDeleteArticle0 = null;
    90   private PreparedStatement pstmtGetArticle0 = null;
    91   private PreparedStatement pstmtGetArticle1 = null;
    92   private PreparedStatement pstmtGetArticleHeaders  = null;
    93   private PreparedStatement pstmtGetArticleHeads = null;
    94   private PreparedStatement pstmtGetArticleIDs   = null;
    95   private PreparedStatement pstmtGetArticleIndex    = null;
    96   private PreparedStatement pstmtGetConfigValue = null;
    97   private PreparedStatement pstmtGetEventsCount0 = null;
    98   private PreparedStatement pstmtGetEventsCount1 = null;
    99   private PreparedStatement pstmtGetGroupForList = null;
   100   private PreparedStatement pstmtGetGroup0     = null;
   101   private PreparedStatement pstmtGetGroup1     = null;
   102   private PreparedStatement pstmtGetFirstArticleNumber = null;
   103   private PreparedStatement pstmtGetListForGroup       = null;
   104   private PreparedStatement pstmtGetLastArticleNumber  = null;
   105   private PreparedStatement pstmtGetMaxArticleID       = null;
   106   private PreparedStatement pstmtGetMaxArticleIndex    = null;
   107   private PreparedStatement pstmtGetPostingsCount      = null;
   108   private PreparedStatement pstmtGetSubscriptions  = null;
   109   private PreparedStatement pstmtIsArticleExisting = null;
   110   private PreparedStatement pstmtIsGroupExisting = null;
   111   private PreparedStatement pstmtSetConfigValue0 = null;
   112   private PreparedStatement pstmtSetConfigValue1 = null;
   113   
   114   /** How many times the database connection was reinitialized */
   115   private int restarts = 0;
   116   
   117   /**
   118    * Rises the database: reconnect and recreate all prepared statements.
   119    * @throws java.lang.SQLException
   120    */
   121   private void arise()
   122     throws SQLException
   123   {
   124     try
   125     {
   126       // Load database driver
   127       Class.forName(
   128               BootstrapConfig.getInstance().get(BootstrapConfig.STORAGE_DBMSDRIVER, "java.lang.Object"));
   129 
   130       // Establish database connection
   131       this.conn = DriverManager.getConnection(
   132               BootstrapConfig.getInstance().get(BootstrapConfig.STORAGE_DATABASE, "<not specified>"),
   133               BootstrapConfig.getInstance().get(BootstrapConfig.STORAGE_USER, "root"),
   134               BootstrapConfig.getInstance().get(BootstrapConfig.STORAGE_PASSWORD, ""));
   135 
   136       this.conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
   137       if(this.conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE)
   138       {
   139         Log.msg("Warning: Database is NOT fully serializable!", false);
   140       }
   141 
   142       // Prepare statements for method addArticle()
   143       this.pstmtAddArticle1 = conn.prepareStatement(
   144         "INSERT INTO articles (article_id, body) VALUES(?, ?)");
   145       this.pstmtAddArticle2 = conn.prepareStatement(
   146         "INSERT INTO headers (article_id, header_key, header_value, header_index) " +
   147         "VALUES (?, ?, ?, ?)");
   148       this.pstmtAddArticle3 = conn.prepareStatement(
   149         "INSERT INTO postings (group_id, article_id, article_index)" +
   150         "VALUES (?, ?, ?)");
   151       this.pstmtAddArticle4 = conn.prepareStatement(
   152         "INSERT INTO article_ids (article_id, message_id) VALUES (?, ?)");
   153 
   154       // Prepare statement for method addStatValue()
   155       this.pstmtAddEvent = conn.prepareStatement(
   156         "INSERT INTO events VALUES (?, ?, ?)");
   157      
   158       // Prepare statement for method addGroup()
   159       this.pstmtAddGroup0 = conn.prepareStatement(
   160         "INSERT INTO groups (name, flags) VALUES (?, ?)");
   161       
   162       // Prepare statement for method countArticles()
   163       this.pstmtCountArticles = conn.prepareStatement(
   164         "SELECT Count(article_id) FROM article_ids");
   165       
   166       // Prepare statement for method countGroups()
   167       this.pstmtCountGroups = conn.prepareStatement(
   168         "SELECT Count(group_id) FROM groups WHERE " +
   169         "flags & " + Group.DELETED + " = 0");
   170       
   171       // Prepare statements for method delete(article)
   172       this.pstmtDeleteArticle0 = conn.prepareStatement(
   173         "DELETE FROM articles WHERE article_id = " +
   174         "(SELECT article_id FROM article_ids WHERE message_id = ?)");
   175 
   176       // Prepare statements for methods getArticle()
   177       this.pstmtGetArticle0 = conn.prepareStatement(
   178         "SELECT * FROM articles  WHERE article_id = " +
   179         "(SELECT article_id FROM article_ids WHERE message_id = ?)");
   180       this.pstmtGetArticle1 = conn.prepareStatement(
   181         "SELECT * FROM articles WHERE article_id = " +
   182         "(SELECT article_id FROM postings WHERE " +
   183         "article_index = ? AND group_id = ?)");
   184       
   185       // Prepare statement for method getArticleHeaders()
   186       this.pstmtGetArticleHeaders = conn.prepareStatement(
   187         "SELECT header_key, header_value FROM headers WHERE article_id = ? " +
   188         "ORDER BY header_index ASC");
   189       
   190       this.pstmtGetArticleIDs = conn.prepareStatement(
   191         "SELECT article_index FROM postings WHERE group_id = ?");
   192       
   193       // Prepare statement for method getArticleIndex
   194       this.pstmtGetArticleIndex = conn.prepareStatement(
   195               "SELECT article_index FROM postings WHERE " +
   196               "article_id = (SELECT article_id FROM article_ids " +
   197               "WHERE message_id = ?) " +
   198               " AND group_id = ?");
   199 
   200       // Prepare statements for method getArticleHeads()
   201       this.pstmtGetArticleHeads = conn.prepareStatement(
   202         "SELECT article_id, article_index FROM postings WHERE " +
   203         "postings.group_id = ? AND article_index >= ? AND " +
   204         "article_index <= ?");
   205 
   206       // Prepare statements for method getConfigValue()
   207       this.pstmtGetConfigValue = conn.prepareStatement(
   208         "SELECT config_value FROM config WHERE config_key = ?");
   209 
   210       // Prepare statements for method getEventsCount()
   211       this.pstmtGetEventsCount0 = conn.prepareStatement(
   212         "SELECT Count(*) FROM events WHERE event_key = ? AND " +
   213         "event_time >= ? AND event_time < ?");
   214 
   215       this.pstmtGetEventsCount1 = conn.prepareStatement(
   216         "SELECT Count(*) FROM events WHERE event_key = ? AND " +
   217         "event_time >= ? AND event_time < ? AND group_id = ?");
   218       
   219       // Prepare statement for method getGroupForList()
   220       this.pstmtGetGroupForList = conn.prepareStatement(
   221         "SELECT name FROM groups INNER JOIN groups2list " +
   222         "ON groups.group_id = groups2list.group_id " +
   223         "WHERE groups2list.listaddress = ?");
   224 
   225       // Prepare statement for method getGroup()
   226       this.pstmtGetGroup0 = conn.prepareStatement(
   227         "SELECT group_id, flags FROM groups WHERE Name = ?");
   228       this.pstmtGetGroup1 = conn.prepareStatement(
   229         "SELECT name FROM groups WHERE group_id = ?");
   230 
   231       // Prepare statement for method getLastArticleNumber()
   232       this.pstmtGetLastArticleNumber = conn.prepareStatement(
   233         "SELECT Max(article_index) FROM postings WHERE group_id = ?");
   234 
   235       // Prepare statement for method getListForGroup()
   236       this.pstmtGetListForGroup = conn.prepareStatement(
   237         "SELECT listaddress FROM groups2list INNER JOIN groups " +
   238         "ON groups.group_id = groups2list.group_id WHERE name = ?");
   239 
   240       // Prepare statement for method getMaxArticleID()
   241       this.pstmtGetMaxArticleID = conn.prepareStatement(
   242         "SELECT Max(article_id) FROM articles");
   243       
   244       // Prepare statement for method getMaxArticleIndex()
   245       this.pstmtGetMaxArticleIndex = conn.prepareStatement(
   246         "SELECT Max(article_index) FROM postings WHERE group_id = ?");
   247       
   248       // Prepare statement for method getFirstArticleNumber()
   249       this.pstmtGetFirstArticleNumber = conn.prepareStatement(
   250         "SELECT Min(article_index) FROM postings WHERE group_id = ?");
   251       
   252       // Prepare statement for method getPostingsCount()
   253       this.pstmtGetPostingsCount = conn.prepareStatement(
   254         "SELECT Count(*) FROM postings NATURAL JOIN groups " +
   255         "WHERE groups.name = ?");
   256       
   257       // Prepare statement for method getSubscriptions()
   258       this.pstmtGetSubscriptions = conn.prepareStatement(
   259         "SELECT host, port, name FROM peers NATURAL JOIN " +
   260         "peer_subscriptions NATURAL JOIN groups WHERE feedtype = ?");
   261       
   262       // Prepare statement for method isArticleExisting()
   263       this.pstmtIsArticleExisting = conn.prepareStatement(
   264         "SELECT Count(article_id) FROM article_ids WHERE message_id = ?");
   265       
   266       // Prepare statement for method isGroupExisting()
   267       this.pstmtIsGroupExisting = conn.prepareStatement(
   268         "SELECT * FROM groups WHERE name = ?");
   269       
   270       // Prepare statement for method setConfigValue()
   271       this.pstmtSetConfigValue0 = conn.prepareStatement(
   272         "DELETE FROM config WHERE config_key = ?");
   273       this.pstmtSetConfigValue1 = conn.prepareStatement(
   274         "INSERT INTO config VALUES(?, ?)");
   275     }
   276     catch(ClassNotFoundException ex)
   277     {
   278       throw new Error("JDBC Driver not found!", ex);
   279     }
   280   }
   281   
   282   /**
   283    * Adds an article to the database.
   284    * @param article
   285    * @return
   286    * @throws java.sql.SQLException
   287    */
   288   public void addArticle(final Article article)
   289     throws SQLException
   290   {
   291     try
   292     {
   293       this.conn.setAutoCommit(false);
   294 
   295       int newArticleID = getMaxArticleID() + 1;
   296 
   297       // Fill prepared statement with values;
   298       // writes body to article table
   299       pstmtAddArticle1.setInt(1, newArticleID);
   300       pstmtAddArticle1.setBytes(2, article.getBody().getBytes());
   301       pstmtAddArticle1.execute();
   302 
   303       // Add headers
   304       Enumeration headers = article.getAllHeaders();
   305       for(int n = 0; headers.hasMoreElements(); n++)
   306       {
   307         Header header = (Header)headers.nextElement();
   308         pstmtAddArticle2.setInt(1, newArticleID);
   309         pstmtAddArticle2.setString(2, header.getName().toLowerCase());
   310         pstmtAddArticle2.setString(3, 
   311           header.getValue().replaceAll("[\r\n]", ""));
   312         pstmtAddArticle2.setInt(4, n);
   313         pstmtAddArticle2.execute();
   314       }
   315       
   316       // For each newsgroup add a reference
   317       List<Group> groups = article.getGroups();
   318       for(Group group : groups)
   319       {
   320         pstmtAddArticle3.setLong(1, group.getID());
   321         pstmtAddArticle3.setInt(2, newArticleID);
   322         pstmtAddArticle3.setLong(3, getMaxArticleIndex(group.getID()) + 1);
   323         pstmtAddArticle3.execute();
   324       }
   325       
   326       // Write message-id to article_ids table
   327       this.pstmtAddArticle4.setInt(1, newArticleID);
   328       this.pstmtAddArticle4.setString(2, article.getMessageID());
   329       this.pstmtAddArticle4.execute();
   330 
   331       this.conn.commit();
   332       this.conn.setAutoCommit(true);
   333 
   334       this.restarts = 0; // Reset error count
   335     }
   336     catch(SQLException ex)
   337     {
   338       try
   339       {
   340         this.conn.rollback();  // Rollback changes
   341       }
   342       catch(SQLException ex2)
   343       {
   344         Log.msg("Rollback of addArticle() failed: " + ex2, false);
   345       }
   346       
   347       try
   348       {
   349         this.conn.setAutoCommit(true); // and release locks
   350       }
   351       catch(SQLException ex2)
   352       {
   353         Log.msg("setAutoCommit(true) of addArticle() failed: " + ex2, false);
   354       }
   355 
   356       restartConnection(ex);
   357       addArticle(article);
   358     }
   359   }
   360   
   361   /**
   362    * Adds a group to the Database. This method is not accessible via NNTP.
   363    * @param name
   364    * @throws java.sql.SQLException
   365    */
   366   public void addGroup(String name, int flags)
   367     throws SQLException
   368   {
   369     try
   370     {
   371       this.conn.setAutoCommit(false);
   372       pstmtAddGroup0.setString(1, name);
   373       pstmtAddGroup0.setInt(2, flags);
   374 
   375       pstmtAddGroup0.executeUpdate();
   376       this.conn.commit();
   377       this.conn.setAutoCommit(true);
   378       this.restarts = 0; // Reset error count
   379     }
   380     catch(SQLException ex)
   381     {
   382       this.conn.rollback();
   383       this.conn.setAutoCommit(true);
   384       restartConnection(ex);
   385       addGroup(name, flags);
   386     }
   387   }
   388   
   389   public void addEvent(long time, byte type, long gid)
   390     throws SQLException
   391   {
   392     try
   393     {
   394       this.conn.setAutoCommit(false);
   395       this.pstmtAddEvent.setLong(1, time);
   396       this.pstmtAddEvent.setInt(2, type);
   397       this.pstmtAddEvent.setLong(3, gid);
   398       this.pstmtAddEvent.executeUpdate();
   399       this.conn.commit();
   400       this.conn.setAutoCommit(true);
   401       this.restarts = 0;
   402     }
   403     catch(SQLException ex)
   404     {
   405       this.conn.rollback();
   406       this.conn.setAutoCommit(true);
   407 
   408       restartConnection(ex);
   409       addEvent(time, type, gid);
   410     }
   411   }
   412   
   413   public int countArticles()
   414     throws SQLException
   415   {
   416     ResultSet rs = null;
   417 
   418     try
   419     {
   420       rs = this.pstmtCountArticles.executeQuery();
   421       if(rs.next())
   422       {
   423         return rs.getInt(1);
   424       }
   425       else
   426       {
   427         return -1;
   428       }
   429     }
   430     catch(SQLException ex)
   431     {
   432       restartConnection(ex);
   433       return countArticles();
   434     }
   435     finally
   436     {
   437       if(rs != null)
   438       {
   439         rs.close();
   440         restarts = 0;
   441       }
   442     }
   443   }
   444   
   445   public int countGroups()
   446     throws SQLException
   447   {
   448     ResultSet rs = null;
   449 
   450     try
   451     {
   452       rs = this.pstmtCountGroups.executeQuery();
   453       if(rs.next())
   454       {
   455         return rs.getInt(1);
   456       }
   457       else
   458       {
   459         return -1;
   460       }
   461     }
   462     catch(SQLException ex)
   463     {
   464       restartConnection(ex);
   465       return countGroups();
   466     }
   467     finally
   468     {
   469       if(rs != null)
   470       {
   471         rs.close();
   472         restarts = 0;
   473       }
   474     }
   475   }
   476   
   477   public void delete(final String messageID)
   478     throws SQLException
   479   {
   480     try
   481     {
   482       this.conn.setAutoCommit(false);
   483       
   484       this.pstmtDeleteArticle0.setString(1, messageID);
   485       ResultSet rs = this.pstmtDeleteArticle0.executeQuery();
   486       rs.next();
   487       
   488       // We trust the ON DELETE CASCADE functionality to delete
   489       // orphaned references
   490       
   491       this.conn.commit();
   492       this.conn.setAutoCommit(true);
   493     }
   494     catch(SQLException ex)
   495     {
   496       throw ex;
   497     }
   498   }
   499   
   500   public Article getArticle(String messageID)
   501     throws SQLException
   502   {
   503     ResultSet rs = null;
   504     try
   505     {
   506       pstmtGetArticle0.setString(1, messageID);
   507       rs = pstmtGetArticle0.executeQuery();
   508 
   509       if(!rs.next())
   510       {
   511         return null;
   512       }
   513       else
   514       {
   515         String body     = new String(rs.getBytes("body"));
   516         String headers  = getArticleHeaders(rs.getInt("article_id"));
   517         return new Article(headers, body);
   518       }
   519     }
   520     catch(SQLException ex)
   521     {
   522       restartConnection(ex);
   523       return getArticle(messageID);
   524     }
   525     finally
   526     {
   527       if(rs != null)
   528       {
   529         rs.close();
   530         restarts = 0; // Reset error count
   531       }
   532     }
   533   }
   534   
   535   /**
   536    * Retrieves an article by its ID.
   537    * @param articleID
   538    * @return
   539    * @throws java.sql.SQLException
   540    */
   541   public Article getArticle(long articleIndex, long gid)
   542     throws SQLException
   543   {  
   544     ResultSet rs = null;
   545 
   546     try
   547     {
   548       this.pstmtGetArticle1.setLong(1, articleIndex);
   549       this.pstmtGetArticle1.setLong(2, gid);
   550 
   551       rs = this.pstmtGetArticle1.executeQuery();
   552 
   553       if(rs.next())
   554       {
   555         String body    = new String(rs.getBytes("body"));
   556         String headers = getArticleHeaders(rs.getInt("article_id"));
   557         return new Article(headers, body);
   558       }
   559       else
   560       {
   561         return null;
   562       }
   563     }
   564     catch(SQLException ex)
   565     {
   566       restartConnection(ex);
   567       return getArticle(articleIndex, gid);
   568     }
   569     finally
   570     {
   571       if(rs != null)
   572       {
   573         rs.close();
   574         restarts = 0;
   575       }
   576     }
   577   }
   578   
   579   public String getArticleHeaders(long articleID)
   580     throws SQLException
   581   {
   582     ResultSet rs = null;
   583     
   584     try
   585     {
   586       this.pstmtGetArticleHeaders.setLong(1, articleID);
   587       rs = this.pstmtGetArticleHeaders.executeQuery();
   588       
   589       StringBuilder buf = new StringBuilder();
   590       if(rs.next())
   591       {
   592         for(;;)
   593         {
   594           buf.append(rs.getString(1)); // key
   595           buf.append(": ");
   596           String foldedValue = MimeUtility.fold(0, rs.getString(2));
   597           buf.append(foldedValue); // value
   598           if(rs.next())
   599           {
   600             buf.append("\r\n");
   601           }
   602           else
   603           {
   604             break;
   605           }
   606         }
   607       }
   608       
   609       return buf.toString();
   610     }
   611     catch(SQLException ex)
   612     {
   613       restartConnection(ex);
   614       return getArticleHeaders(articleID);
   615     }
   616     finally
   617     {
   618       if(rs != null)
   619         rs.close();
   620     }
   621   }
   622   
   623   public long getArticleIndex(Article article, Group group)
   624     throws SQLException
   625   {
   626     ResultSet rs = null;
   627 
   628     try
   629     {
   630       this.pstmtGetArticleIndex.setString(1, article.getMessageID());
   631       this.pstmtGetArticleIndex.setLong(2, group.getID());
   632       
   633       rs = this.pstmtGetArticleIndex.executeQuery();
   634       if(rs.next())
   635       {
   636         return rs.getLong(1);
   637       }
   638       else
   639       {
   640         return -1;
   641       }
   642     }
   643     catch(SQLException ex)
   644     {
   645       restartConnection(ex);
   646       return getArticleIndex(article, group);
   647     }
   648     finally
   649     {
   650       if(rs != null)
   651         rs.close();
   652     }
   653   }
   654   
   655   /**
   656    * Returns a list of Long/Article Pairs.
   657    * @throws java.sql.SQLException
   658    */
   659   public List<Pair<Long, ArticleHead>> getArticleHeads(Group group, int first, int last)
   660     throws SQLException
   661   {
   662     ResultSet rs = null;
   663 
   664     try
   665     {
   666       this.pstmtGetArticleHeads.setLong(1, group.getID());
   667       this.pstmtGetArticleHeads.setInt(2, first);
   668       this.pstmtGetArticleHeads.setInt(3, last);
   669       rs = pstmtGetArticleHeads.executeQuery();
   670 
   671       List<Pair<Long, ArticleHead>> articles 
   672         = new ArrayList<Pair<Long, ArticleHead>>();
   673 
   674       while (rs.next())
   675       {
   676         long aid  = rs.getLong("article_id");
   677         long aidx = rs.getLong("article_index");
   678         String headers = getArticleHeaders(aid);
   679         articles.add(new Pair<Long, ArticleHead>(aidx, 
   680                         new ArticleHead(headers)));
   681       }
   682 
   683       return articles;
   684     }
   685     catch(SQLException ex)
   686     {
   687       restartConnection(ex);
   688       return getArticleHeads(group, first, last);
   689     }
   690     finally
   691     {
   692       if(rs != null)
   693         rs.close();
   694     }
   695   }
   696   
   697   public List<Long> getArticleNumbers(long gid)
   698     throws SQLException
   699   {
   700     ResultSet rs = null;
   701     try
   702     {
   703       List<Long> ids = new ArrayList<Long>();
   704       this.pstmtGetArticleIDs.setLong(1, gid);
   705       rs = this.pstmtGetArticleIDs.executeQuery();
   706       while(rs.next())
   707       {
   708         ids.add(rs.getLong(1));
   709       }
   710       return ids;
   711     }
   712     catch(SQLException ex)
   713     {
   714       restartConnection(ex);
   715       return getArticleNumbers(gid);
   716     }
   717     finally
   718     {
   719       if(rs != null)
   720       {
   721         rs.close();
   722         restarts = 0; // Clear the restart count after successful request
   723       }
   724     }
   725   }
   726   
   727   public String getConfigValue(String key)
   728     throws SQLException
   729   {
   730     ResultSet rs = null;
   731     try
   732     {
   733       this.pstmtGetConfigValue.setString(1, key);
   734 
   735       rs = this.pstmtGetConfigValue.executeQuery();
   736       if(rs.next())
   737       {
   738         return rs.getString(1); // First data on index 1 not 0
   739       }
   740       else
   741       {
   742         return null;
   743       }
   744     }
   745     catch(SQLException ex)
   746     {
   747       restartConnection(ex);
   748       return getConfigValue(key);
   749     }
   750     finally
   751     {
   752       if(rs != null)
   753       {
   754         rs.close();
   755         restarts = 0; // Clear the restart count after successful request
   756       }
   757     }
   758   }
   759   
   760   public int getEventsCount(byte type, long start, long end, Group group)
   761     throws SQLException
   762   {
   763     ResultSet rs = null;
   764     
   765     try
   766     {
   767       if(group == null)
   768       {
   769         this.pstmtGetEventsCount0.setInt(1, type);
   770         this.pstmtGetEventsCount0.setLong(2, start);
   771         this.pstmtGetEventsCount0.setLong(3, end);
   772         rs = this.pstmtGetEventsCount0.executeQuery();
   773       }
   774       else
   775       {
   776         this.pstmtGetEventsCount1.setInt(1, type);
   777         this.pstmtGetEventsCount1.setLong(2, start);
   778         this.pstmtGetEventsCount1.setLong(3, end);
   779         this.pstmtGetEventsCount1.setLong(4, group.getID());
   780         rs = this.pstmtGetEventsCount1.executeQuery();
   781       }
   782       
   783       if(rs.next())
   784       {
   785         return rs.getInt(1);
   786       }
   787       else
   788       {
   789         return -1;
   790       }
   791     }
   792     catch(SQLException ex)
   793     {
   794       restartConnection(ex);
   795       return getEventsCount(type, start, end, group);
   796     }
   797     finally
   798     {
   799       if(rs != null)
   800         rs.close();
   801     }
   802   }
   803   
   804   /**
   805    * Reads all Groups from the Database.
   806    * @return
   807    * @throws java.sql.SQLException
   808    */
   809   public List<Group> getGroups()
   810     throws SQLException
   811   {
   812     ResultSet   rs;
   813     List<Group> buffer = new ArrayList<Group>();
   814     Statement   stmt   = null;
   815 
   816     try
   817     {
   818       stmt = conn.createStatement();
   819       rs = stmt.executeQuery("SELECT * FROM groups ORDER BY name");
   820 
   821       while(rs.next())
   822       {
   823         String name  = rs.getString("name");
   824         long   id    = rs.getLong("group_id");
   825         int    flags = rs.getInt("flags");
   826         
   827         Group group = new Group(name, id, flags);
   828         buffer.add(group);
   829       }
   830 
   831       return buffer;
   832     }
   833     catch(SQLException ex)
   834     {
   835       restartConnection(ex);
   836       return getGroups();
   837     }
   838     finally
   839     {
   840       if(stmt != null)
   841         stmt.close(); // Implicitely closes ResultSets
   842     }
   843   }
   844   
   845   public String getGroupForList(InternetAddress listAddress)
   846     throws SQLException
   847   {
   848     ResultSet rs = null;
   849     
   850     try
   851     {
   852       this.pstmtGetGroupForList.setString(1, listAddress.getAddress());
   853 
   854       rs = this.pstmtGetGroupForList.executeQuery();
   855       if (rs.next())
   856       {
   857         return rs.getString(1);
   858       }
   859       else
   860       {
   861         return null;
   862       }
   863     }
   864     catch(SQLException ex)
   865     {
   866       restartConnection(ex);
   867       return getGroupForList(listAddress);
   868     }
   869     finally
   870     {
   871       if(rs != null)
   872         rs.close();
   873     }
   874   }
   875   
   876   /**
   877    * Returns the Group that is identified by the name.
   878    * @param name
   879    * @return
   880    * @throws java.sql.SQLException
   881    */
   882   public Group getGroup(String name)
   883     throws SQLException
   884   {
   885     ResultSet rs = null;
   886     
   887     try
   888     {
   889       this.pstmtGetGroup0.setString(1, name);
   890       rs = this.pstmtGetGroup0.executeQuery();
   891 
   892       if (!rs.next())
   893       {
   894         return null;
   895       }
   896       else
   897       {
   898         long id = rs.getLong("group_id");
   899         int flags = rs.getInt("flags");
   900         return new Group(name, id, flags);
   901       }
   902     }
   903     catch(SQLException ex)
   904     {
   905       restartConnection(ex);
   906       return getGroup(name);
   907     }
   908     finally
   909     {
   910       if(rs != null)
   911         rs.close();
   912     }
   913   }
   914   
   915   public String getListForGroup(String group)
   916     throws SQLException
   917   {
   918     ResultSet rs = null;
   919 
   920     try
   921     {
   922       this.pstmtGetListForGroup.setString(1, group);
   923       rs = this.pstmtGetListForGroup.executeQuery();
   924       if (rs.next())
   925       {
   926         return rs.getString(1);
   927       }
   928       else
   929       {
   930         return null;
   931       }
   932     }
   933     catch(SQLException ex)
   934     {
   935       restartConnection(ex);
   936       return getListForGroup(group);
   937     }
   938     finally
   939     {
   940       if(rs != null)
   941         rs.close();
   942     }
   943   }
   944   
   945   private int getMaxArticleIndex(long groupID)
   946     throws SQLException
   947   {
   948     ResultSet rs    = null;
   949 
   950     try
   951     {
   952       this.pstmtGetMaxArticleIndex.setLong(1, groupID);
   953       rs = this.pstmtGetMaxArticleIndex.executeQuery();
   954 
   955       int maxIndex = 0;
   956       if (rs.next())
   957       {
   958         maxIndex = rs.getInt(1);
   959       }
   960 
   961       return maxIndex;
   962     }
   963     catch(SQLException ex)
   964     {
   965       restartConnection(ex);
   966       return getMaxArticleIndex(groupID);
   967     }
   968     finally
   969     {
   970       if(rs != null)
   971         rs.close();
   972     }
   973   }
   974   
   975   private int getMaxArticleID()
   976     throws SQLException
   977   {
   978     ResultSet rs    = null;
   979 
   980     try
   981     {
   982       rs = this.pstmtGetMaxArticleID.executeQuery();
   983 
   984       int maxIndex = 0;
   985       if (rs.next())
   986       {
   987         maxIndex = rs.getInt(1);
   988       }
   989 
   990       return maxIndex;
   991     }
   992     catch(SQLException ex)
   993     {
   994       restartConnection(ex);
   995       return getMaxArticleID();
   996     }
   997     finally
   998     {
   999       if(rs != null)
  1000         rs.close();
  1001     }
  1002   }
  1003   
  1004   public int getLastArticleNumber(Group group)
  1005     throws SQLException
  1006   {
  1007     ResultSet rs = null;
  1008 
  1009     try
  1010     {
  1011       this.pstmtGetLastArticleNumber.setLong(1, group.getID());
  1012       rs = this.pstmtGetLastArticleNumber.executeQuery();
  1013       if (rs.next())
  1014       {
  1015         return rs.getInt(1);
  1016       }
  1017       else
  1018       {
  1019         return 0;
  1020       }
  1021     }
  1022     catch(SQLException ex)
  1023     {
  1024       restartConnection(ex);
  1025       return getLastArticleNumber(group);
  1026     }
  1027     finally
  1028     {
  1029       if(rs != null)
  1030         rs.close();
  1031     }
  1032   }
  1033   
  1034   public int getFirstArticleNumber(Group group)
  1035     throws SQLException
  1036   {
  1037     ResultSet rs = null;
  1038     try
  1039     {
  1040       this.pstmtGetFirstArticleNumber.setLong(1, group.getID());
  1041       rs = this.pstmtGetFirstArticleNumber.executeQuery();
  1042       if(rs.next())
  1043       {
  1044         return rs.getInt(1);
  1045       }
  1046       else
  1047       {
  1048         return 0;
  1049       }
  1050     }
  1051     catch(SQLException ex)
  1052     {
  1053       restartConnection(ex);
  1054       return getFirstArticleNumber(group);
  1055     }
  1056     finally
  1057     {
  1058       if(rs != null)
  1059         rs.close();
  1060     }
  1061   }
  1062   
  1063   /**
  1064    * Returns a group name identified by the given id.
  1065    * @param id
  1066    * @return
  1067    * @throws java.sql.SQLException
  1068    */
  1069   public String getGroup(int id)
  1070     throws SQLException
  1071   {
  1072     ResultSet rs = null;
  1073 
  1074     try
  1075     {
  1076       this.pstmtGetGroup1.setInt(1, id);
  1077       rs = this.pstmtGetGroup1.executeQuery();
  1078 
  1079       if (rs.next())
  1080       {
  1081         return rs.getString(1);
  1082       }
  1083       else
  1084       {
  1085         return null;
  1086       }
  1087     }
  1088     catch(SQLException ex)
  1089     {
  1090       restartConnection(ex);
  1091       return getGroup(id);
  1092     }
  1093     finally
  1094     {
  1095       if(rs != null)
  1096         rs.close();
  1097     }
  1098   }
  1099   
  1100   public double getNumberOfEventsPerHour(int key, long gid)
  1101     throws SQLException
  1102   {
  1103     String gidquery = "";
  1104     if(gid >= 0)
  1105     {
  1106       gidquery = " AND group_id = " + gid;
  1107     }
  1108     
  1109     Statement stmt = null;
  1110     ResultSet rs   = null;
  1111     
  1112     try
  1113     {
  1114       stmt = this.conn.createStatement();
  1115       rs = stmt.executeQuery("SELECT Count(*) / (Max(event_time) - Min(event_time))" +
  1116         " * 1000 * 60 * 60 FROM events WHERE event_key = " + key + gidquery);
  1117       
  1118       if(rs.next())
  1119       {
  1120         restarts = 0; // reset error count
  1121         return rs.getDouble(1);
  1122       }
  1123       else
  1124       {
  1125         return Double.NaN;
  1126       }
  1127     }
  1128     catch(SQLException ex)
  1129     {
  1130       restartConnection(ex);
  1131       return getNumberOfEventsPerHour(key, gid);
  1132     }
  1133     finally
  1134     {
  1135       if(stmt != null)
  1136       {
  1137         stmt.close();
  1138       }
  1139       
  1140       if(rs != null)
  1141       {
  1142         rs.close();
  1143       }
  1144     }
  1145   }
  1146   
  1147   public int getPostingsCount(String groupname)
  1148     throws SQLException
  1149   {
  1150     ResultSet rs = null;
  1151     
  1152     try
  1153     {
  1154       this.pstmtGetPostingsCount.setString(1, groupname);
  1155       rs = this.pstmtGetPostingsCount.executeQuery();
  1156       if(rs.next())
  1157       {
  1158         return rs.getInt(1);
  1159       }
  1160       else
  1161       {
  1162         Log.msg("Warning: Count on postings return nothing!", true);
  1163         return 0;
  1164       }
  1165     }
  1166     catch(SQLException ex)
  1167     {
  1168       restartConnection(ex);
  1169       return getPostingsCount(groupname);
  1170     }
  1171     finally
  1172     {
  1173       if(rs != null)
  1174         rs.close();
  1175     }
  1176   }
  1177   
  1178   public List<Subscription> getSubscriptions(int feedtype)
  1179     throws SQLException
  1180   {
  1181     ResultSet rs = null;
  1182     
  1183     try
  1184     {
  1185       List<Subscription> subs = new ArrayList<Subscription>();
  1186       this.pstmtGetSubscriptions.setInt(1, feedtype);
  1187       rs = this.pstmtGetSubscriptions.executeQuery();
  1188       
  1189       while(rs.next())
  1190       {
  1191         String host  = rs.getString("host");
  1192         String group = rs.getString("name");
  1193         int    port  = rs.getInt("port");
  1194         subs.add(new Subscription(host, port, feedtype, group));
  1195       }
  1196       
  1197       return subs;
  1198     }
  1199     catch(SQLException ex)
  1200     {
  1201       restartConnection(ex);
  1202       return getSubscriptions(feedtype);
  1203     }
  1204     finally
  1205     {
  1206       if(rs != null)
  1207         rs.close();
  1208     }
  1209   }
  1210 
  1211   /**
  1212    * Checks if there is an article with the given messageid in the Database.
  1213    * @param name
  1214    * @return
  1215    * @throws java.sql.SQLException
  1216    */
  1217   public boolean isArticleExisting(String messageID)
  1218     throws SQLException
  1219   {
  1220     ResultSet rs = null;
  1221     
  1222     try
  1223     {
  1224       this.pstmtIsArticleExisting.setString(1, messageID);
  1225       rs = this.pstmtIsArticleExisting.executeQuery();
  1226       return rs.next() && rs.getInt(1) == 1;
  1227     }
  1228     catch(SQLException ex)
  1229     {
  1230       restartConnection(ex);
  1231       return isArticleExisting(messageID);
  1232     }
  1233     finally
  1234     {
  1235       if(rs != null)
  1236         rs.close();
  1237     }
  1238   }
  1239   
  1240   /**
  1241    * Checks if there is a group with the given name in the Database.
  1242    * @param name
  1243    * @return
  1244    * @throws java.sql.SQLException
  1245    */
  1246   public boolean isGroupExisting(String name)
  1247     throws SQLException
  1248   {
  1249     ResultSet rs = null;
  1250     
  1251     try
  1252     {
  1253       this.pstmtIsGroupExisting.setString(1, name);
  1254       rs = this.pstmtIsGroupExisting.executeQuery();
  1255       return rs.next();
  1256     }
  1257     catch(SQLException ex)
  1258     {
  1259       restartConnection(ex);
  1260       return isGroupExisting(name);
  1261     }
  1262     finally
  1263     {
  1264       if(rs != null)
  1265         rs.close();
  1266     }
  1267   }
  1268   
  1269   public void setConfigValue(String key, String value)
  1270     throws SQLException
  1271   {
  1272     try
  1273     {
  1274       conn.setAutoCommit(false);
  1275       this.pstmtSetConfigValue0.setString(1, key);
  1276       this.pstmtSetConfigValue0.execute();
  1277       this.pstmtSetConfigValue1.setString(1, key);
  1278       this.pstmtSetConfigValue1.setString(2, value);
  1279       this.pstmtSetConfigValue1.execute();
  1280       conn.commit();
  1281       conn.setAutoCommit(true);
  1282     }
  1283     catch(SQLException ex)
  1284     {
  1285       restartConnection(ex);
  1286       setConfigValue(key, value);
  1287     }
  1288   }
  1289   
  1290   /**
  1291    * Closes the Database connection.
  1292    */
  1293   public void shutdown()
  1294     throws SQLException
  1295   {
  1296     if(this.conn != null)
  1297     {
  1298       this.conn.close();
  1299     }
  1300   }
  1301   
  1302   private void restartConnection(SQLException cause)
  1303     throws SQLException
  1304   {
  1305     restarts++;
  1306     Log.msg(Thread.currentThread() 
  1307       + ": Database connection was closed (restart " + restarts + ").", false);
  1308     
  1309     if(restarts >= MAX_RESTARTS)
  1310     {
  1311       // Delete the current, probably broken Database instance.
  1312       // So no one can use the instance any more.
  1313       Database.instances.remove(Thread.currentThread());
  1314       
  1315       // Throw the exception upwards
  1316       throw cause;
  1317     }
  1318     
  1319     try
  1320     {
  1321       Thread.sleep(1500L * restarts);
  1322     }
  1323     catch(InterruptedException ex)
  1324     {
  1325       Log.msg("Interrupted: " + ex.getMessage(), false);
  1326     }
  1327     
  1328     // Try to properly close the old database connection
  1329     try
  1330     {
  1331       if(this.conn != null)
  1332       {
  1333         this.conn.close();
  1334       }
  1335     }
  1336     catch(SQLException ex)
  1337     {
  1338       Log.msg(ex.getMessage(), true);
  1339     }
  1340     
  1341     try
  1342     {
  1343       // Try to reinitialize database connection
  1344       arise();
  1345     }
  1346     catch(SQLException ex)
  1347     {
  1348       Log.msg(ex.getMessage(), true);
  1349       restartConnection(ex);
  1350     }
  1351   }
  1352 
  1353 }