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