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