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