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