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