# HG changeset patch
# User cli
# Date 1283095738 -7200
# Node ID ed84c8bdd87b32c4e8a75b894a8a5f62cb9c9fee
# Parent  9f0b95aafaa3e861622d4cc8e90dde01f1c4bb54
Moving source files into src/-subdir.

diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/Main.java
--- a/org/sonews/Main.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,198 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews;
-
-import java.sql.Driver;
-import java.sql.DriverManager;
-import java.util.Enumeration;
-import java.util.Date;
-import java.util.logging.Level;
-import org.sonews.config.Config;
-import org.sonews.daemon.ChannelLineBuffers;
-import org.sonews.daemon.CommandSelector;
-import org.sonews.daemon.Connections;
-import org.sonews.daemon.NNTPDaemon;
-import org.sonews.feed.FeedManager;
-import org.sonews.mlgw.MailPoller;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.storage.StorageManager;
-import org.sonews.storage.StorageProvider;
-import org.sonews.util.Log;
-import org.sonews.util.Purger;
-import org.sonews.util.io.Resource;
-
-/**
- * Startup class of the daemon.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public final class Main
-{
-  
-  private Main()
-  {
-  }
-
-  /** Version information of the sonews daemon */
-  public static final String VERSION = "sonews/1.1.0";
-  public static final Date   STARTDATE = new Date();
-  
-  /**
-   * The main entrypoint.
-   * @param args
-   * @throws Exception
-   */
-  public static void main(String[] args) throws Exception
-  {
-    System.out.println(VERSION);
-    Thread.currentThread().setName("Mainthread");
-
-    // Command line arguments
-    boolean feed    = false;  // Enable feeding?
-    boolean mlgw    = false;  // Enable Mailinglist gateway?
-    int     port    = -1;
-    
-    for(int n = 0; n < args.length; n++)
-    {
-      if(args[n].equals("-c") || args[n].equals("-config"))
-      {
-        Config.inst().set(Config.LEVEL_CLI, Config.CONFIGFILE, args[++n]);
-        System.out.println("Using config file " + args[n]);
-      }
-      else if(args[n].equals("-dumpjdbcdriver"))
-      {
-        System.out.println("Available JDBC drivers:");
-        Enumeration<Driver> drvs =  DriverManager.getDrivers();
-        while(drvs.hasMoreElements())
-        {
-          System.out.println(drvs.nextElement());
-        }
-        return;
-      }
-      else if(args[n].equals("-feed"))
-      {
-        feed = true;
-      }
-      else if(args[n].equals("-h") || args[n].equals("-help"))
-      {
-        printArguments();
-        return;
-      }
-      else if(args[n].equals("-mlgw"))
-      {
-        mlgw = true;
-      }
-      else if(args[n].equals("-p"))
-      {
-        port = Integer.parseInt(args[++n]);
-      }
-      else if(args[n].equals("-plugin"))
-      {
-        System.out.println("Warning: -plugin-storage is not implemented!");
-      }
-      else if(args[n].equals("-plugin-command"))
-      {
-        try
-        {
-          CommandSelector.addCommandHandler(args[++n]);
-        }
-        catch(Exception ex)
-        {
-          Log.get().warning("Could not load command plugin: " + args[n]);
-          Log.get().log(Level.INFO, "Main.java", ex);
-        }
-      }
-      else if(args[n].equals("-plugin-storage"))
-      {
-        System.out.println("Warning: -plugin-storage is not implemented!");
-      }
-      else if(args[n].equals("-v") || args[n].equals("-version"))
-      {
-        // Simply return as the version info is already printed above
-        return;
-      }
-    }
-    
-    // Try to load the JDBCDatabase;
-    // Do NOT USE BackendConfig or Log classes before this point because they require
-    // a working JDBCDatabase connection.
-    try
-    {
-      StorageProvider sprov =
-        StorageManager.loadProvider("org.sonews.storage.impl.JDBCDatabaseProvider");
-      StorageManager.enableProvider(sprov);
-      
-      // Make sure some elementary groups are existing
-      if(!StorageManager.current().isGroupExisting("control"))
-      {
-        StorageManager.current().addGroup("control", 0);
-        Log.get().info("Group 'control' created.");
-      }
-    }
-    catch(StorageBackendException ex)
-    {
-      ex.printStackTrace();
-      System.err.println("Database initialization failed with " + ex.toString());
-      System.err.println("Make sure you have specified the correct database" +
-        " settings in sonews.conf!");
-      return;
-    }
-    
-    ChannelLineBuffers.allocateDirect();
-    
-    // Add shutdown hook
-    Runtime.getRuntime().addShutdownHook(new ShutdownHook());
-    
-    // Start the listening daemon
-    if(port <= 0)
-    {
-      port = Config.inst().get(Config.PORT, 119);
-    }
-    final NNTPDaemon daemon = NNTPDaemon.createInstance(port);
-    daemon.start();
-    
-    // Start Connections purger thread...
-    Connections.getInstance().start();
-    
-    // Start mailinglist gateway...
-    if(mlgw)
-    {
-      new MailPoller().start();
-    }
-    
-    // Start feeds
-    if(feed)
-    {
-      FeedManager.startFeeding();
-    }
-
-    Purger purger = new Purger();
-    purger.start();
-    
-    // Wait for main thread to exit (setDaemon(false))
-    daemon.join();
-  }
-  
-  private static void printArguments()
-  {
-    String usage = Resource.getAsString("helpers/usage", true);
-    System.out.println(usage);
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/ShutdownHook.java
--- a/org/sonews/ShutdownHook.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews;
-
-import java.sql.SQLException;
-import java.util.Map;
-import org.sonews.daemon.AbstractDaemon;
-
-/**
- * Will force all other threads to shutdown cleanly.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-class ShutdownHook extends Thread
-{
-
-  /**
-   * Called when the JVM exits.
-   */
-  @Override
-  public void run()
-  {
-    System.out.println("sonews: Trying to shutdown all threads...");
-
-    Map<Thread, StackTraceElement[]> threadsMap = Thread.getAllStackTraces();
-    for(Thread thread : threadsMap.keySet())
-    {
-      // Interrupt the thread if it's a AbstractDaemon
-      AbstractDaemon daemon;
-      if(thread instanceof AbstractDaemon && thread.isAlive())
-      {
-        try
-        {
-          daemon = (AbstractDaemon)thread;
-          daemon.shutdownNow();
-        }
-        catch(SQLException ex)
-        {
-          System.out.println("sonews: " + ex);
-        }
-      }
-    }
-    
-    for(Thread thread : threadsMap.keySet())
-    {
-      AbstractDaemon daemon;
-      if(thread instanceof AbstractDaemon && thread.isAlive())
-      {
-        daemon = (AbstractDaemon)thread;
-        System.out.println("sonews: Waiting for " + daemon + " to exit...");
-        try
-        {
-          daemon.join(500);
-        }
-        catch(InterruptedException ex)
-        {
-          System.out.println(ex.getLocalizedMessage());
-        }
-      }
-    }
-    
-    // We have notified all not-sleeping AbstractDaemons of the shutdown;
-    // all other threads can be simply purged on VM shutdown
-    
-    System.out.println("sonews: Clean shutdown.");
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/acl/AccessControl.java
--- a/org/sonews/acl/AccessControl.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.acl;
-
-/**
- *
- * @author Christian Lins
- * @since sonews/1.1
- */
-public interface AccessControl
-{
-
-  boolean hasPermission(String user, char[] secret, String permission);
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/acl/AuthInfoCommand.java
--- a/org/sonews/acl/AuthInfoCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.acl;
-
-import java.io.IOException;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.daemon.command.Command;
-import org.sonews.storage.StorageBackendException;
-
-/**
- *
- * @author Christian Lins
- * @since sonews/1.1
- */
-public class AuthInfoCommand implements Command
-{
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    throw new UnsupportedOperationException("Not supported yet.");
-  }
-
-  @Override
-  public boolean hasFinished()
-  {
-    throw new UnsupportedOperationException("Not supported yet.");
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    throw new UnsupportedOperationException("Not supported yet.");
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    throw new UnsupportedOperationException("Not supported yet.");
-  }
-
-  @Override
-  public void processLine(NNTPConnection conn, String line, byte[] rawLine) throws IOException, StorageBackendException
-  {
-    throw new UnsupportedOperationException("Not supported yet.");
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/config/AbstractConfig.java
--- a/org/sonews/config/AbstractConfig.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.config;
-
-/**
- * Base class for Config and BootstrapConfig.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public abstract class AbstractConfig 
-{
-  
-  public abstract String get(String key, String defVal);
-  
-  public int get(final String key, final int defVal)
-  {
-    return Integer.parseInt(
-      get(key, Integer.toString(defVal)));
-  }
-  
-  public boolean get(String key, boolean defVal)
-  {
-    String val = get(key, Boolean.toString(defVal));
-    return Boolean.parseBoolean(val);
-  }
-
-  /**
-   * Returns a long config value specified via the given key.
-   * @param key
-   * @param defVal
-   * @return
-   */
-  public long get(String key, long defVal)
-  {
-    String val = get(key, Long.toString(defVal));
-    return Long.parseLong(val);
-  }
-
-  protected abstract void set(String key, String val);
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/config/BackendConfig.java
--- a/org/sonews/config/BackendConfig.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.config;
-
-import java.util.logging.Level;
-import org.sonews.util.Log;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.storage.StorageManager;
-import org.sonews.util.TimeoutMap;
-
-/**
- * Provides access to the program wide configuration that is stored within
- * the server's database.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-class BackendConfig extends AbstractConfig
-{
-
-  private static BackendConfig instance = new BackendConfig();
-  
-  public static BackendConfig getInstance()
-  {
-    return instance;
-  }
-  
-  private final TimeoutMap<String, String> values 
-    = new TimeoutMap<String, String>();
-  
-  private BackendConfig()
-  {
-    super();
-  }
-  
-  /**
-   * Returns the config value for the given key or the defaultValue if the
-   * key is not found in config.
-   * @param key
-   * @param defaultValue
-   * @return
-   */
-  @Override
-  public String get(String key, String defaultValue)
-  {
-    try
-    {
-      String configValue = values.get(key);
-      if(configValue == null)
-      {
-        if(StorageManager.current() == null)
-        {
-          Log.get().warning("BackendConfig not available, using default.");
-          return defaultValue;
-        }
-
-        configValue = StorageManager.current().getConfigValue(key);
-        if(configValue == null)
-        {
-          return defaultValue;
-        }
-        else
-        {
-          values.put(key, configValue);
-          return configValue;
-        }
-      }
-      else
-      {
-        return configValue;
-      }
-    }
-    catch(StorageBackendException ex)
-    {
-      Log.get().log(Level.SEVERE, "Storage backend problem", ex);
-      return defaultValue;
-    }
-  }
-  
-  /**
-   * Sets the config value which is identified by the given key.
-   * @param key
-   * @param value
-   */
-  public void set(String key, String value)
-  {
-    values.put(key, value);
-    
-    try
-    {
-      // Write values to database
-      StorageManager.current().setConfigValue(key, value);
-    }
-    catch(StorageBackendException ex)
-    {
-      ex.printStackTrace();
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/config/CommandLineConfig.java
--- a/org/sonews/config/CommandLineConfig.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.config;
-
-import java.util.Map;
-import java.util.HashMap;
-
-/**
- *
- * @author Christian Lins
- */
-class CommandLineConfig extends AbstractConfig
-{
-
-  private static final CommandLineConfig instance = new CommandLineConfig();
-
-  public static CommandLineConfig getInstance()
-  {
-    return instance;
-  }
-
-  private final Map<String, String> values = new HashMap<String, String>();
-  
-  private CommandLineConfig() {}
-
-  @Override
-  public String get(String key, String def)
-  {
-    synchronized(this.values)
-    {
-      if(this.values.containsKey(key))
-      {
-        def = this.values.get(key);
-      }
-    }
-    return def;
-  }
-
-  @Override
-  public void set(String key, String val)
-  {
-    synchronized(this.values)
-    {
-      this.values.put(key, val);
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/config/Config.java
--- a/org/sonews/config/Config.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,175 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.config;
-
-/**
- * Configuration facade class.
- * @author Christian Lins
- * @since sonews/1.0
- */
-public class Config extends AbstractConfig
-{
-  
-  public static final int LEVEL_CLI     = 1;
-  public static final int LEVEL_FILE    = 2;
-  public static final int LEVEL_BACKEND = 3;
-
-  public static final String CONFIGFILE = "sonews.configfile";
-  
-    /** BackendConfig key constant. Value is the maximum article size in kilobytes. */
-  public static final String ARTICLE_MAXSIZE   = "sonews.article.maxsize";
-
-  /** BackendConfig key constant. Value: Amount of news that are feeded per run. */
-  public static final String EVENTLOG          = "sonews.eventlog";
-  public static final String FEED_NEWSPERRUN   = "sonews.feed.newsperrun";
-  public static final String FEED_PULLINTERVAL = "sonews.feed.pullinterval";
-  public static final String HOSTNAME          = "sonews.hostname";
-  public static final String PORT              = "sonews.port";
-  public static final String TIMEOUT           = "sonews.timeout";
-  public static final String LOGLEVEL          = "sonews.loglevel";
-  public static final String MLPOLL_DELETEUNKNOWN = "sonews.mlpoll.deleteunknown";
-  public static final String MLPOLL_HOST       = "sonews.mlpoll.host";
-  public static final String MLPOLL_PASSWORD   = "sonews.mlpoll.password";
-  public static final String MLPOLL_USER       = "sonews.mlpoll.user";
-  public static final String MLSEND_ADDRESS    = "sonews.mlsend.address";
-  public static final String MLSEND_RW_FROM    = "sonews.mlsend.rewrite.from";
-  public static final String MLSEND_RW_SENDER  = "sonews.mlsend.rewrite.sender";
-  public static final String MLSEND_HOST       = "sonews.mlsend.host";
-  public static final String MLSEND_PASSWORD   = "sonews.mlsend.password";
-  public static final String MLSEND_PORT       = "sonews.mlsend.port";
-  public static final String MLSEND_USER       = "sonews.mlsend.user";
-  
-  /** Key constant. If value is "true" every I/O is written to logfile
-   * (which is a lot!)
-   */
-  public static final String DEBUG              = "sonews.debug";
-
-  /** Key constant. Value is classname of the JDBC driver */
-  public static final String STORAGE_DBMSDRIVER = "sonews.storage.dbmsdriver";
-
-  /** Key constant. Value is JDBC connect String to the database. */
-  public static final String STORAGE_DATABASE   = "sonews.storage.database";
-
-  /** Key constant. Value is the username for the DBMS. */
-  public static final String STORAGE_USER       = "sonews.storage.user";
-
-  /** Key constant. Value is the password for the DBMS. */
-  public static final String STORAGE_PASSWORD   = "sonews.storage.password";
-
-  /** Key constant. Value is the name of the host which is allowed to use the
-   *  XDAEMON command; default: "localhost" */
-  public static final String XDAEMON_HOST       = "sonews.xdaemon.host";
-
-  /** The config key for the filename of the logfile */
-  public static final String LOGFILE = "sonews.log";
-
-  public static final String[] AVAILABLE_KEYS = {
-      ARTICLE_MAXSIZE,
-      EVENTLOG,
-      FEED_NEWSPERRUN,
-      FEED_PULLINTERVAL,
-      HOSTNAME,
-      MLPOLL_DELETEUNKNOWN,
-      MLPOLL_HOST,
-      MLPOLL_PASSWORD,
-      MLPOLL_USER,
-      MLSEND_ADDRESS,
-      MLSEND_HOST,
-      MLSEND_PASSWORD,
-      MLSEND_PORT,
-      MLSEND_RW_FROM,
-      MLSEND_RW_SENDER,
-      MLSEND_USER,
-      PORT,
-      TIMEOUT,
-      XDAEMON_HOST
-  };
-
-  private static Config instance = new Config();
-  
-  public static Config inst()
-  {
-    return instance;
-  }
-  
-  private Config(){}
-
-  @Override
-  public String get(String key, String def)
-  {
-    String val = CommandLineConfig.getInstance().get(key, null);
-    
-    if(val == null)
-    {
-      val = FileConfig.getInstance().get(key, null);
-    }
-
-    if(val == null)
-    {
-      val = BackendConfig.getInstance().get(key, def);
-    }
-
-    return val;
-  }
-
-  public String get(int maxLevel, String key, String def)
-  {
-    String val = CommandLineConfig.getInstance().get(key, null);
-
-    if(val == null && maxLevel >= LEVEL_FILE)
-    {
-      val = FileConfig.getInstance().get(key, null);
-      if(val == null && maxLevel >= LEVEL_BACKEND)
-      {
-        val = BackendConfig.getInstance().get(key, def);
-      }
-    }
-
-    return val != null ? val : def;
-  }
-
-  @Override
-  public void set(String key, String val)
-  {
-    set(LEVEL_BACKEND, key, val);
-  }
-
-  public void set(int level, String key, String val)
-  {
-    switch(level)
-    {
-      case LEVEL_CLI:
-      {
-        CommandLineConfig.getInstance().set(key, val);
-        break;
-      }
-      case LEVEL_FILE:
-      {
-        FileConfig.getInstance().set(key, val);
-        break;
-      }
-      case LEVEL_BACKEND:
-      {
-        BackendConfig.getInstance().set(key, val);
-        break;
-      }
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/config/FileConfig.java
--- a/org/sonews/config/FileConfig.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,170 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.config;
-
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.Properties;
-
-/**
- * Manages the bootstrap configuration. It MUST contain all config values
- * that are needed to establish a database connection.
- * For further configuration values use the Config class instead as that class
- * stores its values within the database.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-class FileConfig extends AbstractConfig
-{
-
-  private static final Properties defaultConfig = new Properties();
-  
-  private static FileConfig instance = null;
-  
-  static
-  {
-    // Set some default values
-    defaultConfig.setProperty(Config.STORAGE_DATABASE, "jdbc:mysql://localhost/sonews");
-    defaultConfig.setProperty(Config.STORAGE_DBMSDRIVER, "com.mysql.jdbc.Driver");
-    defaultConfig.setProperty(Config.STORAGE_USER, "sonews_user");
-    defaultConfig.setProperty(Config.STORAGE_PASSWORD, "mysecret");
-    defaultConfig.setProperty(Config.DEBUG, "false");
-  }
-  
-  /**
-   * Note: this method is not thread-safe
-   * @return A Config instance
-   */
-  public static synchronized FileConfig getInstance()
-  {
-    if(instance == null)
-    {
-      instance = new FileConfig();
-    }
-    return instance;
-  }
-
-  // Every config instance is initialized with the default values.
-  private final Properties settings = (Properties)defaultConfig.clone();
-
-  /**
-   * Config is a singelton class with only one instance at time.
-   * So the constructor is private to prevent the creation of more
-   * then one Config instance.
-   * @see Config.getInstance() to retrieve an instance of Config
-   */
-  private FileConfig()
-  {
-    try
-    {
-      // Load settings from file
-      load();
-    }
-    catch(IOException ex)
-    {
-      ex.printStackTrace();
-    }
-  }
-
-  /**
-   * Loads the configuration from the config file. By default this is done
-   * by the (private) constructor but it can be useful to reload the config
-   * by invoking this method.
-   * @throws IOException
-   */
-  public void load() 
-    throws IOException
-  {
-    FileInputStream in = null;
-    
-    try
-    {
-      in = new FileInputStream(
-        Config.inst().get(Config.LEVEL_CLI, Config.CONFIGFILE, "sonews.conf"));
-      settings.load(in);
-    }
-    catch (FileNotFoundException e)
-    {
-      // MUST NOT use Log otherwise endless loop
-      System.err.println(e.getMessage());
-      save();
-    }
-    finally
-    {
-      if(in != null)
-        in.close();
-    }
-  }
-
-  /**
-   * Saves this Config to the config file. By default this is done
-   * at program end.
-   * @throws FileNotFoundException
-   * @throws IOException
-   */
-  public void save() throws FileNotFoundException, IOException
-  {
-    FileOutputStream out = null;
-    try
-    {
-      out = new FileOutputStream(
-        Config.inst().get(Config.LEVEL_CLI, Config.CONFIGFILE, "sonews.conf"));
-      settings.store(out, "SONEWS Config File");
-      out.flush();
-    }
-    catch(IOException ex)
-    {
-      throw ex;
-    }
-    finally
-    {
-      if(out != null)
-        out.close();
-    }
-  }
-  
-  /**
-   * Returns the value that is stored within this config
-   * identified by the given key. If the key cannot be found
-   * the default value is returned.
-   * @param key Key to identify the value.
-   * @param def The default value that is returned if the key
-   * is not found in this Config.
-   * @return
-   */
-  @Override
-  public String get(String key, String def)
-  {
-    return settings.getProperty(key, def);
-  }
-
-  /**
-   * Sets the value for a given key.
-   * @param key
-   * @param value
-   */
-  @Override
-  public void set(final String key, final String value)
-  {
-    settings.setProperty(key, value);
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/AbstractDaemon.java
--- a/org/sonews/daemon/AbstractDaemon.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon;
-
-import java.sql.SQLException;
-import org.sonews.storage.StorageManager;
-import org.sonews.util.Log;
-
-/**
- * Base class of all sonews threads.
- * Instances of this class will be automatically registered at the ShutdownHook
- * to be cleanly exited when the server is forced to exit.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public abstract class AbstractDaemon extends Thread
-{
-
-  /** This variable is write synchronized through setRunning */
-  private boolean isRunning = false;
-
-  /**
-   * Protected constructor. Will be called by derived classes.
-   */
-  protected AbstractDaemon()
-  {
-    setDaemon(true); // VM will exit when all threads are daemons
-    setName(getClass().getSimpleName());
-  }
-  
-  /**
-   * @return true if shutdown() was not yet called.
-   */
-  public boolean isRunning()
-  {
-    synchronized(this)
-    {
-      return this.isRunning;
-    }
-  }
-  
-  /**
-   * Marks this thread to exit soon. Closes the associated JDBCDatabase connection
-   * if available.
-   * @throws java.sql.SQLException
-   */
-  public void shutdownNow()
-    throws SQLException
-  {
-    synchronized(this)
-    {
-      this.isRunning = false;
-      StorageManager.disableProvider();
-    }
-  }
-  
-  /**
-   * Calls shutdownNow() but catches SQLExceptions if occurring.
-   */
-  public void shutdown()
-  {
-    try
-    {
-      shutdownNow();
-    }
-    catch(SQLException ex)
-    {
-      Log.get().warning(ex.toString());
-    }
-  }
-  
-  /**
-   * Starts this daemon.
-   */
-  @Override
-  public void start()
-  {
-    synchronized(this)
-    {
-      this.isRunning = true;
-    }
-    super.start();
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/ChannelLineBuffers.java
--- a/org/sonews/daemon/ChannelLineBuffers.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,283 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon;
-
-import java.nio.ByteBuffer;
-import java.nio.channels.ClosedChannelException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Class holding ByteBuffers for SocketChannels/NNTPConnection.
- * Due to the complex nature of AIO/NIO we must properly handle the line 
- * buffers for the input and output of the SocketChannels.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class ChannelLineBuffers 
-{
-  
-  /**
-   * Size of one small buffer; 
-   * per default this is 512 bytes to fit one standard line.
-   */
-  public static final int BUFFER_SIZE = 512;
-  
-  private static int maxCachedBuffers = 2048; // Cached buffers maximum
-  
-  private static final List<ByteBuffer> freeSmallBuffers
-    = new ArrayList<ByteBuffer>(maxCachedBuffers);
-  
-  /**
-   * Allocates a predefined number of direct ByteBuffers (allocated via
-   * ByteBuffer.allocateDirect()). This method is Thread-safe, but should only
-   * called at startup.
-   */
-  public static void allocateDirect()
-  {
-    synchronized(freeSmallBuffers)
-    {
-      for(int n = 0; n < maxCachedBuffers; n++)
-      {
-        ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
-        freeSmallBuffers.add(buffer);
-      }
-    }
-  }
-  
-  private ByteBuffer       inputBuffer   = newLineBuffer();
-  private List<ByteBuffer> outputBuffers = new ArrayList<ByteBuffer>();
-  
-  /**
-   * Add the given ByteBuffer to the list of buffers to be send to the client.
-   * This method is Thread-safe.
-   * @param buffer
-   * @throws java.nio.channels.ClosedChannelException If the client channel was
-   * already closed.
-   */
-  public void addOutputBuffer(ByteBuffer buffer)
-    throws ClosedChannelException
-  {
-    if(outputBuffers == null)
-    {
-      throw new ClosedChannelException();
-    }
-    
-    synchronized(outputBuffers)
-    {
-      outputBuffers.add(buffer);
-    }
-  }
-  
-  /**
-   * Currently a channel has only one input buffer. This *may* be a bottleneck
-   * and should investigated in the future.
-   * @param channel
-   * @return The input buffer associated with given channel.
-   */
-  public ByteBuffer getInputBuffer()
-  {
-    return inputBuffer;
-  }
-  
-  /**
-   * Returns the current output buffer for writing(!) to SocketChannel.
-   * @param channel
-   * @return The next input buffer that contains unprocessed data or null
-   * if the connection was closed or there are no more unprocessed buffers.
-   */
-  public ByteBuffer getOutputBuffer()
-  {
-    synchronized(outputBuffers)
-    {
-      if(outputBuffers == null || outputBuffers.isEmpty())
-      {
-        return null;
-      }
-      else
-      {
-        ByteBuffer buffer = outputBuffers.get(0);
-        if(buffer.remaining() == 0)
-        {
-          outputBuffers.remove(0);
-          // Add old buffers to the list of free buffers
-          recycleBuffer(buffer);
-          buffer = getOutputBuffer();
-        }
-        return buffer;
-      }
-    }
-  }
-
-  /**
-   * @return false if there are output buffers pending to be written to the
-   * client.
-   */
-  boolean isOutputBufferEmpty()
-  {
-    synchronized(outputBuffers)
-    {
-      return outputBuffers.isEmpty();
-    }
-  }
-  
-  /**
-   * Goes through the input buffer of the given channel and searches
-   * for next line terminator. If a '\n' is found, the bytes up to the
-   * line terminator are returned as array of bytes (the line terminator
-   * is omitted). If none is found the method returns null.
-   * @param channel
-   * @return A ByteBuffer wrapping the line.
-   */
-  ByteBuffer nextInputLine()
-  {
-    if(inputBuffer == null)
-    {
-      return null;
-    }
-    
-    synchronized(inputBuffer)
-    {
-      ByteBuffer buffer = inputBuffer;
-
-      // Mark the current write position
-      int mark = buffer.position();
-
-      // Set position to 0 and limit to current position
-      buffer.flip();
-
-      ByteBuffer lineBuffer = newLineBuffer();
-
-      while (buffer.position() < buffer.limit())
-      {
-        byte b = buffer.get();
-        if (b == 10) // '\n'
-        {
-          // The bytes between the buffer's current position and its limit, 
-          // if any, are copied to the beginning of the buffer. That is, the 
-          // byte at index p = position() is copied to index zero, the byte at 
-          // index p + 1 is copied to index one, and so forth until the byte 
-          // at index limit() - 1 is copied to index n = limit() - 1 - p. 
-          // The buffer's position is then set to n+1 and its limit is set to 
-          // its capacity.
-          buffer.compact();
-
-          lineBuffer.flip(); // limit to position, position to 0
-          return lineBuffer;
-        }
-        else
-        {
-          lineBuffer.put(b);
-        }
-      }
-
-      buffer.limit(BUFFER_SIZE);
-      buffer.position(mark);
-
-      if(buffer.hasRemaining())
-      {
-        return null;
-      }
-      else
-      {
-        // In the first 512 was no newline found, so the input is not standard
-        // compliant. We return the current buffer as new line and add a space
-        // to the beginning of the next line which corrects some overlong header
-        // lines.
-        inputBuffer = newLineBuffer();
-        inputBuffer.put((byte)' ');
-        buffer.flip();
-        return buffer;
-      }
-    }
-  }
-  
-  /**
-   * Returns a at least 512 bytes long ByteBuffer ready for usage.
-   * The method first try to reuse an already allocated (cached) buffer but
-   * if that fails returns a newly allocated direct buffer.
-   * Use recycleBuffer() method when you do not longer use the allocated buffer.
-   */
-  static ByteBuffer newLineBuffer()
-  {
-    ByteBuffer buf = null;
-    synchronized(freeSmallBuffers)
-    {
-      if(!freeSmallBuffers.isEmpty())
-      {
-        buf = freeSmallBuffers.remove(0);
-      }
-    }
-      
-    if(buf == null)
-    {
-      // Allocate a non-direct buffer
-      buf = ByteBuffer.allocate(BUFFER_SIZE);
-    }
-    
-    assert buf.position() == 0;
-    assert buf.limit() >= BUFFER_SIZE;
-    
-    return buf;
-  }
-  
-  /**
-   * Adds the given buffer to the list of free buffers if it is a valuable
-   * direct allocated buffer.
-   * @param buffer
-   */
-  public static void recycleBuffer(ByteBuffer buffer)
-  {
-    assert buffer != null;
-
-    if(buffer.isDirect())
-    {
-      assert buffer.capacity() >= BUFFER_SIZE;
-      
-      // Add old buffers to the list of free buffers
-      synchronized(freeSmallBuffers)
-      {
-        buffer.clear(); // Set position to 0 and limit to capacity
-        freeSmallBuffers.add(buffer);
-      }
-    } // if(buffer.isDirect())
-  }
-  
-  /**
-   * Recycles all buffers of this ChannelLineBuffers object.
-   */
-  public void recycleBuffers()
-  {
-    synchronized(inputBuffer)
-    {
-      recycleBuffer(inputBuffer);
-      this.inputBuffer = null;
-    }
-    
-    synchronized(outputBuffers)
-    {
-      for(ByteBuffer buf : outputBuffers)
-      {
-        recycleBuffer(buf);
-      }
-      outputBuffers = null;
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/ChannelReader.java
--- a/org/sonews/daemon/ChannelReader.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,202 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.CancelledKeyException;
-import java.nio.channels.SelectionKey;
-import java.nio.channels.Selector;
-import java.nio.channels.SocketChannel;
-import java.util.Iterator;
-import java.util.Set;
-import java.util.logging.Level;
-import org.sonews.util.Log;
-
-/**
- * A Thread task listening for OP_READ events from SocketChannels.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-class ChannelReader extends AbstractDaemon
-{
-
-  private static ChannelReader instance = new ChannelReader();
-
-  /**
-   * @return Active ChannelReader instance.
-   */
-  public static ChannelReader getInstance()
-  {
-    return instance;
-  }
-  
-  private Selector selector = null;
-  
-  protected ChannelReader()
-  {
-  }
-  
-  /**
-   * Sets the selector which is used by this reader to determine the channel
-   * to read from.
-   * @param selector
-   */
-  public void setSelector(final Selector selector)
-  {
-    this.selector = selector;
-  }
-  
-  /**
-   * Run loop. Blocks until some data is available in a channel.
-   */
-  @Override
-  public void run()
-  {
-    assert selector != null;
-
-    while(isRunning())
-    {
-      try
-      {
-        // select() blocks until some SelectableChannels are ready for
-        // processing. There is no need to lock the selector as we have only
-        // one thread per selector.
-        selector.select();
-
-        // Get list of selection keys with pending events.
-        // Note: the selected key set is not thread-safe
-        SocketChannel channel = null;
-        NNTPConnection conn = null;
-        final Set<SelectionKey> selKeys = selector.selectedKeys();
-        SelectionKey selKey = null;
-
-        synchronized (selKeys)
-        {
-          Iterator it = selKeys.iterator();
-
-          // Process the first pending event
-          while (it.hasNext())
-          {
-            selKey = (SelectionKey) it.next();
-            channel = (SocketChannel) selKey.channel();
-            conn = Connections.getInstance().get(channel);
-
-            // Because we cannot lock the selKey as that would cause a deadlock
-            // we lock the connection. To preserve the order of the received
-            // byte blocks a selection key for a connection that has pending
-            // read events is skipped.
-            if (conn == null || conn.tryReadLock())
-            {
-              // Remove from set to indicate that it's being processed
-              it.remove();
-              if (conn != null)
-              {
-                break; // End while loop
-              }
-            }
-            else
-            {
-              selKey = null;
-              channel = null;
-              conn = null;
-            }
-          }
-        }
-
-        // Do not lock the selKeys while processing because this causes
-        // a deadlock in sun.nio.ch.SelectorImpl.lockAndDoSelect()
-        if (selKey != null && channel != null && conn != null)
-        {
-          processSelectionKey(conn, channel, selKey);
-          conn.unlockReadLock();
-        }
-
-      }
-      catch(CancelledKeyException ex)
-      {
-        Log.get().warning("ChannelReader.run(): " + ex);
-        Log.get().log(Level.INFO, "", ex);
-      }
-      catch(Exception ex)
-      {
-        ex.printStackTrace();
-      }
-      
-      // Eventually wait for a register operation
-      synchronized (NNTPDaemon.RegisterGate)
-      {
-      // Do nothing; FindBugs may warn about an empty synchronized 
-      // statement, but we cannot use a wait()/notify() mechanism here.
-      // If we used something like RegisterGate.wait() we block here
-      // until the NNTPDaemon calls notify(). But the daemon only
-      // calls notify() if itself is NOT blocked in the listening socket.
-      }
-    } // while(isRunning())
-  }
-  
-  private void processSelectionKey(final NNTPConnection connection,
-    final SocketChannel socketChannel, final SelectionKey selKey)
-    throws InterruptedException, IOException
-  {
-    assert selKey != null;
-    assert selKey.isReadable();
-    
-    // Some bytes are available for reading
-    if(selKey.isValid())
-    {   
-      // Lock the channel
-      //synchronized(socketChannel)
-      {
-        // Read the data into the appropriate buffer
-        ByteBuffer buf = connection.getInputBuffer();
-        int read = -1;
-        try 
-        {
-          read = socketChannel.read(buf);
-        }
-        catch(IOException ex)
-        {
-          // The connection was probably closed by the remote host
-          // in a non-clean fashion
-          Log.get().info("ChannelReader.processSelectionKey(): " + ex);
-        }
-        catch(Exception ex) 
-        {
-          Log.get().warning("ChannelReader.processSelectionKey(): " + ex);
-        }
-        
-        if(read == -1) // End of stream
-        {
-          selKey.cancel();
-        }
-        else if(read > 0) // If some data was read
-        {
-          ConnectionWorker.addChannel(socketChannel);
-        }
-      }
-    }
-    else
-    {
-      // Should not happen
-      Log.get().severe("Should not happen: " + selKey.toString());
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/ChannelWriter.java
--- a/org/sonews/daemon/ChannelWriter.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,210 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon;
-
-import org.sonews.util.Log;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.CancelledKeyException;
-import java.nio.channels.SelectionKey;
-import java.nio.channels.Selector;
-import java.nio.channels.SocketChannel;
-import java.util.Iterator;
-
-/**
- * A Thread task that processes OP_WRITE events for SocketChannels.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-class ChannelWriter extends AbstractDaemon
-{
-
-  private static ChannelWriter instance = new ChannelWriter();
-
-  /**
-   * @return Returns the active ChannelWriter instance.
-   */
-  public static ChannelWriter getInstance()
-  {
-    return instance;
-  }
-  
-  private Selector selector = null;
-  
-  protected ChannelWriter()
-  {
-  }
-  
-  /**
-   * @return Selector associated with this instance.
-   */
-  public Selector getSelector()
-  {
-    return this.selector;
-  }
-  
-  /**
-   * Sets the selector that is used by this ChannelWriter.
-   * @param selector
-   */
-  public void setSelector(final Selector selector)
-  {
-    this.selector = selector;
-  }
-  
-  /**
-   * Run loop.
-   */
-  @Override
-  public void run()
-  {
-    assert selector != null;
-
-    while(isRunning())
-    {
-      try
-      {
-        SelectionKey   selKey        = null;
-        SocketChannel  socketChannel = null;
-        NNTPConnection connection    = null;
-
-        // select() blocks until some SelectableChannels are ready for
-        // processing. There is no need to synchronize the selector as we
-        // have only one thread per selector.
-        selector.select(); // The return value of select can be ignored
-
-        // Get list of selection keys with pending OP_WRITE events.
-        // The keySET is not thread-safe whereas the keys itself are.
-        Iterator it = selector.selectedKeys().iterator();
-
-        while (it.hasNext())
-        {
-          // We remove the first event from the set and store it for
-          // later processing.
-          selKey = (SelectionKey) it.next();
-          socketChannel = (SocketChannel) selKey.channel();
-          connection = Connections.getInstance().get(socketChannel);
-
-          it.remove();
-          if (connection != null)
-          {
-            break;
-          }
-          else
-          {
-            selKey = null;
-          }
-        }
-        
-        if (selKey != null)
-        {
-          try
-          {
-            // Process the selected key.
-            // As there is only one OP_WRITE key for a given channel, we need
-            // not to synchronize this processing to retain the order.
-            processSelectionKey(connection, socketChannel, selKey);
-          }
-          catch (IOException ex)
-          {
-            Log.get().warning("Error writing to channel: " + ex);
-
-            // Cancel write events for this channel
-            selKey.cancel();
-            connection.shutdownInput();
-            connection.shutdownOutput();
-          }
-        }
-        
-        // Eventually wait for a register operation
-        synchronized(NNTPDaemon.RegisterGate) { /* do nothing */ }
-      }
-      catch(CancelledKeyException ex)
-      {
-        Log.get().info("ChannelWriter.run(): " + ex);
-      }
-      catch(Exception ex)
-      {
-        ex.printStackTrace();
-      }
-    } // while(isRunning())
-  }
-  
-  private void processSelectionKey(final NNTPConnection connection,
-    final SocketChannel socketChannel, final SelectionKey selKey)
-    throws InterruptedException, IOException
-  {
-    assert connection != null;
-    assert socketChannel != null;
-    assert selKey != null;
-    assert selKey.isWritable();
-
-    // SocketChannel is ready for writing
-    if(selKey.isValid())
-    {
-      // Lock the socket channel
-      synchronized(socketChannel)
-      {
-        // Get next output buffer
-        ByteBuffer buf = connection.getOutputBuffer();
-        if(buf == null)
-        {
-          // Currently we have nothing to write, so we stop the writeable
-          // events until we have something to write to the socket channel
-          //selKey.cancel();
-          selKey.interestOps(0);
-          // Update activity timestamp to prevent too early disconnects
-          // on slow client connections
-          connection.setLastActivity(System.currentTimeMillis());
-          return;
-        }
- 
-        while(buf != null) // There is data to be send
-        {
-          // Write buffer to socket channel; this method does not block
-          if(socketChannel.write(buf) <= 0)
-          {
-            // Perhaps there is data to be written, but the SocketChannel's
-            // buffer is full, so we stop writing to until the next event.
-            break;
-          }
-          else
-          {
-            // Retrieve next buffer if available; method may return the same
-            // buffer instance if it still have some bytes remaining
-            buf = connection.getOutputBuffer();
-          }
-        }
-      }
-    }
-    else
-    {
-      Log.get().warning("Invalid OP_WRITE key: " + selKey);
-
-      if(socketChannel.socket().isClosed())
-      {
-        connection.shutdownInput();
-        connection.shutdownOutput();
-        socketChannel.close();
-        Log.get().info("Connection closed.");
-      }
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/CommandSelector.java
--- a/org/sonews/daemon/CommandSelector.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,141 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import org.sonews.daemon.command.Command;
-import org.sonews.daemon.command.UnsupportedCommand;
-import org.sonews.util.Log;
-import org.sonews.util.io.Resource;
-
-/**
- * Selects the correct command processing class.
- * @author Christian Lins
- * @since sonews/1.0
- */
-public class CommandSelector
-{
-
-  private static Map<Thread, CommandSelector> instances
-    = new ConcurrentHashMap<Thread, CommandSelector>();
-  private static Map<String, Class<?>> commandClassesMapping
-    = new ConcurrentHashMap<String, Class<?>>();
-
-  static
-  {
-    String[] classes = Resource.getAsString("helpers/commands.list", true).split("\n");
-    for(String className : classes)
-    {
-      if(className.charAt(0) == '#')
-      {
-        // Skip comments
-        continue;
-      }
-
-      try
-      {
-        addCommandHandler(className);
-      }
-      catch(ClassNotFoundException ex)
-      {
-        Log.get().warning("Could not load command class: " + ex);
-      }
-      catch(InstantiationException ex)
-      {
-        Log.get().severe("Could not instantiate command class: " + ex);
-      }
-      catch(IllegalAccessException ex)
-      {
-        Log.get().severe("Could not access command class: " + ex);
-      }
-    }
-  }
-
-  public static void addCommandHandler(String className)
-    throws ClassNotFoundException, InstantiationException, IllegalAccessException
-  {
-    Class<?> clazz = Class.forName(className);
-    Command cmd = (Command)clazz.newInstance();
-    String[] cmdStrs = cmd.getSupportedCommandStrings();
-    for (String cmdStr : cmdStrs)
-    {
-      commandClassesMapping.put(cmdStr, clazz);
-    }
-  }
-
-  public static Set<String> getCommandNames()
-  {
-    return commandClassesMapping.keySet();
-  }
-
-  public static CommandSelector getInstance()
-  {
-    CommandSelector csel = instances.get(Thread.currentThread());
-    if(csel == null)
-    {
-      csel = new CommandSelector();
-      instances.put(Thread.currentThread(), csel);
-    }
-    return csel;
-  }
-
-  private Map<String, Command> commandMapping = new HashMap<String, Command>();
-  private Command              unsupportedCmd = new UnsupportedCommand();
-
-  private CommandSelector()
-  {}
-
-  public Command get(String commandName)
-  {
-    try
-    {
-      commandName = commandName.toUpperCase();
-      Command cmd = this.commandMapping.get(commandName);
-
-      if(cmd == null)
-      {
-        Class<?> clazz = commandClassesMapping.get(commandName);
-        if(clazz == null)
-        {
-          cmd = this.unsupportedCmd;
-        }
-        else
-        {
-          cmd = (Command)clazz.newInstance();
-          this.commandMapping.put(commandName, cmd);
-        }
-      }
-      else if(cmd.isStateful())
-      {
-        cmd = cmd.getClass().newInstance();
-      }
-
-      return cmd;
-    }
-    catch(Exception ex)
-    {
-      ex.printStackTrace();
-      return this.unsupportedCmd;
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/ConnectionWorker.java
--- a/org/sonews/daemon/ConnectionWorker.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,102 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon;
-
-import org.sonews.util.Log;
-import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
-import java.util.concurrent.ArrayBlockingQueue;
-
-/**
- * Does most of the work: parsing input, talking to client and Database.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-class ConnectionWorker extends AbstractDaemon
-{
-
-  // 256 pending events should be enough
-  private static ArrayBlockingQueue<SocketChannel> pendingChannels
-    = new ArrayBlockingQueue<SocketChannel>(256, true);
-  
-  /**
-   * Registers the given channel for further event processing.
-   * @param channel
-   */
-  public static void addChannel(SocketChannel channel)
-    throws InterruptedException
-  {
-    pendingChannels.put(channel);
-  }
-  
-  /**
-   * Processing loop.
-   */
-  @Override
-  public void run()
-  {
-    while(isRunning())
-    {
-      try
-      {
-        // Retrieve and remove if available, otherwise wait.
-        SocketChannel channel = pendingChannels.take();
-
-        if(channel != null)
-        {
-          // Connections.getInstance().get() MAY return null
-          NNTPConnection conn = Connections.getInstance().get(channel);
-          
-          // Try to lock the connection object
-          if(conn != null && conn.tryReadLock())
-          {
-            ByteBuffer buf = conn.getBuffers().nextInputLine();
-            while(buf != null) // Complete line was received
-            {
-              final byte[] line = new byte[buf.limit()];
-              buf.get(line);
-              ChannelLineBuffers.recycleBuffer(buf);
-              
-              // Here is the actual work done
-              conn.lineReceived(line);
-
-              // Read next line as we could have already received the next line
-              buf = conn.getBuffers().nextInputLine();
-            }
-            conn.unlockReadLock();
-          }
-          else
-          {
-            addChannel(channel);
-          }
-        }
-      }
-      catch(InterruptedException ex)
-      {
-        Log.get().info("ConnectionWorker interrupted: " + ex);
-      }
-      catch(Exception ex)
-      {
-        Log.get().severe("Exception in ConnectionWorker: " + ex);
-        ex.printStackTrace();
-      }
-    } // end while(isRunning())
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/Connections.java
--- a/org/sonews/daemon/Connections.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,181 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon;
-
-import org.sonews.config.Config;
-import org.sonews.util.Log;
-import org.sonews.util.Stats;
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.nio.channels.SocketChannel;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Map;
-
-/**
- * Daemon thread collecting all NNTPConnection instances. The thread
- * checks periodically if there are stale/timed out connections and
- * removes and purges them properly.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public final class Connections extends AbstractDaemon
-{
-
-  private static final Connections instance = new Connections();
-  
-  /**
-   * @return Active Connections instance.
-   */
-  public static Connections getInstance()
-  {
-    return Connections.instance;
-  }
-  
-  private final List<NNTPConnection> connections 
-    = new ArrayList<NNTPConnection>();
-  private final Map<SocketChannel, NNTPConnection> connByChannel 
-    = new HashMap<SocketChannel, NNTPConnection>();
-  
-  private Connections()
-  {
-    setName("Connections");
-  }
-  
-  /**
-   * Adds the given NNTPConnection to the Connections management.
-   * @param conn
-   * @see org.sonews.daemon.NNTPConnection
-   */
-  public void add(final NNTPConnection conn)
-  {
-    synchronized(this.connections)
-    {
-      this.connections.add(conn);
-      this.connByChannel.put(conn.getSocketChannel(), conn);
-    }
-  }
-  
-  /**
-   * @param channel
-   * @return NNTPConnection instance that is associated with the given
-   * SocketChannel.
-   */
-  public NNTPConnection get(final SocketChannel channel)
-  {
-    synchronized(this.connections)
-    {
-      return this.connByChannel.get(channel);
-    }
-  }
-
-  int getConnectionCount(String remote)
-  {
-    int cnt = 0;
-    synchronized(this.connections)
-    {
-      for(NNTPConnection conn : this.connections)
-      {
-        assert conn != null;
-        assert conn.getSocketChannel() != null;
-
-        Socket socket = conn.getSocketChannel().socket();
-        if(socket != null)
-        {
-          InetSocketAddress sockAddr = (InetSocketAddress)socket.getRemoteSocketAddress();
-          if(sockAddr != null)
-          {
-            if(sockAddr.getHostName().equals(remote))
-            {
-              cnt++;
-            }
-          }
-        } // if(socket != null)
-      }
-    }
-    return cnt;
-  }
-  
-  /**
-   * Run loops. Checks periodically for timed out connections and purged them
-   * from the lists.
-   */
-  @Override
-  public void run()
-  {
-    while(isRunning())
-    {
-      int timeoutMillis = 1000 * Config.inst().get(Config.TIMEOUT, 180);
-      
-      synchronized (this.connections)
-      {
-        final ListIterator<NNTPConnection> iter = this.connections.listIterator();
-        NNTPConnection conn;
-
-        while (iter.hasNext())
-        {
-          conn = iter.next();
-          if((System.currentTimeMillis() - conn.getLastActivity()) > timeoutMillis
-              && conn.getBuffers().isOutputBufferEmpty())
-          {
-            // A connection timeout has occurred so purge the connection
-            iter.remove();
-
-            // Close and remove the channel
-            SocketChannel channel = conn.getSocketChannel();
-            connByChannel.remove(channel);
-            
-            try
-            {
-              assert channel != null;
-              assert channel.socket() != null;
-      
-              // Close the channel; implicitely cancels all selectionkeys
-              channel.close();
-              Log.get().info("Disconnected: " + channel.socket().getRemoteSocketAddress() +
-                " (timeout)");
-            }
-            catch(IOException ex)
-            {
-              Log.get().warning("Connections.run(): " + ex);
-            }
-
-            // Recycle the used buffers
-            conn.getBuffers().recycleBuffers();
-            
-            Stats.getInstance().clientDisconnect();
-          }
-        }
-      }
-
-      try
-      {
-        Thread.sleep(10000); // Sleep ten seconds
-      }
-      catch(InterruptedException ex)
-      {
-        Log.get().warning("Connections Thread was interrupted: " + ex.getMessage());
-      }
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/LineEncoder.java
--- a/org/sonews/daemon/LineEncoder.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon;
-
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.channels.ClosedChannelException;
-import java.nio.charset.Charset;
-import java.nio.charset.CharsetEncoder;
-import java.nio.charset.CoderResult;
-
-/**
- * Encodes a line to buffers using the correct charset.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-class LineEncoder
-{
-
-  private CharBuffer    characters;
-  private Charset       charset;
-  
-  /**
-   * Constructs new LineEncoder.
-   * @param characters
-   * @param charset
-   */
-  public LineEncoder(CharBuffer characters, Charset charset)
-  {
-    this.characters = characters;
-    this.charset    = charset;
-  }
-  
-  /**
-   * Encodes the characters of this instance to the given ChannelLineBuffers
-   * using the Charset of this instance.
-   * @param buffer
-   * @throws java.nio.channels.ClosedChannelException
-   */
-  public void encode(ChannelLineBuffers buffer)
-    throws ClosedChannelException
-  {
-    CharsetEncoder encoder = charset.newEncoder();
-    while (characters.hasRemaining())
-    {
-      ByteBuffer buf = ChannelLineBuffers.newLineBuffer();
-      assert buf.position() == 0;
-      assert buf.capacity() >= 512;
-
-      CoderResult res = encoder.encode(characters, buf, true);
-
-      // Set limit to current position and current position to 0;
-      // means make ready for read from buffer
-      buf.flip();
-      buffer.addOutputBuffer(buf);
-
-      if (res.isUnderflow()) // All input processed
-      {
-        break;
-      }
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/NNTPConnection.java
--- a/org/sonews/daemon/NNTPConnection.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,428 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon;
-
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.net.SocketException;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.channels.ClosedChannelException;
-import java.nio.channels.SelectionKey;
-import java.nio.channels.SocketChannel;
-import java.nio.charset.Charset;
-import java.util.Arrays;
-import java.util.Timer;
-import java.util.TimerTask;
-import org.sonews.daemon.command.Command;
-import org.sonews.storage.Article;
-import org.sonews.storage.Channel;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.util.Log;
-import org.sonews.util.Stats;
-
-/**
- * For every SocketChannel (so TCP/IP connection) there is an instance of
- * this class.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public final class NNTPConnection
-{
-
-  public static final String NEWLINE            = "\r\n";    // RFC defines this as newline
-  public static final String MESSAGE_ID_PATTERN = "<[^>]+>";
-  
-  private static final Timer cancelTimer = new Timer(true); // Thread-safe? True for run as daemon
-  
-  /** SocketChannel is generally thread-safe */
-  private SocketChannel   channel        = null;
-  private Charset         charset        = Charset.forName("UTF-8");
-  private Command         command        = null;
-  private Article         currentArticle = null;
-  private Channel         currentGroup   = null;
-  private volatile long   lastActivity   = System.currentTimeMillis();
-  private ChannelLineBuffers lineBuffers = new ChannelLineBuffers();
-  private int             readLock       = 0;
-  private final Object    readLockGate   = new Object();
-  private SelectionKey    writeSelKey    = null;
-  
-  public NNTPConnection(final SocketChannel channel)
-    throws IOException
-  {
-    if(channel == null)
-    {
-      throw new IllegalArgumentException("channel is null");
-    }
-
-    this.channel = channel;
-    Stats.getInstance().clientConnect();
-  }
-  
-  /**
-   * Tries to get the read lock for this NNTPConnection. This method is Thread-
-   * safe and returns true of the read lock was successfully set. If the lock
-   * is still hold by another Thread the method returns false.
-   */
-  boolean tryReadLock()
-  {
-    // As synchronizing simple types may cause deadlocks,
-    // we use a gate object.
-    synchronized(readLockGate)
-    {
-      if(readLock != 0)
-      {
-        return false;
-      }
-      else
-      {
-        readLock = Thread.currentThread().hashCode();
-        return true;
-      }
-    }
-  }
-  
-  /**
-   * Releases the read lock in a Thread-safe way.
-   * @throws IllegalMonitorStateException if a Thread not holding the lock
-   * tries to release it.
-   */
-  void unlockReadLock()
-  {
-    synchronized(readLockGate)
-    {
-      if(readLock == Thread.currentThread().hashCode())
-      {
-        readLock = 0;
-      }
-      else
-      {
-        throw new IllegalMonitorStateException();
-      }
-    }
-  }
-  
-  /**
-   * @return Current input buffer of this NNTPConnection instance.
-   */
-  public ByteBuffer getInputBuffer()
-  {
-    return this.lineBuffers.getInputBuffer();
-  }
-  
-  /**
-   * @return Output buffer of this NNTPConnection which has at least one byte
-   * free storage.
-   */
-  public ByteBuffer getOutputBuffer()
-  {
-    return this.lineBuffers.getOutputBuffer();
-  }
-  
-  /**
-   * @return ChannelLineBuffers instance associated with this NNTPConnection.
-   */
-  public ChannelLineBuffers getBuffers()
-  {
-    return this.lineBuffers;
-  }
-  
-  /**
-   * @return true if this connection comes from a local remote address.
-   */
-  public boolean isLocalConnection()
-  {
-    return ((InetSocketAddress)this.channel.socket().getRemoteSocketAddress())
-      .getHostName().equalsIgnoreCase("localhost");
-  }
-
-  void setWriteSelectionKey(SelectionKey selKey)
-  {
-    this.writeSelKey = selKey;
-  }
-
-  public void shutdownInput()
-  {
-    try
-    {
-      // Closes the input line of the channel's socket, so no new data
-      // will be received and a timeout can be triggered.
-      this.channel.socket().shutdownInput();
-    }
-    catch(IOException ex)
-    {
-      Log.get().warning("Exception in NNTPConnection.shutdownInput(): " + ex);
-    }
-  }
-  
-  public void shutdownOutput()
-  {
-    cancelTimer.schedule(new TimerTask() 
-    {
-      @Override
-      public void run()
-      {
-        try
-        {
-          // Closes the output line of the channel's socket.
-          channel.socket().shutdownOutput();
-          channel.close();
-        }
-        catch(SocketException ex)
-        {
-          // Socket was already disconnected
-          Log.get().info("NNTPConnection.shutdownOutput(): " + ex);
-        }
-        catch(Exception ex)
-        {
-          Log.get().warning("NNTPConnection.shutdownOutput(): " + ex);
-        }
-      }
-    }, 3000);
-  }
-  
-  public SocketChannel getSocketChannel()
-  {
-    return this.channel;
-  }
-  
-  public Article getCurrentArticle()
-  {
-    return this.currentArticle;
-  }
-  
-  public Charset getCurrentCharset()
-  {
-    return this.charset;
-  }
-
-  /**
-   * @return The currently selected communication channel (not SocketChannel)
-   */
-  public Channel getCurrentChannel()
-  {
-    return this.currentGroup;
-  }
-  
-  public void setCurrentArticle(final Article article)
-  {
-    this.currentArticle = article;
-  }
-  
-  public void setCurrentGroup(final Channel group)
-  {
-    this.currentGroup = group;
-  }
-  
-  public long getLastActivity()
-  {
-    return this.lastActivity;
-  }
-  
-  /**
-   * Due to the readLockGate there is no need to synchronize this method.
-   * @param raw
-   * @throws IllegalArgumentException if raw is null.
-   * @throws IllegalStateException if calling thread does not own the readLock.
-   */
-  void lineReceived(byte[] raw)
-  {
-    if(raw == null)
-    {
-      throw new IllegalArgumentException("raw is null");
-    }
-    
-    if(readLock == 0 || readLock != Thread.currentThread().hashCode())
-    {
-      throw new IllegalStateException("readLock not properly set");
-    }
-
-    this.lastActivity = System.currentTimeMillis();
-    
-    String line = new String(raw, this.charset);
-    
-    // There might be a trailing \r, but trim() is a bad idea
-    // as it removes also leading spaces from long header lines.
-    if(line.endsWith("\r"))
-    {
-      line = line.substring(0, line.length() - 1);
-      raw  = Arrays.copyOf(raw, raw.length - 1);
-    }
-    
-    Log.get().fine("<< " + line);
-    
-    if(command == null)
-    {
-      command = parseCommandLine(line);
-      assert command != null;
-    }
-
-    try
-    {
-      // The command object will process the line we just received
-      try
-      {
-        command.processLine(this, line, raw);
-      }
-      catch(StorageBackendException ex)
-      {
-        Log.get().info("Retry command processing after StorageBackendException");
-
-        // Try it a second time, so that the backend has time to recover
-        command.processLine(this, line, raw);
-      }
-    }
-    catch(ClosedChannelException ex0)
-    {
-      try
-      {
-        Log.get().info("Connection to " + channel.socket().getRemoteSocketAddress()
-            + " closed: " + ex0);
-      }
-      catch(Exception ex0a)
-      {
-        ex0a.printStackTrace();
-      }
-    }
-    catch(Exception ex1) // This will catch a second StorageBackendException
-    {
-      try
-      {
-        command = null;
-        ex1.printStackTrace();
-        println("500 Internal server error");
-      }
-      catch(Exception ex2)
-      {
-        ex2.printStackTrace();
-      }
-    }
-
-    if(command == null || command.hasFinished())
-    {
-      command = null;
-      charset = Charset.forName("UTF-8"); // Reset to default
-    }
-  }
-  
-  /**
-   * This method determines the fitting command processing class.
-   * @param line
-   * @return
-   */
-  private Command parseCommandLine(String line)
-  {
-    String cmdStr = line.split(" ")[0];
-    return CommandSelector.getInstance().get(cmdStr);
-  }
-  
-  /**
-   * Puts the given line into the output buffer, adds a newline character
-   * and returns. The method returns immediately and does not block until
-   * the line was sent. If line is longer than 510 octets it is split up in
-   * several lines. Each line is terminated by \r\n (NNTPConnection.NEWLINE).
-   * @param line
-   */
-  public void println(final CharSequence line, final Charset charset)
-    throws IOException
-  {    
-    writeToChannel(CharBuffer.wrap(line), charset, line);
-    writeToChannel(CharBuffer.wrap(NEWLINE), charset, null);
-  }
-
-  /**
-   * Writes the given raw lines to the output buffers and finishes with
-   * a newline character (\r\n).
-   * @param rawLines
-   */
-  public void println(final byte[] rawLines)
-    throws IOException
-  {
-    this.lineBuffers.addOutputBuffer(ByteBuffer.wrap(rawLines));
-    writeToChannel(CharBuffer.wrap(NEWLINE), charset, null);
-  }
-  
-  /**
-   * Encodes the given CharBuffer using the given Charset to a bunch of
-   * ByteBuffers (each 512 bytes large) and enqueues them for writing at the
-   * connected SocketChannel.
-   * @throws java.io.IOException
-   */
-  private void writeToChannel(CharBuffer characters, final Charset charset,
-    CharSequence debugLine)
-    throws IOException
-  {
-    if(!charset.canEncode())
-    {
-      Log.get().severe("FATAL: Charset " + charset + " cannot encode!");
-      return;
-    }
-    
-    // Write characters to output buffers
-    LineEncoder lenc = new LineEncoder(characters, charset);
-    lenc.encode(lineBuffers);
-    
-    enableWriteEvents(debugLine);
-  }
-
-  private void enableWriteEvents(CharSequence debugLine)
-  {
-    // Enable OP_WRITE events so that the buffers are processed
-    try
-    {
-      this.writeSelKey.interestOps(SelectionKey.OP_WRITE);
-      ChannelWriter.getInstance().getSelector().wakeup();
-    }
-    catch(Exception ex) // CancelledKeyException and ChannelCloseException
-    {
-      Log.get().warning("NNTPConnection.writeToChannel(): " + ex);
-      return;
-    }
-
-    // Update last activity timestamp
-    this.lastActivity = System.currentTimeMillis();
-    if(debugLine != null)
-    {
-      Log.get().fine(">> " + debugLine);
-    }
-  }
-  
-  public void println(final CharSequence line)
-    throws IOException
-  {
-    println(line, charset);
-  }
-  
-  public void print(final String line)
-    throws IOException
-  {
-    writeToChannel(CharBuffer.wrap(line), charset, line);
-  }
-  
-  public void setCurrentCharset(final Charset charset)
-  {
-    this.charset = charset;
-  }
-
-  void setLastActivity(long timestamp)
-  {
-    this.lastActivity = timestamp;
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/NNTPDaemon.java
--- a/org/sonews/daemon/NNTPDaemon.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,197 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon;
-
-import org.sonews.config.Config;
-import org.sonews.Main;
-import org.sonews.util.Log;
-import java.io.IOException;
-import java.net.BindException;
-import java.net.InetSocketAddress;
-import java.net.ServerSocket;
-import java.nio.channels.CancelledKeyException;
-import java.nio.channels.ClosedChannelException;
-import java.nio.channels.SelectionKey;
-import java.nio.channels.Selector;
-import java.nio.channels.ServerSocketChannel;
-import java.nio.channels.SocketChannel;
-
-/**
- * NNTP daemon using SelectableChannels.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public final class NNTPDaemon extends AbstractDaemon
-{
-
-  public static final Object RegisterGate = new Object();
-  
-  private static NNTPDaemon instance = null;
-  
-  public static synchronized NNTPDaemon createInstance(int port)
-  {
-    if(instance == null)
-    {
-      instance = new NNTPDaemon(port);
-      return instance;
-    }
-    else
-    {
-      throw new RuntimeException("NNTPDaemon.createInstance() called twice");
-    }
-  }
-  
-  private int port;
-  
-  private NNTPDaemon(final int port)
-  {
-    Log.get().info("Server listening on port " + port);
-    this.port = port;
-  }
-
-  @Override
-  public void run()
-  {
-    try
-    {
-      // Create a Selector that handles the SocketChannel multiplexing
-      final Selector readSelector  = Selector.open();
-      final Selector writeSelector = Selector.open();
-      
-      // Start working threads
-      final int workerThreads = Runtime.getRuntime().availableProcessors() * 4;
-      ConnectionWorker[] cworkers = new ConnectionWorker[workerThreads];
-      for(int n = 0; n < workerThreads; n++)
-      {
-        cworkers[n] = new ConnectionWorker();
-        cworkers[n].start();
-      }
-      
-      ChannelWriter.getInstance().setSelector(writeSelector);
-      ChannelReader.getInstance().setSelector(readSelector);
-      ChannelWriter.getInstance().start();
-      ChannelReader.getInstance().start();
-      
-      final ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
-      serverSocketChannel.configureBlocking(true);  // Set to blocking mode
-      
-      // Configure ServerSocket; bind to socket...
-      final ServerSocket serverSocket = serverSocketChannel.socket();
-      serverSocket.bind(new InetSocketAddress(this.port));
-      
-      while(isRunning())
-      {
-        SocketChannel socketChannel;
-        
-        try
-        {
-          // As we set the server socket channel to blocking mode the accept()
-          // method will block.
-          socketChannel = serverSocketChannel.accept();
-          socketChannel.configureBlocking(false);
-          assert socketChannel.isConnected();
-          assert socketChannel.finishConnect();
-        }
-        catch(IOException ex)
-        {
-          // Under heavy load an IOException "Too many open files may
-          // be thrown. It most cases we should slow down the connection
-          // accepting, to give the worker threads some time to process work.
-          Log.get().severe("IOException while accepting connection: " + ex.getMessage());
-          Log.get().info("Connection accepting sleeping for seconds...");
-          Thread.sleep(5000); // 5 seconds
-          continue;
-        }
-        
-        final NNTPConnection conn;
-        try
-        {
-          conn = new NNTPConnection(socketChannel);
-          Connections.getInstance().add(conn);
-        }
-        catch(IOException ex)
-        {
-          Log.get().warning(ex.toString());
-          socketChannel.close();
-          continue;
-        }
-        
-        try
-        {
-          SelectionKey selKeyWrite =
-            registerSelector(writeSelector, socketChannel, SelectionKey.OP_WRITE);
-          registerSelector(readSelector, socketChannel, SelectionKey.OP_READ);
-          
-          Log.get().info("Connected: " + socketChannel.socket().getRemoteSocketAddress());
-
-          // Set write selection key and send hello to client
-          conn.setWriteSelectionKey(selKeyWrite);
-          conn.println("200 " + Config.inst().get(Config.HOSTNAME, "localhost")
-              + " " + Main.VERSION + " news server ready - (posting ok).");
-        }
-        catch(CancelledKeyException cke)
-        {
-          Log.get().warning("CancelledKeyException " + cke.getMessage() + " was thrown: "
-            + socketChannel.socket());
-        }
-        catch(ClosedChannelException cce)
-        {
-          Log.get().warning("ClosedChannelException " + cce.getMessage() + " was thrown: "
-            + socketChannel.socket());
-        }
-      }
-    }
-    catch(BindException ex)
-    {
-      // Could not bind to socket; this is a fatal problem; so perform shutdown
-      ex.printStackTrace();
-      System.exit(1);
-    }
-    catch(IOException ex)
-    {
-      ex.printStackTrace();
-    }
-    catch(Exception ex)
-    {
-      ex.printStackTrace();
-    }
-  }
-  
-  public static SelectionKey registerSelector(final Selector selector,
-    final SocketChannel channel, final int op)
-    throws CancelledKeyException, ClosedChannelException
-  {
-    // Register the selector at the channel, so that it will be notified
-    // on the socket's events
-    synchronized(RegisterGate)
-    {
-      // Wakeup the currently blocking reader/writer thread; we have locked
-      // the RegisterGate to prevent the awakened thread to block again
-      selector.wakeup();
-      
-      // Lock the selector to prevent the waiting worker threads going into
-      // selector.select() which would block the selector.
-      synchronized (selector)
-      {
-        return channel.register(selector, op, null);
-      }
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/ArticleCommand.java
--- a/org/sonews/daemon/command/ArticleCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,174 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import org.sonews.storage.Article;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.Channel;
-import org.sonews.storage.StorageBackendException;
-
-/**
- * Class handling the ARTICLE, BODY and HEAD commands.
- * @author Christian Lins
- * @author Dennis Schwerdel
- * @since n3tpd/0.1
- */
-public class ArticleCommand implements Command
-{
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[] {"ARTICLE", "BODY", "HEAD"};
-  }
-
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return false;
-  }
-
-  // TODO: Refactor this method to reduce its complexity!
-  @Override
-  public void processLine(NNTPConnection conn, final String line, byte[] raw)
-    throws IOException
-  {
-    final String[] command = line.split(" ");
-    
-    Article article  = null;
-    long    artIndex = -1;
-    if (command.length == 1)
-    {
-      article = conn.getCurrentArticle();
-      if (article == null)
-      {
-        conn.println("420 no current article has been selected");
-        return;
-      }
-    }
-    else if (command[1].matches(NNTPConnection.MESSAGE_ID_PATTERN))
-    {
-      // Message-ID
-      article = Article.getByMessageID(command[1]);
-      if (article == null)
-      {
-        conn.println("430 no such article found");
-        return;
-      }
-    }
-    else
-    {
-      // Message Number
-      try
-      {
-        Channel currentGroup = conn.getCurrentChannel();
-        if(currentGroup == null)
-        {
-          conn.println("400 no group selected");
-          return;
-        }
-        
-        artIndex = Long.parseLong(command[1]);
-        article  = currentGroup.getArticle(artIndex);
-      }
-      catch(NumberFormatException ex)
-      {
-        ex.printStackTrace();
-      }
-      catch(StorageBackendException ex)
-      {
-        ex.printStackTrace();
-      }
-
-      if (article == null)
-      {
-        conn.println("423 no such article number in this group");
-        return;
-      }
-      conn.setCurrentArticle(article);
-    }
-
-    if(command[0].equalsIgnoreCase("ARTICLE"))
-    {
-      conn.println("220 " + artIndex + " " + article.getMessageID()
-          + " article retrieved - head and body follow");
-      conn.println(article.getHeaderSource());
-      conn.println("");
-      conn.println(article.getBody());
-      conn.println(".");
-    }
-    else if(command[0].equalsIgnoreCase("BODY"))
-    {
-      conn.println("222 " + artIndex + " " + article.getMessageID() + " body");
-      conn.println(article.getBody());
-      conn.println(".");
-    }
-    
-    /*
-     * HEAD: This command is mandatory.
-     *
-     * Syntax
-     *    HEAD message-id
-     *    HEAD number
-     *    HEAD
-     *
-     * Responses
-     *
-     * First form (message-id specified)
-     *  221 0|n message-id    Headers follow (multi-line)
-     *  430                   No article with that message-id
-     *
-     * Second form (article number specified)
-     *  221 n message-id      Headers follow (multi-line)
-     *  412                   No newsgroup selected
-     *  423                   No article with that number
-     *
-     * Third form (current article number used)
-     *  221 n message-id      Headers follow (multi-line)
-     *  412                   No newsgroup selected
-     *  420                   Current article number is invalid
-     *
-     * Parameters
-     *  number        Requested article number
-     *  n             Returned article number
-     *  message-id    Article message-id
-     */
-    else if(command[0].equalsIgnoreCase("HEAD"))
-    {
-      conn.println("221 " + artIndex + " " + article.getMessageID()
-          + " Headers follow (multi-line)");
-      conn.println(article.getHeaderSource());
-      conn.println(".");
-    }
-  }  
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/CapabilitiesCommand.java
--- a/org/sonews/daemon/command/CapabilitiesCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import org.sonews.daemon.NNTPConnection;
-
-/**
- * <pre>
- *  The CAPABILITIES command allows a client to determine the
- *  capabilities of the server at any given time.
- *
- *  This command MAY be issued at any time; the server MUST NOT require
- *  it to be issued in order to make use of any capability. The response
- *  generated by this command MAY change during a session because of
- *  other state information (which, in turn, may be changed by the
- *  effects of other commands or by external events).  An NNTP client is
- *  only able to get the current and correct information concerning
- *  available capabilities at any point during a session by issuing a
- *  CAPABILITIES command at that point of that session and processing the
- *  response.
- * </pre>
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class CapabilitiesCommand implements Command
-{
-
-  static final String[] CAPABILITIES = new String[]
-    {
-      "VERSION 2", // MUST be the first one; VERSION 2 refers to RFC3977
-      "READER",    // Server implements commands for reading
-      "POST",      // Server implements POST command
-      "OVER"       // Server implements OVER command
-    };
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[] {"CAPABILITIES"};
-  }
-
-  /**
-   * First called after one call to processLine().
-   * @return
-   */
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-  
-  @Override
-  public boolean isStateful()
-  {
-    return false;
-  }
-
-  @Override
-  public void processLine(NNTPConnection conn, final String line, byte[] raw)
-    throws IOException
-  {
-    conn.println("101 Capabilities list:");
-    for(String cap : CAPABILITIES)
-    {
-      conn.println(cap);
-    }
-    conn.println(".");
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/Command.java
--- a/org/sonews/daemon/command/Command.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.StorageBackendException;
-
-/**
- * Interface for pluggable NNTP commands handling classes.
- * @author Christian Lins
- * @since sonews/0.6.0
- */
-public interface Command
-{
-
-  /**
-   * @return true if this instance can be reused.
-   */
-  boolean hasFinished();
-
-  /**
-   * Returns capability string that is implied by this command class.
-   * MAY return null if the command is required by the NNTP standard.
-   */
-  String impliedCapability();
-
-  boolean isStateful();
-
-  String[] getSupportedCommandStrings();
-
-  void processLine(NNTPConnection conn, String line, byte[] rawLine)
-    throws IOException, StorageBackendException;
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/GroupCommand.java
--- a/org/sonews/daemon/command/GroupCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,102 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.Channel;
-import org.sonews.storage.StorageBackendException;
-
-/**
- * Class handling the GROUP command.
- * <pre>
- *  Syntax
- *    GROUP group
- *
- *  Responses
- *    211 number low high group     Group successfully selected
- *    411                           No such newsgroup
- *
- *  Parameters
- *    group     Name of newsgroup
- *    number    Estimated number of articles in the group
- *    low       Reported low water mark
- *    high      Reported high water mark
- * </pre>
- * (from RFC 3977)
- * 
- * @author Christian Lins
- * @author Dennis Schwerdel
- * @since n3tpd/0.1
- */
-public class GroupCommand implements Command
-{
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[]{"GROUP"};
-  }
-
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return true;
-  }
-  
-  @Override
-  public void processLine(NNTPConnection conn, final String line, byte[] raw)
-    throws IOException, StorageBackendException
-  {
-    final String[] command = line.split(" ");
-
-    Channel group;
-    if(command.length >= 2)
-    {
-      group = Channel.getByName(command[1]);
-      if(group == null || group.isDeleted())
-      {
-        conn.println("411 no such news group");
-      }
-      else
-      {
-        conn.setCurrentGroup(group);
-        conn.println("211 " + group.getPostingsCount() + " " + group.getFirstArticleNumber()
-          + " " + group.getLastArticleNumber() + " " + group.getName() + " group selected");
-      }
-    }
-    else
-    {
-      conn.println("500 no group name given");
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/HelpCommand.java
--- a/org/sonews/daemon/command/HelpCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import java.util.Set;
-import org.sonews.daemon.CommandSelector;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.util.io.Resource;
-
-/**
- * This command provides a short summary of the commands that are
- * understood by this implementation of the server. The help text will
- * be presented as a multi-line data block following the 100 response
- * code (taken from RFC).
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class HelpCommand implements Command
-{
-
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return true;
-  }
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[]{"HELP"};
-  }
-  
-  @Override
-  public void processLine(NNTPConnection conn, final String line, byte[] raw)
-    throws IOException
-  {
-    final String[] command = line.split(" ");
-    conn.println("100 help text follows");
-
-    if(line.length() <= 1)
-    {
-      final String[] help = Resource
-        .getAsString("helpers/helptext", true).split("\n");
-      for(String hstr : help)
-      {
-        conn.println(hstr);
-      }
-
-      Set<String> commandNames = CommandSelector.getCommandNames();
-      for(String cmdName : commandNames)
-      {
-        conn.println(cmdName);
-      }
-    }
-    else
-    {
-      Command cmd = CommandSelector.getInstance().get(command[1]);
-      if(cmd instanceof HelpfulCommand)
-      {
-        conn.println(((HelpfulCommand)cmd).getHelpString());
-      }
-      else
-      {
-        conn.println("No further help information available.");
-      }
-    }
-    
-    conn.println(".");
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/HelpfulCommand.java
--- a/org/sonews/daemon/command/HelpfulCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-/**
- *
- * @since sonews/1.1
- * @author Christian Lins
- */
-public interface HelpfulCommand extends Command
-{
-
-  /**
-   * @return A short description of this command, that is
-   * used within the output of the HELP command.
-   */
-  String getHelpString();
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/ListCommand.java
--- a/org/sonews/daemon/command/ListCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,153 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.Channel;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.util.Log;
-
-/**
- * Class handling the LIST command.
- * @author Christian Lins
- * @author Dennis Schwerdel
- * @since n3tpd/0.1
- */
-public class ListCommand implements Command
-{
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[]{"LIST"};
-  }
-
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return false;
-  }
-  
-  @Override
-  public void processLine(NNTPConnection conn, final String line, byte[] raw)
-    throws IOException, StorageBackendException
-  {
-    final String[] command = line.split(" ");
-    
-    if(command.length >= 2)
-    {
-      if(command[1].equalsIgnoreCase("OVERVIEW.FMT"))
-      {
-        conn.println("215 information follows");
-        conn.println("Subject:\nFrom:\nDate:\nMessage-ID:\nReferences:\nBytes:\nLines:\nXref");
-        conn.println(".");
-      }
-      else if(command[1].equalsIgnoreCase("NEWSGROUPS"))
-      {
-        conn.println("215 information follows");
-        final List<Channel> list = Channel.getAll();
-        for (Channel g : list)
-        {
-          conn.println(g.getName() + "\t" + "-");
-        }
-        conn.println(".");
-      }
-      else if(command[1].equalsIgnoreCase("SUBSCRIPTIONS"))
-      {
-        conn.println("215 information follows");
-        conn.println(".");
-      }
-      else if(command[1].equalsIgnoreCase("EXTENSIONS"))
-      {
-        conn.println("202 Supported NNTP extensions.");
-        conn.println("LISTGROUP");
-        conn.println("XDAEMON");
-        conn.println("XPAT");
-        conn.println(".");
-      }
-      else if(command[1].equalsIgnoreCase("ACTIVE"))
-      {
-        String  pattern  = command.length == 2
-          ? null : command[2].replace("*", "\\w*");
-        printGroupInfo(conn, pattern);
-      }
-      else
-      {
-        conn.println("500 unknown argument to LIST command");
-      }
-    }
-    else
-    {
-      printGroupInfo(conn, null);
-    }
-  }
-
-  private void printGroupInfo(NNTPConnection conn, String pattern)
-    throws IOException, StorageBackendException
-  {
-    final List<Channel> groups = Channel.getAll();
-    if(groups != null)
-    {
-      conn.println("215 list of newsgroups follows");
-      for(Channel g : groups)
-      {
-        try
-        {
-          Matcher matcher = pattern == null ?
-            null : Pattern.compile(pattern).matcher(g.getName());
-          if(!g.isDeleted() &&
-            (matcher == null || matcher.find()))
-          {
-            String writeable = g.isWriteable() ? " y" : " n";
-            // Indeed first the higher article number then the lower
-            conn.println(g.getName() + " " + g.getLastArticleNumber() + " "
-              + g.getFirstArticleNumber() + writeable);
-          }
-        }
-        catch(PatternSyntaxException ex)
-        {
-          Log.get().info(ex.toString());
-        }
-      }
-      conn.println(".");
-    }
-    else
-    {
-      conn.println("500 server backend malfunction");
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/ListGroupCommand.java
--- a/org/sonews/daemon/command/ListGroupCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,94 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import java.util.List;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.Channel;
-import org.sonews.storage.StorageBackendException;
-
-/**
- * Class handling the LISTGROUP command.
- * @author Christian Lins
- * @author Dennis Schwerdel
- * @since n3tpd/0.1
- */
-public class ListGroupCommand implements Command
-{
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[]{"LISTGROUP"};
-  }
-
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return false;
-  }
-
-  @Override
-  public void processLine(NNTPConnection conn, final String commandName, byte[] raw)
-    throws IOException, StorageBackendException
-  {
-    final String[] command = commandName.split(" ");
-
-    Channel group;
-    if(command.length >= 2)
-    {
-      group = Channel.getByName(command[1]);
-    }
-    else
-    {
-      group = conn.getCurrentChannel();
-    }
-
-    if (group == null)
-    {
-      conn.println("412 no group selected; use GROUP <group> command");
-      return;
-    }
-
-    List<Long> ids = group.getArticleNumbers();
-    conn.println("211 " + ids.size() + " " +
-      group.getFirstArticleNumber() + " " + 
-      group.getLastArticleNumber() + " list of article numbers follow");
-    for(long id : ids)
-    {
-      // One index number per line
-      conn.println(Long.toString(id));
-    }
-    conn.println(".");
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/ModeReaderCommand.java
--- a/org/sonews/daemon/command/ModeReaderCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.StorageBackendException;
-
-/**
- * Class handling the MODE READER command. This command actually does nothing
- * but returning a success status code.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class ModeReaderCommand implements Command
-{
-  
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[]{"MODE"};
-  }
-
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return false;
-  }
-
-  @Override
-  public void processLine(NNTPConnection conn, final String line, byte[] raw)
-    throws IOException, StorageBackendException
-  {
-    if(line.equalsIgnoreCase("MODE READER"))
-    {
-      conn.println("200 hello you can post");
-    }
-    else
-    {
-      conn.println("500 I do not know this mode command");
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/NewGroupsCommand.java
--- a/org/sonews/daemon/command/NewGroupsCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.StorageBackendException;
-
-/**
- * Class handling the NEWGROUPS command.
- * @author Christian Lins
- * @author Dennis Schwerdel
- * @since n3tpd/0.1
- */
-public class NewGroupsCommand implements Command
-{
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[]{"NEWGROUPS"};
-  }
-
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return false;
-  }
-
-  @Override
-  public void processLine(NNTPConnection conn, final String line, byte[] raw)
-    throws IOException, StorageBackendException
-  {
-    final String[] command = line.split(" ");
-
-    if(command.length == 3)
-    {
-      conn.println("231 list of new newsgroups follows");
-
-      // Currently we do not store a group's creation date;
-      // so we return an empty list which is a valid response
-      conn.println(".");
-    }
-    else
-    {
-      conn.println("500 invalid command usage");
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/NextPrevCommand.java
--- a/org/sonews/daemon/command/NextPrevCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.Article;
-import org.sonews.storage.Channel;
-import org.sonews.storage.StorageBackendException;
-
-/**
- * Class handling the NEXT and LAST command.
- * @author Christian Lins
- * @author Dennis Schwerdel
- * @since n3tpd/0.1
- */
-public class NextPrevCommand implements Command
-{
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[]{"NEXT", "PREV"};
-  }
-
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return false;
-  }
-
-  @Override
-  public void processLine(NNTPConnection conn, final String line, byte[] raw)
-    throws IOException, StorageBackendException
-  {
-    final Article currA = conn.getCurrentArticle();
-    final Channel currG = conn.getCurrentChannel();
-    
-    if (currA == null)
-    {
-      conn.println("420 no current article has been selected");
-      return;
-    }
-    
-    if (currG == null)
-    {
-      conn.println("412 no newsgroup selected");
-      return;
-    }
-    
-    final String[] command = line.split(" ");
-
-    if(command[0].equalsIgnoreCase("NEXT"))
-    {
-      selectNewArticle(conn, currA, currG, 1);
-    }
-    else if(command[0].equalsIgnoreCase("PREV"))
-    {
-      selectNewArticle(conn, currA, currG, -1);
-    }
-    else
-    {
-      conn.println("500 internal server error");
-    }
-  }
-  
-  private void selectNewArticle(NNTPConnection conn, Article article, Channel grp,
-    final int delta)
-    throws IOException, StorageBackendException
-  {
-    assert article != null;
-
-    article = grp.getArticle(grp.getIndexOf(article) + delta);
-
-    if(article == null)
-    {
-      conn.println("421 no next article in this group");
-    }
-    else
-    {
-      conn.setCurrentArticle(article);
-      conn.println("223 " + conn.getCurrentChannel().getIndexOf(article)
-                    + " " + article.getMessageID()
-                    + " article retrieved - request text separately");
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/OverCommand.java
--- a/org/sonews/daemon/command/OverCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,294 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import java.util.List;
-import org.sonews.util.Log;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.Article;
-import org.sonews.storage.ArticleHead;
-import org.sonews.storage.Headers;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.util.Pair;
-
-/**
- * Class handling the OVER/XOVER command.
- * 
- * Description of the XOVER command:
- * <pre>
- * XOVER [range]
- *
- * The XOVER command returns information from the overview
- * database for the article(s) specified.
- *
- * The optional range argument may be any of the following:
- *              an article number
- *              an article number followed by a dash to indicate
- *                 all following
- *              an article number followed by a dash followed by
- *                 another article number
- *
- * If no argument is specified, then information from the
- * current article is displayed. Successful responses start
- * with a 224 response followed by the overview information
- * for all matched messages. Once the output is complete, a
- * period is sent on a line by itself. If no argument is
- * specified, the information for the current article is
- * returned.  A news group must have been selected earlier,
- * else a 412 error response is returned. If no articles are
- * in the range specified, a 420 error response is returned
- * by the server. A 502 response will be returned if the
- * client only has permission to transfer articles.
- *
- * Each line of output will be formatted with the article number,
- * followed by each of the headers in the overview database or the
- * article itself (when the data is not available in the overview
- * database) for that article separated by a tab character.  The
- * sequence of fields must be in this order: subject, author,
- * date, message-id, references, byte count, and line count. Other
- * optional fields may follow line count. Other optional fields may
- * follow line count. These fields are specified by examining the
- * response to the LIST OVERVIEW.FMT command. Where no data exists,
- * a null field must be provided (i.e. the output will have two tab
- * characters adjacent to each other). Servers should not output
- * fields for articles that have been removed since the XOVER database
- * was created.
- *
- * The LIST OVERVIEW.FMT command should be implemented if XOVER
- * is implemented. A client can use LIST OVERVIEW.FMT to determine
- * what optional fields  and in which order all fields will be
- * supplied by the XOVER command. 
- *
- * Note that any tab and end-of-line characters in any header
- * data that is returned will be converted to a space character.
- *
- * Responses:
- *
- *   224 Overview information follows
- *   412 No news group current selected
- *   420 No article(s) selected
- *   502 no permission
- *
- * OVER defines additional responses:
- *
- *  First form (message-id specified)
- *    224    Overview information follows (multi-line)
- *    430    No article with that message-id
- *
- *  Second form (range specified)
- *    224    Overview information follows (multi-line)
- *    412    No newsgroup selected
- *    423    No articles in that range
- *
- *  Third form (current article number used)
- *    224    Overview information follows (multi-line)
- *    412    No newsgroup selected
- *    420    Current article number is invalid
- *
- * </pre>
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class OverCommand implements Command
-{
-
-  public static final int MAX_LINES_PER_DBREQUEST = 200;
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[]{"OVER", "XOVER"};
-  }
-
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return false;
-  }
-
-  @Override
-  public void processLine(NNTPConnection conn, final String line, byte[] raw)
-    throws IOException, StorageBackendException
-  {
-    if(conn.getCurrentChannel() == null)
-    {
-      conn.println("412 no newsgroup selected");
-    }
-    else
-    {
-      String[] command = line.split(" ");
-
-      // If no parameter was specified, show information about
-      // the currently selected article(s)
-      if(command.length == 1)
-      {
-        final Article art = conn.getCurrentArticle();
-        if(art == null)
-        {
-          conn.println("420 no article(s) selected");
-          return;
-        }
-
-        conn.println(buildOverview(art, -1));
-      }
-      // otherwise print information about the specified range
-      else
-      {
-        long artStart;
-        long artEnd   = conn.getCurrentChannel().getLastArticleNumber();
-        String[] nums = command[1].split("-");
-        if(nums.length >= 1)
-        {
-          try
-          {
-            artStart = Integer.parseInt(nums[0]);
-          }
-          catch(NumberFormatException e) 
-          {
-            Log.get().info(e.getMessage());
-            artStart = Integer.parseInt(command[1]);
-          }
-        }
-        else
-        {
-          artStart = conn.getCurrentChannel().getFirstArticleNumber();
-        }
-
-        if(nums.length >=2)
-        {
-          try
-          {
-            artEnd = Integer.parseInt(nums[1]);
-          }
-          catch(NumberFormatException e) 
-          {
-            e.printStackTrace();
-          }
-        }
-
-        if(artStart > artEnd)
-        {
-          if(command[0].equalsIgnoreCase("OVER"))
-          {
-            conn.println("423 no articles in that range");
-          }
-          else
-          {
-            conn.println("224 (empty) overview information follows:");
-            conn.println(".");
-          }
-        }
-        else
-        {
-          for(long n = artStart; n <= artEnd; n += MAX_LINES_PER_DBREQUEST)
-          {
-            long nEnd = Math.min(n + MAX_LINES_PER_DBREQUEST - 1, artEnd);
-            List<Pair<Long, ArticleHead>> articleHeads = conn.getCurrentChannel()
-              .getArticleHeads(n, nEnd);
-            if(articleHeads.isEmpty() && n == artStart
-              && command[0].equalsIgnoreCase("OVER"))
-            {
-              // This reply is only valid for OVER, not for XOVER command
-              conn.println("423 no articles in that range");
-              return;
-            }
-            else if(n == artStart)
-            {
-              // XOVER replies this although there is no data available
-              conn.println("224 overview information follows");
-            }
-
-            for(Pair<Long, ArticleHead> article : articleHeads)
-            {
-              String overview = buildOverview(article.getB(), article.getA());
-              conn.println(overview);
-            }
-          } // for
-          conn.println(".");
-        }
-      }
-    }
-  }
-  
-  private String buildOverview(ArticleHead art, long nr)
-  {
-    StringBuilder overview = new StringBuilder();
-    overview.append(nr);
-    overview.append('\t');
-
-    String subject = art.getHeader(Headers.SUBJECT)[0];
-    if("".equals(subject))
-    {
-      subject = "<empty>";
-    }
-    overview.append(escapeString(subject));
-    overview.append('\t');
-
-    overview.append(escapeString(art.getHeader(Headers.FROM)[0]));
-    overview.append('\t');
-    overview.append(escapeString(art.getHeader(Headers.DATE)[0]));
-    overview.append('\t');
-    overview.append(escapeString(art.getHeader(Headers.MESSAGE_ID)[0]));
-    overview.append('\t');
-    overview.append(escapeString(art.getHeader(Headers.REFERENCES)[0]));
-    overview.append('\t');
-
-    String bytes = art.getHeader(Headers.BYTES)[0];
-    if("".equals(bytes))
-    {
-      bytes = "0";
-    }
-    overview.append(escapeString(bytes));
-    overview.append('\t');
-
-    String lines = art.getHeader(Headers.LINES)[0];
-    if("".equals(lines))
-    {
-      lines = "0";
-    }
-    overview.append(escapeString(lines));
-    overview.append('\t');
-    overview.append(escapeString(art.getHeader(Headers.XREF)[0]));
-
-    // Remove trailing tabs if some data is empty
-    return overview.toString().trim();
-  }
-  
-  private String escapeString(String str)
-  {
-    String nstr = str.replace("\r", "");
-    nstr = nstr.replace('\n', ' ');
-    nstr = nstr.replace('\t', ' ');
-    return nstr.trim();
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/PostCommand.java
--- a/org/sonews/daemon/command/PostCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,332 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.sql.SQLException;
-import java.util.Arrays;
-import javax.mail.MessagingException;
-import javax.mail.internet.AddressException;
-import javax.mail.internet.InternetHeaders;
-import org.sonews.config.Config;
-import org.sonews.util.Log;
-import org.sonews.mlgw.Dispatcher;
-import org.sonews.storage.Article;
-import org.sonews.storage.Group;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.Headers;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.storage.StorageManager;
-import org.sonews.feed.FeedManager;
-import org.sonews.util.Stats;
-
-/**
- * Implementation of the POST command. This command requires multiple lines
- * from the client, so the handling of asynchronous reading is a little tricky
- * to handle.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class PostCommand implements Command
-{
-  
-  private final Article article   = new Article();
-  private int           lineCount = 0;
-  private long          bodySize  = 0;
-  private InternetHeaders headers = null;
-  private long          maxBodySize  = 
-    Config.inst().get(Config.ARTICLE_MAXSIZE, 128) * 1024L; // Size in bytes
-  private PostState     state     = PostState.WaitForLineOne;
-  private final ByteArrayOutputStream bufBody   = new ByteArrayOutputStream();
-  private final StringBuilder         strHead   = new StringBuilder();
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[]{"POST"};
-  }
-
-  @Override
-  public boolean hasFinished()
-  {
-    return this.state == PostState.Finished;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return true;
-  }
-
-  /**
-   * Process the given line String. line.trim() was called by NNTPConnection.
-   * @param line
-   * @throws java.io.IOException
-   * @throws java.sql.SQLException
-   */
-  @Override // TODO: Refactor this method to reduce complexity!
-  public void processLine(NNTPConnection conn, String line, byte[] raw)
-    throws IOException, StorageBackendException
-  {
-    switch(state)
-    {
-      case WaitForLineOne:
-      {
-        if(line.equalsIgnoreCase("POST"))
-        {
-          conn.println("340 send article to be posted. End with <CR-LF>.<CR-LF>");
-          state = PostState.ReadingHeaders;
-        }
-        else
-        {
-          conn.println("500 invalid command usage");
-        }
-        break;
-      }
-      case ReadingHeaders:
-      {
-        strHead.append(line);
-        strHead.append(NNTPConnection.NEWLINE);
-        
-        if("".equals(line) || ".".equals(line))
-        {
-          // we finally met the blank line
-          // separating headers from body
-          
-          try
-          {
-            // Parse the header using the InternetHeader class from JavaMail API
-            headers = new InternetHeaders(
-              new ByteArrayInputStream(strHead.toString().trim()
-                .getBytes(conn.getCurrentCharset())));
-
-            // add the header entries for the article
-            article.setHeaders(headers);
-          }
-          catch (MessagingException e)
-          {
-            e.printStackTrace();
-            conn.println("500 posting failed - invalid header");
-            state = PostState.Finished;
-            break;
-          }
-
-          // Change charset for reading body; 
-          // for multipart messages UTF-8 is returned
-          //conn.setCurrentCharset(article.getBodyCharset());
-          
-          state = PostState.ReadingBody;
-          
-          if(".".equals(line))
-          {
-            // Post an article without body
-            postArticle(conn, article);
-            state = PostState.Finished;
-          }
-        }
-        break;
-      }
-      case ReadingBody:
-      {
-        if(".".equals(line))
-        {    
-          // Set some headers needed for Over command
-          headers.setHeader(Headers.LINES, Integer.toString(lineCount));
-          headers.setHeader(Headers.BYTES, Long.toString(bodySize));
-
-          byte[] body = bufBody.toByteArray();
-          if(body.length >= 2)
-          {
-            // Remove trailing CRLF
-            body = Arrays.copyOf(body, body.length - 2);
-          }
-          article.setBody(body); // set the article body
-          
-          postArticle(conn, article);
-          state = PostState.Finished;
-        }
-        else
-        {
-          bodySize += line.length() + 1;
-          lineCount++;
-          
-          // Add line to body buffer
-          bufBody.write(raw, 0, raw.length);
-          bufBody.write(NNTPConnection.NEWLINE.getBytes());
-          
-          if(bodySize > maxBodySize)
-          {
-            conn.println("500 article is too long");
-            state = PostState.Finished;
-            break;
-          }
-        }
-        break;
-      }
-      default:
-      {
-        // Should never happen
-        Log.get().severe("PostCommand::processLine(): already finished...");
-      }
-    }
-  }
-  
-  /**
-   * Article is a control message and needs special handling.
-   * @param article
-   */
-  private void controlMessage(NNTPConnection conn, Article article)
-    throws IOException
-  {
-    String[] ctrl = article.getHeader(Headers.CONTROL)[0].split(" ");
-    if(ctrl.length == 2) // "cancel <mid>"
-    {
-      try
-      {
-        StorageManager.current().delete(ctrl[1]);
-        
-        // Move cancel message to "control" group
-        article.setHeader(Headers.NEWSGROUPS, "control");
-        StorageManager.current().addArticle(article);
-        conn.println("240 article cancelled");
-      }
-      catch(StorageBackendException ex)
-      {
-        Log.get().severe(ex.toString());
-        conn.println("500 internal server error");
-      }
-    }
-    else
-    {
-      conn.println("441 unknown control header");
-    }
-  }
-  
-  private void supersedeMessage(NNTPConnection conn, Article article)
-    throws IOException
-  {
-    try
-    {
-      String oldMsg = article.getHeader(Headers.SUPERSEDES)[0];
-      StorageManager.current().delete(oldMsg);
-      StorageManager.current().addArticle(article);
-      conn.println("240 article replaced");
-    }
-    catch(StorageBackendException ex)
-    {
-      Log.get().severe(ex.toString());
-      conn.println("500 internal server error");
-    }
-  }
-  
-  private void postArticle(NNTPConnection conn, Article article)
-    throws IOException
-  {
-    if(article.getHeader(Headers.CONTROL)[0].length() > 0)
-    {
-      controlMessage(conn, article);
-    }
-    else if(article.getHeader(Headers.SUPERSEDES)[0].length() > 0)
-    {
-      supersedeMessage(conn, article);
-    }
-    else // Post the article regularily
-    {
-      // Circle check; note that Path can already contain the hostname here
-      String host = Config.inst().get(Config.HOSTNAME, "localhost");
-      if(article.getHeader(Headers.PATH)[0].indexOf(host + "!", 1) > 0)
-      {
-        Log.get().info(article.getMessageID() + " skipped for host " + host);
-        conn.println("441 I know this article already");
-        return;
-      }
-
-      // Try to create the article in the database or post it to
-      // appropriate mailing list
-      try
-      {
-        boolean success = false;
-        String[] groupnames = article.getHeader(Headers.NEWSGROUPS)[0].split(",");
-        for(String groupname : groupnames)
-        {          
-          Group group = StorageManager.current().getGroup(groupname);
-          if(group != null && !group.isDeleted())
-          {
-            if(group.isMailingList() && !conn.isLocalConnection())
-            {
-              // Send to mailing list; the Dispatcher writes 
-              // statistics to database
-              Dispatcher.toList(article, group.getName());
-              success = true;
-            }
-            else
-            {
-              // Store in database
-              if(!StorageManager.current().isArticleExisting(article.getMessageID()))
-              {
-                StorageManager.current().addArticle(article);
-
-                // Log this posting to statistics
-                Stats.getInstance().mailPosted(
-                  article.getHeader(Headers.NEWSGROUPS)[0]);
-              }
-              success = true;
-            }
-          }
-        } // end for
-
-        if(success)
-        {
-          conn.println("240 article posted ok");
-          FeedManager.queueForPush(article);
-        }
-        else
-        {
-          conn.println("441 newsgroup not found");
-        }
-      }
-      catch(AddressException ex)
-      {
-        Log.get().warning(ex.getMessage());
-        conn.println("441 invalid sender address");
-      }
-      catch(MessagingException ex)
-      {
-        // A MessageException is thrown when the sender email address is
-        // invalid or something is wrong with the SMTP server.
-        System.err.println(ex.getLocalizedMessage());
-        conn.println("441 " + ex.getClass().getCanonicalName() + ": " + ex.getLocalizedMessage());
-      }
-      catch(StorageBackendException ex)
-      {
-        ex.printStackTrace();
-        conn.println("500 internal server error");
-      }
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/PostState.java
--- a/org/sonews/daemon/command/PostState.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-/**
- * States of the POST command's finite state machine.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-enum PostState
-{
-  WaitForLineOne, ReadingHeaders, ReadingBody, Finished
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/QuitCommand.java
--- a/org/sonews/daemon/command/QuitCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.StorageBackendException;
-
-/**
- * Implementation of the QUIT command; client wants to shutdown the connection.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class QuitCommand implements Command
-{
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[]{"QUIT"};
-  }
-  
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return false;
-  }
-
-  @Override
-  public void processLine(NNTPConnection conn, final String line, byte[] raw)
-    throws IOException, StorageBackendException
-  {    
-    conn.println("205 cya");
-    
-    conn.shutdownInput();
-    conn.shutdownOutput();
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/StatCommand.java
--- a/org/sonews/daemon/command/StatCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,114 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import org.sonews.storage.Article;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.StorageBackendException;
-
-/**
- * Implementation of the STAT command.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class StatCommand implements Command
-{
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[]{"STAT"};
-  }
-
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return false;
-  }
-
-  // TODO: Method has various exit points => Refactor!
-  @Override
-  public void processLine(NNTPConnection conn, final String line, byte[] raw)
-    throws IOException, StorageBackendException
-  {
-    final String[] command = line.split(" ");
-
-    Article article = null;
-    if(command.length == 1)
-    {
-      article = conn.getCurrentArticle();
-      if(article == null)
-      {
-        conn.println("420 no current article has been selected");
-        return;
-      }
-    }
-    else if(command[1].matches(NNTPConnection.MESSAGE_ID_PATTERN))
-    {
-      // Message-ID
-      article = Article.getByMessageID(command[1]);
-      if (article == null)
-      {
-        conn.println("430 no such article found");
-        return;
-      }
-    }
-    else
-    {
-      // Message Number
-      try
-      {
-        long aid = Long.parseLong(command[1]);
-        article = conn.getCurrentChannel().getArticle(aid);
-      }
-      catch(NumberFormatException ex)
-      {
-        ex.printStackTrace();
-      }
-      catch(StorageBackendException ex)
-      {
-        ex.printStackTrace();
-      }
-      if (article == null)
-      {
-        conn.println("423 no such article number in this group");
-        return;
-      }
-      conn.setCurrentArticle(article);
-    }
-    
-    conn.println("223 " + conn.getCurrentChannel().getIndexOf(article) + " "
-      + article.getMessageID()
-      + " article retrieved - request text separately");
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/UnsupportedCommand.java
--- a/org/sonews/daemon/command/UnsupportedCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import org.sonews.daemon.NNTPConnection;
-
-/**
- * A default "Unsupported Command". Simply returns error code 500 and a
- * "command not supported" message.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class UnsupportedCommand implements Command
-{
-  
-  /**
-   * @return Always returns null.
-   */
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return false;
-  }
-
-  @Override
-  public void processLine(NNTPConnection conn, final String line, byte[] raw)
-    throws IOException
-  {
-    conn.println("500 command not supported");
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/XDaemonCommand.java
--- a/org/sonews/daemon/command/XDaemonCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,270 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.util.List;
-import org.sonews.config.Config;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.storage.StorageManager;
-import org.sonews.feed.FeedManager;
-import org.sonews.feed.Subscription;
-import org.sonews.storage.Channel;
-import org.sonews.storage.Group;
-import org.sonews.util.Stats;
-
-/**
- * The XDAEMON command allows a client to get/set properties of the
- * running server daemon. Only locally connected clients are allowed to
- * use this command.
- * The restriction to localhost connection can be suppressed by overriding
- * the sonews.xdaemon.host bootstrap config property.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class XDaemonCommand implements Command
-{
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[]{"XDAEMON"};
-  }
-
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return false;
-  }
-
-  private void channelAdd(String[] commands, NNTPConnection conn)
-    throws IOException, StorageBackendException
-  {
-    String groupName = commands[2];
-    if(StorageManager.current().isGroupExisting(groupName))
-    {
-      conn.println("400 group " + groupName + " already existing!");
-    }
-    else
-    {
-      StorageManager.current().addGroup(groupName, Integer.parseInt(commands[3]));
-      conn.println("200 group " + groupName + " created");
-    }
-  }
-
-  // TODO: Refactor this method to reduce complexity!
-  @Override
-  public void processLine(NNTPConnection conn, String line, byte[] raw)
-    throws IOException, StorageBackendException
-  {
-    InetSocketAddress addr = (InetSocketAddress)conn.getSocketChannel().socket()
-      .getRemoteSocketAddress();
-    if(addr.getHostName().equals(
-      Config.inst().get(Config.XDAEMON_HOST, "localhost")))
-    {
-      String[] commands = line.split(" ", 4);
-      if(commands.length == 3 && commands[1].equalsIgnoreCase("LIST"))
-      {
-        if(commands[2].equalsIgnoreCase("CONFIGKEYS"))
-        {
-          conn.println("100 list of available config keys follows");
-          for(String key : Config.AVAILABLE_KEYS)
-          {
-            conn.println(key);
-          }
-          conn.println(".");
-        }
-        else if(commands[2].equalsIgnoreCase("PEERINGRULES"))
-        {
-          List<Subscription> pull = 
-            StorageManager.current().getSubscriptions(FeedManager.TYPE_PULL);
-          List<Subscription> push =
-            StorageManager.current().getSubscriptions(FeedManager.TYPE_PUSH);
-          conn.println("100 list of peering rules follows");
-          for(Subscription sub : pull)
-          {
-            conn.println("PULL " + sub.getHost() + ":" + sub.getPort()
-              + " " + sub.getGroup());
-          }
-          for(Subscription sub : push)
-          {
-            conn.println("PUSH " + sub.getHost() + ":" + sub.getPort()
-              + " " + sub.getGroup());
-          }
-          conn.println(".");
-        }
-        else
-        {
-          conn.println("401 unknown sub command");
-        }
-      }
-      else if(commands.length == 3 && commands[1].equalsIgnoreCase("DELETE"))
-      {
-        StorageManager.current().delete(commands[2]);
-        conn.println("200 article " + commands[2] + " deleted");
-      }
-      else if(commands.length == 4 && commands[1].equalsIgnoreCase("GROUPADD"))
-      {
-        channelAdd(commands, conn);
-      }
-      else if(commands.length == 3 && commands[1].equalsIgnoreCase("GROUPDEL"))
-      {
-        Group group = StorageManager.current().getGroup(commands[2]);
-        if(group == null)
-        {
-          conn.println("400 group not found");
-        }
-        else
-        {
-          group.setFlag(Group.DELETED);
-          group.update();
-          conn.println("200 group " + commands[2] + " marked as deleted");
-        }
-      }
-      else if(commands.length == 4 && commands[1].equalsIgnoreCase("SET"))
-      {
-        String key = commands[2];
-        String val = commands[3];
-        Config.inst().set(key, val);
-        conn.println("200 new config value set");
-      }
-      else if(commands.length == 3 && commands[1].equalsIgnoreCase("GET"))
-      {
-        String key = commands[2];
-        String val = Config.inst().get(key, null);
-        if(val != null)
-        {
-          conn.println("100 config value for " + key + " follows");
-          conn.println(val);
-          conn.println(".");
-        }
-        else
-        {
-          conn.println("400 config value not set");
-        }
-      }
-      else if(commands.length >= 3 && commands[1].equalsIgnoreCase("LOG"))
-      {
-        Group group = null;
-        if(commands.length > 3)
-        {
-          group = (Group)Channel.getByName(commands[3]);
-        }
-
-        if(commands[2].equalsIgnoreCase("CONNECTED_CLIENTS"))
-        {
-          conn.println("100 number of connections follow");
-          conn.println(Integer.toString(Stats.getInstance().connectedClients()));
-          conn.println(".");
-        }
-        else if(commands[2].equalsIgnoreCase("POSTED_NEWS"))
-        {
-          conn.println("100 hourly numbers of posted news yesterday");
-          for(int n = 0; n < 24; n++)
-          {
-            conn.println(n + " " + Stats.getInstance()
-              .getYesterdaysEvents(Stats.POSTED_NEWS, n, group));
-          }
-          conn.println(".");
-        }
-        else if(commands[2].equalsIgnoreCase("GATEWAYED_NEWS"))
-        {
-          conn.println("100 hourly numbers of gatewayed news yesterday");
-          for(int n = 0; n < 24; n++)
-          {
-            conn.println(n + " " + Stats.getInstance()
-              .getYesterdaysEvents(Stats.GATEWAYED_NEWS, n, group));
-          }
-          conn.println(".");
-        }
-        else if(commands[2].equalsIgnoreCase("TRANSMITTED_NEWS"))
-        {
-          conn.println("100 hourly numbers of news transmitted to peers yesterday");
-          for(int n = 0; n < 24; n++)
-          {
-            conn.println(n + " " + Stats.getInstance()
-              .getYesterdaysEvents(Stats.FEEDED_NEWS, n, group));
-          }
-          conn.println(".");
-        }
-        else if(commands[2].equalsIgnoreCase("HOSTED_NEWS"))
-        {
-          conn.println("100 number of overall hosted news");
-          conn.println(Integer.toString(Stats.getInstance().getNumberOfNews()));
-          conn.println(".");
-        }
-        else if(commands[2].equalsIgnoreCase("HOSTED_GROUPS"))
-        {
-          conn.println("100 number of hosted groups");
-          conn.println(Integer.toString(Stats.getInstance().getNumberOfGroups()));
-          conn.println(".");
-        }
-        else if(commands[2].equalsIgnoreCase("POSTED_NEWS_PER_HOUR"))
-        {
-          conn.println("100 posted news per hour");
-          conn.println(Double.toString(Stats.getInstance().postedPerHour(-1)));
-          conn.println(".");
-        }
-        else if(commands[2].equalsIgnoreCase("FEEDED_NEWS_PER_HOUR"))
-        {
-          conn.println("100 feeded news per hour");
-          conn.println(Double.toString(Stats.getInstance().feededPerHour(-1)));
-          conn.println(".");
-        }
-        else if(commands[2].equalsIgnoreCase("GATEWAYED_NEWS_PER_HOUR"))
-        {
-          conn.println("100 gatewayed news per hour");
-          conn.println(Double.toString(Stats.getInstance().gatewayedPerHour(-1)));
-          conn.println(".");
-        }
-        else
-        {
-          conn.println("401 unknown sub command");
-        }
-      }
-      else if(commands.length >= 3 && commands[1].equalsIgnoreCase("PLUGIN"))
-      {
-        
-      }
-      else
-      {
-        conn.println("400 invalid command usage");
-      }
-    }
-    else
-    {
-      conn.println("501 not allowed");
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/XPatCommand.java
--- a/org/sonews/daemon/command/XPatCommand.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,170 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.daemon.command;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.Locale;
-import java.util.regex.PatternSyntaxException;
-import org.sonews.daemon.NNTPConnection;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.storage.StorageManager;
-import org.sonews.util.Pair;
-
-/**
- * <pre>
- *   XPAT header range|<message-id> pat [pat...]
- *
- *   The XPAT command is used to retrieve specific headers from
- *   specific articles, based on pattern matching on the contents of
- *   the header. This command was first available in INN.
- *
- *   The required header parameter is the name of a header line (e.g.
- *   "subject") in a news group article. See RFC-1036 for a list
- *   of valid header lines. The required range argument may be
- *   any of the following:
- *               an article number
- *               an article number followed by a dash to indicate
- *                  all following
- *               an article number followed by a dash followed by
- *                  another article number
- *
- *   The required message-id argument indicates a specific
- *   article. The range and message-id arguments are mutually
- *   exclusive. At least one pattern in wildmat must be specified
- *   as well. If there are additional arguments the are joined
- *   together separated by a single space to form one complete
- *   pattern. Successful responses start with a 221 response
- *   followed by a the headers from all messages in which the
- *   pattern matched the contents of the specified header line. This
- *   includes an empty list. Once the output is complete, a period
- *   is sent on a line by itself. If the optional argument is a
- *   message-id and no such article exists, the 430 error response
- *   is returned. A 502 response will be returned if the client only
- *   has permission to transfer articles.
- *
- *   Responses
- *
- *       221 Header follows
- *       430 no such article
- *       502 no permission
- *
- *   Response Data:
- *
- *       art_nr fitting_header_value
- * 
- * </pre>
- * [Source:"draft-ietf-nntp-imp-02.txt"] [Copyright: 1998 S. Barber]
- * 
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class XPatCommand implements Command
-{
-
-  @Override
-  public String[] getSupportedCommandStrings()
-  {
-    return new String[]{"XPAT"};
-  }
-  
-  @Override
-  public boolean hasFinished()
-  {
-    return true;
-  }
-
-  @Override
-  public String impliedCapability()
-  {
-    return null;
-  }
-
-  @Override
-  public boolean isStateful()
-  {
-    return false;
-  }
-
-  @Override
-  public void processLine(NNTPConnection conn, final String line, byte[] raw)
-    throws IOException, StorageBackendException
-  {
-    if(conn.getCurrentChannel() == null)
-    {
-      conn.println("430 no group selected");
-      return;
-    }
-
-    String[] command = line.split("\\p{Space}+");
-
-    // There may be multiple patterns and Thunderbird produces
-    // additional spaces between range and pattern
-    if(command.length >= 4)
-    {
-      String header  = command[1].toLowerCase(Locale.US);
-      String range   = command[2];
-      String pattern = command[3];
-
-      long start = -1;
-      long end   = -1;
-      if(range.contains("-"))
-      {
-        String[] rsplit = range.split("-", 2);
-        start = Long.parseLong(rsplit[0]);
-        if(rsplit[1].length() > 0)
-        {
-          end = Long.parseLong(rsplit[1]);
-        }
-      }
-      else // TODO: Handle Message-IDs
-      {
-        start = Long.parseLong(range);
-      }
-
-      try
-      {
-        List<Pair<Long, String>> heads = StorageManager.current().
-          getArticleHeaders(conn.getCurrentChannel(), start, end, header, pattern);
-        
-        conn.println("221 header follows");
-        for(Pair<Long, String> head : heads)
-        {
-          conn.println(head.getA() + " " + head.getB());
-        }
-        conn.println(".");
-      }
-      catch(PatternSyntaxException ex)
-      {
-        ex.printStackTrace();
-        conn.println("500 invalid pattern syntax");
-      }
-      catch(StorageBackendException ex)
-      {
-        ex.printStackTrace();
-        conn.println("500 internal server error");
-      }
-    }
-    else
-    {
-      conn.println("430 invalid command usage");
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/package.html
--- a/org/sonews/daemon/command/package.html	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-Contains a class for every NNTP command.
\ No newline at end of file
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/package.html
--- a/org/sonews/daemon/package.html	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-Contains basic classes of the daemon.
\ No newline at end of file
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/feed/FeedManager.java
--- a/org/sonews/feed/FeedManager.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.feed;
-
-import org.sonews.storage.Article;
-
-/**
- * Controlls push and pull feeder.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public final class FeedManager 
-{
-
-  public static final int TYPE_PULL = 0;
-  public static final int TYPE_PUSH = 1;
-  
-  private static PullFeeder pullFeeder = new PullFeeder();
-  private static PushFeeder pushFeeder = new PushFeeder();
-  
-  /**
-   * Reads the peer subscriptions from database and starts the appropriate
-   * PullFeeder or PushFeeder.
-   */
-  public static synchronized void startFeeding()
-  {
-    pullFeeder.start();
-    pushFeeder.start();
-  }
-  
-  public static void queueForPush(Article article)
-  {
-    pushFeeder.queueForPush(article);
-  }
-  
-  private FeedManager() {}
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/feed/PullFeeder.java
--- a/org/sonews/feed/PullFeeder.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,276 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.feed;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.PrintWriter;
-import java.net.Socket;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.logging.Level;
-import org.sonews.config.Config;
-import org.sonews.daemon.AbstractDaemon;
-import org.sonews.util.Log;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.storage.StorageManager;
-import org.sonews.util.Stats;
-import org.sonews.util.io.ArticleReader;
-import org.sonews.util.io.ArticleWriter;
-
-/**
- * The PullFeeder class regularily checks another Newsserver for new
- * messages.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-class PullFeeder extends AbstractDaemon
-{
-  
-  private Map<Subscription, Integer> highMarks = new HashMap<Subscription, Integer>();
-  private BufferedReader             in;
-  private PrintWriter                out;
-  private Set<Subscription>          subscriptions = new HashSet<Subscription>();
-  
-  private void addSubscription(final Subscription sub)
-  {
-    subscriptions.add(sub);
-
-    if(!highMarks.containsKey(sub))
-    {
-      // Set a initial highMark
-      this.highMarks.put(sub, 0);
-    }
-  }
-  
-  /**
-   * Changes to the given group and returns its high mark.
-   * @param groupName
-   * @return
-   */
-  private int changeGroup(String groupName)
-    throws IOException
-  {
-    this.out.print("GROUP " + groupName + "\r\n");
-    this.out.flush();
-    
-    String line = this.in.readLine();
-    if(line.startsWith("211 "))
-    {
-      int highmark = Integer.parseInt(line.split(" ")[3]);
-      return highmark;
-    }
-    else
-    {
-      throw new IOException("GROUP " + groupName + " returned: " + line);
-    }
-  }
-  
-  private void connectTo(final String host, final int port)
-    throws IOException, UnknownHostException
-  {
-    Socket socket = new Socket(host, port);
-    this.out = new PrintWriter(socket.getOutputStream());
-    this.in  = new BufferedReader(new InputStreamReader(socket.getInputStream()));
-
-    String line = in.readLine();
-    if(!(line.charAt(0) == '2')) // Could be 200 or 2xx if posting is not allowed
-    {
-      throw new IOException(line);
-    }
-
-    // Send MODE READER to peer, some newsservers are friendlier then
-    this.out.println("MODE READER\r\n");
-    this.out.flush();
-    line = this.in.readLine();
-  }
-  
-  private void disconnect()
-    throws IOException
-  {
-    this.out.print("QUIT\r\n");
-    this.out.flush();
-    this.out.close();
-    this.in.close();
-    
-    this.out = null;
-    this.in  = null;
-  }
-  
-  /**
-   * Uses the OVER or XOVER command to get a list of message overviews that
-   * may be unknown to this feeder and are about to be peered.
-   * @param start
-   * @param end
-   * @return A list of message ids with potentially interesting messages.
-   */
-  private List<String> over(int start, int end)
-    throws IOException
-  {
-    this.out.print("OVER " + start + "-" + end + "\r\n");
-    this.out.flush();
-    
-    String line = this.in.readLine();
-    if(line.startsWith("500 ")) // OVER not supported
-    {
-      this.out.print("XOVER " + start + "-" + end + "\r\n");
-      this.out.flush();
-      
-      line = this.in.readLine();
-    }
-    
-    if(line.startsWith("224 "))
-    {
-      List<String> messages = new ArrayList<String>();
-      line = this.in.readLine();
-      while(!".".equals(line))
-      {
-        String mid = line.split("\t")[4]; // 5th should be the Message-ID
-        messages.add(mid);
-        line = this.in.readLine();
-      }
-      return messages;
-    }
-    else
-    {
-      throw new IOException("Server return for OVER/XOVER: " + line);
-    }
-  }
-  
-  @Override
-  public void run()
-  {
-    while(isRunning())
-    {
-      int pullInterval = 1000 * 
-        Config.inst().get(Config.FEED_PULLINTERVAL, 3600);
-      String host = "localhost";
-      int    port = 119;
-      
-      Log.get().info("Start PullFeeder run...");
-
-      try
-      {
-        this.subscriptions.clear();
-        List<Subscription> subsPull = StorageManager.current()
-          .getSubscriptions(FeedManager.TYPE_PULL);
-        for(Subscription sub : subsPull)
-        {
-          addSubscription(sub);
-        }
-      }
-      catch(StorageBackendException ex)
-      {
-        Log.get().log(Level.SEVERE, host, ex);
-      }
-
-      try
-      {
-        for(Subscription sub : this.subscriptions)
-        {
-          host = sub.getHost();
-          port = sub.getPort();
-
-          try
-          {
-            Log.get().info("Feeding " + sub.getGroup() + " from " + sub.getHost());
-            try
-            {
-              connectTo(host, port);
-            }
-            catch(SocketException ex)
-            {
-              Log.get().info("Skipping " + sub.getHost() + ": " + ex);
-              continue;
-            }
-            
-            int oldMark = this.highMarks.get(sub);
-            int newMark = changeGroup(sub.getGroup());
-            
-            if(oldMark != newMark)
-            {
-              List<String> messageIDs = over(oldMark, newMark);
-
-              for(String messageID : messageIDs)
-              {
-                if(!StorageManager.current().isArticleExisting(messageID))
-                {
-                  try
-                  {
-                    // Post the message via common socket connection
-                    ArticleReader aread =
-                      new ArticleReader(sub.getHost(), sub.getPort(), messageID);
-                    byte[] abuf = aread.getArticleData();
-                    if(abuf == null)
-                    {
-                      Log.get().warning("Could not feed " + messageID
-                        + " from " + sub.getHost());
-                    }
-                    else
-                    {
-                      Log.get().info("Feeding " + messageID);
-                      ArticleWriter awrite = new ArticleWriter(
-                        "localhost", Config.inst().get(Config.PORT, 119));
-                      awrite.writeArticle(abuf);
-                      awrite.close();
-                    }
-                    Stats.getInstance().mailFeeded(sub.getGroup());
-                  }
-                  catch(IOException ex)
-                  {
-                    // There may be a temporary network failure
-                    ex.printStackTrace();
-                    Log.get().warning("Skipping mail " + messageID + " due to exception.");
-                  }
-                }
-              } // for(;;)
-              this.highMarks.put(sub, newMark);
-            }
-            
-            disconnect();
-          }
-          catch(StorageBackendException ex)
-          {
-            ex.printStackTrace();
-          }
-          catch(IOException ex)
-          {
-            ex.printStackTrace();
-            Log.get().severe("PullFeeder run stopped due to exception.");
-          }
-        } // for(Subscription sub : subscriptions)
-        
-        Log.get().info("PullFeeder run ended. Waiting " + pullInterval / 1000 + "s");
-        Thread.sleep(pullInterval);
-      }
-      catch(InterruptedException ex)
-      {
-        Log.get().warning(ex.getMessage());
-      }
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/feed/PushFeeder.java
--- a/org/sonews/feed/PushFeeder.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.feed;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import org.sonews.daemon.AbstractDaemon;
-import org.sonews.storage.Article;
-import org.sonews.storage.Headers;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.storage.StorageManager;
-import org.sonews.util.Log;
-import org.sonews.util.io.ArticleWriter;
-
-/**
- * Pushes new articles to remote newsservers. This feeder sleeps until a new
- * message is posted to the sonews instance.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-class PushFeeder extends AbstractDaemon
-{
-  
-  private ConcurrentLinkedQueue<Article> articleQueue = 
-    new ConcurrentLinkedQueue<Article>();
-  
-  @Override
-  public void run()
-  {
-    while(isRunning())
-    {
-      try
-      {
-        synchronized(this)
-        {
-          this.wait();
-        }
-        
-        List<Subscription> subscriptions = StorageManager.current()
-          .getSubscriptions(FeedManager.TYPE_PUSH);
-
-        Article  article = this.articleQueue.poll();
-        String[] groups  = article.getHeader(Headers.NEWSGROUPS)[0].split(",");
-        Log.get().info("PushFeed: " + article.getMessageID());
-        for(Subscription sub : subscriptions)
-        {
-          // Circle check
-          if(article.getHeader(Headers.PATH)[0].contains(sub.getHost()))
-          {
-            Log.get().info(article.getMessageID() + " skipped for host "
-              + sub.getHost());
-            continue;
-          }
-
-          try
-          {
-            for(String group : groups)
-            {
-              if(sub.getGroup().equals(group))
-              {
-                // Delete headers that may cause problems
-                article.removeHeader(Headers.NNTP_POSTING_DATE);
-                article.removeHeader(Headers.NNTP_POSTING_HOST);
-                article.removeHeader(Headers.X_COMPLAINTS_TO);
-                article.removeHeader(Headers.X_TRACE);
-                article.removeHeader(Headers.XREF);
-                
-                // POST the message to remote server
-                ArticleWriter awriter = new ArticleWriter(sub.getHost(), sub.getPort());
-                awriter.writeArticle(article);
-                break;
-              }
-            }
-          }
-          catch(IOException ex)
-          {
-            Log.get().warning(ex.toString());
-          }
-        }
-      }
-      catch(StorageBackendException ex)
-      {
-        Log.get().severe(ex.toString());
-      }
-      catch(InterruptedException ex)
-      {
-        Log.get().warning("PushFeeder interrupted: " + ex);
-      }
-    }
-  }
-  
-  public void queueForPush(Article article)
-  {
-    this.articleQueue.add(article);
-    synchronized(this)
-    {
-      this.notifyAll();
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/feed/Subscription.java
--- a/org/sonews/feed/Subscription.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.feed;
-
-/**
- * For every group that is synchronized with or from a remote newsserver 
- * a Subscription instance exists.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class Subscription 
-{
-
-  private String host;
-  private int    port;
-  private int    feedtype;
-  private String group;
-  
-  public Subscription(String host, int port, int feedtype, String group)
-  {
-    this.host     = host;
-    this.port     = port;
-    this.feedtype = feedtype;
-    this.group    = group;
-  }
-
-  @Override
-  public boolean equals(Object obj)
-  {
-    if(obj instanceof Subscription)
-    {
-      Subscription sub = (Subscription)obj;
-      return sub.host.equals(host) && sub.group.equals(group) 
-        && sub.port == port && sub.feedtype == feedtype;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  @Override
-  public int hashCode()
-  {
-    return host.hashCode() + port + feedtype + group.hashCode();
-  }
-
-  public int getFeedtype()
-  {
-    return feedtype;
-  }
-
-  public String getGroup()
-  {
-    return group;
-  }
-
-  public String getHost()
-  {
-    return host;
-  }
-
-  public int getPort()
-  {
-    return port;
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/feed/package.html
--- a/org/sonews/feed/package.html	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-Contains classes for the peering functionality, e.g. pulling and pushing
-mails from and to remote newsservers.
\ No newline at end of file
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/mlgw/Dispatcher.java
--- a/org/sonews/mlgw/Dispatcher.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,301 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.mlgw;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javax.mail.Address;
-import javax.mail.Authenticator;
-import javax.mail.Message;
-import javax.mail.MessagingException;
-import javax.mail.PasswordAuthentication;
-import javax.mail.internet.InternetAddress;
-import org.sonews.config.Config;
-import org.sonews.storage.Article;
-import org.sonews.storage.Group;
-import org.sonews.storage.Headers;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.storage.StorageManager;
-import org.sonews.util.Log;
-import org.sonews.util.Stats;
-
-/**
- * Dispatches messages from mailing list to newsserver or vice versa.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class Dispatcher 
-{
-
-  static class PasswordAuthenticator extends Authenticator
-  {
-    
-    @Override
-    public PasswordAuthentication getPasswordAuthentication()
-    {
-      final String username = 
-        Config.inst().get(Config.MLSEND_USER, "user");
-      final String password = 
-        Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
-
-      return new PasswordAuthentication(username, password);
-    }
-    
-  }
-
-  /**
-   * Chunks out the email address of the full List-Post header field.
-   * @param listPostValue
-   * @return The matching email address or null
-   */
-  private static String chunkListPost(String listPostValue)
-  {
-    // listPostValue is of form "<mailto:dev@openoffice.org>"
-    Pattern mailPattern = Pattern.compile("(\\w+[-|.])*\\w+@(\\w+.)+\\w+");
-    Matcher mailMatcher = mailPattern.matcher(listPostValue);
-    if(mailMatcher.find())
-    {
-      return listPostValue.substring(mailMatcher.start(), mailMatcher.end());
-    }
-    else
-    {
-      return null;
-    }
-  }
-
-  /**
-   * This method inspects the header of the given message, trying
-   * to find the most appropriate recipient.
-   * @param msg
-   * @param fallback If this is false only List-Post and X-List-Post headers
-   *                 are examined.
-   * @return null or fitting group name for the given message.
-   */
-  private static List<String> getGroupFor(final Message msg, final boolean fallback)
-    throws MessagingException, StorageBackendException
-  {
-    List<String> groups = null;
-
-    // Is there a List-Post header?
-    String[]        listPost = msg.getHeader(Headers.LIST_POST);
-    InternetAddress listPostAddr;
-
-    if(listPost == null || listPost.length == 0 || "".equals(listPost[0]))
-    {
-      // Is there a X-List-Post header?
-      listPost = msg.getHeader(Headers.X_LIST_POST);
-    }
-
-    if(listPost != null && listPost.length > 0 
-      && !"".equals(listPost[0]) && chunkListPost(listPost[0]) != null)
-    {
-      // listPost[0] is of form "<mailto:dev@openoffice.org>"
-      listPost[0]  = chunkListPost(listPost[0]);
-      listPostAddr = new InternetAddress(listPost[0], false);
-      groups = StorageManager.current().getGroupsForList(listPostAddr.getAddress());
-    }
-    else if(fallback)
-    {
-      Log.get().info("Using fallback recipient discovery for: " + msg.getSubject());
-      groups = new ArrayList<String>();
-      // Fallback to TO/CC/BCC addresses
-      Address[] to = msg.getAllRecipients();
-      for(Address toa : to) // Address can have '<' '>' around
-      {
-        if(toa instanceof InternetAddress)
-        {
-          List<String> g = StorageManager.current()
-            .getGroupsForList(((InternetAddress)toa).getAddress());
-          groups.addAll(g);
-        }
-      }
-    }
-    
-    return groups;
-  }
-  
-  /**
-   * Posts a message that was received from a mailing list to the 
-   * appropriate newsgroup.
-   * If the message already exists in the storage, this message checks
-   * if it must be posted in an additional group. This can happen for
-   * crosspostings in different mailing lists.
-   * @param msg
-   */
-  public static boolean toGroup(final Message msg)
-  {
-    try
-    {
-      // Create new Article object
-      Article article = new Article(msg);
-      boolean posted  = false;
-
-      // Check if this mail is already existing the storage
-      boolean updateReq = 
-        StorageManager.current().isArticleExisting(article.getMessageID());
-
-      List<String> newsgroups = getGroupFor(msg, !updateReq);
-      List<String> oldgroups  = new ArrayList<String>();
-      if(updateReq)
-      {
-        // Check for duplicate entries of the same group
-        Article oldArticle = StorageManager.current().getArticle(article.getMessageID());
-        List<Group> oldGroups = oldArticle.getGroups();
-        for(Group oldGroup : oldGroups)
-        {
-          if(!newsgroups.contains(oldGroup.getName()))
-          {
-            oldgroups.add(oldGroup.getName());
-          }
-        }
-      }
-
-      if(newsgroups.size() > 0)
-      {
-        newsgroups.addAll(oldgroups);
-        StringBuilder groups = new StringBuilder();
-        for(int n = 0; n < newsgroups.size(); n++)
-        {
-          groups.append(newsgroups.get(n));
-          if (n + 1 != newsgroups.size())
-          {
-            groups.append(',');
-          }
-        }
-        Log.get().info("Posting to group " + groups.toString());
-
-        article.setGroup(groups.toString());
-        //article.removeHeader(Headers.REPLY_TO);
-        //article.removeHeader(Headers.TO);
-
-        // Write article to database
-        if(updateReq)
-        {
-          Log.get().info("Updating " + article.getMessageID()
-            + " with additional groups");
-          StorageManager.current().delete(article.getMessageID());
-          StorageManager.current().addArticle(article);
-        }
-        else
-        {
-          Log.get().info("Gatewaying " + article.getMessageID() + " to "
-            + article.getHeader(Headers.NEWSGROUPS)[0]);
-          StorageManager.current().addArticle(article);
-          Stats.getInstance().mailGatewayed(
-            article.getHeader(Headers.NEWSGROUPS)[0]);
-        }
-        posted = true;
-      }
-      else
-      {
-        StringBuilder buf = new StringBuilder();
-        for (Address toa : msg.getAllRecipients())
-        {
-          buf.append(' ');
-          buf.append(toa.toString());
-        }
-        buf.append(" " + article.getHeader(Headers.LIST_POST)[0]);
-        Log.get().warning("No group for" + buf.toString());
-      }
-      return posted;
-    }
-    catch(Exception ex)
-    {
-      ex.printStackTrace();
-      return false;
-    }
-  }
-  
-  /**
-   * Mails a message received through NNTP to the appropriate mailing list.
-   * This method MAY be called several times by PostCommand for the same
-   * article.
-   */
-  public static void toList(Article article, String group)
-    throws IOException, MessagingException, StorageBackendException
-  {
-    // Get mailing lists for the group of this article
-    List<String> rcptAddresses = StorageManager.current().getListsForGroup(group);
-
-    if(rcptAddresses == null || rcptAddresses.size() == 0)
-    {
-      Log.get().warning("No ML-address for " + group + " found.");
-      return;
-    }
-
-    for(String rcptAddress : rcptAddresses)
-    {
-      // Compose message and send it via given SMTP-Host
-      String smtpHost = Config.inst().get(Config.MLSEND_HOST, "localhost");
-      int smtpPort = Config.inst().get(Config.MLSEND_PORT, 25);
-      String smtpUser = Config.inst().get(Config.MLSEND_USER, "user");
-      String smtpPw = Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
-      String smtpFrom = Config.inst().get(
-        Config.MLSEND_ADDRESS, article.getHeader(Headers.FROM)[0]);
-
-      // TODO: Make Article cloneable()
-      article.getMessageID(); // Make sure an ID is existing
-      article.removeHeader(Headers.NEWSGROUPS);
-      article.removeHeader(Headers.PATH);
-      article.removeHeader(Headers.LINES);
-      article.removeHeader(Headers.BYTES);
-
-      article.setHeader("To", rcptAddress);
-      //article.setHeader("Reply-To", listAddress);
-
-      if (Config.inst().get(Config.MLSEND_RW_SENDER, false))
-      {
-        rewriteSenderAddress(article); // Set the SENDER address
-      }
-
-      SMTPTransport smtpTransport = new SMTPTransport(smtpHost, smtpPort);
-      smtpTransport.send(article, smtpFrom, rcptAddress);
-      smtpTransport.close();
-
-      Stats.getInstance().mailGatewayed(group);
-      Log.get().info("MLGateway: Mail " + article.getHeader("Subject")[0]
-        + " was delivered to " + rcptAddress + ".");
-    }
-  }
-  
-  /**
-   * Sets the SENDER header of the given MimeMessage. This might be necessary
-   * for moderated groups that does not allow the "normal" FROM sender.
-   * @param msg
-   * @throws javax.mail.MessagingException
-   */
-  private static void rewriteSenderAddress(Article msg)
-    throws MessagingException
-  {
-    String mlAddress = Config.inst().get(Config.MLSEND_ADDRESS, null);
-
-    if(mlAddress != null)
-    {
-      msg.setHeader(Headers.SENDER, mlAddress);
-    }
-    else
-    {
-      throw new MessagingException("Cannot rewrite SENDER header!");
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/mlgw/MailPoller.java
--- a/org/sonews/mlgw/MailPoller.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,151 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.mlgw;
-
-import java.util.Properties;
-import javax.mail.AuthenticationFailedException;
-import javax.mail.Authenticator;
-import javax.mail.Flags.Flag;
-import javax.mail.Folder;
-import javax.mail.Message;
-import javax.mail.MessagingException;
-import javax.mail.NoSuchProviderException;
-import javax.mail.PasswordAuthentication;
-import javax.mail.Session;
-import javax.mail.Store;
-import org.sonews.config.Config;
-import org.sonews.daemon.AbstractDaemon;
-import org.sonews.util.Log;
-import org.sonews.util.Stats;
-
-/**
- * Daemon polling for new mails in a POP3 account to be delivered to newsgroups.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class MailPoller extends AbstractDaemon
-{
-
-  static class PasswordAuthenticator extends Authenticator
-  {
-    
-    @Override
-    public PasswordAuthentication getPasswordAuthentication()
-    {
-      final String username = 
-        Config.inst().get(Config.MLPOLL_USER, "user");
-      final String password = 
-        Config.inst().get(Config.MLPOLL_PASSWORD, "mysecret");
-
-      return new PasswordAuthentication(username, password);
-    }
-    
-  }
-  
-  @Override
-  public void run()
-  {
-    Log.get().info("Starting Mailinglist Poller...");
-    int errors = 0;
-    while(isRunning())
-    {
-      try
-      {
-        // Wait some time between runs. At the beginning has advantages,
-        // because the wait is not skipped if an exception occurs.
-        Thread.sleep(60000 * (errors + 1)); // one minute * errors
-        
-        final String host     = 
-          Config.inst().get(Config.MLPOLL_HOST, "samplehost");
-        final String username = 
-          Config.inst().get(Config.MLPOLL_USER, "user");
-        final String password = 
-          Config.inst().get(Config.MLPOLL_PASSWORD, "mysecret");
-        
-        Stats.getInstance().mlgwRunStart();
-        
-        // Create empty properties
-        Properties props = System.getProperties();
-        props.put("mail.pop3.host", host);
-        props.put("mail.mime.address.strict", "false");
-
-        // Get session
-        Session session = Session.getInstance(props);
-
-        // Get the store
-        Store store = session.getStore("pop3");
-        store.connect(host, 110, username, password);
-
-        // Get folder
-        Folder folder = store.getFolder("INBOX");
-        folder.open(Folder.READ_WRITE);
-
-        // Get directory
-        Message[] messages = folder.getMessages();
-
-        // Dispatch messages and delete it afterwards on the inbox
-        for(Message message : messages)
-        {
-          if(Dispatcher.toGroup(message)
-            || Config.inst().get(Config.MLPOLL_DELETEUNKNOWN, false))
-          {
-            // Delete the message
-            message.setFlag(Flag.DELETED, true);
-          }
-        }
-
-        // Close connection 
-        folder.close(true); // true to expunge deleted messages
-        store.close();
-        errors = 0;
-        
-        Stats.getInstance().mlgwRunEnd();
-      }
-      catch(NoSuchProviderException ex)
-      {
-        Log.get().severe(ex.toString());
-        shutdown();
-      }
-      catch(AuthenticationFailedException ex)
-      {
-        // AuthentificationFailedException may be thrown if credentials are
-        // bad or if the Mailbox is in use (locked).
-        ex.printStackTrace();
-        errors = errors < 5 ? errors + 1 : errors;
-      }
-      catch(InterruptedException ex)
-      {
-        System.out.println("sonews: " + this + " returns: " + ex);
-        return;
-      }
-      catch(MessagingException ex)
-      {
-        ex.printStackTrace();
-        errors = errors < 5 ? errors + 1 : errors;
-      }
-      catch(Exception ex)
-      {
-        ex.printStackTrace();
-        errors = errors < 5 ? errors + 1 : errors;
-      }
-    }
-    Log.get().severe("MailPoller exited.");
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/mlgw/SMTPTransport.java
--- a/org/sonews/mlgw/SMTPTransport.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,133 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.mlgw;
-
-import java.io.BufferedOutputStream;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.Socket;
-import java.net.UnknownHostException;
-import org.sonews.config.Config;
-import org.sonews.storage.Article;
-import org.sonews.util.io.ArticleInputStream;
-
-/**
- * Connects to a SMTP server and sends a given Article to it.
- * @author Christian Lins
- * @since sonews/1.0
- */
-class SMTPTransport
-{
-
-  protected BufferedReader       in;
-  protected BufferedOutputStream out;
-  protected Socket               socket;
-
-  public SMTPTransport(String host, int port)
-    throws IOException, UnknownHostException
-  {
-    socket = new Socket(host, port);
-    this.in  = new BufferedReader(new InputStreamReader(socket.getInputStream()));
-    this.out = new BufferedOutputStream(socket.getOutputStream());
-
-    // Read helo from server
-    String line = this.in.readLine();
-    if(line == null || !line.startsWith("220 "))
-    {
-      throw new IOException("Invalid helo from server: " + line);
-    }
-
-    // Send HELO to server
-    this.out.write(
-      ("HELO " + Config.inst().get(Config.HOSTNAME, "localhost") + "\r\n").getBytes("UTF-8"));
-    this.out.flush();
-    line = this.in.readLine();
-    if(line == null || !line.startsWith("250 "))
-    {
-      throw new IOException("Unexpected reply: " + line);
-    }
-  }
-
-  public SMTPTransport(String host)
-    throws IOException
-  {
-    this(host, 25);
-  }
-
-  public void close()
-    throws IOException
-  {
-    this.out.write("QUIT".getBytes("UTF-8"));
-    this.out.flush();
-    this.in.readLine();
-
-    this.socket.close();
-  }
-
-  public void send(Article article, String mailFrom, String rcptTo)
-    throws IOException
-  {
-    assert(article != null);
-    assert(mailFrom != null);
-    assert(rcptTo != null);
-
-    this.out.write(("MAIL FROM: " + mailFrom).getBytes("UTF-8"));
-    this.out.flush();
-    String line = this.in.readLine();
-    if(line == null || !line.startsWith("250 "))
-    {
-      throw new IOException("Unexpected reply: " + line);
-    }
-
-    this.out.write(("RCPT TO: " + rcptTo).getBytes("UTF-8"));
-    this.out.flush();
-    line  = this.in.readLine();
-    if(line == null || !line.startsWith("250 "))
-    {
-      throw new IOException("Unexpected reply: " + line);
-    }
-
-    this.out.write("DATA".getBytes("UTF-8"));
-    this.out.flush();
-    line = this.in.readLine();
-    if(line == null || !line.startsWith("354 "))
-    {
-      throw new IOException("Unexpected reply: " + line);
-    }
-
-    ArticleInputStream   artStream = new ArticleInputStream(article);
-    for(int b = artStream.read(); b >= 0; b = artStream.read())
-    {
-      this.out.write(b);
-    }
-
-    // Flush the binary stream; important because otherwise the output
-    // will be mixed with the PrintWriter.
-    this.out.flush();
-    this.out.write("\r\n.\r\n".getBytes("UTF-8"));
-    this.out.flush();
-    line = this.in.readLine();
-    if(line == null || !line.startsWith("250 "))
-    {
-      throw new IOException("Unexpected reply: " + line);
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/mlgw/package.html
--- a/org/sonews/mlgw/package.html	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-Contains classes of the Mailinglist Gateway.
\ No newline at end of file
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/plugin/Plugin.java
--- a/org/sonews/plugin/Plugin.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.plugin;
-
-/**
- * A generic Plugin for sonews. Implementing classes do not really add new
- * functionality to sonews but can use this interface as convenient procedure
- * for installing functionality plugins, e.g. Command-Plugins or Storage-Plugins.
- * @author Christian Lins
- * @since sonews/1.1
- */
-public interface Plugin
-{
-
-  /**
-   * Called when the Plugin is loaded by sonews. This method can be used
-   * by implementing classes to install additional or required plugins.
-   */
-  void load();
-
-  /**
-   * Called when the Plugin is unloaded by sonews.
-   */
-  void unload();
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/Article.java
--- a/org/sonews/storage/Article.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,253 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.storage;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.UUID;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.List;
-import javax.mail.Header;
-import javax.mail.Message;
-import javax.mail.MessagingException;
-import javax.mail.internet.InternetHeaders;
-import org.sonews.config.Config;
-
-/**
- * Represents a newsgroup article.
- * @author Christian Lins
- * @author Denis Schwerdel
- * @since n3tpd/0.1
- */
-public class Article extends ArticleHead
-{
-  
-  /**
-   * Loads the Article identified by the given ID from the JDBCDatabase.
-   * @param messageID
-   * @return null if Article is not found or if an error occurred.
-   */
-  public static Article getByMessageID(final String messageID)
-  {
-    try
-    {
-      return StorageManager.current().getArticle(messageID);
-    }
-    catch(StorageBackendException ex)
-    {
-      ex.printStackTrace();
-      return null;
-    }
-  }
-  
-  private byte[] body       = new byte[0];
-  
-  /**
-   * Default constructor.
-   */
-  public Article()
-  {
-  }
-  
-  /**
-   * Creates a new Article object using the date from the given
-   * raw data.
-   */
-  public Article(String headers, byte[] body)
-  {
-    try
-    {
-      this.body  = body;
-
-      // Parse the header
-      this.headers = new InternetHeaders(
-        new ByteArrayInputStream(headers.getBytes()));
-      
-      this.headerSrc = headers;
-    }
-    catch(MessagingException ex)
-    {
-      ex.printStackTrace();
-    }
-  }
-
-  /**
-   * Creates an Article instance using the data from the javax.mail.Message
-   * object. This constructor is called by the Mailinglist gateway.
-   * @see javax.mail.Message
-   * @param msg
-   * @throws IOException
-   * @throws MessagingException
-   */
-  public Article(final Message msg)
-    throws IOException, MessagingException
-  {
-    this.headers = new InternetHeaders();
-
-    for(Enumeration e = msg.getAllHeaders() ; e.hasMoreElements();) 
-    {
-      final Header header = (Header)e.nextElement();
-      this.headers.addHeader(header.getName(), header.getValue());
-    }
-
-	// Reads the raw byte body using Message.writeTo(OutputStream out)
-	this.body = readContent(msg);
-    
-    // Validate headers
-    validateHeaders();
-  }
-
-  /**
-   * Reads from the given Message into a byte array.
-   * @param in
-   * @return
-   * @throws IOException
-   */
-  private byte[] readContent(Message in)
-    throws IOException, MessagingException
-  {
-    ByteArrayOutputStream out = new ByteArrayOutputStream();
-    in.writeTo(out);
-    return out.toByteArray();
-  }
-
-  /**
-   * Removes the header identified by the given key.
-   * @param headerKey
-   */
-  public void removeHeader(final String headerKey)
-  {
-    this.headers.removeHeader(headerKey);
-    this.headerSrc = null;
-  }
-
-  /**
-   * Generates a message id for this article and sets it into
-   * the header object. You have to update the JDBCDatabase manually to make this
-   * change persistent.
-   * Note: a Message-ID should never be changed and only generated once.
-   */
-  private String generateMessageID()
-  {
-    String randomString;
-    MessageDigest md5;
-    try
-    {
-      md5 = MessageDigest.getInstance("MD5");
-      md5.reset();
-      md5.update(getBody());
-      md5.update(getHeader(Headers.SUBJECT)[0].getBytes());
-      md5.update(getHeader(Headers.FROM)[0].getBytes());
-      byte[] result = md5.digest();
-      StringBuffer hexString = new StringBuffer();
-      for (int i = 0; i < result.length; i++)
-      {
-        hexString.append(Integer.toHexString(0xFF & result[i]));
-      }
-      randomString = hexString.toString();
-    }
-    catch (NoSuchAlgorithmException e)
-    {
-      e.printStackTrace();
-      randomString = UUID.randomUUID().toString();
-    }
-    String msgID = "<" + randomString + "@"
-        + Config.inst().get(Config.HOSTNAME, "localhost") + ">";
-    
-    this.headers.setHeader(Headers.MESSAGE_ID, msgID);
-    
-    return msgID;
-  }
-
-  /**
-   * Returns the body string.
-   */
-  public byte[] getBody()
-  {
-    return body;
-  }
-  
-  /**
-   * @return Numerical IDs of the newsgroups this Article belongs to.
-   */
-  public List<Group> getGroups()
-  {
-    String[]         groupnames = getHeader(Headers.NEWSGROUPS)[0].split(",");
-    ArrayList<Group> groups     = new ArrayList<Group>();
-
-    try
-    {
-      for(String newsgroup : groupnames)
-      {
-        newsgroup = newsgroup.trim();
-        Group group = StorageManager.current().getGroup(newsgroup);
-        if(group != null &&         // If the server does not provide the group, ignore it
-          !groups.contains(group))  // Yes, there may be duplicates
-        {
-          groups.add(group);
-        }
-      }
-    }
-    catch(StorageBackendException ex)
-    {
-      ex.printStackTrace();
-      return null;
-    }
-    return groups;
-  }
-
-  public void setBody(byte[] body)
-  {
-    this.body = body;
-  }
-  
-  /**
-   * 
-   * @param groupname Name(s) of newsgroups
-   */
-  public void setGroup(String groupname)
-  {
-    this.headers.setHeader(Headers.NEWSGROUPS, groupname);
-  }
-
-  /**
-   * Returns the Message-ID of this Article. If the appropriate header
-   * is empty, a new Message-ID is created.
-   * @return Message-ID of this Article.
-   */
-  public String getMessageID()
-  {
-    String[] msgID = getHeader(Headers.MESSAGE_ID);
-    return msgID[0].equals("") ? generateMessageID() : msgID[0];
-  }
-  
-  /**
-   * @return String containing the Message-ID.
-   */
-  @Override
-  public String toString()
-  {
-    return getMessageID();
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/ArticleHead.java
--- a/org/sonews/storage/ArticleHead.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,161 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.storage;
-
-import java.io.ByteArrayInputStream;
-import java.util.Enumeration;
-import javax.mail.Header;
-import javax.mail.MessagingException;
-import javax.mail.internet.InternetHeaders;
-import javax.mail.internet.MimeUtility;
-import org.sonews.config.Config;
-
-/**
- * An article with no body only headers.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class ArticleHead 
-{
-
-  protected InternetHeaders headers   = null;
-  protected String          headerSrc = null;
-  
-  protected ArticleHead()
-  {
-  }
-  
-  public ArticleHead(String headers)
-  {
-    try
-    {
-      // Parse the header
-      this.headers = new InternetHeaders(
-          new ByteArrayInputStream(headers.getBytes()));
-    }
-    catch(MessagingException ex)
-    {
-      ex.printStackTrace();
-    }
-  }
-  
-  /**
-   * Returns the header field with given name.
-   * @param name Name of the header field(s).
-   * @param returnNull If set to true, this method will return null instead
-   *                   of an empty array if there is no header field found.
-   * @return Header values or empty string.
-   */
-  public String[] getHeader(String name, boolean returnNull)
-  {
-    String[] ret = this.headers.getHeader(name);
-    if(ret == null && !returnNull)
-    {
-      ret = new String[]{""};
-    }
-    return ret;
-  }
-
-  public String[] getHeader(String name)
-  {
-    return getHeader(name, false);
-  }
-  
-  /**
-   * Sets the header value identified through the header name.
-   * @param name
-   * @param value
-   */
-  public void setHeader(String name, String value)
-  {
-    this.headers.setHeader(name, value);
-    this.headerSrc = null;
-  }
-
-    public Enumeration getAllHeaders()
-  {
-    return this.headers.getAllHeaders();
-  }
-
-  /**
-   * @return Header source code of this Article.
-   */
-  public String getHeaderSource()
-  {
-    if(this.headerSrc != null)
-    {
-      return this.headerSrc;
-    }
-
-    StringBuffer buf = new StringBuffer();
-
-    for(Enumeration en = this.headers.getAllHeaders(); en.hasMoreElements();)
-    {
-      Header entry = (Header)en.nextElement();
-
-      String value = entry.getValue().replaceAll("[\r\n]", " ");
-      buf.append(entry.getName());
-      buf.append(": ");
-      buf.append(MimeUtility.fold(entry.getName().length() + 2, value));
-
-      if(en.hasMoreElements())
-      {
-        buf.append("\r\n");
-      }
-    }
-
-    this.headerSrc = buf.toString();
-    return this.headerSrc;
-  }
-
-  /**
-   * Sets the headers of this Article. If headers contain no
-   * Message-Id a new one is created.
-   * @param headers
-   */
-  public void setHeaders(InternetHeaders headers)
-  {
-    this.headers   = headers;
-    this.headerSrc = null;
-    validateHeaders();
-  }
-
-  /**
-   * Checks some headers for their validity and generates an
-   * appropriate Path-header for this host if not yet existing.
-   * This method is called by some Article constructors and the
-   * method setHeaders().
-   * @return true if something on the headers was changed.
-   */
-  protected void validateHeaders()
-  {
-    // Check for valid Path-header
-    final String path = getHeader(Headers.PATH)[0];
-    final String host = Config.inst().get(Config.HOSTNAME, "localhost");
-    if(!path.startsWith(host))
-    {
-      StringBuffer pathBuf = new StringBuffer();
-      pathBuf.append(host);
-      pathBuf.append('!');
-      pathBuf.append(path);
-      this.headers.setHeader(Headers.PATH, pathBuf.toString());
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/Channel.java
--- a/org/sonews/storage/Channel.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.storage;
-
-import java.util.ArrayList;
-import java.util.List;
-import org.sonews.util.Pair;
-
-/**
- * A logical communication Channel is the a generic structural element for sets
- * of messages; e.g. a Newsgroup for a set of Articles.
- * A Channel can either be a real set of messages or an aggregated set of
- * several subsets.
- * @author Christian Lins
- * @since sonews/1.0
- */
-public abstract class Channel
-{
-
-  /**
-   * If this flag is set the Group is no real newsgroup but a mailing list
-   * mirror. In that case every posting and receiving mails must go through
-   * the mailing list gateway.
-   */
-  public static final int MAILINGLIST = 0x1;
-
-  /**
-   * If this flag is set the Group is marked as readonly and the posting
-   * is prohibited. This can be useful for groups that are synced only in
-   * one direction.
-   */
-  public static final int READONLY    = 0x2;
-
-  /**
-   * If this flag is set the Group is marked as deleted and must not occur
-   * in any output. The deletion is done lazily by a low priority daemon.
-   */
-  public static final int DELETED     = 0x80;
-
-  public static List<Channel> getAll()
-  {
-    List<Channel> all = new ArrayList<Channel>();
-
-    /*List<Channel> agroups = AggregatedGroup.getAll();
-    if(agroups != null)
-    {
-      all.addAll(agroups);
-    }*/
-
-    List<Channel> groups = Group.getAll();
-    if(groups != null)
-    {
-      all.addAll(groups);
-    }
-
-    return all;
-  }
-
-  public static Channel getByName(String name)
-    throws StorageBackendException
-  {
-    return StorageManager.current().getGroup(name);
-  }
-
-  public abstract Article getArticle(long idx)
-    throws StorageBackendException;
-
-  public abstract List<Pair<Long, ArticleHead>> getArticleHeads(
-    final long first, final long last)
-    throws StorageBackendException;
-
-  public abstract List<Long> getArticleNumbers()
-    throws StorageBackendException;
-
-  public abstract long getFirstArticleNumber()
-    throws StorageBackendException;
-
-  public abstract long getIndexOf(Article art)
-    throws StorageBackendException;
-
-  public abstract long getInternalID();
-
-  public abstract long getLastArticleNumber()
-    throws StorageBackendException;
-
-  public abstract String getName();
-  
-  public abstract long getPostingsCount()
-    throws StorageBackendException;
-
-  public abstract boolean isDeleted();
-
-  public abstract boolean isWriteable();
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/Group.java
--- a/org/sonews/storage/Group.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,184 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.storage;
-
-import java.sql.SQLException;
-import java.util.List;
-import org.sonews.util.Log;
-import org.sonews.util.Pair;
-
-/**
- * Represents a logical Group within this newsserver.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-// TODO: This class should not be public!
-public class Group extends Channel
-{
-  
-  private long   id     = 0;
-  private int    flags  = -1;
-  private String name   = null;
-
-  /**
-   * @return List of all groups this server handles.
-   */
-  public static List<Channel> getAll()
-  {
-    try
-    {
-      return StorageManager.current().getGroups();
-    }
-    catch(StorageBackendException ex)
-    {
-      Log.get().severe(ex.getMessage());
-      return null;
-    }
-  }
-  
-  /**
-   * @param name
-   * @param id
-   */
-  public Group(final String name, final long id, final int flags)
-  {
-    this.id    = id;
-    this.flags = flags;
-    this.name  = name;
-  }
-
-  @Override
-  public boolean equals(Object obj)
-  {
-    if(obj instanceof Group)
-    {
-      return ((Group)obj).id == this.id;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  public Article getArticle(long idx)
-    throws StorageBackendException
-  {
-    return StorageManager.current().getArticle(idx, this.id);
-  }
-
-  public List<Pair<Long, ArticleHead>> getArticleHeads(final long first, final long last)
-    throws StorageBackendException
-  {
-    return StorageManager.current().getArticleHeads(this, first, last);
-  }
-  
-  public List<Long> getArticleNumbers()
-    throws StorageBackendException
-  {
-    return StorageManager.current().getArticleNumbers(id);
-  }
-
-  public long getFirstArticleNumber()
-    throws StorageBackendException
-  {
-    return StorageManager.current().getFirstArticleNumber(this);
-  }
-
-  public int getFlags()
-  {
-    return this.flags;
-  }
-
-  public long getIndexOf(Article art)
-    throws StorageBackendException
-  {
-    return StorageManager.current().getArticleIndex(art, this);
-  }
-
-  /**
-   * Returns the group id.
-   */
-  public long getInternalID()
-  {
-    assert id > 0;
-
-    return id;
-  }
-
-  public boolean isDeleted()
-  {
-    return (this.flags & DELETED) != 0;
-  }
-
-  public boolean isMailingList()
-  {
-    return (this.flags & MAILINGLIST) != 0;
-  }
-
-  public boolean isWriteable()
-  {
-    return true;
-  }
-
-  public long getLastArticleNumber()
-    throws StorageBackendException
-  {
-    return StorageManager.current().getLastArticleNumber(this);
-  }
-
-  public String getName()
-  {
-    return name;
-  }
-
-  /**
-   * Performs this.flags |= flag to set a specified flag and updates the data
-   * in the JDBCDatabase.
-   * @param flag
-   */
-  public void setFlag(final int flag)
-  {
-    this.flags |= flag;
-  }
-
-  public void setName(final String name)
-  {
-    this.name = name;
-  }
-
-  /**
-   * @return Number of posted articles in this group.
-   * @throws java.sql.SQLException
-   */
-  public long getPostingsCount()
-    throws StorageBackendException
-  {
-    return StorageManager.current().getPostingsCount(this.name);
-  }
-
-  /**
-   * Updates flags and name in the backend.
-   */
-  public void update()
-    throws StorageBackendException
-  {
-    StorageManager.current().update(this);
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/Headers.java
--- a/org/sonews/storage/Headers.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.storage;
-
-/**
- * Contains header constants. These header keys are no way complete but all
- * headers that are relevant for sonews.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public final class Headers
-{
-
-  public static final String BYTES             = "bytes";
-  public static final String CONTENT_TYPE      = "content-type";
-  public static final String CONTROL           = "control";
-  public static final String DATE              = "date";
-  public static final String FROM              = "from";
-  public static final String LINES             = "lines";
-  public static final String LIST_POST         = "list-post";
-  public static final String MESSAGE_ID        = "message-id";
-  public static final String NEWSGROUPS        = "newsgroups";
-  public static final String NNTP_POSTING_DATE = "nntp-posting-date";
-  public static final String NNTP_POSTING_HOST = "nntp-posting-host";
-  public static final String PATH              = "path";
-  public static final String REFERENCES        = "references";
-  public static final String REPLY_TO          = "reply-to";
-  public static final String SENDER            = "sender";
-  public static final String SUBJECT           = "subject";
-  public static final String SUPERSEDES        = "subersedes";
-  public static final String TO                = "to";
-  public static final String X_COMPLAINTS_TO   = "x-complaints-to";
-  public static final String X_LIST_POST       = "x-list-post";
-  public static final String X_TRACE           = "x-trace";
-  public static final String XREF              = "xref";
-
-  private Headers()
-  {}
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/Storage.java
--- a/org/sonews/storage/Storage.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,150 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.storage;
-
-import java.util.List;
-import org.sonews.feed.Subscription;
-import org.sonews.util.Pair;
-
-/**
- * A generic storage backend interface.
- * @author Christian Lins
- * @since sonews/1.0
- */
-public interface Storage
-{
-
-  /**
-   * Stores the given Article in the storage.
-   * @param art
-   * @throws StorageBackendException
-   */
-  void addArticle(Article art)
-    throws StorageBackendException;
-
-  void addEvent(long timestamp, int type, long groupID)
-    throws StorageBackendException;
-
-  void addGroup(String groupname, int flags)
-    throws StorageBackendException;
-
-  int countArticles()
-    throws StorageBackendException;
-
-  int countGroups()
-    throws StorageBackendException;
-
-  void delete(String messageID)
-    throws StorageBackendException;
-
-  Article getArticle(String messageID)
-    throws StorageBackendException;
-
-  Article getArticle(long articleIndex, long groupID)
-    throws StorageBackendException;
-
-  List<Pair<Long, ArticleHead>> getArticleHeads(Group group, long first, long last)
-    throws StorageBackendException;
-
-  List<Pair<Long, String>> getArticleHeaders(Channel channel, long start, long end,
-    String header, String pattern)
-    throws StorageBackendException;
-
-  long getArticleIndex(Article art, Group group)
-    throws StorageBackendException;
-
-  List<Long> getArticleNumbers(long groupID)
-    throws StorageBackendException;
-
-  String getConfigValue(String key)
-    throws StorageBackendException;
-
-  int getEventsCount(int eventType, long startTimestamp, long endTimestamp,
-    Channel channel)
-    throws StorageBackendException;
-
-  double getEventsPerHour(int key, long gid)
-    throws StorageBackendException;
-
-  int getFirstArticleNumber(Group group)
-    throws StorageBackendException;
-
-  Group getGroup(String name)
-    throws StorageBackendException;
-
-  List<Channel> getGroups()
-    throws StorageBackendException;
-
-  /**
-   * Retrieves the collection of groupnames that are associated with the
-   * given list address.
-   * @param inetaddress
-   * @return
-   * @throws StorageBackendException
-   */
-  List<String> getGroupsForList(String listAddress)
-    throws StorageBackendException;
-
-  int getLastArticleNumber(Group group)
-    throws StorageBackendException;
-
-  /**
-   * Returns a list of email addresses that are related to the given
-   * groupname. In most cases the list may contain only one entry.
-   * @param groupname
-   * @return
-   * @throws StorageBackendException
-   */
-  List<String> getListsForGroup(String groupname)
-    throws StorageBackendException;
-
-  String getOldestArticle()
-    throws StorageBackendException;
-
-  int getPostingsCount(String groupname)
-    throws StorageBackendException;
-
-  List<Subscription> getSubscriptions(int type)
-    throws StorageBackendException;
-
-  boolean isArticleExisting(String messageID)
-    throws StorageBackendException;
-
-  boolean isGroupExisting(String groupname)
-    throws StorageBackendException;
-
-  void purgeGroup(Group group)
-    throws StorageBackendException;
-
-  void setConfigValue(String key, String value)
-    throws StorageBackendException;
-
-  /**
-   * Updates headers and channel references of the given article.
-   * @param article
-   * @return
-   * @throws StorageBackendException
-   */
-  boolean update(Article article)
-    throws StorageBackendException;
-
-  boolean update(Group group)
-    throws StorageBackendException;
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/StorageBackendException.java
--- a/org/sonews/storage/StorageBackendException.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.storage;
-
-/**
- *
- * @author Christian Lins
- * @since sonews/1.0
- */
-public class StorageBackendException extends Exception
-{
-
-  public StorageBackendException(Throwable cause)
-  {
-    super(cause);
-  }
-
-  public StorageBackendException(String msg)
-  {
-    super(msg);
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/StorageManager.java
--- a/org/sonews/storage/StorageManager.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,89 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.storage;
-
-/**
- *
- * @author Christian Lins
- * @since sonews/1.0
- */
-public final class StorageManager
-{
-
-  private static StorageProvider provider;
-
-  public static Storage current()
-    throws StorageBackendException
-  {
-    synchronized(StorageManager.class)
-    {
-      if(provider == null)
-      {
-        return null;
-      }
-      else
-      {
-        return provider.storage(Thread.currentThread());
-      }
-    }
-  }
-
-  public static StorageProvider loadProvider(String pluginClassName)
-  {
-    try
-    {
-      Class<?> clazz = Class.forName(pluginClassName);
-      Object   inst  = clazz.newInstance();
-      return (StorageProvider)inst;
-    }
-    catch(Exception ex)
-    {
-      System.err.println(ex);
-      return null;
-    }
-  }
-
-  /**
-   * Sets the current storage provider.
-   * @param provider
-   */
-  public static void enableProvider(StorageProvider provider)
-  {
-    synchronized(StorageManager.class)
-    {
-      if(StorageManager.provider != null)
-      {
-        disableProvider();
-      }
-      StorageManager.provider = provider;
-    }
-  }
-
-  /**
-   * Disables the current provider.
-   */
-  public static void disableProvider()
-  {
-    synchronized(StorageManager.class)
-    {
-      provider = null;
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/StorageProvider.java
--- a/org/sonews/storage/StorageProvider.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.storage;
-
-/**
- * Provides access to storage backend instances.
- * @author Christian Lins
- * @since sonews/1.0
- */
-public interface StorageProvider
-{
-
-  public boolean isSupported(String uri);
-
-  /**
-   * This method returns the reference to the associated storage.
-   * The reference MAY be unique for each thread. In any case it MUST be
-   * thread-safe to use this method.
-   * @return The reference to the associated Storage.
-   */
-  public Storage storage(Thread thread)
-    throws StorageBackendException;
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/impl/JDBCDatabase.java
--- a/org/sonews/storage/impl/JDBCDatabase.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1782 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.storage.impl;
-
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.sql.PreparedStatement;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
-import javax.mail.Header;
-import javax.mail.internet.MimeUtility;
-import org.sonews.config.Config;
-import org.sonews.util.Log;
-import org.sonews.feed.Subscription;
-import org.sonews.storage.Article;
-import org.sonews.storage.ArticleHead;
-import org.sonews.storage.Channel;
-import org.sonews.storage.Group;
-import org.sonews.storage.Storage;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.util.Pair;
-
-/**
- * JDBCDatabase facade class.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-// TODO: Refactor this class to reduce size (e.g. ArticleDatabase GroupDatabase)
-public class JDBCDatabase implements Storage
-{
-
-  public static final int MAX_RESTARTS = 2;
-  
-  private Connection        conn = null;
-  private PreparedStatement pstmtAddArticle1 = null;
-  private PreparedStatement pstmtAddArticle2 = null;
-  private PreparedStatement pstmtAddArticle3 = null;
-  private PreparedStatement pstmtAddArticle4 = null;
-  private PreparedStatement pstmtAddGroup0   = null;
-  private PreparedStatement pstmtAddEvent = null;
-  private PreparedStatement pstmtCountArticles = null;
-  private PreparedStatement pstmtCountGroups   = null;
-  private PreparedStatement pstmtDeleteArticle0 = null;
-  private PreparedStatement pstmtDeleteArticle1 = null;
-  private PreparedStatement pstmtDeleteArticle2 = null;
-  private PreparedStatement pstmtDeleteArticle3 = null;
-  private PreparedStatement pstmtGetArticle0 = null;
-  private PreparedStatement pstmtGetArticle1 = null;
-  private PreparedStatement pstmtGetArticleHeaders0 = null;
-  private PreparedStatement pstmtGetArticleHeaders1 = null;
-  private PreparedStatement pstmtGetArticleHeads = null;
-  private PreparedStatement pstmtGetArticleIDs   = null;
-  private PreparedStatement pstmtGetArticleIndex    = null;
-  private PreparedStatement pstmtGetConfigValue = null;
-  private PreparedStatement pstmtGetEventsCount0 = null;
-  private PreparedStatement pstmtGetEventsCount1 = null;
-  private PreparedStatement pstmtGetGroupForList = null;
-  private PreparedStatement pstmtGetGroup0     = null;
-  private PreparedStatement pstmtGetGroup1     = null;
-  private PreparedStatement pstmtGetFirstArticleNumber = null;
-  private PreparedStatement pstmtGetListForGroup       = null;
-  private PreparedStatement pstmtGetLastArticleNumber  = null;
-  private PreparedStatement pstmtGetMaxArticleID       = null;
-  private PreparedStatement pstmtGetMaxArticleIndex    = null;
-  private PreparedStatement pstmtGetOldestArticle      = null;
-  private PreparedStatement pstmtGetPostingsCount      = null;
-  private PreparedStatement pstmtGetSubscriptions  = null;
-  private PreparedStatement pstmtIsArticleExisting = null;
-  private PreparedStatement pstmtIsGroupExisting = null;
-  private PreparedStatement pstmtPurgeGroup0     = null;
-  private PreparedStatement pstmtPurgeGroup1     = null;
-  private PreparedStatement pstmtSetConfigValue0 = null;
-  private PreparedStatement pstmtSetConfigValue1 = null;
-  private PreparedStatement pstmtUpdateGroup     = null;
-  
-  /** How many times the database connection was reinitialized */
-  private int restarts = 0;
-  
-  /**
-   * Rises the database: reconnect and recreate all prepared statements.
-   * @throws java.lang.SQLException
-   */
-  protected void arise()
-    throws SQLException
-  {
-    try
-    {
-      // Load database driver
-      Class.forName(
-        Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DBMSDRIVER, "java.lang.Object"));
-
-      // Establish database connection
-      this.conn = DriverManager.getConnection(
-        Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DATABASE, "<not specified>"),
-        Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_USER, "root"),
-        Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_PASSWORD, ""));
-
-      this.conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
-      if(this.conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE)
-      {
-        Log.get().warning("Database is NOT fully serializable!");
-      }
-
-      // Prepare statements for method addArticle()
-      this.pstmtAddArticle1 = conn.prepareStatement(
-        "INSERT INTO articles (article_id, body) VALUES(?, ?)");
-      this.pstmtAddArticle2 = conn.prepareStatement(
-        "INSERT INTO headers (article_id, header_key, header_value, header_index) " +
-        "VALUES (?, ?, ?, ?)");
-      this.pstmtAddArticle3 = conn.prepareStatement(
-        "INSERT INTO postings (group_id, article_id, article_index)" +
-        "VALUES (?, ?, ?)");
-      this.pstmtAddArticle4 = conn.prepareStatement(
-        "INSERT INTO article_ids (article_id, message_id) VALUES (?, ?)");
-
-      // Prepare statement for method addStatValue()
-      this.pstmtAddEvent = conn.prepareStatement(
-        "INSERT INTO events VALUES (?, ?, ?)");
-     
-      // Prepare statement for method addGroup()
-      this.pstmtAddGroup0 = conn.prepareStatement(
-        "INSERT INTO groups (name, flags) VALUES (?, ?)");
-      
-      // Prepare statement for method countArticles()
-      this.pstmtCountArticles = conn.prepareStatement(
-        "SELECT Count(article_id) FROM article_ids");
-      
-      // Prepare statement for method countGroups()
-      this.pstmtCountGroups = conn.prepareStatement(
-        "SELECT Count(group_id) FROM groups WHERE " +
-        "flags & " + Channel.DELETED + " = 0");
-      
-      // Prepare statements for method delete(article)
-      this.pstmtDeleteArticle0 = conn.prepareStatement(
-        "DELETE FROM articles WHERE article_id = " +
-        "(SELECT article_id FROM article_ids WHERE message_id = ?)");
-      this.pstmtDeleteArticle1 = conn.prepareStatement(
-        "DELETE FROM headers WHERE article_id = " +
-        "(SELECT article_id FROM article_ids WHERE message_id = ?)");
-      this.pstmtDeleteArticle2 = conn.prepareStatement(
-        "DELETE FROM postings WHERE article_id = " +
-        "(SELECT article_id FROM article_ids WHERE message_id = ?)");
-      this.pstmtDeleteArticle3 = conn.prepareStatement(
-        "DELETE FROM article_ids WHERE message_id = ?");
-
-      // Prepare statements for methods getArticle()
-      this.pstmtGetArticle0 = conn.prepareStatement(
-        "SELECT * FROM articles  WHERE article_id = " +
-        "(SELECT article_id FROM article_ids WHERE message_id = ?)");
-      this.pstmtGetArticle1 = conn.prepareStatement(
-        "SELECT * FROM articles WHERE article_id = " +
-        "(SELECT article_id FROM postings WHERE " +
-        "article_index = ? AND group_id = ?)");
-      
-      // Prepare statement for method getArticleHeaders()
-      this.pstmtGetArticleHeaders0 = conn.prepareStatement(
-        "SELECT header_key, header_value FROM headers WHERE article_id = ? " +
-        "ORDER BY header_index ASC");
-
-      // Prepare statement for method getArticleHeaders(regular expr pattern)
-      this.pstmtGetArticleHeaders1 = conn.prepareStatement(
-        "SELECT p.article_index, h.header_value FROM headers h " +
-          "INNER JOIN postings p ON h.article_id = p.article_id " +
-          "INNER JOIN groups g ON p.group_id = g.group_id " +
-            "WHERE g.name          =  ? AND " +
-                  "h.header_key    =  ? AND " +
-                  "p.article_index >= ? " +
-        "ORDER BY p.article_index ASC");
-
-      this.pstmtGetArticleIDs = conn.prepareStatement(
-        "SELECT article_index FROM postings WHERE group_id = ?");
-      
-      // Prepare statement for method getArticleIndex
-      this.pstmtGetArticleIndex = conn.prepareStatement(
-              "SELECT article_index FROM postings WHERE " +
-              "article_id = (SELECT article_id FROM article_ids " +
-              "WHERE message_id = ?) " +
-              " AND group_id = ?");
-
-      // Prepare statements for method getArticleHeads()
-      this.pstmtGetArticleHeads = conn.prepareStatement(
-        "SELECT article_id, article_index FROM postings WHERE " +
-        "postings.group_id = ? AND article_index >= ? AND " +
-        "article_index <= ?");
-
-      // Prepare statements for method getConfigValue()
-      this.pstmtGetConfigValue = conn.prepareStatement(
-        "SELECT config_value FROM config WHERE config_key = ?");
-
-      // Prepare statements for method getEventsCount()
-      this.pstmtGetEventsCount0 = conn.prepareStatement(
-        "SELECT Count(*) FROM events WHERE event_key = ? AND " +
-        "event_time >= ? AND event_time < ?");
-
-      this.pstmtGetEventsCount1 = conn.prepareStatement(
-        "SELECT Count(*) FROM events WHERE event_key = ? AND " +
-        "event_time >= ? AND event_time < ? AND group_id = ?");
-      
-      // Prepare statement for method getGroupForList()
-      this.pstmtGetGroupForList = conn.prepareStatement(
-        "SELECT name FROM groups INNER JOIN groups2list " +
-        "ON groups.group_id = groups2list.group_id " +
-        "WHERE groups2list.listaddress = ?");
-
-      // Prepare statement for method getGroup()
-      this.pstmtGetGroup0 = conn.prepareStatement(
-        "SELECT group_id, flags FROM groups WHERE Name = ?");
-      this.pstmtGetGroup1 = conn.prepareStatement(
-        "SELECT name FROM groups WHERE group_id = ?");
-
-      // Prepare statement for method getLastArticleNumber()
-      this.pstmtGetLastArticleNumber = conn.prepareStatement(
-        "SELECT Max(article_index) FROM postings WHERE group_id = ?");
-
-      // Prepare statement for method getListForGroup()
-      this.pstmtGetListForGroup = conn.prepareStatement(
-        "SELECT listaddress FROM groups2list INNER JOIN groups " +
-        "ON groups.group_id = groups2list.group_id WHERE name = ?");
-
-      // Prepare statement for method getMaxArticleID()
-      this.pstmtGetMaxArticleID = conn.prepareStatement(
-        "SELECT Max(article_id) FROM articles");
-      
-      // Prepare statement for method getMaxArticleIndex()
-      this.pstmtGetMaxArticleIndex = conn.prepareStatement(
-        "SELECT Max(article_index) FROM postings WHERE group_id = ?");
-      
-      // Prepare statement for method getOldestArticle()
-      this.pstmtGetOldestArticle = conn.prepareStatement(
-        "SELECT message_id FROM article_ids WHERE article_id = " +
-        "(SELECT Min(article_id) FROM article_ids)");
-
-      // Prepare statement for method getFirstArticleNumber()
-      this.pstmtGetFirstArticleNumber = conn.prepareStatement(
-        "SELECT Min(article_index) FROM postings WHERE group_id = ?");
-      
-      // Prepare statement for method getPostingsCount()
-      this.pstmtGetPostingsCount = conn.prepareStatement(
-        "SELECT Count(*) FROM postings NATURAL JOIN groups " +
-        "WHERE groups.name = ?");
-      
-      // Prepare statement for method getSubscriptions()
-      this.pstmtGetSubscriptions = conn.prepareStatement(
-        "SELECT host, port, name FROM peers NATURAL JOIN " +
-        "peer_subscriptions NATURAL JOIN groups WHERE feedtype = ?");
-      
-      // Prepare statement for method isArticleExisting()
-      this.pstmtIsArticleExisting = conn.prepareStatement(
-        "SELECT Count(article_id) FROM article_ids WHERE message_id = ?");
-      
-      // Prepare statement for method isGroupExisting()
-      this.pstmtIsGroupExisting = conn.prepareStatement(
-        "SELECT * FROM groups WHERE name = ?");
-      
-      // Prepare statement for method setConfigValue()
-      this.pstmtSetConfigValue0 = conn.prepareStatement(
-        "DELETE FROM config WHERE config_key = ?");
-      this.pstmtSetConfigValue1 = conn.prepareStatement(
-        "INSERT INTO config VALUES(?, ?)");
-
-      // Prepare statements for method purgeGroup()
-      this.pstmtPurgeGroup0 = conn.prepareStatement(
-        "DELETE FROM peer_subscriptions WHERE group_id = ?");
-      this.pstmtPurgeGroup1 = conn.prepareStatement(
-        "DELETE FROM groups WHERE group_id = ?");
-
-      // Prepare statement for method update(Group)
-      this.pstmtUpdateGroup = conn.prepareStatement(
-        "UPDATE groups SET flags = ?, name = ? WHERE group_id = ?");
-    }
-    catch(ClassNotFoundException ex)
-    {
-      throw new Error("JDBC Driver not found!", ex);
-    }
-  }
-  
-  /**
-   * Adds an article to the database.
-   * @param article
-   * @return
-   * @throws java.sql.SQLException
-   */
-  @Override
-  public void addArticle(final Article article)
-    throws StorageBackendException
-  {
-    try
-    {
-      this.conn.setAutoCommit(false);
-
-      int newArticleID = getMaxArticleID() + 1;
-
-      // Fill prepared statement with values;
-      // writes body to article table
-      pstmtAddArticle1.setInt(1, newArticleID);
-      pstmtAddArticle1.setBytes(2, article.getBody());
-      pstmtAddArticle1.execute();
-
-      // Add headers
-      Enumeration headers = article.getAllHeaders();
-      for(int n = 0; headers.hasMoreElements(); n++)
-      {
-        Header header = (Header)headers.nextElement();
-        pstmtAddArticle2.setInt(1, newArticleID);
-        pstmtAddArticle2.setString(2, header.getName().toLowerCase());
-        pstmtAddArticle2.setString(3, 
-          header.getValue().replaceAll("[\r\n]", ""));
-        pstmtAddArticle2.setInt(4, n);
-        pstmtAddArticle2.execute();
-      }
-      
-      // For each newsgroup add a reference
-      List<Group> groups = article.getGroups();
-      for(Group group : groups)
-      {
-        pstmtAddArticle3.setLong(1, group.getInternalID());
-        pstmtAddArticle3.setInt(2, newArticleID);
-        pstmtAddArticle3.setLong(3, getMaxArticleIndex(group.getInternalID()) + 1);
-        pstmtAddArticle3.execute();
-      }
-      
-      // Write message-id to article_ids table
-      this.pstmtAddArticle4.setInt(1, newArticleID);
-      this.pstmtAddArticle4.setString(2, article.getMessageID());
-      this.pstmtAddArticle4.execute();
-
-      this.conn.commit();
-      this.conn.setAutoCommit(true);
-
-      this.restarts = 0; // Reset error count
-    }
-    catch(SQLException ex)
-    {
-      try
-      {
-        this.conn.rollback();  // Rollback changes
-      }
-      catch(SQLException ex2)
-      {
-        Log.get().severe("Rollback of addArticle() failed: " + ex2);
-      }
-      
-      try
-      {
-        this.conn.setAutoCommit(true); // and release locks
-      }
-      catch(SQLException ex2)
-      {
-        Log.get().severe("setAutoCommit(true) of addArticle() failed: " + ex2);
-      }
-
-      restartConnection(ex);
-      addArticle(article);
-    }
-  }
-  
-  /**
-   * Adds a group to the JDBCDatabase. This method is not accessible via NNTP.
-   * @param name
-   * @throws java.sql.SQLException
-   */
-  @Override
-  public void addGroup(String name, int flags)
-    throws StorageBackendException
-  {
-    try
-    {
-      this.conn.setAutoCommit(false);
-      pstmtAddGroup0.setString(1, name);
-      pstmtAddGroup0.setInt(2, flags);
-
-      pstmtAddGroup0.executeUpdate();
-      this.conn.commit();
-      this.conn.setAutoCommit(true);
-      this.restarts = 0; // Reset error count
-    }
-    catch(SQLException ex)
-    {
-      try
-      {
-        this.conn.rollback();
-        this.conn.setAutoCommit(true);
-      }
-      catch(SQLException ex2)
-      {
-        ex2.printStackTrace();
-      }
-
-      restartConnection(ex);
-      addGroup(name, flags);
-    }
-  }
-
-  @Override
-  public void addEvent(long time, int type, long gid)
-    throws StorageBackendException
-  {
-    try
-    {
-      this.conn.setAutoCommit(false);
-      this.pstmtAddEvent.setLong(1, time);
-      this.pstmtAddEvent.setInt(2, type);
-      this.pstmtAddEvent.setLong(3, gid);
-      this.pstmtAddEvent.executeUpdate();
-      this.conn.commit();
-      this.conn.setAutoCommit(true);
-      this.restarts = 0;
-    }
-    catch(SQLException ex)
-    {
-      try
-      {
-        this.conn.rollback();
-        this.conn.setAutoCommit(true);
-      }
-      catch(SQLException ex2)
-      {
-        ex2.printStackTrace();
-      }
-
-      restartConnection(ex);
-      addEvent(time, type, gid);
-    }
-  }
-
-  @Override
-  public int countArticles()
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-
-    try
-    {
-      rs = this.pstmtCountArticles.executeQuery();
-      if(rs.next())
-      {
-        return rs.getInt(1);
-      }
-      else
-      {
-        return -1;
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return countArticles();
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-        restarts = 0;
-      }
-    }
-  }
-
-  @Override
-  public int countGroups()
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-
-    try
-    {
-      rs = this.pstmtCountGroups.executeQuery();
-      if(rs.next())
-      {
-        return rs.getInt(1);
-      }
-      else
-      {
-        return -1;
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return countGroups();
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-        restarts = 0;
-      }
-    }
-  }
-
-  @Override
-  public void delete(final String messageID)
-    throws StorageBackendException
-  {
-    try
-    {
-      this.conn.setAutoCommit(false);
-      
-      this.pstmtDeleteArticle0.setString(1, messageID);
-      int rs = this.pstmtDeleteArticle0.executeUpdate();
-      
-      // We do not trust the ON DELETE CASCADE functionality to delete
-      // orphaned references...
-      this.pstmtDeleteArticle1.setString(1, messageID);
-      rs = this.pstmtDeleteArticle1.executeUpdate();
-
-      this.pstmtDeleteArticle2.setString(1, messageID);
-      rs = this.pstmtDeleteArticle2.executeUpdate();
-
-      this.pstmtDeleteArticle3.setString(1, messageID);
-      rs = this.pstmtDeleteArticle3.executeUpdate();
-      
-      this.conn.commit();
-      this.conn.setAutoCommit(true);
-    }
-    catch(SQLException ex)
-    {
-      throw new StorageBackendException(ex);
-    }
-  }
-
-  @Override
-  public Article getArticle(String messageID)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-    try
-    {
-      pstmtGetArticle0.setString(1, messageID);
-      rs = pstmtGetArticle0.executeQuery();
-
-      if(!rs.next())
-      {
-        return null;
-      }
-      else
-      {
-        byte[] body     = rs.getBytes("body");
-        String headers  = getArticleHeaders(rs.getInt("article_id"));
-        return new Article(headers, body);
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getArticle(messageID);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-        restarts = 0; // Reset error count
-      }
-    }
-  }
-  
-  /**
-   * Retrieves an article by its ID.
-   * @param articleID
-   * @return
-   * @throws StorageBackendException
-   */
-  @Override
-  public Article getArticle(long articleIndex, long gid)
-    throws StorageBackendException
-  {  
-    ResultSet rs = null;
-
-    try
-    {
-      this.pstmtGetArticle1.setLong(1, articleIndex);
-      this.pstmtGetArticle1.setLong(2, gid);
-
-      rs = this.pstmtGetArticle1.executeQuery();
-
-      if(rs.next())
-      {
-        byte[] body    = rs.getBytes("body");
-        String headers = getArticleHeaders(rs.getInt("article_id"));
-        return new Article(headers, body);
-      }
-      else
-      {
-        return null;
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getArticle(articleIndex, gid);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-        restarts = 0;
-      }
-    }
-  }
-
-  /**
-   * Searches for fitting header values using the given regular expression.
-   * @param group
-   * @param start
-   * @param end
-   * @param headerKey
-   * @param pattern
-   * @return
-   * @throws StorageBackendException
-   */
-  @Override
-  public List<Pair<Long, String>> getArticleHeaders(Channel group, long start,
-    long end, String headerKey, String patStr)
-    throws StorageBackendException, PatternSyntaxException
-  {
-    ResultSet rs = null;
-    List<Pair<Long, String>> heads = new ArrayList<Pair<Long, String>>();
-
-    try
-    {
-      this.pstmtGetArticleHeaders1.setString(1, group.getName());
-      this.pstmtGetArticleHeaders1.setString(2, headerKey);
-      this.pstmtGetArticleHeaders1.setLong(3, start);
-
-      rs = this.pstmtGetArticleHeaders1.executeQuery();
-
-      // Convert the "NNTP" regex to Java regex
-      patStr = patStr.replace("*", ".*");
-      Pattern pattern = Pattern.compile(patStr);
-
-      while(rs.next())
-      {
-        Long articleIndex = rs.getLong(1);
-        if(end < 0 || articleIndex <= end) // Match start is done via SQL
-        {
-          String headerValue  = rs.getString(2);
-          Matcher matcher = pattern.matcher(headerValue);
-          if(matcher.matches())
-          {
-            heads.add(new Pair<Long, String>(articleIndex, headerValue));
-          }
-        }
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getArticleHeaders(group, start, end, headerKey, patStr);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-
-    return heads;
-  }
-
-  private String getArticleHeaders(long articleID)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-    
-    try
-    {
-      this.pstmtGetArticleHeaders0.setLong(1, articleID);
-      rs = this.pstmtGetArticleHeaders0.executeQuery();
-      
-      StringBuilder buf = new StringBuilder();
-      if(rs.next())
-      {
-        for(;;)
-        {
-          buf.append(rs.getString(1)); // key
-          buf.append(": ");
-          String foldedValue = MimeUtility.fold(0, rs.getString(2));
-          buf.append(foldedValue); // value
-          if(rs.next())
-          {
-            buf.append("\r\n");
-          }
-          else
-          {
-            break;
-          }
-        }
-      }
-      
-      return buf.toString();
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getArticleHeaders(articleID);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-
-  @Override
-  public long getArticleIndex(Article article, Group group)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-
-    try
-    {
-      this.pstmtGetArticleIndex.setString(1, article.getMessageID());
-      this.pstmtGetArticleIndex.setLong(2, group.getInternalID());
-      
-      rs = this.pstmtGetArticleIndex.executeQuery();
-      if(rs.next())
-      {
-        return rs.getLong(1);
-      }
-      else
-      {
-        return -1;
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getArticleIndex(article, group);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-  
-  /**
-   * Returns a list of Long/Article Pairs.
-   * @throws java.sql.SQLException
-   */
-  @Override
-  public List<Pair<Long, ArticleHead>> getArticleHeads(Group group, long first,
-    long last)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-
-    try
-    {
-      this.pstmtGetArticleHeads.setLong(1, group.getInternalID());
-      this.pstmtGetArticleHeads.setLong(2, first);
-      this.pstmtGetArticleHeads.setLong(3, last);
-      rs = pstmtGetArticleHeads.executeQuery();
-
-      List<Pair<Long, ArticleHead>> articles 
-        = new ArrayList<Pair<Long, ArticleHead>>();
-
-      while (rs.next())
-      {
-        long aid  = rs.getLong("article_id");
-        long aidx = rs.getLong("article_index");
-        String headers = getArticleHeaders(aid);
-        articles.add(new Pair<Long, ArticleHead>(aidx, 
-                        new ArticleHead(headers)));
-      }
-
-      return articles;
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getArticleHeads(group, first, last);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-
-  @Override
-  public List<Long> getArticleNumbers(long gid)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-    try
-    {
-      List<Long> ids = new ArrayList<Long>();
-      this.pstmtGetArticleIDs.setLong(1, gid);
-      rs = this.pstmtGetArticleIDs.executeQuery();
-      while(rs.next())
-      {
-        ids.add(rs.getLong(1));
-      }
-      return ids;
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getArticleNumbers(gid);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-          restarts = 0; // Clear the restart count after successful request
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-
-  @Override
-  public String getConfigValue(String key)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-    try
-    {
-      this.pstmtGetConfigValue.setString(1, key);
-
-      rs = this.pstmtGetConfigValue.executeQuery();
-      if(rs.next())
-      {
-        return rs.getString(1); // First data on index 1 not 0
-      }
-      else
-      {
-        return null;
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getConfigValue(key);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-        restarts = 0; // Clear the restart count after successful request
-      }
-    }
-  }
-
-  @Override
-  public int getEventsCount(int type, long start, long end, Channel channel)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-    
-    try
-    {
-      if(channel == null)
-      {
-        this.pstmtGetEventsCount0.setInt(1, type);
-        this.pstmtGetEventsCount0.setLong(2, start);
-        this.pstmtGetEventsCount0.setLong(3, end);
-        rs = this.pstmtGetEventsCount0.executeQuery();
-      }
-      else
-      {
-        this.pstmtGetEventsCount1.setInt(1, type);
-        this.pstmtGetEventsCount1.setLong(2, start);
-        this.pstmtGetEventsCount1.setLong(3, end);
-        this.pstmtGetEventsCount1.setLong(4, channel.getInternalID());
-        rs = this.pstmtGetEventsCount1.executeQuery();
-      }
-      
-      if(rs.next())
-      {
-        return rs.getInt(1);
-      }
-      else
-      {
-        return -1;
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getEventsCount(type, start, end, channel);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-  
-  /**
-   * Reads all Groups from the JDBCDatabase.
-   * @return
-   * @throws StorageBackendException
-   */
-  @Override
-  public List<Channel> getGroups()
-    throws StorageBackendException
-  {
-    ResultSet   rs;
-    List<Channel> buffer = new ArrayList<Channel>();
-    Statement   stmt   = null;
-
-    try
-    {
-      stmt = conn.createStatement();
-      rs = stmt.executeQuery("SELECT * FROM groups ORDER BY name");
-
-      while(rs.next())
-      {
-        String name  = rs.getString("name");
-        long   id    = rs.getLong("group_id");
-        int    flags = rs.getInt("flags");
-        
-        Group group = new Group(name, id, flags);
-        buffer.add(group);
-      }
-
-      return buffer;
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getGroups();
-    }
-    finally
-    {
-      if(stmt != null)
-      {
-        try
-        {
-          stmt.close(); // Implicitely closes ResultSets
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-
-  @Override
-  public List<String> getGroupsForList(String listAddress)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-    
-    try
-    {
-      this.pstmtGetGroupForList.setString(1, listAddress);
-
-      rs = this.pstmtGetGroupForList.executeQuery();
-      List<String> groups = new ArrayList<String>();
-      while(rs.next())
-      {
-        String group = rs.getString(1);
-        groups.add(group);
-      }
-      return groups;
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getGroupsForList(listAddress);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-  
-  /**
-   * Returns the Group that is identified by the name.
-   * @param name
-   * @return
-   * @throws StorageBackendException
-   */
-  @Override
-  public Group getGroup(String name)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-    
-    try
-    {
-      this.pstmtGetGroup0.setString(1, name);
-      rs = this.pstmtGetGroup0.executeQuery();
-
-      if (!rs.next())
-      {
-        return null;
-      }
-      else
-      {
-        long id = rs.getLong("group_id");
-        int flags = rs.getInt("flags");
-        return new Group(name, id, flags);
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getGroup(name);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-
-  @Override
-  public List<String> getListsForGroup(String group)
-    throws StorageBackendException
-  {
-    ResultSet     rs    = null;
-    List<String>  lists = new ArrayList<String>();
-
-    try
-    {
-      this.pstmtGetListForGroup.setString(1, group);
-      rs = this.pstmtGetListForGroup.executeQuery();
-
-      while(rs.next())
-      {
-        lists.add(rs.getString(1));
-      }
-      return lists;
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getListsForGroup(group);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-  
-  private int getMaxArticleIndex(long groupID)
-    throws StorageBackendException
-  {
-    ResultSet rs    = null;
-
-    try
-    {
-      this.pstmtGetMaxArticleIndex.setLong(1, groupID);
-      rs = this.pstmtGetMaxArticleIndex.executeQuery();
-
-      int maxIndex = 0;
-      if (rs.next())
-      {
-        maxIndex = rs.getInt(1);
-      }
-
-      return maxIndex;
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getMaxArticleIndex(groupID);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-  
-  private int getMaxArticleID()
-    throws StorageBackendException
-  {
-    ResultSet rs    = null;
-
-    try
-    {
-      rs = this.pstmtGetMaxArticleID.executeQuery();
-
-      int maxIndex = 0;
-      if (rs.next())
-      {
-        maxIndex = rs.getInt(1);
-      }
-
-      return maxIndex;
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getMaxArticleID();
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-
-  @Override
-  public int getLastArticleNumber(Group group)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-
-    try
-    {
-      this.pstmtGetLastArticleNumber.setLong(1, group.getInternalID());
-      rs = this.pstmtGetLastArticleNumber.executeQuery();
-      if (rs.next())
-      {
-        return rs.getInt(1);
-      }
-      else
-      {
-        return 0;
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getLastArticleNumber(group);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-
-  @Override
-  public int getFirstArticleNumber(Group group)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-    try
-    {
-      this.pstmtGetFirstArticleNumber.setLong(1, group.getInternalID());
-      rs = this.pstmtGetFirstArticleNumber.executeQuery();
-      if(rs.next())
-      {
-        return rs.getInt(1);
-      }
-      else
-      {
-        return 0;
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getFirstArticleNumber(group);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-  
-  /**
-   * Returns a group name identified by the given id.
-   * @param id
-   * @return
-   * @throws StorageBackendException
-   */
-  public String getGroup(int id)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-
-    try
-    {
-      this.pstmtGetGroup1.setInt(1, id);
-      rs = this.pstmtGetGroup1.executeQuery();
-
-      if (rs.next())
-      {
-        return rs.getString(1);
-      }
-      else
-      {
-        return null;
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getGroup(id);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-
-  @Override
-  public double getEventsPerHour(int key, long gid)
-    throws StorageBackendException
-  {
-    String gidquery = "";
-    if(gid >= 0)
-    {
-      gidquery = " AND group_id = " + gid;
-    }
-    
-    Statement stmt = null;
-    ResultSet rs   = null;
-    
-    try
-    {
-      stmt = this.conn.createStatement();
-      rs = stmt.executeQuery("SELECT Count(*) / (Max(event_time) - Min(event_time))" +
-        " * 1000 * 60 * 60 FROM events WHERE event_key = " + key + gidquery);
-      
-      if(rs.next())
-      {
-        restarts = 0; // reset error count
-        return rs.getDouble(1);
-      }
-      else
-      {
-        return Double.NaN;
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getEventsPerHour(key, gid);
-    }
-    finally
-    {
-      try
-      {
-        if(stmt != null)
-        {
-          stmt.close(); // Implicitely closes the result sets
-        }
-      }
-      catch(SQLException ex)
-      {
-        ex.printStackTrace();
-      }
-    }
-  }
-
-  @Override
-  public String getOldestArticle()
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-
-    try
-    {
-      rs = this.pstmtGetOldestArticle.executeQuery();
-      if(rs.next())
-      {
-        return rs.getString(1);
-      }
-      else
-      {
-        return null;
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getOldestArticle();
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-
-  @Override
-  public int getPostingsCount(String groupname)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-    
-    try
-    {
-      this.pstmtGetPostingsCount.setString(1, groupname);
-      rs = this.pstmtGetPostingsCount.executeQuery();
-      if(rs.next())
-      {
-        return rs.getInt(1);
-      }
-      else
-      {
-        Log.get().warning("Count on postings return nothing!");
-        return 0;
-      }
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getPostingsCount(groupname);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-
-  @Override
-  public List<Subscription> getSubscriptions(int feedtype)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-    
-    try
-    {
-      List<Subscription> subs = new ArrayList<Subscription>();
-      this.pstmtGetSubscriptions.setInt(1, feedtype);
-      rs = this.pstmtGetSubscriptions.executeQuery();
-      
-      while(rs.next())
-      {
-        String host  = rs.getString("host");
-        String group = rs.getString("name");
-        int    port  = rs.getInt("port");
-        subs.add(new Subscription(host, port, feedtype, group));
-      }
-      
-      return subs;
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return getSubscriptions(feedtype);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-
-  /**
-   * Checks if there is an article with the given messageid in the JDBCDatabase.
-   * @param name
-   * @return
-   * @throws StorageBackendException
-   */
-  @Override
-  public boolean isArticleExisting(String messageID)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-    
-    try
-    {
-      this.pstmtIsArticleExisting.setString(1, messageID);
-      rs = this.pstmtIsArticleExisting.executeQuery();
-      return rs.next() && rs.getInt(1) == 1;
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return isArticleExisting(messageID);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-  
-  /**
-   * Checks if there is a group with the given name in the JDBCDatabase.
-   * @param name
-   * @return
-   * @throws StorageBackendException
-   */
-  @Override
-  public boolean isGroupExisting(String name)
-    throws StorageBackendException
-  {
-    ResultSet rs = null;
-    
-    try
-    {
-      this.pstmtIsGroupExisting.setString(1, name);
-      rs = this.pstmtIsGroupExisting.executeQuery();
-      return rs.next();
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return isGroupExisting(name);
-    }
-    finally
-    {
-      if(rs != null)
-      {
-        try
-        {
-          rs.close();
-        }
-        catch(SQLException ex)
-        {
-          ex.printStackTrace();
-        }
-      }
-    }
-  }
-
-  @Override
-  public void setConfigValue(String key, String value)
-    throws StorageBackendException
-  {
-    try
-    {
-      conn.setAutoCommit(false);
-      this.pstmtSetConfigValue0.setString(1, key);
-      this.pstmtSetConfigValue0.execute();
-      this.pstmtSetConfigValue1.setString(1, key);
-      this.pstmtSetConfigValue1.setString(2, value);
-      this.pstmtSetConfigValue1.execute();
-      conn.commit();
-      conn.setAutoCommit(true);
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      setConfigValue(key, value);
-    }
-  }
-  
-  /**
-   * Closes the JDBCDatabase connection.
-   */
-  public void shutdown()
-    throws StorageBackendException
-  {
-    try
-    {
-      if(this.conn != null)
-      {
-        this.conn.close();
-      }
-    }
-    catch(SQLException ex)
-    {
-      throw new StorageBackendException(ex);
-    }
-  }
-
-  @Override
-  public void purgeGroup(Group group)
-    throws StorageBackendException
-  {
-    try
-    {
-      this.pstmtPurgeGroup0.setLong(1, group.getInternalID());
-      this.pstmtPurgeGroup0.executeUpdate();
-
-      this.pstmtPurgeGroup1.setLong(1, group.getInternalID());
-      this.pstmtPurgeGroup1.executeUpdate();
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      purgeGroup(group);
-    }
-  }
-  
-  private void restartConnection(SQLException cause)
-    throws StorageBackendException
-  {
-    restarts++;
-    Log.get().severe(Thread.currentThread()
-      + ": Database connection was closed (restart " + restarts + ").");
-    
-    if(restarts >= MAX_RESTARTS)
-    {
-      // Delete the current, probably broken JDBCDatabase instance.
-      // So no one can use the instance any more.
-      JDBCDatabaseProvider.instances.remove(Thread.currentThread());
-      
-      // Throw the exception upwards
-      throw new StorageBackendException(cause);
-    }
-    
-    try
-    {
-      Thread.sleep(1500L * restarts);
-    }
-    catch(InterruptedException ex)
-    {
-      Log.get().warning("Interrupted: " + ex.getMessage());
-    }
-    
-    // Try to properly close the old database connection
-    try
-    {
-      if(this.conn != null)
-      {
-        this.conn.close();
-      }
-    }
-    catch(SQLException ex)
-    {
-      Log.get().warning(ex.getMessage());
-    }
-    
-    try
-    {
-      // Try to reinitialize database connection
-      arise();
-    }
-    catch(SQLException ex)
-    {
-      Log.get().warning(ex.getMessage());
-      restartConnection(ex);
-    }
-  }
-
-  @Override
-  public boolean update(Article article)
-    throws StorageBackendException
-  {
-    // DELETE FROM headers WHERE article_id = ?
-
-    // INSERT INTO headers ...
-
-    // SELECT * FROM postings WHERE article_id = ? AND group_id = ?
-    return false;
-  }
-
-  /**
-   * Writes the flags and the name of the given group to the database.
-   * @param group
-   * @throws StorageBackendException
-   */
-  @Override
-  public boolean update(Group group)
-    throws StorageBackendException
-  {
-    try
-    {
-      this.pstmtUpdateGroup.setInt(1, group.getFlags());
-      this.pstmtUpdateGroup.setString(2, group.getName());
-      this.pstmtUpdateGroup.setLong(3, group.getInternalID());
-      int rs = this.pstmtUpdateGroup.executeUpdate();
-      return rs == 1;
-    }
-    catch(SQLException ex)
-    {
-      restartConnection(ex);
-      return update(group);
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/impl/JDBCDatabaseProvider.java
--- a/org/sonews/storage/impl/JDBCDatabaseProvider.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.storage.impl;
-
-import java.sql.SQLException;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import org.sonews.storage.Storage;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.storage.StorageProvider;
-
-/**
- *
- * @author Christian Lins
- * @since sonews/1.0
- */
-public class JDBCDatabaseProvider implements StorageProvider
-{
-
-  protected static final Map<Thread, JDBCDatabase> instances
-    = new ConcurrentHashMap<Thread, JDBCDatabase>();
-
-  @Override
-  public boolean isSupported(String uri)
-  {
-    throw new UnsupportedOperationException("Not supported yet.");
-  }
-
-  @Override
-  public Storage storage(Thread thread)
-    throws StorageBackendException
-  {
-    try
-    {
-    if(!instances.containsKey(Thread.currentThread()))
-    {
-      JDBCDatabase db = new JDBCDatabase();
-      db.arise();
-      instances.put(Thread.currentThread(), db);
-      return db;
-    }
-    else
-    {
-      return instances.get(Thread.currentThread());
-    }
-    }
-    catch(SQLException ex)
-    {
-      throw new StorageBackendException(ex);
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/package.html
--- a/org/sonews/storage/package.html	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-Contains classes of the storage backend and the Group and Article
-abstraction.
\ No newline at end of file
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/DatabaseSetup.java
--- a/org/sonews/util/DatabaseSetup.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,127 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.util;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.Statement;
-import java.util.HashMap;
-import java.util.Map;
-import org.sonews.config.Config;
-import org.sonews.util.io.Resource;
-
-/**
- * Database setup utility class.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public final class DatabaseSetup 
-{
-
-  private static final Map<String, String> templateMap 
-    = new HashMap<String, String>();
-  private static final Map<String, StringTemplate> urlMap
-    = new HashMap<String, StringTemplate>();
-  private static final Map<String, String> driverMap
-    = new HashMap<String, String>();
-  
-  static
-  {
-    templateMap.put("1", "helpers/database_mysql5_tmpl.sql");
-    templateMap.put("2", "helpers/database_postgresql8_tmpl.sql");
-    
-    urlMap.put("1", new StringTemplate("jdbc:mysql://%HOSTNAME/%DB"));
-    urlMap.put("2", new StringTemplate("jdbc:postgresql://%HOSTNAME/%DB"));
-    
-    driverMap.put("1", "com.mysql.jdbc.Driver");
-    driverMap.put("2", "org.postgresql.Driver");
-  }
-  
-  public static void main(String[] args)
-    throws Exception
-  {
-    System.out.println("sonews Database setup helper");
-    System.out.println("This program will create a initial database table structure");
-    System.out.println("for the sonews Newsserver.");
-    System.out.println("You need to create a database and a db user manually before!");
-    
-    System.out.println("Select DBMS type:");
-    System.out.println("[1] MySQL 5.x or higher");
-    System.out.println("[2] PostgreSQL 8.x or higher");
-    System.out.print("Your choice: ");
-    
-    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
-    String dbmsType = in.readLine();
-    String tmplName = templateMap.get(dbmsType);
-    if(tmplName == null)
-    {
-      System.err.println("Invalid choice. Try again you fool!");
-      main(args);
-      return;
-    }
-    
-    // Load JDBC Driver class
-    Class.forName(driverMap.get(dbmsType));
-    
-    String tmpl = Resource.getAsString(tmplName, true);
-    
-    System.out.print("Database server hostname (e.g. localhost): ");
-    String dbHostname = in.readLine();
-    
-    System.out.print("Database name: ");
-    String dbName = in.readLine();
-
-    System.out.print("Give name of DB user that can create tables: ");
-    String dbUser = in.readLine();
-
-    System.out.print("Password: ");
-    String dbPassword = in.readLine();
-    
-    String url = urlMap.get(dbmsType)
-      .set("HOSTNAME", dbHostname)
-      .set("DB", dbName).toString();
-    
-    Connection conn = 
-      DriverManager.getConnection(url, dbUser, dbPassword);
-    conn.setAutoCommit(false);
-    
-    String[] tmplChunks = tmpl.split(";");
-    
-    for(String chunk : tmplChunks)
-    {
-      if(chunk.trim().equals(""))
-      {
-        continue;
-      }
-      
-      Statement stmt = conn.createStatement();
-      stmt.execute(chunk);
-    }
-    
-    conn.commit();
-    conn.setAutoCommit(true);
-    
-    // Create config file
-    
-    System.out.println("Ok");
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/Log.java
--- a/org/sonews/util/Log.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.util;
-
-import java.util.logging.Level;
-import java.util.logging.LogManager;
-import java.util.logging.Logger;
-import java.util.logging.SimpleFormatter;
-import java.util.logging.StreamHandler;
-import org.sonews.config.Config;
-
-/**
- * Provides logging and debugging methods.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class Log extends Logger
-{
-
-  private static Log instance = new Log();
-
-  private Log()
-  {
-    super("org.sonews", null);
-
-    StreamHandler handler = new StreamHandler(System.out, new SimpleFormatter());
-    Level level = Level.parse(Config.inst().get(Config.LOGLEVEL, "INFO"));
-    handler.setLevel(level);
-    addHandler(handler);
-    setLevel(level);
-    LogManager.getLogManager().addLogger(this);
-  }
-
-  public static Logger get()
-  {
-    Level level = Level.parse(Config.inst().get(Config.LOGLEVEL, "INFO"));
-    instance.setLevel(level);
-    return instance;
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/Pair.java
--- a/org/sonews/util/Pair.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.util;
-
-/**
- * A pair of two objects.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class Pair<T1, T2> 
-{
- 
-  private T1 a;
-  private T2 b;
-  
-  public Pair(T1 a, T2 b)
-  {
-    this.a = a;
-    this.b = b;
-  }
-
-  public T1 getA()
-  {
-    return a;
-  }
-
-  public T2 getB()
-  {
-    return b;
-  } 
- 
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/Purger.java
--- a/org/sonews/util/Purger.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.util;
-
-import org.sonews.daemon.AbstractDaemon;
-import org.sonews.config.Config;
-import org.sonews.storage.Article;
-import org.sonews.storage.Headers;
-import java.util.Date;
-import java.util.List;
-import org.sonews.storage.Channel;
-import org.sonews.storage.Group;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.storage.StorageManager;
-
-/**
- * The purger is started in configurable intervals to search
- * for messages that can be purged. A message must be deleted if its lifetime
- * has exceeded, if it was marked as deleted or if the maximum number of
- * articles in the database is reached.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class Purger extends AbstractDaemon
-{
-
-  /**
-   * Loops through all messages and deletes them if their time
-   * has come.
-   */
-  @Override
-  public void run()
-  {
-    try
-    {
-      while(isRunning())
-      {
-        purgeDeleted();
-        purgeOutdated();
-
-        Thread.sleep(120000); // Sleep for two minutes
-      }
-    }
-    catch(StorageBackendException ex)
-    {
-      ex.printStackTrace();
-    }
-    catch(InterruptedException ex)
-    {
-      Log.get().warning("Purger interrupted: " + ex);
-    }
-  }
-
-  private void purgeDeleted()
-    throws StorageBackendException
-  {
-    List<Channel> groups = StorageManager.current().getGroups();
-    for(Channel channel : groups)
-    {
-      if(!(channel instanceof Group))
-        continue;
-      
-      Group group = (Group)channel;
-      // Look for groups that are marked as deleted
-      if(group.isDeleted())
-      {
-        List<Long> ids = StorageManager.current().getArticleNumbers(group.getInternalID());
-        if(ids.size() == 0)
-        {
-          StorageManager.current().purgeGroup(group);
-          Log.get().info("Group " + group.getName() + " purged.");
-        }
-
-        for(int n = 0; n < ids.size() && n < 10; n++)
-        {
-          Article art = StorageManager.current().getArticle(ids.get(n), group.getInternalID());
-          StorageManager.current().delete(art.getMessageID());
-          Log.get().info("Article " + art.getMessageID() + " purged.");
-        }
-      }
-    }
-  }
-
-  private void purgeOutdated()
-    throws InterruptedException, StorageBackendException
-  {
-    long articleMaximum =
-      Config.inst().get("sonews.article.maxnum", Long.MAX_VALUE);
-    long lifetime =
-      Config.inst().get("sonews.article.lifetime", -1);
-
-    if(lifetime > 0 || articleMaximum < Stats.getInstance().getNumberOfNews())
-    {
-      Log.get().info("Purging old messages...");
-      String mid = StorageManager.current().getOldestArticle();
-      if (mid == null) // No articles in the database
-      {
-        return;
-      }
-
-      Article art = StorageManager.current().getArticle(mid);
-      long artDate = 0;
-      String dateStr = art.getHeader(Headers.DATE)[0];
-      try
-      {
-        artDate = Date.parse(dateStr) / 1000 / 60 / 60 / 24;
-      }
-      catch (IllegalArgumentException ex)
-      {
-        Log.get().warning("Could not parse date string: " + dateStr + " " + ex);
-      }
-
-      // Should we delete the message because of its age or because the
-      // article maximum was reached?
-      if (lifetime < 0 || artDate < (new Date().getTime() + lifetime))
-      {
-        StorageManager.current().delete(mid);
-        System.out.println("Deleted: " + mid);
-      }
-      else
-      {
-        Thread.sleep(1000 * 60); // Wait 60 seconds
-        return;
-      }
-    }
-    else
-    {
-      Log.get().info("Lifetime purger is disabled");
-      Thread.sleep(1000 * 60 * 30); // Wait 30 minutes
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/Stats.java
--- a/org/sonews/util/Stats.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,206 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.util;
-
-import java.util.Calendar;
-import org.sonews.config.Config;
-import org.sonews.storage.Channel;
-import org.sonews.storage.StorageBackendException;
-import org.sonews.storage.StorageManager;
-
-/**
- * Class that capsulates statistical data gathering.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public final class Stats 
-{
-      
-  public static final byte CONNECTIONS    = 1;
-  public static final byte POSTED_NEWS    = 2;
-  public static final byte GATEWAYED_NEWS = 3;
-  public static final byte FEEDED_NEWS    = 4;
-  public static final byte MLGW_RUNSTART  = 5;
-  public static final byte MLGW_RUNEND    = 6;
-
-  private static Stats instance = new Stats();
-  
-  public static Stats getInstance()
-  {
-    return Stats.instance;
-  }
-  
-  private Stats() {}
-  
-  private volatile int connectedClients = 0;
-
-  /**
-   * A generic method that writes event data to the storage backend.
-   * If event logging is disabled with sonews.eventlog=false this method
-   * simply does nothing.
-   * @param type
-   * @param groupname
-   */
-  private void addEvent(byte type, String groupname)
-  {
-    try
-    {
-      if (Config.inst().get(Config.EVENTLOG, true))
-      {
-
-        Channel group = Channel.getByName(groupname);
-        if (group != null)
-        {
-          StorageManager.current().addEvent(
-                  System.currentTimeMillis(), type, group.getInternalID());
-        }
-      } 
-      else
-      {
-        Log.get().info("Group " + groupname + " does not exist.");
-      }
-    } 
-    catch (StorageBackendException ex)
-    {
-      ex.printStackTrace();
-    }
-  }
-  
-  public void clientConnect()
-  {
-    this.connectedClients++;
-  }
-  
-  public void clientDisconnect()
-  {
-    this.connectedClients--;
-  }
-  
-  public int connectedClients()
-  {
-    return this.connectedClients;
-  }
-  
-  public int getNumberOfGroups()
-  {
-    try
-    {
-      return StorageManager.current().countGroups();
-    }
-    catch(StorageBackendException ex)
-    {
-      ex.printStackTrace();
-      return -1;
-    }
-  }
-  
-  public int getNumberOfNews()
-  {
-    try
-    {
-      return StorageManager.current().countArticles();
-    }
-    catch(StorageBackendException ex)
-    {
-      ex.printStackTrace();
-      return -1;
-    }
-  }
-  
-  public int getYesterdaysEvents(final byte eventType, final int hour,
-    final Channel group)
-  {
-    // Determine the timestamp values for yesterday and the given hour
-    Calendar cal = Calendar.getInstance();
-    int year  = cal.get(Calendar.YEAR);
-    int month = cal.get(Calendar.MONTH);
-    int dayom = cal.get(Calendar.DAY_OF_MONTH) - 1; // Yesterday
-    
-    cal.set(year, month, dayom, hour, 0, 0);
-    long startTimestamp = cal.getTimeInMillis();
-    
-    cal.set(year, month, dayom, hour + 1, 0, 0);
-    long endTimestamp = cal.getTimeInMillis();
-    
-    try
-    {
-      return StorageManager.current()
-        .getEventsCount(eventType, startTimestamp, endTimestamp, group);
-    }
-    catch(StorageBackendException ex)
-    {
-      ex.printStackTrace();
-      return -1;
-    }
-  }
-  
-  public void mailPosted(String groupname)
-  {
-    addEvent(POSTED_NEWS, groupname);
-  }
-  
-  public void mailGatewayed(String groupname)
-  {
-    addEvent(GATEWAYED_NEWS, groupname);
-  }
-  
-  public void mailFeeded(String groupname)
-  {
-    addEvent(FEEDED_NEWS, groupname);
-  }
-  
-  public void mlgwRunStart()
-  {
-    addEvent(MLGW_RUNSTART, "control");
-  }
-  
-  public void mlgwRunEnd()
-  {
-    addEvent(MLGW_RUNEND, "control");
-  }
-  
-  private double perHour(int key, long gid)
-  {
-    try
-    {
-      return StorageManager.current().getEventsPerHour(key, gid);
-    }
-    catch(StorageBackendException ex)
-    {
-      ex.printStackTrace();
-      return -1;
-    }
-  }
-  
-  public double postedPerHour(long gid)
-  {
-    return perHour(POSTED_NEWS, gid);
-  }
-  
-  public double gatewayedPerHour(long gid)
-  {
-    return perHour(GATEWAYED_NEWS, gid);
-  }
-  
-  public double feededPerHour(long gid)
-  {
-    return perHour(FEEDED_NEWS, gid);
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/StringTemplate.java
--- a/org/sonews/util/StringTemplate.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.util;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Class that allows simple String template handling.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class StringTemplate 
-{
-
-  private String              str               = null;
-  private String              templateDelimiter = "%";
-  private Map<String, String> templateValues    = new HashMap<String, String>();
-  
-  public StringTemplate(String str, final String templateDelimiter)
-  {
-    if(str == null || templateDelimiter == null)
-    {
-      throw new IllegalArgumentException("null arguments not allowed");
-    }
-
-    this.str               = str;
-    this.templateDelimiter = templateDelimiter;
-  }
-  
-  public StringTemplate(String str)
-  {
-    this(str, "%");
-  }
-  
-  public StringTemplate set(String template, String value)
-  {
-    if(template == null || value == null)
-    {
-      throw new IllegalArgumentException("null arguments not allowed");
-    }
-    
-    this.templateValues.put(template, value);
-    return this;
-  }
-  
-  public StringTemplate set(String template, long value)
-  {
-    return set(template, Long.toString(value));
-  }
-  
-  public StringTemplate set(String template, double value)
-  {
-    return set(template, Double.toString(value));
-  }
-  
-  public StringTemplate set(String template, Object obj)
-  {
-    if(template == null || obj == null)
-    {
-      throw new IllegalArgumentException("null arguments not allowed");
-    }
-
-    return set(template, obj.toString());
-  }
-  
-  @Override
-  public String toString()
-  {
-    String ret = str;
-
-    for(String key : this.templateValues.keySet())
-    {
-      String value = this.templateValues.get(key);
-      ret = ret.replace(templateDelimiter + key, value);
-    }
-    
-    return ret;
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/TimeoutMap.java
--- a/org/sonews/util/TimeoutMap.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,145 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.util;
-
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * Implementation of a Map that will loose its stored values after a 
- * configurable amount of time.
- * This class may be used to cache config values for example.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class TimeoutMap<K,V> extends ConcurrentHashMap<K, V>
-{
-  
-  private static final long serialVersionUID = 453453467700345L;
-
-  private int                    timeout     = 60000; // 60 sec
-  private transient Map<K, Long> timeoutMap  = new HashMap<K, Long>();
-  
-  /**
-   * Constructor.
-   * @param timeout Timeout in milliseconds
-   */
-  public TimeoutMap(final int timeout)
-  {
-    this.timeout = timeout;
-  }
-  
-  /**
-   * Uses default timeout (60 sec).
-   */
-  public TimeoutMap()
-  {
-  }
-  
-  /**
-   * 
-   * @param key
-   * @return true if key is still valid.
-   */
-  protected boolean checkTimeOut(Object key)
-  {
-    synchronized(this.timeoutMap)
-    {
-      if(this.timeoutMap.containsKey(key))
-      {
-        long keytime = this.timeoutMap.get(key);
-        if((System.currentTimeMillis() - keytime) < this.timeout)
-        {
-          return true;
-        }
-        else
-        {
-          remove(key);
-          return false;
-        }
-      }
-      else
-      {
-        return false;
-      }
-    }
-  }
-  
-  @Override
-  public boolean containsKey(Object key)
-  {
-    return checkTimeOut(key);
-  }
-
-  @Override
-  public synchronized V get(Object key)
-  {
-    if(checkTimeOut(key))
-    {
-      return super.get(key);
-    }
-    else
-    {
-      return null;
-    }
-  }
-
-  @Override
-  public V put(K key, V value)
-  {
-    synchronized(this.timeoutMap)
-    {
-      removeStaleKeys();
-      this.timeoutMap.put(key, System.currentTimeMillis());
-      return super.put(key, value);
-    }
-  }
-
-  /**
-   * @param arg0
-   * @return
-   */
-  @Override
-  public V remove(Object arg0)
-  {
-    synchronized(this.timeoutMap)
-    {
-      this.timeoutMap.remove(arg0);
-      V val = super.remove(arg0);
-      return val;
-    }
-  }
-
-  protected void removeStaleKeys()
-  {
-    synchronized(this.timeoutMap)
-    {
-      Set<Object> keySet = new HashSet<Object>(this.timeoutMap.keySet());
-      for(Object key : keySet)
-      {
-        // The key/value is removed by the checkTimeOut() method if true
-        checkTimeOut(key);
-      }
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/io/ArticleInputStream.java
--- a/org/sonews/util/io/ArticleInputStream.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.util.io;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import org.sonews.storage.Article;
-
-/**
- * Capsulates an Article to provide a raw InputStream.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class ArticleInputStream extends InputStream
-{
-
-  private byte[] buf;
-  private int    pos = 0;
-  
-  public ArticleInputStream(final Article art)
-    throws IOException, UnsupportedEncodingException
-  {
-    final ByteArrayOutputStream out = new ByteArrayOutputStream();
-    out.write(art.getHeaderSource().getBytes("UTF-8"));
-    out.write("\r\n\r\n".getBytes());
-    out.write(art.getBody()); // Without CRLF
-    out.flush();
-    this.buf = out.toByteArray();
-  }
-
-  /**
-   * This method reads one byte from the stream.  The <code>pos</code>
-   * counter is advanced to the next byte to be read.  The byte read is
-   * returned as an int in the range of 0-255.  If the stream position
-   * is already at the end of the buffer, no byte is read and a -1 is
-   * returned in order to indicate the end of the stream.
-   *
-   * @return The byte read, or -1 if end of stream
-   */
-  @Override
-  public synchronized int read()
-  {
-    if(pos < buf.length)
-    {
-      return ((int)buf[pos++]) & 0xFF;
-    }
-    else
-    {
-      return -1;
-    }
-  }
-  
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/io/ArticleReader.java
--- a/org/sonews/util/io/ArticleReader.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,135 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.util.io;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.Socket;
-import java.net.UnknownHostException;
-import org.sonews.config.Config;
-import org.sonews.util.Log;
-
-/**
- * Reads an news article from a NNTP server.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class ArticleReader 
-{
-
-  private BufferedOutputStream out;
-  private BufferedInputStream  in;
-  private String               messageID;
-  
-  public ArticleReader(String host, int port, String messageID)
-    throws IOException, UnknownHostException
-  {
-    this.messageID = messageID;
-
-    // Connect to NNTP server
-    Socket socket = new Socket(host, port);
-    this.out = new BufferedOutputStream(socket.getOutputStream());
-    this.in  = new BufferedInputStream(socket.getInputStream());
-    String line = readln(this.in);
-    if(!line.startsWith("200 "))
-    {
-      throw new IOException("Invalid hello from server: " + line);
-    }
-  }
-  
-  private boolean eofArticle(byte[] buf)
-  {
-    if(buf.length < 4)
-    {
-      return false;
-    }
-    
-    int l = buf.length - 1;
-    return buf[l-3] == 10 // '*\n'
-        && buf[l-2] == '.'                   // '.'
-        && buf[l-1] == 13 && buf[l] == 10;  // '\r\n'
-  }
-  
-  public byte[] getArticleData()
-    throws IOException, UnsupportedEncodingException
-  {
-    long maxSize = Config.inst().get(Config.ARTICLE_MAXSIZE, 1024) * 1024L;
-
-    try
-    {
-      this.out.write(("ARTICLE " + this.messageID + "\r\n").getBytes("UTF-8"));
-      this.out.flush();
-
-      String line = readln(this.in);
-      if(line.startsWith("220 "))
-      {
-        ByteArrayOutputStream buf = new ByteArrayOutputStream();
-        
-        while(!eofArticle(buf.toByteArray()))
-        {
-          for(int b = in.read(); b != 10; b = in.read())
-          {
-            buf.write(b);
-          }
-
-          buf.write(10);
-          if(buf.size() > maxSize)
-          {
-            Log.get().warning("Skipping message that is too large: " + buf.size());
-            return null;
-          }
-        }
-        
-        return buf.toByteArray();
-      }
-      else
-      {
-        Log.get().warning("ArticleReader: " + line);
-        return null;
-      }
-    }
-    catch(IOException ex)
-    {
-      throw ex;
-    }
-    finally
-    {
-      this.out.write("QUIT\r\n".getBytes("UTF-8"));
-      this.out.flush();
-      this.out.close();
-    }
-  }
-  
-  private String readln(InputStream in)
-    throws IOException
-  {
-    ByteArrayOutputStream buf = new ByteArrayOutputStream();
-    for(int b = in.read(); b != 10 /* \n */; b = in.read())
-    {
-      buf.write(b);
-    }
-    
-    return new String(buf.toByteArray());
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/io/ArticleWriter.java
--- a/org/sonews/util/io/ArticleWriter.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,133 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.util.io;
-
-import java.io.BufferedOutputStream;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.UnsupportedEncodingException;
-import java.net.Socket;
-import java.net.UnknownHostException;
-import org.sonews.storage.Article;
-
-/**
- * Posts an Article to a NNTP server using the POST command.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public class ArticleWriter 
-{
-  
-  private BufferedOutputStream out;
-  private BufferedReader       inr;
-
-  public ArticleWriter(String host, int port)
-    throws IOException, UnknownHostException
-  {
-    // Connect to NNTP server
-    Socket socket = new Socket(host, port);
-    this.out = new BufferedOutputStream(socket.getOutputStream());
-    this.inr = new BufferedReader(new InputStreamReader(socket.getInputStream()));
-    String line = inr.readLine();
-    if(line == null || !line.startsWith("200 "))
-    {
-      throw new IOException("Invalid hello from server: " + line);
-    }
-  }
-  
-  public void close()
-    throws IOException, UnsupportedEncodingException
-  {
-    this.out.write("QUIT\r\n".getBytes("UTF-8"));
-    this.out.flush();
-  }
-
-  protected void finishPOST()
-    throws IOException
-  {
-    this.out.write("\r\n.\r\n".getBytes());
-    this.out.flush();
-    String line = inr.readLine();
-    if(line == null || !line.startsWith("240 ") || !line.startsWith("441 "))
-    {
-      throw new IOException(line);
-    }
-  }
-
-  protected void preparePOST()
-    throws IOException
-  {
-    this.out.write("POST\r\n".getBytes("UTF-8"));
-    this.out.flush();
-
-    String line = this.inr.readLine();
-    if(line == null || !line.startsWith("340 "))
-    {
-      throw new IOException(line);
-    }
-  }
-
-  public void writeArticle(Article article)
-    throws IOException, UnsupportedEncodingException
-  {
-    byte[] buf = new byte[512];
-    ArticleInputStream in = new ArticleInputStream(article);
-
-    preparePOST();
-    
-    int len = in.read(buf);
-    while(len != -1)
-    {
-      writeLine(buf, len);
-      len = in.read(buf);
-    }
-
-    finishPOST();
-  }
-
-  /**
-   * Writes the raw content of an article to the remote server. This method
-   * does no charset conversion/handling of any kind so its the preferred
-   * method for sending an article to remote peers.
-   * @param rawArticle
-   * @throws IOException
-   */
-  public void writeArticle(byte[] rawArticle)
-    throws IOException
-  {
-    preparePOST();
-    writeLine(rawArticle, rawArticle.length);
-    finishPOST();
-  }
-
-  /**
-   * Writes the given buffer to the connect remote server.
-   * @param buffer
-   * @param len
-   * @throws IOException
-   */
-  protected void writeLine(byte[] buffer, int len)
-    throws IOException
-  {
-    this.out.write(buffer, 0, len);
-    this.out.flush();
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/io/Resource.java
--- a/org/sonews/util/io/Resource.java	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,132 +0,0 @@
-/*
- *   SONEWS News Server
- *   see AUTHORS for the list of contributors
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sonews.util.io;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.net.URL;
-import java.nio.charset.Charset;
-
-/**
- * Provides method for loading of resources.
- * @author Christian Lins
- * @since sonews/0.5.0
- */
-public final class Resource
-{
-  
-  /**
-   * Loads a resource and returns it as URL reference.
-   * The Resource's classloader is used to load the resource, not
-   * the System's ClassLoader so it may be safe to use this method
-   * in a sandboxed environment.
-   * @return
-   */
-  public static URL getAsURL(final String name)
-  {
-    if(name == null)
-    {
-      return null;
-    }
-
-    return Resource.class.getClassLoader().getResource(name);
-  }
-  
-  /**
-   * Loads a resource and returns an InputStream to it.
-   * @param name
-   * @return
-   */
-  public static InputStream getAsStream(String name)
-  {
-    try
-    {
-      URL url = getAsURL(name);
-      if(url == null)
-      {
-        return null;
-      }
-      else
-      {
-        return url.openStream();
-      }
-    }
-    catch(IOException e)
-    {
-      e.printStackTrace();
-      return null;
-    }
-  }
-
-  /**
-   * Loads a plain text resource.
-   * @param withNewline If false all newlines are removed from the 
-   * return String
-   */
-  public static String getAsString(String name, boolean withNewline)
-  {
-    if(name == null)
-      return null;
-
-    BufferedReader in = null;
-    try
-    {
-      InputStream ins = getAsStream(name);
-      if(ins == null)
-        return null;
-
-      in = new BufferedReader(
-        new InputStreamReader(ins, Charset.forName("UTF-8")));
-      StringBuffer buf = new StringBuffer();
-
-      for(;;)
-      {
-        String line = in.readLine();
-        if(line == null)
-          break;
-
-        buf.append(line);
-        if(withNewline)
-          buf.append('\n');
-      }
-
-      return buf.toString();
-    }
-    catch(Exception e)
-    {
-      e.printStackTrace();
-      return null;
-    }
-    finally
-    {
-      try
-      {
-        if(in != null)
-          in.close();
-      }
-      catch(IOException ex)
-      {
-        ex.printStackTrace();
-      }
-    }
-  }
-
-}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/io/package.html
--- a/org/sonews/util/io/package.html	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-Contains I/O utilitiy classes.
\ No newline at end of file
diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/package.html
--- a/org/sonews/util/package.html	Sun Aug 29 17:04:25 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-Contains various utility classes.
\ No newline at end of file
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/Main.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/Main.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,198 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews;
+
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.util.Enumeration;
+import java.util.Date;
+import java.util.logging.Level;
+import org.sonews.config.Config;
+import org.sonews.daemon.ChannelLineBuffers;
+import org.sonews.daemon.CommandSelector;
+import org.sonews.daemon.Connections;
+import org.sonews.daemon.NNTPDaemon;
+import org.sonews.feed.FeedManager;
+import org.sonews.mlgw.MailPoller;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.storage.StorageManager;
+import org.sonews.storage.StorageProvider;
+import org.sonews.util.Log;
+import org.sonews.util.Purger;
+import org.sonews.util.io.Resource;
+
+/**
+ * Startup class of the daemon.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public final class Main
+{
+  
+  private Main()
+  {
+  }
+
+  /** Version information of the sonews daemon */
+  public static final String VERSION = "sonews/1.1.0";
+  public static final Date   STARTDATE = new Date();
+  
+  /**
+   * The main entrypoint.
+   * @param args
+   * @throws Exception
+   */
+  public static void main(String[] args) throws Exception
+  {
+    System.out.println(VERSION);
+    Thread.currentThread().setName("Mainthread");
+
+    // Command line arguments
+    boolean feed    = false;  // Enable feeding?
+    boolean mlgw    = false;  // Enable Mailinglist gateway?
+    int     port    = -1;
+    
+    for(int n = 0; n < args.length; n++)
+    {
+      if(args[n].equals("-c") || args[n].equals("-config"))
+      {
+        Config.inst().set(Config.LEVEL_CLI, Config.CONFIGFILE, args[++n]);
+        System.out.println("Using config file " + args[n]);
+      }
+      else if(args[n].equals("-dumpjdbcdriver"))
+      {
+        System.out.println("Available JDBC drivers:");
+        Enumeration<Driver> drvs =  DriverManager.getDrivers();
+        while(drvs.hasMoreElements())
+        {
+          System.out.println(drvs.nextElement());
+        }
+        return;
+      }
+      else if(args[n].equals("-feed"))
+      {
+        feed = true;
+      }
+      else if(args[n].equals("-h") || args[n].equals("-help"))
+      {
+        printArguments();
+        return;
+      }
+      else if(args[n].equals("-mlgw"))
+      {
+        mlgw = true;
+      }
+      else if(args[n].equals("-p"))
+      {
+        port = Integer.parseInt(args[++n]);
+      }
+      else if(args[n].equals("-plugin"))
+      {
+        System.out.println("Warning: -plugin-storage is not implemented!");
+      }
+      else if(args[n].equals("-plugin-command"))
+      {
+        try
+        {
+          CommandSelector.addCommandHandler(args[++n]);
+        }
+        catch(Exception ex)
+        {
+          Log.get().warning("Could not load command plugin: " + args[n]);
+          Log.get().log(Level.INFO, "Main.java", ex);
+        }
+      }
+      else if(args[n].equals("-plugin-storage"))
+      {
+        System.out.println("Warning: -plugin-storage is not implemented!");
+      }
+      else if(args[n].equals("-v") || args[n].equals("-version"))
+      {
+        // Simply return as the version info is already printed above
+        return;
+      }
+    }
+    
+    // Try to load the JDBCDatabase;
+    // Do NOT USE BackendConfig or Log classes before this point because they require
+    // a working JDBCDatabase connection.
+    try
+    {
+      StorageProvider sprov =
+        StorageManager.loadProvider("org.sonews.storage.impl.JDBCDatabaseProvider");
+      StorageManager.enableProvider(sprov);
+      
+      // Make sure some elementary groups are existing
+      if(!StorageManager.current().isGroupExisting("control"))
+      {
+        StorageManager.current().addGroup("control", 0);
+        Log.get().info("Group 'control' created.");
+      }
+    }
+    catch(StorageBackendException ex)
+    {
+      ex.printStackTrace();
+      System.err.println("Database initialization failed with " + ex.toString());
+      System.err.println("Make sure you have specified the correct database" +
+        " settings in sonews.conf!");
+      return;
+    }
+    
+    ChannelLineBuffers.allocateDirect();
+    
+    // Add shutdown hook
+    Runtime.getRuntime().addShutdownHook(new ShutdownHook());
+    
+    // Start the listening daemon
+    if(port <= 0)
+    {
+      port = Config.inst().get(Config.PORT, 119);
+    }
+    final NNTPDaemon daemon = NNTPDaemon.createInstance(port);
+    daemon.start();
+    
+    // Start Connections purger thread...
+    Connections.getInstance().start();
+    
+    // Start mailinglist gateway...
+    if(mlgw)
+    {
+      new MailPoller().start();
+    }
+    
+    // Start feeds
+    if(feed)
+    {
+      FeedManager.startFeeding();
+    }
+
+    Purger purger = new Purger();
+    purger.start();
+    
+    // Wait for main thread to exit (setDaemon(false))
+    daemon.join();
+  }
+  
+  private static void printArguments()
+  {
+    String usage = Resource.getAsString("helpers/usage", true);
+    System.out.println(usage);
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/ShutdownHook.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/ShutdownHook.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,84 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews;
+
+import java.sql.SQLException;
+import java.util.Map;
+import org.sonews.daemon.AbstractDaemon;
+
+/**
+ * Will force all other threads to shutdown cleanly.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+class ShutdownHook extends Thread
+{
+
+  /**
+   * Called when the JVM exits.
+   */
+  @Override
+  public void run()
+  {
+    System.out.println("sonews: Trying to shutdown all threads...");
+
+    Map<Thread, StackTraceElement[]> threadsMap = Thread.getAllStackTraces();
+    for(Thread thread : threadsMap.keySet())
+    {
+      // Interrupt the thread if it's a AbstractDaemon
+      AbstractDaemon daemon;
+      if(thread instanceof AbstractDaemon && thread.isAlive())
+      {
+        try
+        {
+          daemon = (AbstractDaemon)thread;
+          daemon.shutdownNow();
+        }
+        catch(SQLException ex)
+        {
+          System.out.println("sonews: " + ex);
+        }
+      }
+    }
+    
+    for(Thread thread : threadsMap.keySet())
+    {
+      AbstractDaemon daemon;
+      if(thread instanceof AbstractDaemon && thread.isAlive())
+      {
+        daemon = (AbstractDaemon)thread;
+        System.out.println("sonews: Waiting for " + daemon + " to exit...");
+        try
+        {
+          daemon.join(500);
+        }
+        catch(InterruptedException ex)
+        {
+          System.out.println(ex.getLocalizedMessage());
+        }
+      }
+    }
+    
+    // We have notified all not-sleeping AbstractDaemons of the shutdown;
+    // all other threads can be simply purged on VM shutdown
+    
+    System.out.println("sonews: Clean shutdown.");
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/acl/AccessControl.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/acl/AccessControl.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,31 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.acl;
+
+/**
+ *
+ * @author Christian Lins
+ * @since sonews/1.1
+ */
+public interface AccessControl
+{
+
+  boolean hasPermission(String user, char[] secret, String permission);
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/acl/AuthInfoCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/acl/AuthInfoCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,64 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.acl;
+
+import java.io.IOException;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.daemon.command.Command;
+import org.sonews.storage.StorageBackendException;
+
+/**
+ *
+ * @author Christian Lins
+ * @since sonews/1.1
+ */
+public class AuthInfoCommand implements Command
+{
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    throw new UnsupportedOperationException("Not supported yet.");
+  }
+
+  @Override
+  public boolean hasFinished()
+  {
+    throw new UnsupportedOperationException("Not supported yet.");
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    throw new UnsupportedOperationException("Not supported yet.");
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    throw new UnsupportedOperationException("Not supported yet.");
+  }
+
+  @Override
+  public void processLine(NNTPConnection conn, String line, byte[] rawLine) throws IOException, StorageBackendException
+  {
+    throw new UnsupportedOperationException("Not supported yet.");
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/config/AbstractConfig.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/config/AbstractConfig.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,57 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.config;
+
+/**
+ * Base class for Config and BootstrapConfig.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public abstract class AbstractConfig 
+{
+  
+  public abstract String get(String key, String defVal);
+  
+  public int get(final String key, final int defVal)
+  {
+    return Integer.parseInt(
+      get(key, Integer.toString(defVal)));
+  }
+  
+  public boolean get(String key, boolean defVal)
+  {
+    String val = get(key, Boolean.toString(defVal));
+    return Boolean.parseBoolean(val);
+  }
+
+  /**
+   * Returns a long config value specified via the given key.
+   * @param key
+   * @param defVal
+   * @return
+   */
+  public long get(String key, long defVal)
+  {
+    String val = get(key, Long.toString(defVal));
+    return Long.parseLong(val);
+  }
+
+  protected abstract void set(String key, String val);
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/config/BackendConfig.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/config/BackendConfig.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,115 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.config;
+
+import java.util.logging.Level;
+import org.sonews.util.Log;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.storage.StorageManager;
+import org.sonews.util.TimeoutMap;
+
+/**
+ * Provides access to the program wide configuration that is stored within
+ * the server's database.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+class BackendConfig extends AbstractConfig
+{
+
+  private static BackendConfig instance = new BackendConfig();
+  
+  public static BackendConfig getInstance()
+  {
+    return instance;
+  }
+  
+  private final TimeoutMap<String, String> values 
+    = new TimeoutMap<String, String>();
+  
+  private BackendConfig()
+  {
+    super();
+  }
+  
+  /**
+   * Returns the config value for the given key or the defaultValue if the
+   * key is not found in config.
+   * @param key
+   * @param defaultValue
+   * @return
+   */
+  @Override
+  public String get(String key, String defaultValue)
+  {
+    try
+    {
+      String configValue = values.get(key);
+      if(configValue == null)
+      {
+        if(StorageManager.current() == null)
+        {
+          Log.get().warning("BackendConfig not available, using default.");
+          return defaultValue;
+        }
+
+        configValue = StorageManager.current().getConfigValue(key);
+        if(configValue == null)
+        {
+          return defaultValue;
+        }
+        else
+        {
+          values.put(key, configValue);
+          return configValue;
+        }
+      }
+      else
+      {
+        return configValue;
+      }
+    }
+    catch(StorageBackendException ex)
+    {
+      Log.get().log(Level.SEVERE, "Storage backend problem", ex);
+      return defaultValue;
+    }
+  }
+  
+  /**
+   * Sets the config value which is identified by the given key.
+   * @param key
+   * @param value
+   */
+  public void set(String key, String value)
+  {
+    values.put(key, value);
+    
+    try
+    {
+      // Write values to database
+      StorageManager.current().setConfigValue(key, value);
+    }
+    catch(StorageBackendException ex)
+    {
+      ex.printStackTrace();
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/config/CommandLineConfig.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/config/CommandLineConfig.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,64 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.config;
+
+import java.util.Map;
+import java.util.HashMap;
+
+/**
+ *
+ * @author Christian Lins
+ */
+class CommandLineConfig extends AbstractConfig
+{
+
+  private static final CommandLineConfig instance = new CommandLineConfig();
+
+  public static CommandLineConfig getInstance()
+  {
+    return instance;
+  }
+
+  private final Map<String, String> values = new HashMap<String, String>();
+  
+  private CommandLineConfig() {}
+
+  @Override
+  public String get(String key, String def)
+  {
+    synchronized(this.values)
+    {
+      if(this.values.containsKey(key))
+      {
+        def = this.values.get(key);
+      }
+    }
+    return def;
+  }
+
+  @Override
+  public void set(String key, String val)
+  {
+    synchronized(this.values)
+    {
+      this.values.put(key, val);
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/config/Config.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/config/Config.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,175 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.config;
+
+/**
+ * Configuration facade class.
+ * @author Christian Lins
+ * @since sonews/1.0
+ */
+public class Config extends AbstractConfig
+{
+  
+  public static final int LEVEL_CLI     = 1;
+  public static final int LEVEL_FILE    = 2;
+  public static final int LEVEL_BACKEND = 3;
+
+  public static final String CONFIGFILE = "sonews.configfile";
+  
+    /** BackendConfig key constant. Value is the maximum article size in kilobytes. */
+  public static final String ARTICLE_MAXSIZE   = "sonews.article.maxsize";
+
+  /** BackendConfig key constant. Value: Amount of news that are feeded per run. */
+  public static final String EVENTLOG          = "sonews.eventlog";
+  public static final String FEED_NEWSPERRUN   = "sonews.feed.newsperrun";
+  public static final String FEED_PULLINTERVAL = "sonews.feed.pullinterval";
+  public static final String HOSTNAME          = "sonews.hostname";
+  public static final String PORT              = "sonews.port";
+  public static final String TIMEOUT           = "sonews.timeout";
+  public static final String LOGLEVEL          = "sonews.loglevel";
+  public static final String MLPOLL_DELETEUNKNOWN = "sonews.mlpoll.deleteunknown";
+  public static final String MLPOLL_HOST       = "sonews.mlpoll.host";
+  public static final String MLPOLL_PASSWORD   = "sonews.mlpoll.password";
+  public static final String MLPOLL_USER       = "sonews.mlpoll.user";
+  public static final String MLSEND_ADDRESS    = "sonews.mlsend.address";
+  public static final String MLSEND_RW_FROM    = "sonews.mlsend.rewrite.from";
+  public static final String MLSEND_RW_SENDER  = "sonews.mlsend.rewrite.sender";
+  public static final String MLSEND_HOST       = "sonews.mlsend.host";
+  public static final String MLSEND_PASSWORD   = "sonews.mlsend.password";
+  public static final String MLSEND_PORT       = "sonews.mlsend.port";
+  public static final String MLSEND_USER       = "sonews.mlsend.user";
+  
+  /** Key constant. If value is "true" every I/O is written to logfile
+   * (which is a lot!)
+   */
+  public static final String DEBUG              = "sonews.debug";
+
+  /** Key constant. Value is classname of the JDBC driver */
+  public static final String STORAGE_DBMSDRIVER = "sonews.storage.dbmsdriver";
+
+  /** Key constant. Value is JDBC connect String to the database. */
+  public static final String STORAGE_DATABASE   = "sonews.storage.database";
+
+  /** Key constant. Value is the username for the DBMS. */
+  public static final String STORAGE_USER       = "sonews.storage.user";
+
+  /** Key constant. Value is the password for the DBMS. */
+  public static final String STORAGE_PASSWORD   = "sonews.storage.password";
+
+  /** Key constant. Value is the name of the host which is allowed to use the
+   *  XDAEMON command; default: "localhost" */
+  public static final String XDAEMON_HOST       = "sonews.xdaemon.host";
+
+  /** The config key for the filename of the logfile */
+  public static final String LOGFILE = "sonews.log";
+
+  public static final String[] AVAILABLE_KEYS = {
+      ARTICLE_MAXSIZE,
+      EVENTLOG,
+      FEED_NEWSPERRUN,
+      FEED_PULLINTERVAL,
+      HOSTNAME,
+      MLPOLL_DELETEUNKNOWN,
+      MLPOLL_HOST,
+      MLPOLL_PASSWORD,
+      MLPOLL_USER,
+      MLSEND_ADDRESS,
+      MLSEND_HOST,
+      MLSEND_PASSWORD,
+      MLSEND_PORT,
+      MLSEND_RW_FROM,
+      MLSEND_RW_SENDER,
+      MLSEND_USER,
+      PORT,
+      TIMEOUT,
+      XDAEMON_HOST
+  };
+
+  private static Config instance = new Config();
+  
+  public static Config inst()
+  {
+    return instance;
+  }
+  
+  private Config(){}
+
+  @Override
+  public String get(String key, String def)
+  {
+    String val = CommandLineConfig.getInstance().get(key, null);
+    
+    if(val == null)
+    {
+      val = FileConfig.getInstance().get(key, null);
+    }
+
+    if(val == null)
+    {
+      val = BackendConfig.getInstance().get(key, def);
+    }
+
+    return val;
+  }
+
+  public String get(int maxLevel, String key, String def)
+  {
+    String val = CommandLineConfig.getInstance().get(key, null);
+
+    if(val == null && maxLevel >= LEVEL_FILE)
+    {
+      val = FileConfig.getInstance().get(key, null);
+      if(val == null && maxLevel >= LEVEL_BACKEND)
+      {
+        val = BackendConfig.getInstance().get(key, def);
+      }
+    }
+
+    return val != null ? val : def;
+  }
+
+  @Override
+  public void set(String key, String val)
+  {
+    set(LEVEL_BACKEND, key, val);
+  }
+
+  public void set(int level, String key, String val)
+  {
+    switch(level)
+    {
+      case LEVEL_CLI:
+      {
+        CommandLineConfig.getInstance().set(key, val);
+        break;
+      }
+      case LEVEL_FILE:
+      {
+        FileConfig.getInstance().set(key, val);
+        break;
+      }
+      case LEVEL_BACKEND:
+      {
+        BackendConfig.getInstance().set(key, val);
+        break;
+      }
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/config/FileConfig.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/config/FileConfig.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,170 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.config;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * Manages the bootstrap configuration. It MUST contain all config values
+ * that are needed to establish a database connection.
+ * For further configuration values use the Config class instead as that class
+ * stores its values within the database.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+class FileConfig extends AbstractConfig
+{
+
+  private static final Properties defaultConfig = new Properties();
+  
+  private static FileConfig instance = null;
+  
+  static
+  {
+    // Set some default values
+    defaultConfig.setProperty(Config.STORAGE_DATABASE, "jdbc:mysql://localhost/sonews");
+    defaultConfig.setProperty(Config.STORAGE_DBMSDRIVER, "com.mysql.jdbc.Driver");
+    defaultConfig.setProperty(Config.STORAGE_USER, "sonews_user");
+    defaultConfig.setProperty(Config.STORAGE_PASSWORD, "mysecret");
+    defaultConfig.setProperty(Config.DEBUG, "false");
+  }
+  
+  /**
+   * Note: this method is not thread-safe
+   * @return A Config instance
+   */
+  public static synchronized FileConfig getInstance()
+  {
+    if(instance == null)
+    {
+      instance = new FileConfig();
+    }
+    return instance;
+  }
+
+  // Every config instance is initialized with the default values.
+  private final Properties settings = (Properties)defaultConfig.clone();
+
+  /**
+   * Config is a singelton class with only one instance at time.
+   * So the constructor is private to prevent the creation of more
+   * then one Config instance.
+   * @see Config.getInstance() to retrieve an instance of Config
+   */
+  private FileConfig()
+  {
+    try
+    {
+      // Load settings from file
+      load();
+    }
+    catch(IOException ex)
+    {
+      ex.printStackTrace();
+    }
+  }
+
+  /**
+   * Loads the configuration from the config file. By default this is done
+   * by the (private) constructor but it can be useful to reload the config
+   * by invoking this method.
+   * @throws IOException
+   */
+  public void load() 
+    throws IOException
+  {
+    FileInputStream in = null;
+    
+    try
+    {
+      in = new FileInputStream(
+        Config.inst().get(Config.LEVEL_CLI, Config.CONFIGFILE, "sonews.conf"));
+      settings.load(in);
+    }
+    catch (FileNotFoundException e)
+    {
+      // MUST NOT use Log otherwise endless loop
+      System.err.println(e.getMessage());
+      save();
+    }
+    finally
+    {
+      if(in != null)
+        in.close();
+    }
+  }
+
+  /**
+   * Saves this Config to the config file. By default this is done
+   * at program end.
+   * @throws FileNotFoundException
+   * @throws IOException
+   */
+  public void save() throws FileNotFoundException, IOException
+  {
+    FileOutputStream out = null;
+    try
+    {
+      out = new FileOutputStream(
+        Config.inst().get(Config.LEVEL_CLI, Config.CONFIGFILE, "sonews.conf"));
+      settings.store(out, "SONEWS Config File");
+      out.flush();
+    }
+    catch(IOException ex)
+    {
+      throw ex;
+    }
+    finally
+    {
+      if(out != null)
+        out.close();
+    }
+  }
+  
+  /**
+   * Returns the value that is stored within this config
+   * identified by the given key. If the key cannot be found
+   * the default value is returned.
+   * @param key Key to identify the value.
+   * @param def The default value that is returned if the key
+   * is not found in this Config.
+   * @return
+   */
+  @Override
+  public String get(String key, String def)
+  {
+    return settings.getProperty(key, def);
+  }
+
+  /**
+   * Sets the value for a given key.
+   * @param key
+   * @param value
+   */
+  @Override
+  public void set(final String key, final String value)
+  {
+    settings.setProperty(key, value);
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/AbstractDaemon.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/AbstractDaemon.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,101 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon;
+
+import java.sql.SQLException;
+import org.sonews.storage.StorageManager;
+import org.sonews.util.Log;
+
+/**
+ * Base class of all sonews threads.
+ * Instances of this class will be automatically registered at the ShutdownHook
+ * to be cleanly exited when the server is forced to exit.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public abstract class AbstractDaemon extends Thread
+{
+
+  /** This variable is write synchronized through setRunning */
+  private boolean isRunning = false;
+
+  /**
+   * Protected constructor. Will be called by derived classes.
+   */
+  protected AbstractDaemon()
+  {
+    setDaemon(true); // VM will exit when all threads are daemons
+    setName(getClass().getSimpleName());
+  }
+  
+  /**
+   * @return true if shutdown() was not yet called.
+   */
+  public boolean isRunning()
+  {
+    synchronized(this)
+    {
+      return this.isRunning;
+    }
+  }
+  
+  /**
+   * Marks this thread to exit soon. Closes the associated JDBCDatabase connection
+   * if available.
+   * @throws java.sql.SQLException
+   */
+  public void shutdownNow()
+    throws SQLException
+  {
+    synchronized(this)
+    {
+      this.isRunning = false;
+      StorageManager.disableProvider();
+    }
+  }
+  
+  /**
+   * Calls shutdownNow() but catches SQLExceptions if occurring.
+   */
+  public void shutdown()
+  {
+    try
+    {
+      shutdownNow();
+    }
+    catch(SQLException ex)
+    {
+      Log.get().warning(ex.toString());
+    }
+  }
+  
+  /**
+   * Starts this daemon.
+   */
+  @Override
+  public void start()
+  {
+    synchronized(this)
+    {
+      this.isRunning = true;
+    }
+    super.start();
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/ChannelLineBuffers.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/ChannelLineBuffers.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,283 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon;
+
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class holding ByteBuffers for SocketChannels/NNTPConnection.
+ * Due to the complex nature of AIO/NIO we must properly handle the line 
+ * buffers for the input and output of the SocketChannels.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class ChannelLineBuffers 
+{
+  
+  /**
+   * Size of one small buffer; 
+   * per default this is 512 bytes to fit one standard line.
+   */
+  public static final int BUFFER_SIZE = 512;
+  
+  private static int maxCachedBuffers = 2048; // Cached buffers maximum
+  
+  private static final List<ByteBuffer> freeSmallBuffers
+    = new ArrayList<ByteBuffer>(maxCachedBuffers);
+  
+  /**
+   * Allocates a predefined number of direct ByteBuffers (allocated via
+   * ByteBuffer.allocateDirect()). This method is Thread-safe, but should only
+   * called at startup.
+   */
+  public static void allocateDirect()
+  {
+    synchronized(freeSmallBuffers)
+    {
+      for(int n = 0; n < maxCachedBuffers; n++)
+      {
+        ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
+        freeSmallBuffers.add(buffer);
+      }
+    }
+  }
+  
+  private ByteBuffer       inputBuffer   = newLineBuffer();
+  private List<ByteBuffer> outputBuffers = new ArrayList<ByteBuffer>();
+  
+  /**
+   * Add the given ByteBuffer to the list of buffers to be send to the client.
+   * This method is Thread-safe.
+   * @param buffer
+   * @throws java.nio.channels.ClosedChannelException If the client channel was
+   * already closed.
+   */
+  public void addOutputBuffer(ByteBuffer buffer)
+    throws ClosedChannelException
+  {
+    if(outputBuffers == null)
+    {
+      throw new ClosedChannelException();
+    }
+    
+    synchronized(outputBuffers)
+    {
+      outputBuffers.add(buffer);
+    }
+  }
+  
+  /**
+   * Currently a channel has only one input buffer. This *may* be a bottleneck
+   * and should investigated in the future.
+   * @param channel
+   * @return The input buffer associated with given channel.
+   */
+  public ByteBuffer getInputBuffer()
+  {
+    return inputBuffer;
+  }
+  
+  /**
+   * Returns the current output buffer for writing(!) to SocketChannel.
+   * @param channel
+   * @return The next input buffer that contains unprocessed data or null
+   * if the connection was closed or there are no more unprocessed buffers.
+   */
+  public ByteBuffer getOutputBuffer()
+  {
+    synchronized(outputBuffers)
+    {
+      if(outputBuffers == null || outputBuffers.isEmpty())
+      {
+        return null;
+      }
+      else
+      {
+        ByteBuffer buffer = outputBuffers.get(0);
+        if(buffer.remaining() == 0)
+        {
+          outputBuffers.remove(0);
+          // Add old buffers to the list of free buffers
+          recycleBuffer(buffer);
+          buffer = getOutputBuffer();
+        }
+        return buffer;
+      }
+    }
+  }
+
+  /**
+   * @return false if there are output buffers pending to be written to the
+   * client.
+   */
+  boolean isOutputBufferEmpty()
+  {
+    synchronized(outputBuffers)
+    {
+      return outputBuffers.isEmpty();
+    }
+  }
+  
+  /**
+   * Goes through the input buffer of the given channel and searches
+   * for next line terminator. If a '\n' is found, the bytes up to the
+   * line terminator are returned as array of bytes (the line terminator
+   * is omitted). If none is found the method returns null.
+   * @param channel
+   * @return A ByteBuffer wrapping the line.
+   */
+  ByteBuffer nextInputLine()
+  {
+    if(inputBuffer == null)
+    {
+      return null;
+    }
+    
+    synchronized(inputBuffer)
+    {
+      ByteBuffer buffer = inputBuffer;
+
+      // Mark the current write position
+      int mark = buffer.position();
+
+      // Set position to 0 and limit to current position
+      buffer.flip();
+
+      ByteBuffer lineBuffer = newLineBuffer();
+
+      while (buffer.position() < buffer.limit())
+      {
+        byte b = buffer.get();
+        if (b == 10) // '\n'
+        {
+          // The bytes between the buffer's current position and its limit, 
+          // if any, are copied to the beginning of the buffer. That is, the 
+          // byte at index p = position() is copied to index zero, the byte at 
+          // index p + 1 is copied to index one, and so forth until the byte 
+          // at index limit() - 1 is copied to index n = limit() - 1 - p. 
+          // The buffer's position is then set to n+1 and its limit is set to 
+          // its capacity.
+          buffer.compact();
+
+          lineBuffer.flip(); // limit to position, position to 0
+          return lineBuffer;
+        }
+        else
+        {
+          lineBuffer.put(b);
+        }
+      }
+
+      buffer.limit(BUFFER_SIZE);
+      buffer.position(mark);
+
+      if(buffer.hasRemaining())
+      {
+        return null;
+      }
+      else
+      {
+        // In the first 512 was no newline found, so the input is not standard
+        // compliant. We return the current buffer as new line and add a space
+        // to the beginning of the next line which corrects some overlong header
+        // lines.
+        inputBuffer = newLineBuffer();
+        inputBuffer.put((byte)' ');
+        buffer.flip();
+        return buffer;
+      }
+    }
+  }
+  
+  /**
+   * Returns a at least 512 bytes long ByteBuffer ready for usage.
+   * The method first try to reuse an already allocated (cached) buffer but
+   * if that fails returns a newly allocated direct buffer.
+   * Use recycleBuffer() method when you do not longer use the allocated buffer.
+   */
+  static ByteBuffer newLineBuffer()
+  {
+    ByteBuffer buf = null;
+    synchronized(freeSmallBuffers)
+    {
+      if(!freeSmallBuffers.isEmpty())
+      {
+        buf = freeSmallBuffers.remove(0);
+      }
+    }
+      
+    if(buf == null)
+    {
+      // Allocate a non-direct buffer
+      buf = ByteBuffer.allocate(BUFFER_SIZE);
+    }
+    
+    assert buf.position() == 0;
+    assert buf.limit() >= BUFFER_SIZE;
+    
+    return buf;
+  }
+  
+  /**
+   * Adds the given buffer to the list of free buffers if it is a valuable
+   * direct allocated buffer.
+   * @param buffer
+   */
+  public static void recycleBuffer(ByteBuffer buffer)
+  {
+    assert buffer != null;
+
+    if(buffer.isDirect())
+    {
+      assert buffer.capacity() >= BUFFER_SIZE;
+      
+      // Add old buffers to the list of free buffers
+      synchronized(freeSmallBuffers)
+      {
+        buffer.clear(); // Set position to 0 and limit to capacity
+        freeSmallBuffers.add(buffer);
+      }
+    } // if(buffer.isDirect())
+  }
+  
+  /**
+   * Recycles all buffers of this ChannelLineBuffers object.
+   */
+  public void recycleBuffers()
+  {
+    synchronized(inputBuffer)
+    {
+      recycleBuffer(inputBuffer);
+      this.inputBuffer = null;
+    }
+    
+    synchronized(outputBuffers)
+    {
+      for(ByteBuffer buf : outputBuffers)
+      {
+        recycleBuffer(buf);
+      }
+      outputBuffers = null;
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/ChannelReader.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/ChannelReader.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,202 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.CancelledKeyException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.logging.Level;
+import org.sonews.util.Log;
+
+/**
+ * A Thread task listening for OP_READ events from SocketChannels.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+class ChannelReader extends AbstractDaemon
+{
+
+  private static ChannelReader instance = new ChannelReader();
+
+  /**
+   * @return Active ChannelReader instance.
+   */
+  public static ChannelReader getInstance()
+  {
+    return instance;
+  }
+  
+  private Selector selector = null;
+  
+  protected ChannelReader()
+  {
+  }
+  
+  /**
+   * Sets the selector which is used by this reader to determine the channel
+   * to read from.
+   * @param selector
+   */
+  public void setSelector(final Selector selector)
+  {
+    this.selector = selector;
+  }
+  
+  /**
+   * Run loop. Blocks until some data is available in a channel.
+   */
+  @Override
+  public void run()
+  {
+    assert selector != null;
+
+    while(isRunning())
+    {
+      try
+      {
+        // select() blocks until some SelectableChannels are ready for
+        // processing. There is no need to lock the selector as we have only
+        // one thread per selector.
+        selector.select();
+
+        // Get list of selection keys with pending events.
+        // Note: the selected key set is not thread-safe
+        SocketChannel channel = null;
+        NNTPConnection conn = null;
+        final Set<SelectionKey> selKeys = selector.selectedKeys();
+        SelectionKey selKey = null;
+
+        synchronized (selKeys)
+        {
+          Iterator it = selKeys.iterator();
+
+          // Process the first pending event
+          while (it.hasNext())
+          {
+            selKey = (SelectionKey) it.next();
+            channel = (SocketChannel) selKey.channel();
+            conn = Connections.getInstance().get(channel);
+
+            // Because we cannot lock the selKey as that would cause a deadlock
+            // we lock the connection. To preserve the order of the received
+            // byte blocks a selection key for a connection that has pending
+            // read events is skipped.
+            if (conn == null || conn.tryReadLock())
+            {
+              // Remove from set to indicate that it's being processed
+              it.remove();
+              if (conn != null)
+              {
+                break; // End while loop
+              }
+            }
+            else
+            {
+              selKey = null;
+              channel = null;
+              conn = null;
+            }
+          }
+        }
+
+        // Do not lock the selKeys while processing because this causes
+        // a deadlock in sun.nio.ch.SelectorImpl.lockAndDoSelect()
+        if (selKey != null && channel != null && conn != null)
+        {
+          processSelectionKey(conn, channel, selKey);
+          conn.unlockReadLock();
+        }
+
+      }
+      catch(CancelledKeyException ex)
+      {
+        Log.get().warning("ChannelReader.run(): " + ex);
+        Log.get().log(Level.INFO, "", ex);
+      }
+      catch(Exception ex)
+      {
+        ex.printStackTrace();
+      }
+      
+      // Eventually wait for a register operation
+      synchronized (NNTPDaemon.RegisterGate)
+      {
+      // Do nothing; FindBugs may warn about an empty synchronized 
+      // statement, but we cannot use a wait()/notify() mechanism here.
+      // If we used something like RegisterGate.wait() we block here
+      // until the NNTPDaemon calls notify(). But the daemon only
+      // calls notify() if itself is NOT blocked in the listening socket.
+      }
+    } // while(isRunning())
+  }
+  
+  private void processSelectionKey(final NNTPConnection connection,
+    final SocketChannel socketChannel, final SelectionKey selKey)
+    throws InterruptedException, IOException
+  {
+    assert selKey != null;
+    assert selKey.isReadable();
+    
+    // Some bytes are available for reading
+    if(selKey.isValid())
+    {   
+      // Lock the channel
+      //synchronized(socketChannel)
+      {
+        // Read the data into the appropriate buffer
+        ByteBuffer buf = connection.getInputBuffer();
+        int read = -1;
+        try 
+        {
+          read = socketChannel.read(buf);
+        }
+        catch(IOException ex)
+        {
+          // The connection was probably closed by the remote host
+          // in a non-clean fashion
+          Log.get().info("ChannelReader.processSelectionKey(): " + ex);
+        }
+        catch(Exception ex) 
+        {
+          Log.get().warning("ChannelReader.processSelectionKey(): " + ex);
+        }
+        
+        if(read == -1) // End of stream
+        {
+          selKey.cancel();
+        }
+        else if(read > 0) // If some data was read
+        {
+          ConnectionWorker.addChannel(socketChannel);
+        }
+      }
+    }
+    else
+    {
+      // Should not happen
+      Log.get().severe("Should not happen: " + selKey.toString());
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/ChannelWriter.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/ChannelWriter.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,210 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon;
+
+import org.sonews.util.Log;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.CancelledKeyException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.util.Iterator;
+
+/**
+ * A Thread task that processes OP_WRITE events for SocketChannels.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+class ChannelWriter extends AbstractDaemon
+{
+
+  private static ChannelWriter instance = new ChannelWriter();
+
+  /**
+   * @return Returns the active ChannelWriter instance.
+   */
+  public static ChannelWriter getInstance()
+  {
+    return instance;
+  }
+  
+  private Selector selector = null;
+  
+  protected ChannelWriter()
+  {
+  }
+  
+  /**
+   * @return Selector associated with this instance.
+   */
+  public Selector getSelector()
+  {
+    return this.selector;
+  }
+  
+  /**
+   * Sets the selector that is used by this ChannelWriter.
+   * @param selector
+   */
+  public void setSelector(final Selector selector)
+  {
+    this.selector = selector;
+  }
+  
+  /**
+   * Run loop.
+   */
+  @Override
+  public void run()
+  {
+    assert selector != null;
+
+    while(isRunning())
+    {
+      try
+      {
+        SelectionKey   selKey        = null;
+        SocketChannel  socketChannel = null;
+        NNTPConnection connection    = null;
+
+        // select() blocks until some SelectableChannels are ready for
+        // processing. There is no need to synchronize the selector as we
+        // have only one thread per selector.
+        selector.select(); // The return value of select can be ignored
+
+        // Get list of selection keys with pending OP_WRITE events.
+        // The keySET is not thread-safe whereas the keys itself are.
+        Iterator it = selector.selectedKeys().iterator();
+
+        while (it.hasNext())
+        {
+          // We remove the first event from the set and store it for
+          // later processing.
+          selKey = (SelectionKey) it.next();
+          socketChannel = (SocketChannel) selKey.channel();
+          connection = Connections.getInstance().get(socketChannel);
+
+          it.remove();
+          if (connection != null)
+          {
+            break;
+          }
+          else
+          {
+            selKey = null;
+          }
+        }
+        
+        if (selKey != null)
+        {
+          try
+          {
+            // Process the selected key.
+            // As there is only one OP_WRITE key for a given channel, we need
+            // not to synchronize this processing to retain the order.
+            processSelectionKey(connection, socketChannel, selKey);
+          }
+          catch (IOException ex)
+          {
+            Log.get().warning("Error writing to channel: " + ex);
+
+            // Cancel write events for this channel
+            selKey.cancel();
+            connection.shutdownInput();
+            connection.shutdownOutput();
+          }
+        }
+        
+        // Eventually wait for a register operation
+        synchronized(NNTPDaemon.RegisterGate) { /* do nothing */ }
+      }
+      catch(CancelledKeyException ex)
+      {
+        Log.get().info("ChannelWriter.run(): " + ex);
+      }
+      catch(Exception ex)
+      {
+        ex.printStackTrace();
+      }
+    } // while(isRunning())
+  }
+  
+  private void processSelectionKey(final NNTPConnection connection,
+    final SocketChannel socketChannel, final SelectionKey selKey)
+    throws InterruptedException, IOException
+  {
+    assert connection != null;
+    assert socketChannel != null;
+    assert selKey != null;
+    assert selKey.isWritable();
+
+    // SocketChannel is ready for writing
+    if(selKey.isValid())
+    {
+      // Lock the socket channel
+      synchronized(socketChannel)
+      {
+        // Get next output buffer
+        ByteBuffer buf = connection.getOutputBuffer();
+        if(buf == null)
+        {
+          // Currently we have nothing to write, so we stop the writeable
+          // events until we have something to write to the socket channel
+          //selKey.cancel();
+          selKey.interestOps(0);
+          // Update activity timestamp to prevent too early disconnects
+          // on slow client connections
+          connection.setLastActivity(System.currentTimeMillis());
+          return;
+        }
+ 
+        while(buf != null) // There is data to be send
+        {
+          // Write buffer to socket channel; this method does not block
+          if(socketChannel.write(buf) <= 0)
+          {
+            // Perhaps there is data to be written, but the SocketChannel's
+            // buffer is full, so we stop writing to until the next event.
+            break;
+          }
+          else
+          {
+            // Retrieve next buffer if available; method may return the same
+            // buffer instance if it still have some bytes remaining
+            buf = connection.getOutputBuffer();
+          }
+        }
+      }
+    }
+    else
+    {
+      Log.get().warning("Invalid OP_WRITE key: " + selKey);
+
+      if(socketChannel.socket().isClosed())
+      {
+        connection.shutdownInput();
+        connection.shutdownOutput();
+        socketChannel.close();
+        Log.get().info("Connection closed.");
+      }
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/CommandSelector.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/CommandSelector.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,141 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import org.sonews.daemon.command.Command;
+import org.sonews.daemon.command.UnsupportedCommand;
+import org.sonews.util.Log;
+import org.sonews.util.io.Resource;
+
+/**
+ * Selects the correct command processing class.
+ * @author Christian Lins
+ * @since sonews/1.0
+ */
+public class CommandSelector
+{
+
+  private static Map<Thread, CommandSelector> instances
+    = new ConcurrentHashMap<Thread, CommandSelector>();
+  private static Map<String, Class<?>> commandClassesMapping
+    = new ConcurrentHashMap<String, Class<?>>();
+
+  static
+  {
+    String[] classes = Resource.getAsString("helpers/commands.list", true).split("\n");
+    for(String className : classes)
+    {
+      if(className.charAt(0) == '#')
+      {
+        // Skip comments
+        continue;
+      }
+
+      try
+      {
+        addCommandHandler(className);
+      }
+      catch(ClassNotFoundException ex)
+      {
+        Log.get().warning("Could not load command class: " + ex);
+      }
+      catch(InstantiationException ex)
+      {
+        Log.get().severe("Could not instantiate command class: " + ex);
+      }
+      catch(IllegalAccessException ex)
+      {
+        Log.get().severe("Could not access command class: " + ex);
+      }
+    }
+  }
+
+  public static void addCommandHandler(String className)
+    throws ClassNotFoundException, InstantiationException, IllegalAccessException
+  {
+    Class<?> clazz = Class.forName(className);
+    Command cmd = (Command)clazz.newInstance();
+    String[] cmdStrs = cmd.getSupportedCommandStrings();
+    for (String cmdStr : cmdStrs)
+    {
+      commandClassesMapping.put(cmdStr, clazz);
+    }
+  }
+
+  public static Set<String> getCommandNames()
+  {
+    return commandClassesMapping.keySet();
+  }
+
+  public static CommandSelector getInstance()
+  {
+    CommandSelector csel = instances.get(Thread.currentThread());
+    if(csel == null)
+    {
+      csel = new CommandSelector();
+      instances.put(Thread.currentThread(), csel);
+    }
+    return csel;
+  }
+
+  private Map<String, Command> commandMapping = new HashMap<String, Command>();
+  private Command              unsupportedCmd = new UnsupportedCommand();
+
+  private CommandSelector()
+  {}
+
+  public Command get(String commandName)
+  {
+    try
+    {
+      commandName = commandName.toUpperCase();
+      Command cmd = this.commandMapping.get(commandName);
+
+      if(cmd == null)
+      {
+        Class<?> clazz = commandClassesMapping.get(commandName);
+        if(clazz == null)
+        {
+          cmd = this.unsupportedCmd;
+        }
+        else
+        {
+          cmd = (Command)clazz.newInstance();
+          this.commandMapping.put(commandName, cmd);
+        }
+      }
+      else if(cmd.isStateful())
+      {
+        cmd = cmd.getClass().newInstance();
+      }
+
+      return cmd;
+    }
+    catch(Exception ex)
+    {
+      ex.printStackTrace();
+      return this.unsupportedCmd;
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/ConnectionWorker.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/ConnectionWorker.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,102 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon;
+
+import org.sonews.util.Log;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.ArrayBlockingQueue;
+
+/**
+ * Does most of the work: parsing input, talking to client and Database.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+class ConnectionWorker extends AbstractDaemon
+{
+
+  // 256 pending events should be enough
+  private static ArrayBlockingQueue<SocketChannel> pendingChannels
+    = new ArrayBlockingQueue<SocketChannel>(256, true);
+  
+  /**
+   * Registers the given channel for further event processing.
+   * @param channel
+   */
+  public static void addChannel(SocketChannel channel)
+    throws InterruptedException
+  {
+    pendingChannels.put(channel);
+  }
+  
+  /**
+   * Processing loop.
+   */
+  @Override
+  public void run()
+  {
+    while(isRunning())
+    {
+      try
+      {
+        // Retrieve and remove if available, otherwise wait.
+        SocketChannel channel = pendingChannels.take();
+
+        if(channel != null)
+        {
+          // Connections.getInstance().get() MAY return null
+          NNTPConnection conn = Connections.getInstance().get(channel);
+          
+          // Try to lock the connection object
+          if(conn != null && conn.tryReadLock())
+          {
+            ByteBuffer buf = conn.getBuffers().nextInputLine();
+            while(buf != null) // Complete line was received
+            {
+              final byte[] line = new byte[buf.limit()];
+              buf.get(line);
+              ChannelLineBuffers.recycleBuffer(buf);
+              
+              // Here is the actual work done
+              conn.lineReceived(line);
+
+              // Read next line as we could have already received the next line
+              buf = conn.getBuffers().nextInputLine();
+            }
+            conn.unlockReadLock();
+          }
+          else
+          {
+            addChannel(channel);
+          }
+        }
+      }
+      catch(InterruptedException ex)
+      {
+        Log.get().info("ConnectionWorker interrupted: " + ex);
+      }
+      catch(Exception ex)
+      {
+        Log.get().severe("Exception in ConnectionWorker: " + ex);
+        ex.printStackTrace();
+      }
+    } // end while(isRunning())
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/Connections.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/Connections.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,181 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon;
+
+import org.sonews.config.Config;
+import org.sonews.util.Log;
+import org.sonews.util.Stats;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+/**
+ * Daemon thread collecting all NNTPConnection instances. The thread
+ * checks periodically if there are stale/timed out connections and
+ * removes and purges them properly.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public final class Connections extends AbstractDaemon
+{
+
+  private static final Connections instance = new Connections();
+  
+  /**
+   * @return Active Connections instance.
+   */
+  public static Connections getInstance()
+  {
+    return Connections.instance;
+  }
+  
+  private final List<NNTPConnection> connections 
+    = new ArrayList<NNTPConnection>();
+  private final Map<SocketChannel, NNTPConnection> connByChannel 
+    = new HashMap<SocketChannel, NNTPConnection>();
+  
+  private Connections()
+  {
+    setName("Connections");
+  }
+  
+  /**
+   * Adds the given NNTPConnection to the Connections management.
+   * @param conn
+   * @see org.sonews.daemon.NNTPConnection
+   */
+  public void add(final NNTPConnection conn)
+  {
+    synchronized(this.connections)
+    {
+      this.connections.add(conn);
+      this.connByChannel.put(conn.getSocketChannel(), conn);
+    }
+  }
+  
+  /**
+   * @param channel
+   * @return NNTPConnection instance that is associated with the given
+   * SocketChannel.
+   */
+  public NNTPConnection get(final SocketChannel channel)
+  {
+    synchronized(this.connections)
+    {
+      return this.connByChannel.get(channel);
+    }
+  }
+
+  int getConnectionCount(String remote)
+  {
+    int cnt = 0;
+    synchronized(this.connections)
+    {
+      for(NNTPConnection conn : this.connections)
+      {
+        assert conn != null;
+        assert conn.getSocketChannel() != null;
+
+        Socket socket = conn.getSocketChannel().socket();
+        if(socket != null)
+        {
+          InetSocketAddress sockAddr = (InetSocketAddress)socket.getRemoteSocketAddress();
+          if(sockAddr != null)
+          {
+            if(sockAddr.getHostName().equals(remote))
+            {
+              cnt++;
+            }
+          }
+        } // if(socket != null)
+      }
+    }
+    return cnt;
+  }
+  
+  /**
+   * Run loops. Checks periodically for timed out connections and purged them
+   * from the lists.
+   */
+  @Override
+  public void run()
+  {
+    while(isRunning())
+    {
+      int timeoutMillis = 1000 * Config.inst().get(Config.TIMEOUT, 180);
+      
+      synchronized (this.connections)
+      {
+        final ListIterator<NNTPConnection> iter = this.connections.listIterator();
+        NNTPConnection conn;
+
+        while (iter.hasNext())
+        {
+          conn = iter.next();
+          if((System.currentTimeMillis() - conn.getLastActivity()) > timeoutMillis
+              && conn.getBuffers().isOutputBufferEmpty())
+          {
+            // A connection timeout has occurred so purge the connection
+            iter.remove();
+
+            // Close and remove the channel
+            SocketChannel channel = conn.getSocketChannel();
+            connByChannel.remove(channel);
+            
+            try
+            {
+              assert channel != null;
+              assert channel.socket() != null;
+      
+              // Close the channel; implicitely cancels all selectionkeys
+              channel.close();
+              Log.get().info("Disconnected: " + channel.socket().getRemoteSocketAddress() +
+                " (timeout)");
+            }
+            catch(IOException ex)
+            {
+              Log.get().warning("Connections.run(): " + ex);
+            }
+
+            // Recycle the used buffers
+            conn.getBuffers().recycleBuffers();
+            
+            Stats.getInstance().clientDisconnect();
+          }
+        }
+      }
+
+      try
+      {
+        Thread.sleep(10000); // Sleep ten seconds
+      }
+      catch(InterruptedException ex)
+      {
+        Log.get().warning("Connections Thread was interrupted: " + ex.getMessage());
+      }
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/LineEncoder.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/LineEncoder.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,80 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+
+/**
+ * Encodes a line to buffers using the correct charset.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+class LineEncoder
+{
+
+  private CharBuffer    characters;
+  private Charset       charset;
+  
+  /**
+   * Constructs new LineEncoder.
+   * @param characters
+   * @param charset
+   */
+  public LineEncoder(CharBuffer characters, Charset charset)
+  {
+    this.characters = characters;
+    this.charset    = charset;
+  }
+  
+  /**
+   * Encodes the characters of this instance to the given ChannelLineBuffers
+   * using the Charset of this instance.
+   * @param buffer
+   * @throws java.nio.channels.ClosedChannelException
+   */
+  public void encode(ChannelLineBuffers buffer)
+    throws ClosedChannelException
+  {
+    CharsetEncoder encoder = charset.newEncoder();
+    while (characters.hasRemaining())
+    {
+      ByteBuffer buf = ChannelLineBuffers.newLineBuffer();
+      assert buf.position() == 0;
+      assert buf.capacity() >= 512;
+
+      CoderResult res = encoder.encode(characters, buf, true);
+
+      // Set limit to current position and current position to 0;
+      // means make ready for read from buffer
+      buf.flip();
+      buffer.addOutputBuffer(buf);
+
+      if (res.isUnderflow()) // All input processed
+      {
+        break;
+      }
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/NNTPConnection.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/NNTPConnection.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,428 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Timer;
+import java.util.TimerTask;
+import org.sonews.daemon.command.Command;
+import org.sonews.storage.Article;
+import org.sonews.storage.Channel;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.util.Log;
+import org.sonews.util.Stats;
+
+/**
+ * For every SocketChannel (so TCP/IP connection) there is an instance of
+ * this class.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public final class NNTPConnection
+{
+
+  public static final String NEWLINE            = "\r\n";    // RFC defines this as newline
+  public static final String MESSAGE_ID_PATTERN = "<[^>]+>";
+  
+  private static final Timer cancelTimer = new Timer(true); // Thread-safe? True for run as daemon
+  
+  /** SocketChannel is generally thread-safe */
+  private SocketChannel   channel        = null;
+  private Charset         charset        = Charset.forName("UTF-8");
+  private Command         command        = null;
+  private Article         currentArticle = null;
+  private Channel         currentGroup   = null;
+  private volatile long   lastActivity   = System.currentTimeMillis();
+  private ChannelLineBuffers lineBuffers = new ChannelLineBuffers();
+  private int             readLock       = 0;
+  private final Object    readLockGate   = new Object();
+  private SelectionKey    writeSelKey    = null;
+  
+  public NNTPConnection(final SocketChannel channel)
+    throws IOException
+  {
+    if(channel == null)
+    {
+      throw new IllegalArgumentException("channel is null");
+    }
+
+    this.channel = channel;
+    Stats.getInstance().clientConnect();
+  }
+  
+  /**
+   * Tries to get the read lock for this NNTPConnection. This method is Thread-
+   * safe and returns true of the read lock was successfully set. If the lock
+   * is still hold by another Thread the method returns false.
+   */
+  boolean tryReadLock()
+  {
+    // As synchronizing simple types may cause deadlocks,
+    // we use a gate object.
+    synchronized(readLockGate)
+    {
+      if(readLock != 0)
+      {
+        return false;
+      }
+      else
+      {
+        readLock = Thread.currentThread().hashCode();
+        return true;
+      }
+    }
+  }
+  
+  /**
+   * Releases the read lock in a Thread-safe way.
+   * @throws IllegalMonitorStateException if a Thread not holding the lock
+   * tries to release it.
+   */
+  void unlockReadLock()
+  {
+    synchronized(readLockGate)
+    {
+      if(readLock == Thread.currentThread().hashCode())
+      {
+        readLock = 0;
+      }
+      else
+      {
+        throw new IllegalMonitorStateException();
+      }
+    }
+  }
+  
+  /**
+   * @return Current input buffer of this NNTPConnection instance.
+   */
+  public ByteBuffer getInputBuffer()
+  {
+    return this.lineBuffers.getInputBuffer();
+  }
+  
+  /**
+   * @return Output buffer of this NNTPConnection which has at least one byte
+   * free storage.
+   */
+  public ByteBuffer getOutputBuffer()
+  {
+    return this.lineBuffers.getOutputBuffer();
+  }
+  
+  /**
+   * @return ChannelLineBuffers instance associated with this NNTPConnection.
+   */
+  public ChannelLineBuffers getBuffers()
+  {
+    return this.lineBuffers;
+  }
+  
+  /**
+   * @return true if this connection comes from a local remote address.
+   */
+  public boolean isLocalConnection()
+  {
+    return ((InetSocketAddress)this.channel.socket().getRemoteSocketAddress())
+      .getHostName().equalsIgnoreCase("localhost");
+  }
+
+  void setWriteSelectionKey(SelectionKey selKey)
+  {
+    this.writeSelKey = selKey;
+  }
+
+  public void shutdownInput()
+  {
+    try
+    {
+      // Closes the input line of the channel's socket, so no new data
+      // will be received and a timeout can be triggered.
+      this.channel.socket().shutdownInput();
+    }
+    catch(IOException ex)
+    {
+      Log.get().warning("Exception in NNTPConnection.shutdownInput(): " + ex);
+    }
+  }
+  
+  public void shutdownOutput()
+  {
+    cancelTimer.schedule(new TimerTask() 
+    {
+      @Override
+      public void run()
+      {
+        try
+        {
+          // Closes the output line of the channel's socket.
+          channel.socket().shutdownOutput();
+          channel.close();
+        }
+        catch(SocketException ex)
+        {
+          // Socket was already disconnected
+          Log.get().info("NNTPConnection.shutdownOutput(): " + ex);
+        }
+        catch(Exception ex)
+        {
+          Log.get().warning("NNTPConnection.shutdownOutput(): " + ex);
+        }
+      }
+    }, 3000);
+  }
+  
+  public SocketChannel getSocketChannel()
+  {
+    return this.channel;
+  }
+  
+  public Article getCurrentArticle()
+  {
+    return this.currentArticle;
+  }
+  
+  public Charset getCurrentCharset()
+  {
+    return this.charset;
+  }
+
+  /**
+   * @return The currently selected communication channel (not SocketChannel)
+   */
+  public Channel getCurrentChannel()
+  {
+    return this.currentGroup;
+  }
+  
+  public void setCurrentArticle(final Article article)
+  {
+    this.currentArticle = article;
+  }
+  
+  public void setCurrentGroup(final Channel group)
+  {
+    this.currentGroup = group;
+  }
+  
+  public long getLastActivity()
+  {
+    return this.lastActivity;
+  }
+  
+  /**
+   * Due to the readLockGate there is no need to synchronize this method.
+   * @param raw
+   * @throws IllegalArgumentException if raw is null.
+   * @throws IllegalStateException if calling thread does not own the readLock.
+   */
+  void lineReceived(byte[] raw)
+  {
+    if(raw == null)
+    {
+      throw new IllegalArgumentException("raw is null");
+    }
+    
+    if(readLock == 0 || readLock != Thread.currentThread().hashCode())
+    {
+      throw new IllegalStateException("readLock not properly set");
+    }
+
+    this.lastActivity = System.currentTimeMillis();
+    
+    String line = new String(raw, this.charset);
+    
+    // There might be a trailing \r, but trim() is a bad idea
+    // as it removes also leading spaces from long header lines.
+    if(line.endsWith("\r"))
+    {
+      line = line.substring(0, line.length() - 1);
+      raw  = Arrays.copyOf(raw, raw.length - 1);
+    }
+    
+    Log.get().fine("<< " + line);
+    
+    if(command == null)
+    {
+      command = parseCommandLine(line);
+      assert command != null;
+    }
+
+    try
+    {
+      // The command object will process the line we just received
+      try
+      {
+        command.processLine(this, line, raw);
+      }
+      catch(StorageBackendException ex)
+      {
+        Log.get().info("Retry command processing after StorageBackendException");
+
+        // Try it a second time, so that the backend has time to recover
+        command.processLine(this, line, raw);
+      }
+    }
+    catch(ClosedChannelException ex0)
+    {
+      try
+      {
+        Log.get().info("Connection to " + channel.socket().getRemoteSocketAddress()
+            + " closed: " + ex0);
+      }
+      catch(Exception ex0a)
+      {
+        ex0a.printStackTrace();
+      }
+    }
+    catch(Exception ex1) // This will catch a second StorageBackendException
+    {
+      try
+      {
+        command = null;
+        ex1.printStackTrace();
+        println("500 Internal server error");
+      }
+      catch(Exception ex2)
+      {
+        ex2.printStackTrace();
+      }
+    }
+
+    if(command == null || command.hasFinished())
+    {
+      command = null;
+      charset = Charset.forName("UTF-8"); // Reset to default
+    }
+  }
+  
+  /**
+   * This method determines the fitting command processing class.
+   * @param line
+   * @return
+   */
+  private Command parseCommandLine(String line)
+  {
+    String cmdStr = line.split(" ")[0];
+    return CommandSelector.getInstance().get(cmdStr);
+  }
+  
+  /**
+   * Puts the given line into the output buffer, adds a newline character
+   * and returns. The method returns immediately and does not block until
+   * the line was sent. If line is longer than 510 octets it is split up in
+   * several lines. Each line is terminated by \r\n (NNTPConnection.NEWLINE).
+   * @param line
+   */
+  public void println(final CharSequence line, final Charset charset)
+    throws IOException
+  {    
+    writeToChannel(CharBuffer.wrap(line), charset, line);
+    writeToChannel(CharBuffer.wrap(NEWLINE), charset, null);
+  }
+
+  /**
+   * Writes the given raw lines to the output buffers and finishes with
+   * a newline character (\r\n).
+   * @param rawLines
+   */
+  public void println(final byte[] rawLines)
+    throws IOException
+  {
+    this.lineBuffers.addOutputBuffer(ByteBuffer.wrap(rawLines));
+    writeToChannel(CharBuffer.wrap(NEWLINE), charset, null);
+  }
+  
+  /**
+   * Encodes the given CharBuffer using the given Charset to a bunch of
+   * ByteBuffers (each 512 bytes large) and enqueues them for writing at the
+   * connected SocketChannel.
+   * @throws java.io.IOException
+   */
+  private void writeToChannel(CharBuffer characters, final Charset charset,
+    CharSequence debugLine)
+    throws IOException
+  {
+    if(!charset.canEncode())
+    {
+      Log.get().severe("FATAL: Charset " + charset + " cannot encode!");
+      return;
+    }
+    
+    // Write characters to output buffers
+    LineEncoder lenc = new LineEncoder(characters, charset);
+    lenc.encode(lineBuffers);
+    
+    enableWriteEvents(debugLine);
+  }
+
+  private void enableWriteEvents(CharSequence debugLine)
+  {
+    // Enable OP_WRITE events so that the buffers are processed
+    try
+    {
+      this.writeSelKey.interestOps(SelectionKey.OP_WRITE);
+      ChannelWriter.getInstance().getSelector().wakeup();
+    }
+    catch(Exception ex) // CancelledKeyException and ChannelCloseException
+    {
+      Log.get().warning("NNTPConnection.writeToChannel(): " + ex);
+      return;
+    }
+
+    // Update last activity timestamp
+    this.lastActivity = System.currentTimeMillis();
+    if(debugLine != null)
+    {
+      Log.get().fine(">> " + debugLine);
+    }
+  }
+  
+  public void println(final CharSequence line)
+    throws IOException
+  {
+    println(line, charset);
+  }
+  
+  public void print(final String line)
+    throws IOException
+  {
+    writeToChannel(CharBuffer.wrap(line), charset, line);
+  }
+  
+  public void setCurrentCharset(final Charset charset)
+  {
+    this.charset = charset;
+  }
+
+  void setLastActivity(long timestamp)
+  {
+    this.lastActivity = timestamp;
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/NNTPDaemon.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/NNTPDaemon.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,197 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon;
+
+import org.sonews.config.Config;
+import org.sonews.Main;
+import org.sonews.util.Log;
+import java.io.IOException;
+import java.net.BindException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.nio.channels.CancelledKeyException;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+
+/**
+ * NNTP daemon using SelectableChannels.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public final class NNTPDaemon extends AbstractDaemon
+{
+
+  public static final Object RegisterGate = new Object();
+  
+  private static NNTPDaemon instance = null;
+  
+  public static synchronized NNTPDaemon createInstance(int port)
+  {
+    if(instance == null)
+    {
+      instance = new NNTPDaemon(port);
+      return instance;
+    }
+    else
+    {
+      throw new RuntimeException("NNTPDaemon.createInstance() called twice");
+    }
+  }
+  
+  private int port;
+  
+  private NNTPDaemon(final int port)
+  {
+    Log.get().info("Server listening on port " + port);
+    this.port = port;
+  }
+
+  @Override
+  public void run()
+  {
+    try
+    {
+      // Create a Selector that handles the SocketChannel multiplexing
+      final Selector readSelector  = Selector.open();
+      final Selector writeSelector = Selector.open();
+      
+      // Start working threads
+      final int workerThreads = Runtime.getRuntime().availableProcessors() * 4;
+      ConnectionWorker[] cworkers = new ConnectionWorker[workerThreads];
+      for(int n = 0; n < workerThreads; n++)
+      {
+        cworkers[n] = new ConnectionWorker();
+        cworkers[n].start();
+      }
+      
+      ChannelWriter.getInstance().setSelector(writeSelector);
+      ChannelReader.getInstance().setSelector(readSelector);
+      ChannelWriter.getInstance().start();
+      ChannelReader.getInstance().start();
+      
+      final ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
+      serverSocketChannel.configureBlocking(true);  // Set to blocking mode
+      
+      // Configure ServerSocket; bind to socket...
+      final ServerSocket serverSocket = serverSocketChannel.socket();
+      serverSocket.bind(new InetSocketAddress(this.port));
+      
+      while(isRunning())
+      {
+        SocketChannel socketChannel;
+        
+        try
+        {
+          // As we set the server socket channel to blocking mode the accept()
+          // method will block.
+          socketChannel = serverSocketChannel.accept();
+          socketChannel.configureBlocking(false);
+          assert socketChannel.isConnected();
+          assert socketChannel.finishConnect();
+        }
+        catch(IOException ex)
+        {
+          // Under heavy load an IOException "Too many open files may
+          // be thrown. It most cases we should slow down the connection
+          // accepting, to give the worker threads some time to process work.
+          Log.get().severe("IOException while accepting connection: " + ex.getMessage());
+          Log.get().info("Connection accepting sleeping for seconds...");
+          Thread.sleep(5000); // 5 seconds
+          continue;
+        }
+        
+        final NNTPConnection conn;
+        try
+        {
+          conn = new NNTPConnection(socketChannel);
+          Connections.getInstance().add(conn);
+        }
+        catch(IOException ex)
+        {
+          Log.get().warning(ex.toString());
+          socketChannel.close();
+          continue;
+        }
+        
+        try
+        {
+          SelectionKey selKeyWrite =
+            registerSelector(writeSelector, socketChannel, SelectionKey.OP_WRITE);
+          registerSelector(readSelector, socketChannel, SelectionKey.OP_READ);
+          
+          Log.get().info("Connected: " + socketChannel.socket().getRemoteSocketAddress());
+
+          // Set write selection key and send hello to client
+          conn.setWriteSelectionKey(selKeyWrite);
+          conn.println("200 " + Config.inst().get(Config.HOSTNAME, "localhost")
+              + " " + Main.VERSION + " news server ready - (posting ok).");
+        }
+        catch(CancelledKeyException cke)
+        {
+          Log.get().warning("CancelledKeyException " + cke.getMessage() + " was thrown: "
+            + socketChannel.socket());
+        }
+        catch(ClosedChannelException cce)
+        {
+          Log.get().warning("ClosedChannelException " + cce.getMessage() + " was thrown: "
+            + socketChannel.socket());
+        }
+      }
+    }
+    catch(BindException ex)
+    {
+      // Could not bind to socket; this is a fatal problem; so perform shutdown
+      ex.printStackTrace();
+      System.exit(1);
+    }
+    catch(IOException ex)
+    {
+      ex.printStackTrace();
+    }
+    catch(Exception ex)
+    {
+      ex.printStackTrace();
+    }
+  }
+  
+  public static SelectionKey registerSelector(final Selector selector,
+    final SocketChannel channel, final int op)
+    throws CancelledKeyException, ClosedChannelException
+  {
+    // Register the selector at the channel, so that it will be notified
+    // on the socket's events
+    synchronized(RegisterGate)
+    {
+      // Wakeup the currently blocking reader/writer thread; we have locked
+      // the RegisterGate to prevent the awakened thread to block again
+      selector.wakeup();
+      
+      // Lock the selector to prevent the waiting worker threads going into
+      // selector.select() which would block the selector.
+      synchronized (selector)
+      {
+        return channel.register(selector, op, null);
+      }
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/ArticleCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/ArticleCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,174 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import org.sonews.storage.Article;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.Channel;
+import org.sonews.storage.StorageBackendException;
+
+/**
+ * Class handling the ARTICLE, BODY and HEAD commands.
+ * @author Christian Lins
+ * @author Dennis Schwerdel
+ * @since n3tpd/0.1
+ */
+public class ArticleCommand implements Command
+{
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[] {"ARTICLE", "BODY", "HEAD"};
+  }
+
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return false;
+  }
+
+  // TODO: Refactor this method to reduce its complexity!
+  @Override
+  public void processLine(NNTPConnection conn, final String line, byte[] raw)
+    throws IOException
+  {
+    final String[] command = line.split(" ");
+    
+    Article article  = null;
+    long    artIndex = -1;
+    if (command.length == 1)
+    {
+      article = conn.getCurrentArticle();
+      if (article == null)
+      {
+        conn.println("420 no current article has been selected");
+        return;
+      }
+    }
+    else if (command[1].matches(NNTPConnection.MESSAGE_ID_PATTERN))
+    {
+      // Message-ID
+      article = Article.getByMessageID(command[1]);
+      if (article == null)
+      {
+        conn.println("430 no such article found");
+        return;
+      }
+    }
+    else
+    {
+      // Message Number
+      try
+      {
+        Channel currentGroup = conn.getCurrentChannel();
+        if(currentGroup == null)
+        {
+          conn.println("400 no group selected");
+          return;
+        }
+        
+        artIndex = Long.parseLong(command[1]);
+        article  = currentGroup.getArticle(artIndex);
+      }
+      catch(NumberFormatException ex)
+      {
+        ex.printStackTrace();
+      }
+      catch(StorageBackendException ex)
+      {
+        ex.printStackTrace();
+      }
+
+      if (article == null)
+      {
+        conn.println("423 no such article number in this group");
+        return;
+      }
+      conn.setCurrentArticle(article);
+    }
+
+    if(command[0].equalsIgnoreCase("ARTICLE"))
+    {
+      conn.println("220 " + artIndex + " " + article.getMessageID()
+          + " article retrieved - head and body follow");
+      conn.println(article.getHeaderSource());
+      conn.println("");
+      conn.println(article.getBody());
+      conn.println(".");
+    }
+    else if(command[0].equalsIgnoreCase("BODY"))
+    {
+      conn.println("222 " + artIndex + " " + article.getMessageID() + " body");
+      conn.println(article.getBody());
+      conn.println(".");
+    }
+    
+    /*
+     * HEAD: This command is mandatory.
+     *
+     * Syntax
+     *    HEAD message-id
+     *    HEAD number
+     *    HEAD
+     *
+     * Responses
+     *
+     * First form (message-id specified)
+     *  221 0|n message-id    Headers follow (multi-line)
+     *  430                   No article with that message-id
+     *
+     * Second form (article number specified)
+     *  221 n message-id      Headers follow (multi-line)
+     *  412                   No newsgroup selected
+     *  423                   No article with that number
+     *
+     * Third form (current article number used)
+     *  221 n message-id      Headers follow (multi-line)
+     *  412                   No newsgroup selected
+     *  420                   Current article number is invalid
+     *
+     * Parameters
+     *  number        Requested article number
+     *  n             Returned article number
+     *  message-id    Article message-id
+     */
+    else if(command[0].equalsIgnoreCase("HEAD"))
+    {
+      conn.println("221 " + artIndex + " " + article.getMessageID()
+          + " Headers follow (multi-line)");
+      conn.println(article.getHeaderSource());
+      conn.println(".");
+    }
+  }  
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/CapabilitiesCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/CapabilitiesCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,93 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import org.sonews.daemon.NNTPConnection;
+
+/**
+ * <pre>
+ *  The CAPABILITIES command allows a client to determine the
+ *  capabilities of the server at any given time.
+ *
+ *  This command MAY be issued at any time; the server MUST NOT require
+ *  it to be issued in order to make use of any capability. The response
+ *  generated by this command MAY change during a session because of
+ *  other state information (which, in turn, may be changed by the
+ *  effects of other commands or by external events).  An NNTP client is
+ *  only able to get the current and correct information concerning
+ *  available capabilities at any point during a session by issuing a
+ *  CAPABILITIES command at that point of that session and processing the
+ *  response.
+ * </pre>
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class CapabilitiesCommand implements Command
+{
+
+  static final String[] CAPABILITIES = new String[]
+    {
+      "VERSION 2", // MUST be the first one; VERSION 2 refers to RFC3977
+      "READER",    // Server implements commands for reading
+      "POST",      // Server implements POST command
+      "OVER"       // Server implements OVER command
+    };
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[] {"CAPABILITIES"};
+  }
+
+  /**
+   * First called after one call to processLine().
+   * @return
+   */
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+  
+  @Override
+  public boolean isStateful()
+  {
+    return false;
+  }
+
+  @Override
+  public void processLine(NNTPConnection conn, final String line, byte[] raw)
+    throws IOException
+  {
+    conn.println("101 Capabilities list:");
+    for(String cap : CAPABILITIES)
+    {
+      conn.println(cap);
+    }
+    conn.println(".");
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/Command.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/Command.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,51 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.StorageBackendException;
+
+/**
+ * Interface for pluggable NNTP commands handling classes.
+ * @author Christian Lins
+ * @since sonews/0.6.0
+ */
+public interface Command
+{
+
+  /**
+   * @return true if this instance can be reused.
+   */
+  boolean hasFinished();
+
+  /**
+   * Returns capability string that is implied by this command class.
+   * MAY return null if the command is required by the NNTP standard.
+   */
+  String impliedCapability();
+
+  boolean isStateful();
+
+  String[] getSupportedCommandStrings();
+
+  void processLine(NNTPConnection conn, String line, byte[] rawLine)
+    throws IOException, StorageBackendException;
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/GroupCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/GroupCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,102 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.Channel;
+import org.sonews.storage.StorageBackendException;
+
+/**
+ * Class handling the GROUP command.
+ * <pre>
+ *  Syntax
+ *    GROUP group
+ *
+ *  Responses
+ *    211 number low high group     Group successfully selected
+ *    411                           No such newsgroup
+ *
+ *  Parameters
+ *    group     Name of newsgroup
+ *    number    Estimated number of articles in the group
+ *    low       Reported low water mark
+ *    high      Reported high water mark
+ * </pre>
+ * (from RFC 3977)
+ * 
+ * @author Christian Lins
+ * @author Dennis Schwerdel
+ * @since n3tpd/0.1
+ */
+public class GroupCommand implements Command
+{
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[]{"GROUP"};
+  }
+
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return true;
+  }
+  
+  @Override
+  public void processLine(NNTPConnection conn, final String line, byte[] raw)
+    throws IOException, StorageBackendException
+  {
+    final String[] command = line.split(" ");
+
+    Channel group;
+    if(command.length >= 2)
+    {
+      group = Channel.getByName(command[1]);
+      if(group == null || group.isDeleted())
+      {
+        conn.println("411 no such news group");
+      }
+      else
+      {
+        conn.setCurrentGroup(group);
+        conn.println("211 " + group.getPostingsCount() + " " + group.getFirstArticleNumber()
+          + " " + group.getLastArticleNumber() + " " + group.getName() + " group selected");
+      }
+    }
+    else
+    {
+      conn.println("500 no group name given");
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/HelpCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/HelpCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,100 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import java.util.Set;
+import org.sonews.daemon.CommandSelector;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.util.io.Resource;
+
+/**
+ * This command provides a short summary of the commands that are
+ * understood by this implementation of the server. The help text will
+ * be presented as a multi-line data block following the 100 response
+ * code (taken from RFC).
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class HelpCommand implements Command
+{
+
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return true;
+  }
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[]{"HELP"};
+  }
+  
+  @Override
+  public void processLine(NNTPConnection conn, final String line, byte[] raw)
+    throws IOException
+  {
+    final String[] command = line.split(" ");
+    conn.println("100 help text follows");
+
+    if(line.length() <= 1)
+    {
+      final String[] help = Resource
+        .getAsString("helpers/helptext", true).split("\n");
+      for(String hstr : help)
+      {
+        conn.println(hstr);
+      }
+
+      Set<String> commandNames = CommandSelector.getCommandNames();
+      for(String cmdName : commandNames)
+      {
+        conn.println(cmdName);
+      }
+    }
+    else
+    {
+      Command cmd = CommandSelector.getInstance().get(command[1]);
+      if(cmd instanceof HelpfulCommand)
+      {
+        conn.println(((HelpfulCommand)cmd).getHelpString());
+      }
+      else
+      {
+        conn.println("No further help information available.");
+      }
+    }
+    
+    conn.println(".");
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/HelpfulCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/HelpfulCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,35 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+/**
+ *
+ * @since sonews/1.1
+ * @author Christian Lins
+ */
+public interface HelpfulCommand extends Command
+{
+
+  /**
+   * @return A short description of this command, that is
+   * used within the output of the HELP command.
+   */
+  String getHelpString();
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/ListCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/ListCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,153 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.Channel;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.util.Log;
+
+/**
+ * Class handling the LIST command.
+ * @author Christian Lins
+ * @author Dennis Schwerdel
+ * @since n3tpd/0.1
+ */
+public class ListCommand implements Command
+{
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[]{"LIST"};
+  }
+
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return false;
+  }
+  
+  @Override
+  public void processLine(NNTPConnection conn, final String line, byte[] raw)
+    throws IOException, StorageBackendException
+  {
+    final String[] command = line.split(" ");
+    
+    if(command.length >= 2)
+    {
+      if(command[1].equalsIgnoreCase("OVERVIEW.FMT"))
+      {
+        conn.println("215 information follows");
+        conn.println("Subject:\nFrom:\nDate:\nMessage-ID:\nReferences:\nBytes:\nLines:\nXref");
+        conn.println(".");
+      }
+      else if(command[1].equalsIgnoreCase("NEWSGROUPS"))
+      {
+        conn.println("215 information follows");
+        final List<Channel> list = Channel.getAll();
+        for (Channel g : list)
+        {
+          conn.println(g.getName() + "\t" + "-");
+        }
+        conn.println(".");
+      }
+      else if(command[1].equalsIgnoreCase("SUBSCRIPTIONS"))
+      {
+        conn.println("215 information follows");
+        conn.println(".");
+      }
+      else if(command[1].equalsIgnoreCase("EXTENSIONS"))
+      {
+        conn.println("202 Supported NNTP extensions.");
+        conn.println("LISTGROUP");
+        conn.println("XDAEMON");
+        conn.println("XPAT");
+        conn.println(".");
+      }
+      else if(command[1].equalsIgnoreCase("ACTIVE"))
+      {
+        String  pattern  = command.length == 2
+          ? null : command[2].replace("*", "\\w*");
+        printGroupInfo(conn, pattern);
+      }
+      else
+      {
+        conn.println("500 unknown argument to LIST command");
+      }
+    }
+    else
+    {
+      printGroupInfo(conn, null);
+    }
+  }
+
+  private void printGroupInfo(NNTPConnection conn, String pattern)
+    throws IOException, StorageBackendException
+  {
+    final List<Channel> groups = Channel.getAll();
+    if(groups != null)
+    {
+      conn.println("215 list of newsgroups follows");
+      for(Channel g : groups)
+      {
+        try
+        {
+          Matcher matcher = pattern == null ?
+            null : Pattern.compile(pattern).matcher(g.getName());
+          if(!g.isDeleted() &&
+            (matcher == null || matcher.find()))
+          {
+            String writeable = g.isWriteable() ? " y" : " n";
+            // Indeed first the higher article number then the lower
+            conn.println(g.getName() + " " + g.getLastArticleNumber() + " "
+              + g.getFirstArticleNumber() + writeable);
+          }
+        }
+        catch(PatternSyntaxException ex)
+        {
+          Log.get().info(ex.toString());
+        }
+      }
+      conn.println(".");
+    }
+    else
+    {
+      conn.println("500 server backend malfunction");
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/ListGroupCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/ListGroupCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,94 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import java.util.List;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.Channel;
+import org.sonews.storage.StorageBackendException;
+
+/**
+ * Class handling the LISTGROUP command.
+ * @author Christian Lins
+ * @author Dennis Schwerdel
+ * @since n3tpd/0.1
+ */
+public class ListGroupCommand implements Command
+{
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[]{"LISTGROUP"};
+  }
+
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return false;
+  }
+
+  @Override
+  public void processLine(NNTPConnection conn, final String commandName, byte[] raw)
+    throws IOException, StorageBackendException
+  {
+    final String[] command = commandName.split(" ");
+
+    Channel group;
+    if(command.length >= 2)
+    {
+      group = Channel.getByName(command[1]);
+    }
+    else
+    {
+      group = conn.getCurrentChannel();
+    }
+
+    if (group == null)
+    {
+      conn.println("412 no group selected; use GROUP <group> command");
+      return;
+    }
+
+    List<Long> ids = group.getArticleNumbers();
+    conn.println("211 " + ids.size() + " " +
+      group.getFirstArticleNumber() + " " + 
+      group.getLastArticleNumber() + " list of article numbers follow");
+    for(long id : ids)
+    {
+      // One index number per line
+      conn.println(Long.toString(id));
+    }
+    conn.println(".");
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/ModeReaderCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/ModeReaderCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,72 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.StorageBackendException;
+
+/**
+ * Class handling the MODE READER command. This command actually does nothing
+ * but returning a success status code.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class ModeReaderCommand implements Command
+{
+  
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[]{"MODE"};
+  }
+
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return false;
+  }
+
+  @Override
+  public void processLine(NNTPConnection conn, final String line, byte[] raw)
+    throws IOException, StorageBackendException
+  {
+    if(line.equalsIgnoreCase("MODE READER"))
+    {
+      conn.println("200 hello you can post");
+    }
+    else
+    {
+      conn.println("500 I do not know this mode command");
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/NewGroupsCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/NewGroupsCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,78 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.StorageBackendException;
+
+/**
+ * Class handling the NEWGROUPS command.
+ * @author Christian Lins
+ * @author Dennis Schwerdel
+ * @since n3tpd/0.1
+ */
+public class NewGroupsCommand implements Command
+{
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[]{"NEWGROUPS"};
+  }
+
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return false;
+  }
+
+  @Override
+  public void processLine(NNTPConnection conn, final String line, byte[] raw)
+    throws IOException, StorageBackendException
+  {
+    final String[] command = line.split(" ");
+
+    if(command.length == 3)
+    {
+      conn.println("231 list of new newsgroups follows");
+
+      // Currently we do not store a group's creation date;
+      // so we return an empty list which is a valid response
+      conn.println(".");
+    }
+    else
+    {
+      conn.println("500 invalid command usage");
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/NextPrevCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/NextPrevCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,116 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.Article;
+import org.sonews.storage.Channel;
+import org.sonews.storage.StorageBackendException;
+
+/**
+ * Class handling the NEXT and LAST command.
+ * @author Christian Lins
+ * @author Dennis Schwerdel
+ * @since n3tpd/0.1
+ */
+public class NextPrevCommand implements Command
+{
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[]{"NEXT", "PREV"};
+  }
+
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return false;
+  }
+
+  @Override
+  public void processLine(NNTPConnection conn, final String line, byte[] raw)
+    throws IOException, StorageBackendException
+  {
+    final Article currA = conn.getCurrentArticle();
+    final Channel currG = conn.getCurrentChannel();
+    
+    if (currA == null)
+    {
+      conn.println("420 no current article has been selected");
+      return;
+    }
+    
+    if (currG == null)
+    {
+      conn.println("412 no newsgroup selected");
+      return;
+    }
+    
+    final String[] command = line.split(" ");
+
+    if(command[0].equalsIgnoreCase("NEXT"))
+    {
+      selectNewArticle(conn, currA, currG, 1);
+    }
+    else if(command[0].equalsIgnoreCase("PREV"))
+    {
+      selectNewArticle(conn, currA, currG, -1);
+    }
+    else
+    {
+      conn.println("500 internal server error");
+    }
+  }
+  
+  private void selectNewArticle(NNTPConnection conn, Article article, Channel grp,
+    final int delta)
+    throws IOException, StorageBackendException
+  {
+    assert article != null;
+
+    article = grp.getArticle(grp.getIndexOf(article) + delta);
+
+    if(article == null)
+    {
+      conn.println("421 no next article in this group");
+    }
+    else
+    {
+      conn.setCurrentArticle(article);
+      conn.println("223 " + conn.getCurrentChannel().getIndexOf(article)
+                    + " " + article.getMessageID()
+                    + " article retrieved - request text separately");
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/OverCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/OverCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,294 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import java.util.List;
+import org.sonews.util.Log;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.Article;
+import org.sonews.storage.ArticleHead;
+import org.sonews.storage.Headers;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.util.Pair;
+
+/**
+ * Class handling the OVER/XOVER command.
+ * 
+ * Description of the XOVER command:
+ * <pre>
+ * XOVER [range]
+ *
+ * The XOVER command returns information from the overview
+ * database for the article(s) specified.
+ *
+ * The optional range argument may be any of the following:
+ *              an article number
+ *              an article number followed by a dash to indicate
+ *                 all following
+ *              an article number followed by a dash followed by
+ *                 another article number
+ *
+ * If no argument is specified, then information from the
+ * current article is displayed. Successful responses start
+ * with a 224 response followed by the overview information
+ * for all matched messages. Once the output is complete, a
+ * period is sent on a line by itself. If no argument is
+ * specified, the information for the current article is
+ * returned.  A news group must have been selected earlier,
+ * else a 412 error response is returned. If no articles are
+ * in the range specified, a 420 error response is returned
+ * by the server. A 502 response will be returned if the
+ * client only has permission to transfer articles.
+ *
+ * Each line of output will be formatted with the article number,
+ * followed by each of the headers in the overview database or the
+ * article itself (when the data is not available in the overview
+ * database) for that article separated by a tab character.  The
+ * sequence of fields must be in this order: subject, author,
+ * date, message-id, references, byte count, and line count. Other
+ * optional fields may follow line count. Other optional fields may
+ * follow line count. These fields are specified by examining the
+ * response to the LIST OVERVIEW.FMT command. Where no data exists,
+ * a null field must be provided (i.e. the output will have two tab
+ * characters adjacent to each other). Servers should not output
+ * fields for articles that have been removed since the XOVER database
+ * was created.
+ *
+ * The LIST OVERVIEW.FMT command should be implemented if XOVER
+ * is implemented. A client can use LIST OVERVIEW.FMT to determine
+ * what optional fields  and in which order all fields will be
+ * supplied by the XOVER command. 
+ *
+ * Note that any tab and end-of-line characters in any header
+ * data that is returned will be converted to a space character.
+ *
+ * Responses:
+ *
+ *   224 Overview information follows
+ *   412 No news group current selected
+ *   420 No article(s) selected
+ *   502 no permission
+ *
+ * OVER defines additional responses:
+ *
+ *  First form (message-id specified)
+ *    224    Overview information follows (multi-line)
+ *    430    No article with that message-id
+ *
+ *  Second form (range specified)
+ *    224    Overview information follows (multi-line)
+ *    412    No newsgroup selected
+ *    423    No articles in that range
+ *
+ *  Third form (current article number used)
+ *    224    Overview information follows (multi-line)
+ *    412    No newsgroup selected
+ *    420    Current article number is invalid
+ *
+ * </pre>
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class OverCommand implements Command
+{
+
+  public static final int MAX_LINES_PER_DBREQUEST = 200;
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[]{"OVER", "XOVER"};
+  }
+
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return false;
+  }
+
+  @Override
+  public void processLine(NNTPConnection conn, final String line, byte[] raw)
+    throws IOException, StorageBackendException
+  {
+    if(conn.getCurrentChannel() == null)
+    {
+      conn.println("412 no newsgroup selected");
+    }
+    else
+    {
+      String[] command = line.split(" ");
+
+      // If no parameter was specified, show information about
+      // the currently selected article(s)
+      if(command.length == 1)
+      {
+        final Article art = conn.getCurrentArticle();
+        if(art == null)
+        {
+          conn.println("420 no article(s) selected");
+          return;
+        }
+
+        conn.println(buildOverview(art, -1));
+      }
+      // otherwise print information about the specified range
+      else
+      {
+        long artStart;
+        long artEnd   = conn.getCurrentChannel().getLastArticleNumber();
+        String[] nums = command[1].split("-");
+        if(nums.length >= 1)
+        {
+          try
+          {
+            artStart = Integer.parseInt(nums[0]);
+          }
+          catch(NumberFormatException e) 
+          {
+            Log.get().info(e.getMessage());
+            artStart = Integer.parseInt(command[1]);
+          }
+        }
+        else
+        {
+          artStart = conn.getCurrentChannel().getFirstArticleNumber();
+        }
+
+        if(nums.length >=2)
+        {
+          try
+          {
+            artEnd = Integer.parseInt(nums[1]);
+          }
+          catch(NumberFormatException e) 
+          {
+            e.printStackTrace();
+          }
+        }
+
+        if(artStart > artEnd)
+        {
+          if(command[0].equalsIgnoreCase("OVER"))
+          {
+            conn.println("423 no articles in that range");
+          }
+          else
+          {
+            conn.println("224 (empty) overview information follows:");
+            conn.println(".");
+          }
+        }
+        else
+        {
+          for(long n = artStart; n <= artEnd; n += MAX_LINES_PER_DBREQUEST)
+          {
+            long nEnd = Math.min(n + MAX_LINES_PER_DBREQUEST - 1, artEnd);
+            List<Pair<Long, ArticleHead>> articleHeads = conn.getCurrentChannel()
+              .getArticleHeads(n, nEnd);
+            if(articleHeads.isEmpty() && n == artStart
+              && command[0].equalsIgnoreCase("OVER"))
+            {
+              // This reply is only valid for OVER, not for XOVER command
+              conn.println("423 no articles in that range");
+              return;
+            }
+            else if(n == artStart)
+            {
+              // XOVER replies this although there is no data available
+              conn.println("224 overview information follows");
+            }
+
+            for(Pair<Long, ArticleHead> article : articleHeads)
+            {
+              String overview = buildOverview(article.getB(), article.getA());
+              conn.println(overview);
+            }
+          } // for
+          conn.println(".");
+        }
+      }
+    }
+  }
+  
+  private String buildOverview(ArticleHead art, long nr)
+  {
+    StringBuilder overview = new StringBuilder();
+    overview.append(nr);
+    overview.append('\t');
+
+    String subject = art.getHeader(Headers.SUBJECT)[0];
+    if("".equals(subject))
+    {
+      subject = "<empty>";
+    }
+    overview.append(escapeString(subject));
+    overview.append('\t');
+
+    overview.append(escapeString(art.getHeader(Headers.FROM)[0]));
+    overview.append('\t');
+    overview.append(escapeString(art.getHeader(Headers.DATE)[0]));
+    overview.append('\t');
+    overview.append(escapeString(art.getHeader(Headers.MESSAGE_ID)[0]));
+    overview.append('\t');
+    overview.append(escapeString(art.getHeader(Headers.REFERENCES)[0]));
+    overview.append('\t');
+
+    String bytes = art.getHeader(Headers.BYTES)[0];
+    if("".equals(bytes))
+    {
+      bytes = "0";
+    }
+    overview.append(escapeString(bytes));
+    overview.append('\t');
+
+    String lines = art.getHeader(Headers.LINES)[0];
+    if("".equals(lines))
+    {
+      lines = "0";
+    }
+    overview.append(escapeString(lines));
+    overview.append('\t');
+    overview.append(escapeString(art.getHeader(Headers.XREF)[0]));
+
+    // Remove trailing tabs if some data is empty
+    return overview.toString().trim();
+  }
+  
+  private String escapeString(String str)
+  {
+    String nstr = str.replace("\r", "");
+    nstr = nstr.replace('\n', ' ');
+    nstr = nstr.replace('\t', ' ');
+    return nstr.trim();
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/PostCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/PostCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,332 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.sql.SQLException;
+import java.util.Arrays;
+import javax.mail.MessagingException;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetHeaders;
+import org.sonews.config.Config;
+import org.sonews.util.Log;
+import org.sonews.mlgw.Dispatcher;
+import org.sonews.storage.Article;
+import org.sonews.storage.Group;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.Headers;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.storage.StorageManager;
+import org.sonews.feed.FeedManager;
+import org.sonews.util.Stats;
+
+/**
+ * Implementation of the POST command. This command requires multiple lines
+ * from the client, so the handling of asynchronous reading is a little tricky
+ * to handle.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class PostCommand implements Command
+{
+  
+  private final Article article   = new Article();
+  private int           lineCount = 0;
+  private long          bodySize  = 0;
+  private InternetHeaders headers = null;
+  private long          maxBodySize  = 
+    Config.inst().get(Config.ARTICLE_MAXSIZE, 128) * 1024L; // Size in bytes
+  private PostState     state     = PostState.WaitForLineOne;
+  private final ByteArrayOutputStream bufBody   = new ByteArrayOutputStream();
+  private final StringBuilder         strHead   = new StringBuilder();
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[]{"POST"};
+  }
+
+  @Override
+  public boolean hasFinished()
+  {
+    return this.state == PostState.Finished;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return true;
+  }
+
+  /**
+   * Process the given line String. line.trim() was called by NNTPConnection.
+   * @param line
+   * @throws java.io.IOException
+   * @throws java.sql.SQLException
+   */
+  @Override // TODO: Refactor this method to reduce complexity!
+  public void processLine(NNTPConnection conn, String line, byte[] raw)
+    throws IOException, StorageBackendException
+  {
+    switch(state)
+    {
+      case WaitForLineOne:
+      {
+        if(line.equalsIgnoreCase("POST"))
+        {
+          conn.println("340 send article to be posted. End with <CR-LF>.<CR-LF>");
+          state = PostState.ReadingHeaders;
+        }
+        else
+        {
+          conn.println("500 invalid command usage");
+        }
+        break;
+      }
+      case ReadingHeaders:
+      {
+        strHead.append(line);
+        strHead.append(NNTPConnection.NEWLINE);
+        
+        if("".equals(line) || ".".equals(line))
+        {
+          // we finally met the blank line
+          // separating headers from body
+          
+          try
+          {
+            // Parse the header using the InternetHeader class from JavaMail API
+            headers = new InternetHeaders(
+              new ByteArrayInputStream(strHead.toString().trim()
+                .getBytes(conn.getCurrentCharset())));
+
+            // add the header entries for the article
+            article.setHeaders(headers);
+          }
+          catch (MessagingException e)
+          {
+            e.printStackTrace();
+            conn.println("500 posting failed - invalid header");
+            state = PostState.Finished;
+            break;
+          }
+
+          // Change charset for reading body; 
+          // for multipart messages UTF-8 is returned
+          //conn.setCurrentCharset(article.getBodyCharset());
+          
+          state = PostState.ReadingBody;
+          
+          if(".".equals(line))
+          {
+            // Post an article without body
+            postArticle(conn, article);
+            state = PostState.Finished;
+          }
+        }
+        break;
+      }
+      case ReadingBody:
+      {
+        if(".".equals(line))
+        {    
+          // Set some headers needed for Over command
+          headers.setHeader(Headers.LINES, Integer.toString(lineCount));
+          headers.setHeader(Headers.BYTES, Long.toString(bodySize));
+
+          byte[] body = bufBody.toByteArray();
+          if(body.length >= 2)
+          {
+            // Remove trailing CRLF
+            body = Arrays.copyOf(body, body.length - 2);
+          }
+          article.setBody(body); // set the article body
+          
+          postArticle(conn, article);
+          state = PostState.Finished;
+        }
+        else
+        {
+          bodySize += line.length() + 1;
+          lineCount++;
+          
+          // Add line to body buffer
+          bufBody.write(raw, 0, raw.length);
+          bufBody.write(NNTPConnection.NEWLINE.getBytes());
+          
+          if(bodySize > maxBodySize)
+          {
+            conn.println("500 article is too long");
+            state = PostState.Finished;
+            break;
+          }
+        }
+        break;
+      }
+      default:
+      {
+        // Should never happen
+        Log.get().severe("PostCommand::processLine(): already finished...");
+      }
+    }
+  }
+  
+  /**
+   * Article is a control message and needs special handling.
+   * @param article
+   */
+  private void controlMessage(NNTPConnection conn, Article article)
+    throws IOException
+  {
+    String[] ctrl = article.getHeader(Headers.CONTROL)[0].split(" ");
+    if(ctrl.length == 2) // "cancel <mid>"
+    {
+      try
+      {
+        StorageManager.current().delete(ctrl[1]);
+        
+        // Move cancel message to "control" group
+        article.setHeader(Headers.NEWSGROUPS, "control");
+        StorageManager.current().addArticle(article);
+        conn.println("240 article cancelled");
+      }
+      catch(StorageBackendException ex)
+      {
+        Log.get().severe(ex.toString());
+        conn.println("500 internal server error");
+      }
+    }
+    else
+    {
+      conn.println("441 unknown control header");
+    }
+  }
+  
+  private void supersedeMessage(NNTPConnection conn, Article article)
+    throws IOException
+  {
+    try
+    {
+      String oldMsg = article.getHeader(Headers.SUPERSEDES)[0];
+      StorageManager.current().delete(oldMsg);
+      StorageManager.current().addArticle(article);
+      conn.println("240 article replaced");
+    }
+    catch(StorageBackendException ex)
+    {
+      Log.get().severe(ex.toString());
+      conn.println("500 internal server error");
+    }
+  }
+  
+  private void postArticle(NNTPConnection conn, Article article)
+    throws IOException
+  {
+    if(article.getHeader(Headers.CONTROL)[0].length() > 0)
+    {
+      controlMessage(conn, article);
+    }
+    else if(article.getHeader(Headers.SUPERSEDES)[0].length() > 0)
+    {
+      supersedeMessage(conn, article);
+    }
+    else // Post the article regularily
+    {
+      // Circle check; note that Path can already contain the hostname here
+      String host = Config.inst().get(Config.HOSTNAME, "localhost");
+      if(article.getHeader(Headers.PATH)[0].indexOf(host + "!", 1) > 0)
+      {
+        Log.get().info(article.getMessageID() + " skipped for host " + host);
+        conn.println("441 I know this article already");
+        return;
+      }
+
+      // Try to create the article in the database or post it to
+      // appropriate mailing list
+      try
+      {
+        boolean success = false;
+        String[] groupnames = article.getHeader(Headers.NEWSGROUPS)[0].split(",");
+        for(String groupname : groupnames)
+        {          
+          Group group = StorageManager.current().getGroup(groupname);
+          if(group != null && !group.isDeleted())
+          {
+            if(group.isMailingList() && !conn.isLocalConnection())
+            {
+              // Send to mailing list; the Dispatcher writes 
+              // statistics to database
+              Dispatcher.toList(article, group.getName());
+              success = true;
+            }
+            else
+            {
+              // Store in database
+              if(!StorageManager.current().isArticleExisting(article.getMessageID()))
+              {
+                StorageManager.current().addArticle(article);
+
+                // Log this posting to statistics
+                Stats.getInstance().mailPosted(
+                  article.getHeader(Headers.NEWSGROUPS)[0]);
+              }
+              success = true;
+            }
+          }
+        } // end for
+
+        if(success)
+        {
+          conn.println("240 article posted ok");
+          FeedManager.queueForPush(article);
+        }
+        else
+        {
+          conn.println("441 newsgroup not found");
+        }
+      }
+      catch(AddressException ex)
+      {
+        Log.get().warning(ex.getMessage());
+        conn.println("441 invalid sender address");
+      }
+      catch(MessagingException ex)
+      {
+        // A MessageException is thrown when the sender email address is
+        // invalid or something is wrong with the SMTP server.
+        System.err.println(ex.getLocalizedMessage());
+        conn.println("441 " + ex.getClass().getCanonicalName() + ": " + ex.getLocalizedMessage());
+      }
+      catch(StorageBackendException ex)
+      {
+        ex.printStackTrace();
+        conn.println("500 internal server error");
+      }
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/PostState.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/PostState.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,29 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+/**
+ * States of the POST command's finite state machine.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+enum PostState
+{
+  WaitForLineOne, ReadingHeaders, ReadingBody, Finished
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/QuitCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/QuitCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,67 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.StorageBackendException;
+
+/**
+ * Implementation of the QUIT command; client wants to shutdown the connection.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class QuitCommand implements Command
+{
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[]{"QUIT"};
+  }
+  
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return false;
+  }
+
+  @Override
+  public void processLine(NNTPConnection conn, final String line, byte[] raw)
+    throws IOException, StorageBackendException
+  {    
+    conn.println("205 cya");
+    
+    conn.shutdownInput();
+    conn.shutdownOutput();
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/StatCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/StatCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,114 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import org.sonews.storage.Article;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.StorageBackendException;
+
+/**
+ * Implementation of the STAT command.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class StatCommand implements Command
+{
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[]{"STAT"};
+  }
+
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return false;
+  }
+
+  // TODO: Method has various exit points => Refactor!
+  @Override
+  public void processLine(NNTPConnection conn, final String line, byte[] raw)
+    throws IOException, StorageBackendException
+  {
+    final String[] command = line.split(" ");
+
+    Article article = null;
+    if(command.length == 1)
+    {
+      article = conn.getCurrentArticle();
+      if(article == null)
+      {
+        conn.println("420 no current article has been selected");
+        return;
+      }
+    }
+    else if(command[1].matches(NNTPConnection.MESSAGE_ID_PATTERN))
+    {
+      // Message-ID
+      article = Article.getByMessageID(command[1]);
+      if (article == null)
+      {
+        conn.println("430 no such article found");
+        return;
+      }
+    }
+    else
+    {
+      // Message Number
+      try
+      {
+        long aid = Long.parseLong(command[1]);
+        article = conn.getCurrentChannel().getArticle(aid);
+      }
+      catch(NumberFormatException ex)
+      {
+        ex.printStackTrace();
+      }
+      catch(StorageBackendException ex)
+      {
+        ex.printStackTrace();
+      }
+      if (article == null)
+      {
+        conn.println("423 no such article number in this group");
+        return;
+      }
+      conn.setCurrentArticle(article);
+    }
+    
+    conn.println("223 " + conn.getCurrentChannel().getIndexOf(article) + " "
+      + article.getMessageID()
+      + " article retrieved - request text separately");
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/UnsupportedCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/UnsupportedCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,67 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import org.sonews.daemon.NNTPConnection;
+
+/**
+ * A default "Unsupported Command". Simply returns error code 500 and a
+ * "command not supported" message.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class UnsupportedCommand implements Command
+{
+  
+  /**
+   * @return Always returns null.
+   */
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return false;
+  }
+
+  @Override
+  public void processLine(NNTPConnection conn, final String line, byte[] raw)
+    throws IOException
+  {
+    conn.println("500 command not supported");
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/XDaemonCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/XDaemonCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,270 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.List;
+import org.sonews.config.Config;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.storage.StorageManager;
+import org.sonews.feed.FeedManager;
+import org.sonews.feed.Subscription;
+import org.sonews.storage.Channel;
+import org.sonews.storage.Group;
+import org.sonews.util.Stats;
+
+/**
+ * The XDAEMON command allows a client to get/set properties of the
+ * running server daemon. Only locally connected clients are allowed to
+ * use this command.
+ * The restriction to localhost connection can be suppressed by overriding
+ * the sonews.xdaemon.host bootstrap config property.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class XDaemonCommand implements Command
+{
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[]{"XDAEMON"};
+  }
+
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return false;
+  }
+
+  private void channelAdd(String[] commands, NNTPConnection conn)
+    throws IOException, StorageBackendException
+  {
+    String groupName = commands[2];
+    if(StorageManager.current().isGroupExisting(groupName))
+    {
+      conn.println("400 group " + groupName + " already existing!");
+    }
+    else
+    {
+      StorageManager.current().addGroup(groupName, Integer.parseInt(commands[3]));
+      conn.println("200 group " + groupName + " created");
+    }
+  }
+
+  // TODO: Refactor this method to reduce complexity!
+  @Override
+  public void processLine(NNTPConnection conn, String line, byte[] raw)
+    throws IOException, StorageBackendException
+  {
+    InetSocketAddress addr = (InetSocketAddress)conn.getSocketChannel().socket()
+      .getRemoteSocketAddress();
+    if(addr.getHostName().equals(
+      Config.inst().get(Config.XDAEMON_HOST, "localhost")))
+    {
+      String[] commands = line.split(" ", 4);
+      if(commands.length == 3 && commands[1].equalsIgnoreCase("LIST"))
+      {
+        if(commands[2].equalsIgnoreCase("CONFIGKEYS"))
+        {
+          conn.println("100 list of available config keys follows");
+          for(String key : Config.AVAILABLE_KEYS)
+          {
+            conn.println(key);
+          }
+          conn.println(".");
+        }
+        else if(commands[2].equalsIgnoreCase("PEERINGRULES"))
+        {
+          List<Subscription> pull = 
+            StorageManager.current().getSubscriptions(FeedManager.TYPE_PULL);
+          List<Subscription> push =
+            StorageManager.current().getSubscriptions(FeedManager.TYPE_PUSH);
+          conn.println("100 list of peering rules follows");
+          for(Subscription sub : pull)
+          {
+            conn.println("PULL " + sub.getHost() + ":" + sub.getPort()
+              + " " + sub.getGroup());
+          }
+          for(Subscription sub : push)
+          {
+            conn.println("PUSH " + sub.getHost() + ":" + sub.getPort()
+              + " " + sub.getGroup());
+          }
+          conn.println(".");
+        }
+        else
+        {
+          conn.println("401 unknown sub command");
+        }
+      }
+      else if(commands.length == 3 && commands[1].equalsIgnoreCase("DELETE"))
+      {
+        StorageManager.current().delete(commands[2]);
+        conn.println("200 article " + commands[2] + " deleted");
+      }
+      else if(commands.length == 4 && commands[1].equalsIgnoreCase("GROUPADD"))
+      {
+        channelAdd(commands, conn);
+      }
+      else if(commands.length == 3 && commands[1].equalsIgnoreCase("GROUPDEL"))
+      {
+        Group group = StorageManager.current().getGroup(commands[2]);
+        if(group == null)
+        {
+          conn.println("400 group not found");
+        }
+        else
+        {
+          group.setFlag(Group.DELETED);
+          group.update();
+          conn.println("200 group " + commands[2] + " marked as deleted");
+        }
+      }
+      else if(commands.length == 4 && commands[1].equalsIgnoreCase("SET"))
+      {
+        String key = commands[2];
+        String val = commands[3];
+        Config.inst().set(key, val);
+        conn.println("200 new config value set");
+      }
+      else if(commands.length == 3 && commands[1].equalsIgnoreCase("GET"))
+      {
+        String key = commands[2];
+        String val = Config.inst().get(key, null);
+        if(val != null)
+        {
+          conn.println("100 config value for " + key + " follows");
+          conn.println(val);
+          conn.println(".");
+        }
+        else
+        {
+          conn.println("400 config value not set");
+        }
+      }
+      else if(commands.length >= 3 && commands[1].equalsIgnoreCase("LOG"))
+      {
+        Group group = null;
+        if(commands.length > 3)
+        {
+          group = (Group)Channel.getByName(commands[3]);
+        }
+
+        if(commands[2].equalsIgnoreCase("CONNECTED_CLIENTS"))
+        {
+          conn.println("100 number of connections follow");
+          conn.println(Integer.toString(Stats.getInstance().connectedClients()));
+          conn.println(".");
+        }
+        else if(commands[2].equalsIgnoreCase("POSTED_NEWS"))
+        {
+          conn.println("100 hourly numbers of posted news yesterday");
+          for(int n = 0; n < 24; n++)
+          {
+            conn.println(n + " " + Stats.getInstance()
+              .getYesterdaysEvents(Stats.POSTED_NEWS, n, group));
+          }
+          conn.println(".");
+        }
+        else if(commands[2].equalsIgnoreCase("GATEWAYED_NEWS"))
+        {
+          conn.println("100 hourly numbers of gatewayed news yesterday");
+          for(int n = 0; n < 24; n++)
+          {
+            conn.println(n + " " + Stats.getInstance()
+              .getYesterdaysEvents(Stats.GATEWAYED_NEWS, n, group));
+          }
+          conn.println(".");
+        }
+        else if(commands[2].equalsIgnoreCase("TRANSMITTED_NEWS"))
+        {
+          conn.println("100 hourly numbers of news transmitted to peers yesterday");
+          for(int n = 0; n < 24; n++)
+          {
+            conn.println(n + " " + Stats.getInstance()
+              .getYesterdaysEvents(Stats.FEEDED_NEWS, n, group));
+          }
+          conn.println(".");
+        }
+        else if(commands[2].equalsIgnoreCase("HOSTED_NEWS"))
+        {
+          conn.println("100 number of overall hosted news");
+          conn.println(Integer.toString(Stats.getInstance().getNumberOfNews()));
+          conn.println(".");
+        }
+        else if(commands[2].equalsIgnoreCase("HOSTED_GROUPS"))
+        {
+          conn.println("100 number of hosted groups");
+          conn.println(Integer.toString(Stats.getInstance().getNumberOfGroups()));
+          conn.println(".");
+        }
+        else if(commands[2].equalsIgnoreCase("POSTED_NEWS_PER_HOUR"))
+        {
+          conn.println("100 posted news per hour");
+          conn.println(Double.toString(Stats.getInstance().postedPerHour(-1)));
+          conn.println(".");
+        }
+        else if(commands[2].equalsIgnoreCase("FEEDED_NEWS_PER_HOUR"))
+        {
+          conn.println("100 feeded news per hour");
+          conn.println(Double.toString(Stats.getInstance().feededPerHour(-1)));
+          conn.println(".");
+        }
+        else if(commands[2].equalsIgnoreCase("GATEWAYED_NEWS_PER_HOUR"))
+        {
+          conn.println("100 gatewayed news per hour");
+          conn.println(Double.toString(Stats.getInstance().gatewayedPerHour(-1)));
+          conn.println(".");
+        }
+        else
+        {
+          conn.println("401 unknown sub command");
+        }
+      }
+      else if(commands.length >= 3 && commands[1].equalsIgnoreCase("PLUGIN"))
+      {
+        
+      }
+      else
+      {
+        conn.println("400 invalid command usage");
+      }
+    }
+    else
+    {
+      conn.println("501 not allowed");
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/XPatCommand.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/XPatCommand.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,170 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.daemon.command;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.PatternSyntaxException;
+import org.sonews.daemon.NNTPConnection;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.storage.StorageManager;
+import org.sonews.util.Pair;
+
+/**
+ * <pre>
+ *   XPAT header range|<message-id> pat [pat...]
+ *
+ *   The XPAT command is used to retrieve specific headers from
+ *   specific articles, based on pattern matching on the contents of
+ *   the header. This command was first available in INN.
+ *
+ *   The required header parameter is the name of a header line (e.g.
+ *   "subject") in a news group article. See RFC-1036 for a list
+ *   of valid header lines. The required range argument may be
+ *   any of the following:
+ *               an article number
+ *               an article number followed by a dash to indicate
+ *                  all following
+ *               an article number followed by a dash followed by
+ *                  another article number
+ *
+ *   The required message-id argument indicates a specific
+ *   article. The range and message-id arguments are mutually
+ *   exclusive. At least one pattern in wildmat must be specified
+ *   as well. If there are additional arguments the are joined
+ *   together separated by a single space to form one complete
+ *   pattern. Successful responses start with a 221 response
+ *   followed by a the headers from all messages in which the
+ *   pattern matched the contents of the specified header line. This
+ *   includes an empty list. Once the output is complete, a period
+ *   is sent on a line by itself. If the optional argument is a
+ *   message-id and no such article exists, the 430 error response
+ *   is returned. A 502 response will be returned if the client only
+ *   has permission to transfer articles.
+ *
+ *   Responses
+ *
+ *       221 Header follows
+ *       430 no such article
+ *       502 no permission
+ *
+ *   Response Data:
+ *
+ *       art_nr fitting_header_value
+ * 
+ * </pre>
+ * [Source:"draft-ietf-nntp-imp-02.txt"] [Copyright: 1998 S. Barber]
+ * 
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class XPatCommand implements Command
+{
+
+  @Override
+  public String[] getSupportedCommandStrings()
+  {
+    return new String[]{"XPAT"};
+  }
+  
+  @Override
+  public boolean hasFinished()
+  {
+    return true;
+  }
+
+  @Override
+  public String impliedCapability()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isStateful()
+  {
+    return false;
+  }
+
+  @Override
+  public void processLine(NNTPConnection conn, final String line, byte[] raw)
+    throws IOException, StorageBackendException
+  {
+    if(conn.getCurrentChannel() == null)
+    {
+      conn.println("430 no group selected");
+      return;
+    }
+
+    String[] command = line.split("\\p{Space}+");
+
+    // There may be multiple patterns and Thunderbird produces
+    // additional spaces between range and pattern
+    if(command.length >= 4)
+    {
+      String header  = command[1].toLowerCase(Locale.US);
+      String range   = command[2];
+      String pattern = command[3];
+
+      long start = -1;
+      long end   = -1;
+      if(range.contains("-"))
+      {
+        String[] rsplit = range.split("-", 2);
+        start = Long.parseLong(rsplit[0]);
+        if(rsplit[1].length() > 0)
+        {
+          end = Long.parseLong(rsplit[1]);
+        }
+      }
+      else // TODO: Handle Message-IDs
+      {
+        start = Long.parseLong(range);
+      }
+
+      try
+      {
+        List<Pair<Long, String>> heads = StorageManager.current().
+          getArticleHeaders(conn.getCurrentChannel(), start, end, header, pattern);
+        
+        conn.println("221 header follows");
+        for(Pair<Long, String> head : heads)
+        {
+          conn.println(head.getA() + " " + head.getB());
+        }
+        conn.println(".");
+      }
+      catch(PatternSyntaxException ex)
+      {
+        ex.printStackTrace();
+        conn.println("500 invalid pattern syntax");
+      }
+      catch(StorageBackendException ex)
+      {
+        ex.printStackTrace();
+        conn.println("500 internal server error");
+      }
+    }
+    else
+    {
+      conn.println("430 invalid command usage");
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/package.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/command/package.html	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,1 @@
+Contains a class for every NNTP command.
\ No newline at end of file
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/package.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/daemon/package.html	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,1 @@
+Contains basic classes of the daemon.
\ No newline at end of file
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/feed/FeedManager.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/feed/FeedManager.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,54 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.feed;
+
+import org.sonews.storage.Article;
+
+/**
+ * Controlls push and pull feeder.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public final class FeedManager 
+{
+
+  public static final int TYPE_PULL = 0;
+  public static final int TYPE_PUSH = 1;
+  
+  private static PullFeeder pullFeeder = new PullFeeder();
+  private static PushFeeder pushFeeder = new PushFeeder();
+  
+  /**
+   * Reads the peer subscriptions from database and starts the appropriate
+   * PullFeeder or PushFeeder.
+   */
+  public static synchronized void startFeeding()
+  {
+    pullFeeder.start();
+    pushFeeder.start();
+  }
+  
+  public static void queueForPush(Article article)
+  {
+    pushFeeder.queueForPush(article);
+  }
+  
+  private FeedManager() {}
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/feed/PullFeeder.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/feed/PullFeeder.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,276 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.feed;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import org.sonews.config.Config;
+import org.sonews.daemon.AbstractDaemon;
+import org.sonews.util.Log;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.storage.StorageManager;
+import org.sonews.util.Stats;
+import org.sonews.util.io.ArticleReader;
+import org.sonews.util.io.ArticleWriter;
+
+/**
+ * The PullFeeder class regularily checks another Newsserver for new
+ * messages.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+class PullFeeder extends AbstractDaemon
+{
+  
+  private Map<Subscription, Integer> highMarks = new HashMap<Subscription, Integer>();
+  private BufferedReader             in;
+  private PrintWriter                out;
+  private Set<Subscription>          subscriptions = new HashSet<Subscription>();
+  
+  private void addSubscription(final Subscription sub)
+  {
+    subscriptions.add(sub);
+
+    if(!highMarks.containsKey(sub))
+    {
+      // Set a initial highMark
+      this.highMarks.put(sub, 0);
+    }
+  }
+  
+  /**
+   * Changes to the given group and returns its high mark.
+   * @param groupName
+   * @return
+   */
+  private int changeGroup(String groupName)
+    throws IOException
+  {
+    this.out.print("GROUP " + groupName + "\r\n");
+    this.out.flush();
+    
+    String line = this.in.readLine();
+    if(line.startsWith("211 "))
+    {
+      int highmark = Integer.parseInt(line.split(" ")[3]);
+      return highmark;
+    }
+    else
+    {
+      throw new IOException("GROUP " + groupName + " returned: " + line);
+    }
+  }
+  
+  private void connectTo(final String host, final int port)
+    throws IOException, UnknownHostException
+  {
+    Socket socket = new Socket(host, port);
+    this.out = new PrintWriter(socket.getOutputStream());
+    this.in  = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+
+    String line = in.readLine();
+    if(!(line.charAt(0) == '2')) // Could be 200 or 2xx if posting is not allowed
+    {
+      throw new IOException(line);
+    }
+
+    // Send MODE READER to peer, some newsservers are friendlier then
+    this.out.println("MODE READER\r\n");
+    this.out.flush();
+    line = this.in.readLine();
+  }
+  
+  private void disconnect()
+    throws IOException
+  {
+    this.out.print("QUIT\r\n");
+    this.out.flush();
+    this.out.close();
+    this.in.close();
+    
+    this.out = null;
+    this.in  = null;
+  }
+  
+  /**
+   * Uses the OVER or XOVER command to get a list of message overviews that
+   * may be unknown to this feeder and are about to be peered.
+   * @param start
+   * @param end
+   * @return A list of message ids with potentially interesting messages.
+   */
+  private List<String> over(int start, int end)
+    throws IOException
+  {
+    this.out.print("OVER " + start + "-" + end + "\r\n");
+    this.out.flush();
+    
+    String line = this.in.readLine();
+    if(line.startsWith("500 ")) // OVER not supported
+    {
+      this.out.print("XOVER " + start + "-" + end + "\r\n");
+      this.out.flush();
+      
+      line = this.in.readLine();
+    }
+    
+    if(line.startsWith("224 "))
+    {
+      List<String> messages = new ArrayList<String>();
+      line = this.in.readLine();
+      while(!".".equals(line))
+      {
+        String mid = line.split("\t")[4]; // 5th should be the Message-ID
+        messages.add(mid);
+        line = this.in.readLine();
+      }
+      return messages;
+    }
+    else
+    {
+      throw new IOException("Server return for OVER/XOVER: " + line);
+    }
+  }
+  
+  @Override
+  public void run()
+  {
+    while(isRunning())
+    {
+      int pullInterval = 1000 * 
+        Config.inst().get(Config.FEED_PULLINTERVAL, 3600);
+      String host = "localhost";
+      int    port = 119;
+      
+      Log.get().info("Start PullFeeder run...");
+
+      try
+      {
+        this.subscriptions.clear();
+        List<Subscription> subsPull = StorageManager.current()
+          .getSubscriptions(FeedManager.TYPE_PULL);
+        for(Subscription sub : subsPull)
+        {
+          addSubscription(sub);
+        }
+      }
+      catch(StorageBackendException ex)
+      {
+        Log.get().log(Level.SEVERE, host, ex);
+      }
+
+      try
+      {
+        for(Subscription sub : this.subscriptions)
+        {
+          host = sub.getHost();
+          port = sub.getPort();
+
+          try
+          {
+            Log.get().info("Feeding " + sub.getGroup() + " from " + sub.getHost());
+            try
+            {
+              connectTo(host, port);
+            }
+            catch(SocketException ex)
+            {
+              Log.get().info("Skipping " + sub.getHost() + ": " + ex);
+              continue;
+            }
+            
+            int oldMark = this.highMarks.get(sub);
+            int newMark = changeGroup(sub.getGroup());
+            
+            if(oldMark != newMark)
+            {
+              List<String> messageIDs = over(oldMark, newMark);
+
+              for(String messageID : messageIDs)
+              {
+                if(!StorageManager.current().isArticleExisting(messageID))
+                {
+                  try
+                  {
+                    // Post the message via common socket connection
+                    ArticleReader aread =
+                      new ArticleReader(sub.getHost(), sub.getPort(), messageID);
+                    byte[] abuf = aread.getArticleData();
+                    if(abuf == null)
+                    {
+                      Log.get().warning("Could not feed " + messageID
+                        + " from " + sub.getHost());
+                    }
+                    else
+                    {
+                      Log.get().info("Feeding " + messageID);
+                      ArticleWriter awrite = new ArticleWriter(
+                        "localhost", Config.inst().get(Config.PORT, 119));
+                      awrite.writeArticle(abuf);
+                      awrite.close();
+                    }
+                    Stats.getInstance().mailFeeded(sub.getGroup());
+                  }
+                  catch(IOException ex)
+                  {
+                    // There may be a temporary network failure
+                    ex.printStackTrace();
+                    Log.get().warning("Skipping mail " + messageID + " due to exception.");
+                  }
+                }
+              } // for(;;)
+              this.highMarks.put(sub, newMark);
+            }
+            
+            disconnect();
+          }
+          catch(StorageBackendException ex)
+          {
+            ex.printStackTrace();
+          }
+          catch(IOException ex)
+          {
+            ex.printStackTrace();
+            Log.get().severe("PullFeeder run stopped due to exception.");
+          }
+        } // for(Subscription sub : subscriptions)
+        
+        Log.get().info("PullFeeder run ended. Waiting " + pullInterval / 1000 + "s");
+        Thread.sleep(pullInterval);
+      }
+      catch(InterruptedException ex)
+      {
+        Log.get().warning(ex.getMessage());
+      }
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/feed/PushFeeder.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/feed/PushFeeder.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,118 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.feed;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import org.sonews.daemon.AbstractDaemon;
+import org.sonews.storage.Article;
+import org.sonews.storage.Headers;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.storage.StorageManager;
+import org.sonews.util.Log;
+import org.sonews.util.io.ArticleWriter;
+
+/**
+ * Pushes new articles to remote newsservers. This feeder sleeps until a new
+ * message is posted to the sonews instance.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+class PushFeeder extends AbstractDaemon
+{
+  
+  private ConcurrentLinkedQueue<Article> articleQueue = 
+    new ConcurrentLinkedQueue<Article>();
+  
+  @Override
+  public void run()
+  {
+    while(isRunning())
+    {
+      try
+      {
+        synchronized(this)
+        {
+          this.wait();
+        }
+        
+        List<Subscription> subscriptions = StorageManager.current()
+          .getSubscriptions(FeedManager.TYPE_PUSH);
+
+        Article  article = this.articleQueue.poll();
+        String[] groups  = article.getHeader(Headers.NEWSGROUPS)[0].split(",");
+        Log.get().info("PushFeed: " + article.getMessageID());
+        for(Subscription sub : subscriptions)
+        {
+          // Circle check
+          if(article.getHeader(Headers.PATH)[0].contains(sub.getHost()))
+          {
+            Log.get().info(article.getMessageID() + " skipped for host "
+              + sub.getHost());
+            continue;
+          }
+
+          try
+          {
+            for(String group : groups)
+            {
+              if(sub.getGroup().equals(group))
+              {
+                // Delete headers that may cause problems
+                article.removeHeader(Headers.NNTP_POSTING_DATE);
+                article.removeHeader(Headers.NNTP_POSTING_HOST);
+                article.removeHeader(Headers.X_COMPLAINTS_TO);
+                article.removeHeader(Headers.X_TRACE);
+                article.removeHeader(Headers.XREF);
+                
+                // POST the message to remote server
+                ArticleWriter awriter = new ArticleWriter(sub.getHost(), sub.getPort());
+                awriter.writeArticle(article);
+                break;
+              }
+            }
+          }
+          catch(IOException ex)
+          {
+            Log.get().warning(ex.toString());
+          }
+        }
+      }
+      catch(StorageBackendException ex)
+      {
+        Log.get().severe(ex.toString());
+      }
+      catch(InterruptedException ex)
+      {
+        Log.get().warning("PushFeeder interrupted: " + ex);
+      }
+    }
+  }
+  
+  public void queueForPush(Article article)
+  {
+    this.articleQueue.add(article);
+    synchronized(this)
+    {
+      this.notifyAll();
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/feed/Subscription.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/feed/Subscription.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,84 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.feed;
+
+/**
+ * For every group that is synchronized with or from a remote newsserver 
+ * a Subscription instance exists.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class Subscription 
+{
+
+  private String host;
+  private int    port;
+  private int    feedtype;
+  private String group;
+  
+  public Subscription(String host, int port, int feedtype, String group)
+  {
+    this.host     = host;
+    this.port     = port;
+    this.feedtype = feedtype;
+    this.group    = group;
+  }
+
+  @Override
+  public boolean equals(Object obj)
+  {
+    if(obj instanceof Subscription)
+    {
+      Subscription sub = (Subscription)obj;
+      return sub.host.equals(host) && sub.group.equals(group) 
+        && sub.port == port && sub.feedtype == feedtype;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  @Override
+  public int hashCode()
+  {
+    return host.hashCode() + port + feedtype + group.hashCode();
+  }
+
+  public int getFeedtype()
+  {
+    return feedtype;
+  }
+
+  public String getGroup()
+  {
+    return group;
+  }
+
+  public String getHost()
+  {
+    return host;
+  }
+
+  public int getPort()
+  {
+    return port;
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/feed/package.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/feed/package.html	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,2 @@
+Contains classes for the peering functionality, e.g. pulling and pushing
+mails from and to remote newsservers.
\ No newline at end of file
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/mlgw/Dispatcher.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/mlgw/Dispatcher.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,301 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.mlgw;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.mail.Address;
+import javax.mail.Authenticator;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.PasswordAuthentication;
+import javax.mail.internet.InternetAddress;
+import org.sonews.config.Config;
+import org.sonews.storage.Article;
+import org.sonews.storage.Group;
+import org.sonews.storage.Headers;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.storage.StorageManager;
+import org.sonews.util.Log;
+import org.sonews.util.Stats;
+
+/**
+ * Dispatches messages from mailing list to newsserver or vice versa.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class Dispatcher 
+{
+
+  static class PasswordAuthenticator extends Authenticator
+  {
+    
+    @Override
+    public PasswordAuthentication getPasswordAuthentication()
+    {
+      final String username = 
+        Config.inst().get(Config.MLSEND_USER, "user");
+      final String password = 
+        Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
+
+      return new PasswordAuthentication(username, password);
+    }
+    
+  }
+
+  /**
+   * Chunks out the email address of the full List-Post header field.
+   * @param listPostValue
+   * @return The matching email address or null
+   */
+  private static String chunkListPost(String listPostValue)
+  {
+    // listPostValue is of form "<mailto:dev@openoffice.org>"
+    Pattern mailPattern = Pattern.compile("(\\w+[-|.])*\\w+@(\\w+.)+\\w+");
+    Matcher mailMatcher = mailPattern.matcher(listPostValue);
+    if(mailMatcher.find())
+    {
+      return listPostValue.substring(mailMatcher.start(), mailMatcher.end());
+    }
+    else
+    {
+      return null;
+    }
+  }
+
+  /**
+   * This method inspects the header of the given message, trying
+   * to find the most appropriate recipient.
+   * @param msg
+   * @param fallback If this is false only List-Post and X-List-Post headers
+   *                 are examined.
+   * @return null or fitting group name for the given message.
+   */
+  private static List<String> getGroupFor(final Message msg, final boolean fallback)
+    throws MessagingException, StorageBackendException
+  {
+    List<String> groups = null;
+
+    // Is there a List-Post header?
+    String[]        listPost = msg.getHeader(Headers.LIST_POST);
+    InternetAddress listPostAddr;
+
+    if(listPost == null || listPost.length == 0 || "".equals(listPost[0]))
+    {
+      // Is there a X-List-Post header?
+      listPost = msg.getHeader(Headers.X_LIST_POST);
+    }
+
+    if(listPost != null && listPost.length > 0 
+      && !"".equals(listPost[0]) && chunkListPost(listPost[0]) != null)
+    {
+      // listPost[0] is of form "<mailto:dev@openoffice.org>"
+      listPost[0]  = chunkListPost(listPost[0]);
+      listPostAddr = new InternetAddress(listPost[0], false);
+      groups = StorageManager.current().getGroupsForList(listPostAddr.getAddress());
+    }
+    else if(fallback)
+    {
+      Log.get().info("Using fallback recipient discovery for: " + msg.getSubject());
+      groups = new ArrayList<String>();
+      // Fallback to TO/CC/BCC addresses
+      Address[] to = msg.getAllRecipients();
+      for(Address toa : to) // Address can have '<' '>' around
+      {
+        if(toa instanceof InternetAddress)
+        {
+          List<String> g = StorageManager.current()
+            .getGroupsForList(((InternetAddress)toa).getAddress());
+          groups.addAll(g);
+        }
+      }
+    }
+    
+    return groups;
+  }
+  
+  /**
+   * Posts a message that was received from a mailing list to the 
+   * appropriate newsgroup.
+   * If the message already exists in the storage, this message checks
+   * if it must be posted in an additional group. This can happen for
+   * crosspostings in different mailing lists.
+   * @param msg
+   */
+  public static boolean toGroup(final Message msg)
+  {
+    try
+    {
+      // Create new Article object
+      Article article = new Article(msg);
+      boolean posted  = false;
+
+      // Check if this mail is already existing the storage
+      boolean updateReq = 
+        StorageManager.current().isArticleExisting(article.getMessageID());
+
+      List<String> newsgroups = getGroupFor(msg, !updateReq);
+      List<String> oldgroups  = new ArrayList<String>();
+      if(updateReq)
+      {
+        // Check for duplicate entries of the same group
+        Article oldArticle = StorageManager.current().getArticle(article.getMessageID());
+        List<Group> oldGroups = oldArticle.getGroups();
+        for(Group oldGroup : oldGroups)
+        {
+          if(!newsgroups.contains(oldGroup.getName()))
+          {
+            oldgroups.add(oldGroup.getName());
+          }
+        }
+      }
+
+      if(newsgroups.size() > 0)
+      {
+        newsgroups.addAll(oldgroups);
+        StringBuilder groups = new StringBuilder();
+        for(int n = 0; n < newsgroups.size(); n++)
+        {
+          groups.append(newsgroups.get(n));
+          if (n + 1 != newsgroups.size())
+          {
+            groups.append(',');
+          }
+        }
+        Log.get().info("Posting to group " + groups.toString());
+
+        article.setGroup(groups.toString());
+        //article.removeHeader(Headers.REPLY_TO);
+        //article.removeHeader(Headers.TO);
+
+        // Write article to database
+        if(updateReq)
+        {
+          Log.get().info("Updating " + article.getMessageID()
+            + " with additional groups");
+          StorageManager.current().delete(article.getMessageID());
+          StorageManager.current().addArticle(article);
+        }
+        else
+        {
+          Log.get().info("Gatewaying " + article.getMessageID() + " to "
+            + article.getHeader(Headers.NEWSGROUPS)[0]);
+          StorageManager.current().addArticle(article);
+          Stats.getInstance().mailGatewayed(
+            article.getHeader(Headers.NEWSGROUPS)[0]);
+        }
+        posted = true;
+      }
+      else
+      {
+        StringBuilder buf = new StringBuilder();
+        for (Address toa : msg.getAllRecipients())
+        {
+          buf.append(' ');
+          buf.append(toa.toString());
+        }
+        buf.append(" " + article.getHeader(Headers.LIST_POST)[0]);
+        Log.get().warning("No group for" + buf.toString());
+      }
+      return posted;
+    }
+    catch(Exception ex)
+    {
+      ex.printStackTrace();
+      return false;
+    }
+  }
+  
+  /**
+   * Mails a message received through NNTP to the appropriate mailing list.
+   * This method MAY be called several times by PostCommand for the same
+   * article.
+   */
+  public static void toList(Article article, String group)
+    throws IOException, MessagingException, StorageBackendException
+  {
+    // Get mailing lists for the group of this article
+    List<String> rcptAddresses = StorageManager.current().getListsForGroup(group);
+
+    if(rcptAddresses == null || rcptAddresses.size() == 0)
+    {
+      Log.get().warning("No ML-address for " + group + " found.");
+      return;
+    }
+
+    for(String rcptAddress : rcptAddresses)
+    {
+      // Compose message and send it via given SMTP-Host
+      String smtpHost = Config.inst().get(Config.MLSEND_HOST, "localhost");
+      int smtpPort = Config.inst().get(Config.MLSEND_PORT, 25);
+      String smtpUser = Config.inst().get(Config.MLSEND_USER, "user");
+      String smtpPw = Config.inst().get(Config.MLSEND_PASSWORD, "mysecret");
+      String smtpFrom = Config.inst().get(
+        Config.MLSEND_ADDRESS, article.getHeader(Headers.FROM)[0]);
+
+      // TODO: Make Article cloneable()
+      article.getMessageID(); // Make sure an ID is existing
+      article.removeHeader(Headers.NEWSGROUPS);
+      article.removeHeader(Headers.PATH);
+      article.removeHeader(Headers.LINES);
+      article.removeHeader(Headers.BYTES);
+
+      article.setHeader("To", rcptAddress);
+      //article.setHeader("Reply-To", listAddress);
+
+      if (Config.inst().get(Config.MLSEND_RW_SENDER, false))
+      {
+        rewriteSenderAddress(article); // Set the SENDER address
+      }
+
+      SMTPTransport smtpTransport = new SMTPTransport(smtpHost, smtpPort);
+      smtpTransport.send(article, smtpFrom, rcptAddress);
+      smtpTransport.close();
+
+      Stats.getInstance().mailGatewayed(group);
+      Log.get().info("MLGateway: Mail " + article.getHeader("Subject")[0]
+        + " was delivered to " + rcptAddress + ".");
+    }
+  }
+  
+  /**
+   * Sets the SENDER header of the given MimeMessage. This might be necessary
+   * for moderated groups that does not allow the "normal" FROM sender.
+   * @param msg
+   * @throws javax.mail.MessagingException
+   */
+  private static void rewriteSenderAddress(Article msg)
+    throws MessagingException
+  {
+    String mlAddress = Config.inst().get(Config.MLSEND_ADDRESS, null);
+
+    if(mlAddress != null)
+    {
+      msg.setHeader(Headers.SENDER, mlAddress);
+    }
+    else
+    {
+      throw new MessagingException("Cannot rewrite SENDER header!");
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/mlgw/MailPoller.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/mlgw/MailPoller.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,151 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.mlgw;
+
+import java.util.Properties;
+import javax.mail.AuthenticationFailedException;
+import javax.mail.Authenticator;
+import javax.mail.Flags.Flag;
+import javax.mail.Folder;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.NoSuchProviderException;
+import javax.mail.PasswordAuthentication;
+import javax.mail.Session;
+import javax.mail.Store;
+import org.sonews.config.Config;
+import org.sonews.daemon.AbstractDaemon;
+import org.sonews.util.Log;
+import org.sonews.util.Stats;
+
+/**
+ * Daemon polling for new mails in a POP3 account to be delivered to newsgroups.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class MailPoller extends AbstractDaemon
+{
+
+  static class PasswordAuthenticator extends Authenticator
+  {
+    
+    @Override
+    public PasswordAuthentication getPasswordAuthentication()
+    {
+      final String username = 
+        Config.inst().get(Config.MLPOLL_USER, "user");
+      final String password = 
+        Config.inst().get(Config.MLPOLL_PASSWORD, "mysecret");
+
+      return new PasswordAuthentication(username, password);
+    }
+    
+  }
+  
+  @Override
+  public void run()
+  {
+    Log.get().info("Starting Mailinglist Poller...");
+    int errors = 0;
+    while(isRunning())
+    {
+      try
+      {
+        // Wait some time between runs. At the beginning has advantages,
+        // because the wait is not skipped if an exception occurs.
+        Thread.sleep(60000 * (errors + 1)); // one minute * errors
+        
+        final String host     = 
+          Config.inst().get(Config.MLPOLL_HOST, "samplehost");
+        final String username = 
+          Config.inst().get(Config.MLPOLL_USER, "user");
+        final String password = 
+          Config.inst().get(Config.MLPOLL_PASSWORD, "mysecret");
+        
+        Stats.getInstance().mlgwRunStart();
+        
+        // Create empty properties
+        Properties props = System.getProperties();
+        props.put("mail.pop3.host", host);
+        props.put("mail.mime.address.strict", "false");
+
+        // Get session
+        Session session = Session.getInstance(props);
+
+        // Get the store
+        Store store = session.getStore("pop3");
+        store.connect(host, 110, username, password);
+
+        // Get folder
+        Folder folder = store.getFolder("INBOX");
+        folder.open(Folder.READ_WRITE);
+
+        // Get directory
+        Message[] messages = folder.getMessages();
+
+        // Dispatch messages and delete it afterwards on the inbox
+        for(Message message : messages)
+        {
+          if(Dispatcher.toGroup(message)
+            || Config.inst().get(Config.MLPOLL_DELETEUNKNOWN, false))
+          {
+            // Delete the message
+            message.setFlag(Flag.DELETED, true);
+          }
+        }
+
+        // Close connection 
+        folder.close(true); // true to expunge deleted messages
+        store.close();
+        errors = 0;
+        
+        Stats.getInstance().mlgwRunEnd();
+      }
+      catch(NoSuchProviderException ex)
+      {
+        Log.get().severe(ex.toString());
+        shutdown();
+      }
+      catch(AuthenticationFailedException ex)
+      {
+        // AuthentificationFailedException may be thrown if credentials are
+        // bad or if the Mailbox is in use (locked).
+        ex.printStackTrace();
+        errors = errors < 5 ? errors + 1 : errors;
+      }
+      catch(InterruptedException ex)
+      {
+        System.out.println("sonews: " + this + " returns: " + ex);
+        return;
+      }
+      catch(MessagingException ex)
+      {
+        ex.printStackTrace();
+        errors = errors < 5 ? errors + 1 : errors;
+      }
+      catch(Exception ex)
+      {
+        ex.printStackTrace();
+        errors = errors < 5 ? errors + 1 : errors;
+      }
+    }
+    Log.get().severe("MailPoller exited.");
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/mlgw/SMTPTransport.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/mlgw/SMTPTransport.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,133 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.mlgw;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import org.sonews.config.Config;
+import org.sonews.storage.Article;
+import org.sonews.util.io.ArticleInputStream;
+
+/**
+ * Connects to a SMTP server and sends a given Article to it.
+ * @author Christian Lins
+ * @since sonews/1.0
+ */
+class SMTPTransport
+{
+
+  protected BufferedReader       in;
+  protected BufferedOutputStream out;
+  protected Socket               socket;
+
+  public SMTPTransport(String host, int port)
+    throws IOException, UnknownHostException
+  {
+    socket = new Socket(host, port);
+    this.in  = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+    this.out = new BufferedOutputStream(socket.getOutputStream());
+
+    // Read helo from server
+    String line = this.in.readLine();
+    if(line == null || !line.startsWith("220 "))
+    {
+      throw new IOException("Invalid helo from server: " + line);
+    }
+
+    // Send HELO to server
+    this.out.write(
+      ("HELO " + Config.inst().get(Config.HOSTNAME, "localhost") + "\r\n").getBytes("UTF-8"));
+    this.out.flush();
+    line = this.in.readLine();
+    if(line == null || !line.startsWith("250 "))
+    {
+      throw new IOException("Unexpected reply: " + line);
+    }
+  }
+
+  public SMTPTransport(String host)
+    throws IOException
+  {
+    this(host, 25);
+  }
+
+  public void close()
+    throws IOException
+  {
+    this.out.write("QUIT".getBytes("UTF-8"));
+    this.out.flush();
+    this.in.readLine();
+
+    this.socket.close();
+  }
+
+  public void send(Article article, String mailFrom, String rcptTo)
+    throws IOException
+  {
+    assert(article != null);
+    assert(mailFrom != null);
+    assert(rcptTo != null);
+
+    this.out.write(("MAIL FROM: " + mailFrom).getBytes("UTF-8"));
+    this.out.flush();
+    String line = this.in.readLine();
+    if(line == null || !line.startsWith("250 "))
+    {
+      throw new IOException("Unexpected reply: " + line);
+    }
+
+    this.out.write(("RCPT TO: " + rcptTo).getBytes("UTF-8"));
+    this.out.flush();
+    line  = this.in.readLine();
+    if(line == null || !line.startsWith("250 "))
+    {
+      throw new IOException("Unexpected reply: " + line);
+    }
+
+    this.out.write("DATA".getBytes("UTF-8"));
+    this.out.flush();
+    line = this.in.readLine();
+    if(line == null || !line.startsWith("354 "))
+    {
+      throw new IOException("Unexpected reply: " + line);
+    }
+
+    ArticleInputStream   artStream = new ArticleInputStream(article);
+    for(int b = artStream.read(); b >= 0; b = artStream.read())
+    {
+      this.out.write(b);
+    }
+
+    // Flush the binary stream; important because otherwise the output
+    // will be mixed with the PrintWriter.
+    this.out.flush();
+    this.out.write("\r\n.\r\n".getBytes("UTF-8"));
+    this.out.flush();
+    line = this.in.readLine();
+    if(line == null || !line.startsWith("250 "))
+    {
+      throw new IOException("Unexpected reply: " + line);
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/mlgw/package.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/mlgw/package.html	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,1 @@
+Contains classes of the Mailinglist Gateway.
\ No newline at end of file
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/plugin/Plugin.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/plugin/Plugin.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,42 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.plugin;
+
+/**
+ * A generic Plugin for sonews. Implementing classes do not really add new
+ * functionality to sonews but can use this interface as convenient procedure
+ * for installing functionality plugins, e.g. Command-Plugins or Storage-Plugins.
+ * @author Christian Lins
+ * @since sonews/1.1
+ */
+public interface Plugin
+{
+
+  /**
+   * Called when the Plugin is loaded by sonews. This method can be used
+   * by implementing classes to install additional or required plugins.
+   */
+  void load();
+
+  /**
+   * Called when the Plugin is unloaded by sonews.
+   */
+  void unload();
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/Article.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/storage/Article.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,253 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.storage;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.UUID;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import javax.mail.Header;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.internet.InternetHeaders;
+import org.sonews.config.Config;
+
+/**
+ * Represents a newsgroup article.
+ * @author Christian Lins
+ * @author Denis Schwerdel
+ * @since n3tpd/0.1
+ */
+public class Article extends ArticleHead
+{
+  
+  /**
+   * Loads the Article identified by the given ID from the JDBCDatabase.
+   * @param messageID
+   * @return null if Article is not found or if an error occurred.
+   */
+  public static Article getByMessageID(final String messageID)
+  {
+    try
+    {
+      return StorageManager.current().getArticle(messageID);
+    }
+    catch(StorageBackendException ex)
+    {
+      ex.printStackTrace();
+      return null;
+    }
+  }
+  
+  private byte[] body       = new byte[0];
+  
+  /**
+   * Default constructor.
+   */
+  public Article()
+  {
+  }
+  
+  /**
+   * Creates a new Article object using the date from the given
+   * raw data.
+   */
+  public Article(String headers, byte[] body)
+  {
+    try
+    {
+      this.body  = body;
+
+      // Parse the header
+      this.headers = new InternetHeaders(
+        new ByteArrayInputStream(headers.getBytes()));
+      
+      this.headerSrc = headers;
+    }
+    catch(MessagingException ex)
+    {
+      ex.printStackTrace();
+    }
+  }
+
+  /**
+   * Creates an Article instance using the data from the javax.mail.Message
+   * object. This constructor is called by the Mailinglist gateway.
+   * @see javax.mail.Message
+   * @param msg
+   * @throws IOException
+   * @throws MessagingException
+   */
+  public Article(final Message msg)
+    throws IOException, MessagingException
+  {
+    this.headers = new InternetHeaders();
+
+    for(Enumeration e = msg.getAllHeaders() ; e.hasMoreElements();) 
+    {
+      final Header header = (Header)e.nextElement();
+      this.headers.addHeader(header.getName(), header.getValue());
+    }
+
+	// Reads the raw byte body using Message.writeTo(OutputStream out)
+	this.body = readContent(msg);
+    
+    // Validate headers
+    validateHeaders();
+  }
+
+  /**
+   * Reads from the given Message into a byte array.
+   * @param in
+   * @return
+   * @throws IOException
+   */
+  private byte[] readContent(Message in)
+    throws IOException, MessagingException
+  {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    in.writeTo(out);
+    return out.toByteArray();
+  }
+
+  /**
+   * Removes the header identified by the given key.
+   * @param headerKey
+   */
+  public void removeHeader(final String headerKey)
+  {
+    this.headers.removeHeader(headerKey);
+    this.headerSrc = null;
+  }
+
+  /**
+   * Generates a message id for this article and sets it into
+   * the header object. You have to update the JDBCDatabase manually to make this
+   * change persistent.
+   * Note: a Message-ID should never be changed and only generated once.
+   */
+  private String generateMessageID()
+  {
+    String randomString;
+    MessageDigest md5;
+    try
+    {
+      md5 = MessageDigest.getInstance("MD5");
+      md5.reset();
+      md5.update(getBody());
+      md5.update(getHeader(Headers.SUBJECT)[0].getBytes());
+      md5.update(getHeader(Headers.FROM)[0].getBytes());
+      byte[] result = md5.digest();
+      StringBuffer hexString = new StringBuffer();
+      for (int i = 0; i < result.length; i++)
+      {
+        hexString.append(Integer.toHexString(0xFF & result[i]));
+      }
+      randomString = hexString.toString();
+    }
+    catch (NoSuchAlgorithmException e)
+    {
+      e.printStackTrace();
+      randomString = UUID.randomUUID().toString();
+    }
+    String msgID = "<" + randomString + "@"
+        + Config.inst().get(Config.HOSTNAME, "localhost") + ">";
+    
+    this.headers.setHeader(Headers.MESSAGE_ID, msgID);
+    
+    return msgID;
+  }
+
+  /**
+   * Returns the body string.
+   */
+  public byte[] getBody()
+  {
+    return body;
+  }
+  
+  /**
+   * @return Numerical IDs of the newsgroups this Article belongs to.
+   */
+  public List<Group> getGroups()
+  {
+    String[]         groupnames = getHeader(Headers.NEWSGROUPS)[0].split(",");
+    ArrayList<Group> groups     = new ArrayList<Group>();
+
+    try
+    {
+      for(String newsgroup : groupnames)
+      {
+        newsgroup = newsgroup.trim();
+        Group group = StorageManager.current().getGroup(newsgroup);
+        if(group != null &&         // If the server does not provide the group, ignore it
+          !groups.contains(group))  // Yes, there may be duplicates
+        {
+          groups.add(group);
+        }
+      }
+    }
+    catch(StorageBackendException ex)
+    {
+      ex.printStackTrace();
+      return null;
+    }
+    return groups;
+  }
+
+  public void setBody(byte[] body)
+  {
+    this.body = body;
+  }
+  
+  /**
+   * 
+   * @param groupname Name(s) of newsgroups
+   */
+  public void setGroup(String groupname)
+  {
+    this.headers.setHeader(Headers.NEWSGROUPS, groupname);
+  }
+
+  /**
+   * Returns the Message-ID of this Article. If the appropriate header
+   * is empty, a new Message-ID is created.
+   * @return Message-ID of this Article.
+   */
+  public String getMessageID()
+  {
+    String[] msgID = getHeader(Headers.MESSAGE_ID);
+    return msgID[0].equals("") ? generateMessageID() : msgID[0];
+  }
+  
+  /**
+   * @return String containing the Message-ID.
+   */
+  @Override
+  public String toString()
+  {
+    return getMessageID();
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/ArticleHead.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/storage/ArticleHead.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,161 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.storage;
+
+import java.io.ByteArrayInputStream;
+import java.util.Enumeration;
+import javax.mail.Header;
+import javax.mail.MessagingException;
+import javax.mail.internet.InternetHeaders;
+import javax.mail.internet.MimeUtility;
+import org.sonews.config.Config;
+
+/**
+ * An article with no body only headers.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class ArticleHead 
+{
+
+  protected InternetHeaders headers   = null;
+  protected String          headerSrc = null;
+  
+  protected ArticleHead()
+  {
+  }
+  
+  public ArticleHead(String headers)
+  {
+    try
+    {
+      // Parse the header
+      this.headers = new InternetHeaders(
+          new ByteArrayInputStream(headers.getBytes()));
+    }
+    catch(MessagingException ex)
+    {
+      ex.printStackTrace();
+    }
+  }
+  
+  /**
+   * Returns the header field with given name.
+   * @param name Name of the header field(s).
+   * @param returnNull If set to true, this method will return null instead
+   *                   of an empty array if there is no header field found.
+   * @return Header values or empty string.
+   */
+  public String[] getHeader(String name, boolean returnNull)
+  {
+    String[] ret = this.headers.getHeader(name);
+    if(ret == null && !returnNull)
+    {
+      ret = new String[]{""};
+    }
+    return ret;
+  }
+
+  public String[] getHeader(String name)
+  {
+    return getHeader(name, false);
+  }
+  
+  /**
+   * Sets the header value identified through the header name.
+   * @param name
+   * @param value
+   */
+  public void setHeader(String name, String value)
+  {
+    this.headers.setHeader(name, value);
+    this.headerSrc = null;
+  }
+
+    public Enumeration getAllHeaders()
+  {
+    return this.headers.getAllHeaders();
+  }
+
+  /**
+   * @return Header source code of this Article.
+   */
+  public String getHeaderSource()
+  {
+    if(this.headerSrc != null)
+    {
+      return this.headerSrc;
+    }
+
+    StringBuffer buf = new StringBuffer();
+
+    for(Enumeration en = this.headers.getAllHeaders(); en.hasMoreElements();)
+    {
+      Header entry = (Header)en.nextElement();
+
+      String value = entry.getValue().replaceAll("[\r\n]", " ");
+      buf.append(entry.getName());
+      buf.append(": ");
+      buf.append(MimeUtility.fold(entry.getName().length() + 2, value));
+
+      if(en.hasMoreElements())
+      {
+        buf.append("\r\n");
+      }
+    }
+
+    this.headerSrc = buf.toString();
+    return this.headerSrc;
+  }
+
+  /**
+   * Sets the headers of this Article. If headers contain no
+   * Message-Id a new one is created.
+   * @param headers
+   */
+  public void setHeaders(InternetHeaders headers)
+  {
+    this.headers   = headers;
+    this.headerSrc = null;
+    validateHeaders();
+  }
+
+  /**
+   * Checks some headers for their validity and generates an
+   * appropriate Path-header for this host if not yet existing.
+   * This method is called by some Article constructors and the
+   * method setHeaders().
+   * @return true if something on the headers was changed.
+   */
+  protected void validateHeaders()
+  {
+    // Check for valid Path-header
+    final String path = getHeader(Headers.PATH)[0];
+    final String host = Config.inst().get(Config.HOSTNAME, "localhost");
+    if(!path.startsWith(host))
+    {
+      StringBuffer pathBuf = new StringBuffer();
+      pathBuf.append(host);
+      pathBuf.append('!');
+      pathBuf.append(path);
+      this.headers.setHeader(Headers.PATH, pathBuf.toString());
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/Channel.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/storage/Channel.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,111 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.storage;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.sonews.util.Pair;
+
+/**
+ * A logical communication Channel is the a generic structural element for sets
+ * of messages; e.g. a Newsgroup for a set of Articles.
+ * A Channel can either be a real set of messages or an aggregated set of
+ * several subsets.
+ * @author Christian Lins
+ * @since sonews/1.0
+ */
+public abstract class Channel
+{
+
+  /**
+   * If this flag is set the Group is no real newsgroup but a mailing list
+   * mirror. In that case every posting and receiving mails must go through
+   * the mailing list gateway.
+   */
+  public static final int MAILINGLIST = 0x1;
+
+  /**
+   * If this flag is set the Group is marked as readonly and the posting
+   * is prohibited. This can be useful for groups that are synced only in
+   * one direction.
+   */
+  public static final int READONLY    = 0x2;
+
+  /**
+   * If this flag is set the Group is marked as deleted and must not occur
+   * in any output. The deletion is done lazily by a low priority daemon.
+   */
+  public static final int DELETED     = 0x80;
+
+  public static List<Channel> getAll()
+  {
+    List<Channel> all = new ArrayList<Channel>();
+
+    /*List<Channel> agroups = AggregatedGroup.getAll();
+    if(agroups != null)
+    {
+      all.addAll(agroups);
+    }*/
+
+    List<Channel> groups = Group.getAll();
+    if(groups != null)
+    {
+      all.addAll(groups);
+    }
+
+    return all;
+  }
+
+  public static Channel getByName(String name)
+    throws StorageBackendException
+  {
+    return StorageManager.current().getGroup(name);
+  }
+
+  public abstract Article getArticle(long idx)
+    throws StorageBackendException;
+
+  public abstract List<Pair<Long, ArticleHead>> getArticleHeads(
+    final long first, final long last)
+    throws StorageBackendException;
+
+  public abstract List<Long> getArticleNumbers()
+    throws StorageBackendException;
+
+  public abstract long getFirstArticleNumber()
+    throws StorageBackendException;
+
+  public abstract long getIndexOf(Article art)
+    throws StorageBackendException;
+
+  public abstract long getInternalID();
+
+  public abstract long getLastArticleNumber()
+    throws StorageBackendException;
+
+  public abstract String getName();
+  
+  public abstract long getPostingsCount()
+    throws StorageBackendException;
+
+  public abstract boolean isDeleted();
+
+  public abstract boolean isWriteable();
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/Group.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/storage/Group.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,184 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.storage;
+
+import java.sql.SQLException;
+import java.util.List;
+import org.sonews.util.Log;
+import org.sonews.util.Pair;
+
+/**
+ * Represents a logical Group within this newsserver.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+// TODO: This class should not be public!
+public class Group extends Channel
+{
+  
+  private long   id     = 0;
+  private int    flags  = -1;
+  private String name   = null;
+
+  /**
+   * @return List of all groups this server handles.
+   */
+  public static List<Channel> getAll()
+  {
+    try
+    {
+      return StorageManager.current().getGroups();
+    }
+    catch(StorageBackendException ex)
+    {
+      Log.get().severe(ex.getMessage());
+      return null;
+    }
+  }
+  
+  /**
+   * @param name
+   * @param id
+   */
+  public Group(final String name, final long id, final int flags)
+  {
+    this.id    = id;
+    this.flags = flags;
+    this.name  = name;
+  }
+
+  @Override
+  public boolean equals(Object obj)
+  {
+    if(obj instanceof Group)
+    {
+      return ((Group)obj).id == this.id;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  public Article getArticle(long idx)
+    throws StorageBackendException
+  {
+    return StorageManager.current().getArticle(idx, this.id);
+  }
+
+  public List<Pair<Long, ArticleHead>> getArticleHeads(final long first, final long last)
+    throws StorageBackendException
+  {
+    return StorageManager.current().getArticleHeads(this, first, last);
+  }
+  
+  public List<Long> getArticleNumbers()
+    throws StorageBackendException
+  {
+    return StorageManager.current().getArticleNumbers(id);
+  }
+
+  public long getFirstArticleNumber()
+    throws StorageBackendException
+  {
+    return StorageManager.current().getFirstArticleNumber(this);
+  }
+
+  public int getFlags()
+  {
+    return this.flags;
+  }
+
+  public long getIndexOf(Article art)
+    throws StorageBackendException
+  {
+    return StorageManager.current().getArticleIndex(art, this);
+  }
+
+  /**
+   * Returns the group id.
+   */
+  public long getInternalID()
+  {
+    assert id > 0;
+
+    return id;
+  }
+
+  public boolean isDeleted()
+  {
+    return (this.flags & DELETED) != 0;
+  }
+
+  public boolean isMailingList()
+  {
+    return (this.flags & MAILINGLIST) != 0;
+  }
+
+  public boolean isWriteable()
+  {
+    return true;
+  }
+
+  public long getLastArticleNumber()
+    throws StorageBackendException
+  {
+    return StorageManager.current().getLastArticleNumber(this);
+  }
+
+  public String getName()
+  {
+    return name;
+  }
+
+  /**
+   * Performs this.flags |= flag to set a specified flag and updates the data
+   * in the JDBCDatabase.
+   * @param flag
+   */
+  public void setFlag(final int flag)
+  {
+    this.flags |= flag;
+  }
+
+  public void setName(final String name)
+  {
+    this.name = name;
+  }
+
+  /**
+   * @return Number of posted articles in this group.
+   * @throws java.sql.SQLException
+   */
+  public long getPostingsCount()
+    throws StorageBackendException
+  {
+    return StorageManager.current().getPostingsCount(this.name);
+  }
+
+  /**
+   * Updates flags and name in the backend.
+   */
+  public void update()
+    throws StorageBackendException
+  {
+    StorageManager.current().update(this);
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/Headers.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/storage/Headers.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,56 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.storage;
+
+/**
+ * Contains header constants. These header keys are no way complete but all
+ * headers that are relevant for sonews.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public final class Headers
+{
+
+  public static final String BYTES             = "bytes";
+  public static final String CONTENT_TYPE      = "content-type";
+  public static final String CONTROL           = "control";
+  public static final String DATE              = "date";
+  public static final String FROM              = "from";
+  public static final String LINES             = "lines";
+  public static final String LIST_POST         = "list-post";
+  public static final String MESSAGE_ID        = "message-id";
+  public static final String NEWSGROUPS        = "newsgroups";
+  public static final String NNTP_POSTING_DATE = "nntp-posting-date";
+  public static final String NNTP_POSTING_HOST = "nntp-posting-host";
+  public static final String PATH              = "path";
+  public static final String REFERENCES        = "references";
+  public static final String REPLY_TO          = "reply-to";
+  public static final String SENDER            = "sender";
+  public static final String SUBJECT           = "subject";
+  public static final String SUPERSEDES        = "subersedes";
+  public static final String TO                = "to";
+  public static final String X_COMPLAINTS_TO   = "x-complaints-to";
+  public static final String X_LIST_POST       = "x-list-post";
+  public static final String X_TRACE           = "x-trace";
+  public static final String XREF              = "xref";
+
+  private Headers()
+  {}
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/Storage.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/storage/Storage.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,150 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.storage;
+
+import java.util.List;
+import org.sonews.feed.Subscription;
+import org.sonews.util.Pair;
+
+/**
+ * A generic storage backend interface.
+ * @author Christian Lins
+ * @since sonews/1.0
+ */
+public interface Storage
+{
+
+  /**
+   * Stores the given Article in the storage.
+   * @param art
+   * @throws StorageBackendException
+   */
+  void addArticle(Article art)
+    throws StorageBackendException;
+
+  void addEvent(long timestamp, int type, long groupID)
+    throws StorageBackendException;
+
+  void addGroup(String groupname, int flags)
+    throws StorageBackendException;
+
+  int countArticles()
+    throws StorageBackendException;
+
+  int countGroups()
+    throws StorageBackendException;
+
+  void delete(String messageID)
+    throws StorageBackendException;
+
+  Article getArticle(String messageID)
+    throws StorageBackendException;
+
+  Article getArticle(long articleIndex, long groupID)
+    throws StorageBackendException;
+
+  List<Pair<Long, ArticleHead>> getArticleHeads(Group group, long first, long last)
+    throws StorageBackendException;
+
+  List<Pair<Long, String>> getArticleHeaders(Channel channel, long start, long end,
+    String header, String pattern)
+    throws StorageBackendException;
+
+  long getArticleIndex(Article art, Group group)
+    throws StorageBackendException;
+
+  List<Long> getArticleNumbers(long groupID)
+    throws StorageBackendException;
+
+  String getConfigValue(String key)
+    throws StorageBackendException;
+
+  int getEventsCount(int eventType, long startTimestamp, long endTimestamp,
+    Channel channel)
+    throws StorageBackendException;
+
+  double getEventsPerHour(int key, long gid)
+    throws StorageBackendException;
+
+  int getFirstArticleNumber(Group group)
+    throws StorageBackendException;
+
+  Group getGroup(String name)
+    throws StorageBackendException;
+
+  List<Channel> getGroups()
+    throws StorageBackendException;
+
+  /**
+   * Retrieves the collection of groupnames that are associated with the
+   * given list address.
+   * @param inetaddress
+   * @return
+   * @throws StorageBackendException
+   */
+  List<String> getGroupsForList(String listAddress)
+    throws StorageBackendException;
+
+  int getLastArticleNumber(Group group)
+    throws StorageBackendException;
+
+  /**
+   * Returns a list of email addresses that are related to the given
+   * groupname. In most cases the list may contain only one entry.
+   * @param groupname
+   * @return
+   * @throws StorageBackendException
+   */
+  List<String> getListsForGroup(String groupname)
+    throws StorageBackendException;
+
+  String getOldestArticle()
+    throws StorageBackendException;
+
+  int getPostingsCount(String groupname)
+    throws StorageBackendException;
+
+  List<Subscription> getSubscriptions(int type)
+    throws StorageBackendException;
+
+  boolean isArticleExisting(String messageID)
+    throws StorageBackendException;
+
+  boolean isGroupExisting(String groupname)
+    throws StorageBackendException;
+
+  void purgeGroup(Group group)
+    throws StorageBackendException;
+
+  void setConfigValue(String key, String value)
+    throws StorageBackendException;
+
+  /**
+   * Updates headers and channel references of the given article.
+   * @param article
+   * @return
+   * @throws StorageBackendException
+   */
+  boolean update(Article article)
+    throws StorageBackendException;
+
+  boolean update(Group group)
+    throws StorageBackendException;
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/StorageBackendException.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/storage/StorageBackendException.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,39 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.storage;
+
+/**
+ *
+ * @author Christian Lins
+ * @since sonews/1.0
+ */
+public class StorageBackendException extends Exception
+{
+
+  public StorageBackendException(Throwable cause)
+  {
+    super(cause);
+  }
+
+  public StorageBackendException(String msg)
+  {
+    super(msg);
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/StorageManager.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/storage/StorageManager.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,89 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.storage;
+
+/**
+ *
+ * @author Christian Lins
+ * @since sonews/1.0
+ */
+public final class StorageManager
+{
+
+  private static StorageProvider provider;
+
+  public static Storage current()
+    throws StorageBackendException
+  {
+    synchronized(StorageManager.class)
+    {
+      if(provider == null)
+      {
+        return null;
+      }
+      else
+      {
+        return provider.storage(Thread.currentThread());
+      }
+    }
+  }
+
+  public static StorageProvider loadProvider(String pluginClassName)
+  {
+    try
+    {
+      Class<?> clazz = Class.forName(pluginClassName);
+      Object   inst  = clazz.newInstance();
+      return (StorageProvider)inst;
+    }
+    catch(Exception ex)
+    {
+      System.err.println(ex);
+      return null;
+    }
+  }
+
+  /**
+   * Sets the current storage provider.
+   * @param provider
+   */
+  public static void enableProvider(StorageProvider provider)
+  {
+    synchronized(StorageManager.class)
+    {
+      if(StorageManager.provider != null)
+      {
+        disableProvider();
+      }
+      StorageManager.provider = provider;
+    }
+  }
+
+  /**
+   * Disables the current provider.
+   */
+  public static void disableProvider()
+  {
+    synchronized(StorageManager.class)
+    {
+      provider = null;
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/StorageProvider.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/storage/StorageProvider.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,40 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.storage;
+
+/**
+ * Provides access to storage backend instances.
+ * @author Christian Lins
+ * @since sonews/1.0
+ */
+public interface StorageProvider
+{
+
+  public boolean isSupported(String uri);
+
+  /**
+   * This method returns the reference to the associated storage.
+   * The reference MAY be unique for each thread. In any case it MUST be
+   * thread-safe to use this method.
+   * @return The reference to the associated Storage.
+   */
+  public Storage storage(Thread thread)
+    throws StorageBackendException;
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/impl/JDBCDatabase.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/storage/impl/JDBCDatabase.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,1782 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.storage.impl;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.PreparedStatement;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import javax.mail.Header;
+import javax.mail.internet.MimeUtility;
+import org.sonews.config.Config;
+import org.sonews.util.Log;
+import org.sonews.feed.Subscription;
+import org.sonews.storage.Article;
+import org.sonews.storage.ArticleHead;
+import org.sonews.storage.Channel;
+import org.sonews.storage.Group;
+import org.sonews.storage.Storage;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.util.Pair;
+
+/**
+ * JDBCDatabase facade class.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+// TODO: Refactor this class to reduce size (e.g. ArticleDatabase GroupDatabase)
+public class JDBCDatabase implements Storage
+{
+
+  public static final int MAX_RESTARTS = 2;
+  
+  private Connection        conn = null;
+  private PreparedStatement pstmtAddArticle1 = null;
+  private PreparedStatement pstmtAddArticle2 = null;
+  private PreparedStatement pstmtAddArticle3 = null;
+  private PreparedStatement pstmtAddArticle4 = null;
+  private PreparedStatement pstmtAddGroup0   = null;
+  private PreparedStatement pstmtAddEvent = null;
+  private PreparedStatement pstmtCountArticles = null;
+  private PreparedStatement pstmtCountGroups   = null;
+  private PreparedStatement pstmtDeleteArticle0 = null;
+  private PreparedStatement pstmtDeleteArticle1 = null;
+  private PreparedStatement pstmtDeleteArticle2 = null;
+  private PreparedStatement pstmtDeleteArticle3 = null;
+  private PreparedStatement pstmtGetArticle0 = null;
+  private PreparedStatement pstmtGetArticle1 = null;
+  private PreparedStatement pstmtGetArticleHeaders0 = null;
+  private PreparedStatement pstmtGetArticleHeaders1 = null;
+  private PreparedStatement pstmtGetArticleHeads = null;
+  private PreparedStatement pstmtGetArticleIDs   = null;
+  private PreparedStatement pstmtGetArticleIndex    = null;
+  private PreparedStatement pstmtGetConfigValue = null;
+  private PreparedStatement pstmtGetEventsCount0 = null;
+  private PreparedStatement pstmtGetEventsCount1 = null;
+  private PreparedStatement pstmtGetGroupForList = null;
+  private PreparedStatement pstmtGetGroup0     = null;
+  private PreparedStatement pstmtGetGroup1     = null;
+  private PreparedStatement pstmtGetFirstArticleNumber = null;
+  private PreparedStatement pstmtGetListForGroup       = null;
+  private PreparedStatement pstmtGetLastArticleNumber  = null;
+  private PreparedStatement pstmtGetMaxArticleID       = null;
+  private PreparedStatement pstmtGetMaxArticleIndex    = null;
+  private PreparedStatement pstmtGetOldestArticle      = null;
+  private PreparedStatement pstmtGetPostingsCount      = null;
+  private PreparedStatement pstmtGetSubscriptions  = null;
+  private PreparedStatement pstmtIsArticleExisting = null;
+  private PreparedStatement pstmtIsGroupExisting = null;
+  private PreparedStatement pstmtPurgeGroup0     = null;
+  private PreparedStatement pstmtPurgeGroup1     = null;
+  private PreparedStatement pstmtSetConfigValue0 = null;
+  private PreparedStatement pstmtSetConfigValue1 = null;
+  private PreparedStatement pstmtUpdateGroup     = null;
+  
+  /** How many times the database connection was reinitialized */
+  private int restarts = 0;
+  
+  /**
+   * Rises the database: reconnect and recreate all prepared statements.
+   * @throws java.lang.SQLException
+   */
+  protected void arise()
+    throws SQLException
+  {
+    try
+    {
+      // Load database driver
+      Class.forName(
+        Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DBMSDRIVER, "java.lang.Object"));
+
+      // Establish database connection
+      this.conn = DriverManager.getConnection(
+        Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DATABASE, "<not specified>"),
+        Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_USER, "root"),
+        Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_PASSWORD, ""));
+
+      this.conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
+      if(this.conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE)
+      {
+        Log.get().warning("Database is NOT fully serializable!");
+      }
+
+      // Prepare statements for method addArticle()
+      this.pstmtAddArticle1 = conn.prepareStatement(
+        "INSERT INTO articles (article_id, body) VALUES(?, ?)");
+      this.pstmtAddArticle2 = conn.prepareStatement(
+        "INSERT INTO headers (article_id, header_key, header_value, header_index) " +
+        "VALUES (?, ?, ?, ?)");
+      this.pstmtAddArticle3 = conn.prepareStatement(
+        "INSERT INTO postings (group_id, article_id, article_index)" +
+        "VALUES (?, ?, ?)");
+      this.pstmtAddArticle4 = conn.prepareStatement(
+        "INSERT INTO article_ids (article_id, message_id) VALUES (?, ?)");
+
+      // Prepare statement for method addStatValue()
+      this.pstmtAddEvent = conn.prepareStatement(
+        "INSERT INTO events VALUES (?, ?, ?)");
+     
+      // Prepare statement for method addGroup()
+      this.pstmtAddGroup0 = conn.prepareStatement(
+        "INSERT INTO groups (name, flags) VALUES (?, ?)");
+      
+      // Prepare statement for method countArticles()
+      this.pstmtCountArticles = conn.prepareStatement(
+        "SELECT Count(article_id) FROM article_ids");
+      
+      // Prepare statement for method countGroups()
+      this.pstmtCountGroups = conn.prepareStatement(
+        "SELECT Count(group_id) FROM groups WHERE " +
+        "flags & " + Channel.DELETED + " = 0");
+      
+      // Prepare statements for method delete(article)
+      this.pstmtDeleteArticle0 = conn.prepareStatement(
+        "DELETE FROM articles WHERE article_id = " +
+        "(SELECT article_id FROM article_ids WHERE message_id = ?)");
+      this.pstmtDeleteArticle1 = conn.prepareStatement(
+        "DELETE FROM headers WHERE article_id = " +
+        "(SELECT article_id FROM article_ids WHERE message_id = ?)");
+      this.pstmtDeleteArticle2 = conn.prepareStatement(
+        "DELETE FROM postings WHERE article_id = " +
+        "(SELECT article_id FROM article_ids WHERE message_id = ?)");
+      this.pstmtDeleteArticle3 = conn.prepareStatement(
+        "DELETE FROM article_ids WHERE message_id = ?");
+
+      // Prepare statements for methods getArticle()
+      this.pstmtGetArticle0 = conn.prepareStatement(
+        "SELECT * FROM articles  WHERE article_id = " +
+        "(SELECT article_id FROM article_ids WHERE message_id = ?)");
+      this.pstmtGetArticle1 = conn.prepareStatement(
+        "SELECT * FROM articles WHERE article_id = " +
+        "(SELECT article_id FROM postings WHERE " +
+        "article_index = ? AND group_id = ?)");
+      
+      // Prepare statement for method getArticleHeaders()
+      this.pstmtGetArticleHeaders0 = conn.prepareStatement(
+        "SELECT header_key, header_value FROM headers WHERE article_id = ? " +
+        "ORDER BY header_index ASC");
+
+      // Prepare statement for method getArticleHeaders(regular expr pattern)
+      this.pstmtGetArticleHeaders1 = conn.prepareStatement(
+        "SELECT p.article_index, h.header_value FROM headers h " +
+          "INNER JOIN postings p ON h.article_id = p.article_id " +
+          "INNER JOIN groups g ON p.group_id = g.group_id " +
+            "WHERE g.name          =  ? AND " +
+                  "h.header_key    =  ? AND " +
+                  "p.article_index >= ? " +
+        "ORDER BY p.article_index ASC");
+
+      this.pstmtGetArticleIDs = conn.prepareStatement(
+        "SELECT article_index FROM postings WHERE group_id = ?");
+      
+      // Prepare statement for method getArticleIndex
+      this.pstmtGetArticleIndex = conn.prepareStatement(
+              "SELECT article_index FROM postings WHERE " +
+              "article_id = (SELECT article_id FROM article_ids " +
+              "WHERE message_id = ?) " +
+              " AND group_id = ?");
+
+      // Prepare statements for method getArticleHeads()
+      this.pstmtGetArticleHeads = conn.prepareStatement(
+        "SELECT article_id, article_index FROM postings WHERE " +
+        "postings.group_id = ? AND article_index >= ? AND " +
+        "article_index <= ?");
+
+      // Prepare statements for method getConfigValue()
+      this.pstmtGetConfigValue = conn.prepareStatement(
+        "SELECT config_value FROM config WHERE config_key = ?");
+
+      // Prepare statements for method getEventsCount()
+      this.pstmtGetEventsCount0 = conn.prepareStatement(
+        "SELECT Count(*) FROM events WHERE event_key = ? AND " +
+        "event_time >= ? AND event_time < ?");
+
+      this.pstmtGetEventsCount1 = conn.prepareStatement(
+        "SELECT Count(*) FROM events WHERE event_key = ? AND " +
+        "event_time >= ? AND event_time < ? AND group_id = ?");
+      
+      // Prepare statement for method getGroupForList()
+      this.pstmtGetGroupForList = conn.prepareStatement(
+        "SELECT name FROM groups INNER JOIN groups2list " +
+        "ON groups.group_id = groups2list.group_id " +
+        "WHERE groups2list.listaddress = ?");
+
+      // Prepare statement for method getGroup()
+      this.pstmtGetGroup0 = conn.prepareStatement(
+        "SELECT group_id, flags FROM groups WHERE Name = ?");
+      this.pstmtGetGroup1 = conn.prepareStatement(
+        "SELECT name FROM groups WHERE group_id = ?");
+
+      // Prepare statement for method getLastArticleNumber()
+      this.pstmtGetLastArticleNumber = conn.prepareStatement(
+        "SELECT Max(article_index) FROM postings WHERE group_id = ?");
+
+      // Prepare statement for method getListForGroup()
+      this.pstmtGetListForGroup = conn.prepareStatement(
+        "SELECT listaddress FROM groups2list INNER JOIN groups " +
+        "ON groups.group_id = groups2list.group_id WHERE name = ?");
+
+      // Prepare statement for method getMaxArticleID()
+      this.pstmtGetMaxArticleID = conn.prepareStatement(
+        "SELECT Max(article_id) FROM articles");
+      
+      // Prepare statement for method getMaxArticleIndex()
+      this.pstmtGetMaxArticleIndex = conn.prepareStatement(
+        "SELECT Max(article_index) FROM postings WHERE group_id = ?");
+      
+      // Prepare statement for method getOldestArticle()
+      this.pstmtGetOldestArticle = conn.prepareStatement(
+        "SELECT message_id FROM article_ids WHERE article_id = " +
+        "(SELECT Min(article_id) FROM article_ids)");
+
+      // Prepare statement for method getFirstArticleNumber()
+      this.pstmtGetFirstArticleNumber = conn.prepareStatement(
+        "SELECT Min(article_index) FROM postings WHERE group_id = ?");
+      
+      // Prepare statement for method getPostingsCount()
+      this.pstmtGetPostingsCount = conn.prepareStatement(
+        "SELECT Count(*) FROM postings NATURAL JOIN groups " +
+        "WHERE groups.name = ?");
+      
+      // Prepare statement for method getSubscriptions()
+      this.pstmtGetSubscriptions = conn.prepareStatement(
+        "SELECT host, port, name FROM peers NATURAL JOIN " +
+        "peer_subscriptions NATURAL JOIN groups WHERE feedtype = ?");
+      
+      // Prepare statement for method isArticleExisting()
+      this.pstmtIsArticleExisting = conn.prepareStatement(
+        "SELECT Count(article_id) FROM article_ids WHERE message_id = ?");
+      
+      // Prepare statement for method isGroupExisting()
+      this.pstmtIsGroupExisting = conn.prepareStatement(
+        "SELECT * FROM groups WHERE name = ?");
+      
+      // Prepare statement for method setConfigValue()
+      this.pstmtSetConfigValue0 = conn.prepareStatement(
+        "DELETE FROM config WHERE config_key = ?");
+      this.pstmtSetConfigValue1 = conn.prepareStatement(
+        "INSERT INTO config VALUES(?, ?)");
+
+      // Prepare statements for method purgeGroup()
+      this.pstmtPurgeGroup0 = conn.prepareStatement(
+        "DELETE FROM peer_subscriptions WHERE group_id = ?");
+      this.pstmtPurgeGroup1 = conn.prepareStatement(
+        "DELETE FROM groups WHERE group_id = ?");
+
+      // Prepare statement for method update(Group)
+      this.pstmtUpdateGroup = conn.prepareStatement(
+        "UPDATE groups SET flags = ?, name = ? WHERE group_id = ?");
+    }
+    catch(ClassNotFoundException ex)
+    {
+      throw new Error("JDBC Driver not found!", ex);
+    }
+  }
+  
+  /**
+   * Adds an article to the database.
+   * @param article
+   * @return
+   * @throws java.sql.SQLException
+   */
+  @Override
+  public void addArticle(final Article article)
+    throws StorageBackendException
+  {
+    try
+    {
+      this.conn.setAutoCommit(false);
+
+      int newArticleID = getMaxArticleID() + 1;
+
+      // Fill prepared statement with values;
+      // writes body to article table
+      pstmtAddArticle1.setInt(1, newArticleID);
+      pstmtAddArticle1.setBytes(2, article.getBody());
+      pstmtAddArticle1.execute();
+
+      // Add headers
+      Enumeration headers = article.getAllHeaders();
+      for(int n = 0; headers.hasMoreElements(); n++)
+      {
+        Header header = (Header)headers.nextElement();
+        pstmtAddArticle2.setInt(1, newArticleID);
+        pstmtAddArticle2.setString(2, header.getName().toLowerCase());
+        pstmtAddArticle2.setString(3, 
+          header.getValue().replaceAll("[\r\n]", ""));
+        pstmtAddArticle2.setInt(4, n);
+        pstmtAddArticle2.execute();
+      }
+      
+      // For each newsgroup add a reference
+      List<Group> groups = article.getGroups();
+      for(Group group : groups)
+      {
+        pstmtAddArticle3.setLong(1, group.getInternalID());
+        pstmtAddArticle3.setInt(2, newArticleID);
+        pstmtAddArticle3.setLong(3, getMaxArticleIndex(group.getInternalID()) + 1);
+        pstmtAddArticle3.execute();
+      }
+      
+      // Write message-id to article_ids table
+      this.pstmtAddArticle4.setInt(1, newArticleID);
+      this.pstmtAddArticle4.setString(2, article.getMessageID());
+      this.pstmtAddArticle4.execute();
+
+      this.conn.commit();
+      this.conn.setAutoCommit(true);
+
+      this.restarts = 0; // Reset error count
+    }
+    catch(SQLException ex)
+    {
+      try
+      {
+        this.conn.rollback();  // Rollback changes
+      }
+      catch(SQLException ex2)
+      {
+        Log.get().severe("Rollback of addArticle() failed: " + ex2);
+      }
+      
+      try
+      {
+        this.conn.setAutoCommit(true); // and release locks
+      }
+      catch(SQLException ex2)
+      {
+        Log.get().severe("setAutoCommit(true) of addArticle() failed: " + ex2);
+      }
+
+      restartConnection(ex);
+      addArticle(article);
+    }
+  }
+  
+  /**
+   * Adds a group to the JDBCDatabase. This method is not accessible via NNTP.
+   * @param name
+   * @throws java.sql.SQLException
+   */
+  @Override
+  public void addGroup(String name, int flags)
+    throws StorageBackendException
+  {
+    try
+    {
+      this.conn.setAutoCommit(false);
+      pstmtAddGroup0.setString(1, name);
+      pstmtAddGroup0.setInt(2, flags);
+
+      pstmtAddGroup0.executeUpdate();
+      this.conn.commit();
+      this.conn.setAutoCommit(true);
+      this.restarts = 0; // Reset error count
+    }
+    catch(SQLException ex)
+    {
+      try
+      {
+        this.conn.rollback();
+        this.conn.setAutoCommit(true);
+      }
+      catch(SQLException ex2)
+      {
+        ex2.printStackTrace();
+      }
+
+      restartConnection(ex);
+      addGroup(name, flags);
+    }
+  }
+
+  @Override
+  public void addEvent(long time, int type, long gid)
+    throws StorageBackendException
+  {
+    try
+    {
+      this.conn.setAutoCommit(false);
+      this.pstmtAddEvent.setLong(1, time);
+      this.pstmtAddEvent.setInt(2, type);
+      this.pstmtAddEvent.setLong(3, gid);
+      this.pstmtAddEvent.executeUpdate();
+      this.conn.commit();
+      this.conn.setAutoCommit(true);
+      this.restarts = 0;
+    }
+    catch(SQLException ex)
+    {
+      try
+      {
+        this.conn.rollback();
+        this.conn.setAutoCommit(true);
+      }
+      catch(SQLException ex2)
+      {
+        ex2.printStackTrace();
+      }
+
+      restartConnection(ex);
+      addEvent(time, type, gid);
+    }
+  }
+
+  @Override
+  public int countArticles()
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+
+    try
+    {
+      rs = this.pstmtCountArticles.executeQuery();
+      if(rs.next())
+      {
+        return rs.getInt(1);
+      }
+      else
+      {
+        return -1;
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return countArticles();
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+        restarts = 0;
+      }
+    }
+  }
+
+  @Override
+  public int countGroups()
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+
+    try
+    {
+      rs = this.pstmtCountGroups.executeQuery();
+      if(rs.next())
+      {
+        return rs.getInt(1);
+      }
+      else
+      {
+        return -1;
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return countGroups();
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+        restarts = 0;
+      }
+    }
+  }
+
+  @Override
+  public void delete(final String messageID)
+    throws StorageBackendException
+  {
+    try
+    {
+      this.conn.setAutoCommit(false);
+      
+      this.pstmtDeleteArticle0.setString(1, messageID);
+      int rs = this.pstmtDeleteArticle0.executeUpdate();
+      
+      // We do not trust the ON DELETE CASCADE functionality to delete
+      // orphaned references...
+      this.pstmtDeleteArticle1.setString(1, messageID);
+      rs = this.pstmtDeleteArticle1.executeUpdate();
+
+      this.pstmtDeleteArticle2.setString(1, messageID);
+      rs = this.pstmtDeleteArticle2.executeUpdate();
+
+      this.pstmtDeleteArticle3.setString(1, messageID);
+      rs = this.pstmtDeleteArticle3.executeUpdate();
+      
+      this.conn.commit();
+      this.conn.setAutoCommit(true);
+    }
+    catch(SQLException ex)
+    {
+      throw new StorageBackendException(ex);
+    }
+  }
+
+  @Override
+  public Article getArticle(String messageID)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+    try
+    {
+      pstmtGetArticle0.setString(1, messageID);
+      rs = pstmtGetArticle0.executeQuery();
+
+      if(!rs.next())
+      {
+        return null;
+      }
+      else
+      {
+        byte[] body     = rs.getBytes("body");
+        String headers  = getArticleHeaders(rs.getInt("article_id"));
+        return new Article(headers, body);
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getArticle(messageID);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+        restarts = 0; // Reset error count
+      }
+    }
+  }
+  
+  /**
+   * Retrieves an article by its ID.
+   * @param articleID
+   * @return
+   * @throws StorageBackendException
+   */
+  @Override
+  public Article getArticle(long articleIndex, long gid)
+    throws StorageBackendException
+  {  
+    ResultSet rs = null;
+
+    try
+    {
+      this.pstmtGetArticle1.setLong(1, articleIndex);
+      this.pstmtGetArticle1.setLong(2, gid);
+
+      rs = this.pstmtGetArticle1.executeQuery();
+
+      if(rs.next())
+      {
+        byte[] body    = rs.getBytes("body");
+        String headers = getArticleHeaders(rs.getInt("article_id"));
+        return new Article(headers, body);
+      }
+      else
+      {
+        return null;
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getArticle(articleIndex, gid);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+        restarts = 0;
+      }
+    }
+  }
+
+  /**
+   * Searches for fitting header values using the given regular expression.
+   * @param group
+   * @param start
+   * @param end
+   * @param headerKey
+   * @param pattern
+   * @return
+   * @throws StorageBackendException
+   */
+  @Override
+  public List<Pair<Long, String>> getArticleHeaders(Channel group, long start,
+    long end, String headerKey, String patStr)
+    throws StorageBackendException, PatternSyntaxException
+  {
+    ResultSet rs = null;
+    List<Pair<Long, String>> heads = new ArrayList<Pair<Long, String>>();
+
+    try
+    {
+      this.pstmtGetArticleHeaders1.setString(1, group.getName());
+      this.pstmtGetArticleHeaders1.setString(2, headerKey);
+      this.pstmtGetArticleHeaders1.setLong(3, start);
+
+      rs = this.pstmtGetArticleHeaders1.executeQuery();
+
+      // Convert the "NNTP" regex to Java regex
+      patStr = patStr.replace("*", ".*");
+      Pattern pattern = Pattern.compile(patStr);
+
+      while(rs.next())
+      {
+        Long articleIndex = rs.getLong(1);
+        if(end < 0 || articleIndex <= end) // Match start is done via SQL
+        {
+          String headerValue  = rs.getString(2);
+          Matcher matcher = pattern.matcher(headerValue);
+          if(matcher.matches())
+          {
+            heads.add(new Pair<Long, String>(articleIndex, headerValue));
+          }
+        }
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getArticleHeaders(group, start, end, headerKey, patStr);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+
+    return heads;
+  }
+
+  private String getArticleHeaders(long articleID)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+    
+    try
+    {
+      this.pstmtGetArticleHeaders0.setLong(1, articleID);
+      rs = this.pstmtGetArticleHeaders0.executeQuery();
+      
+      StringBuilder buf = new StringBuilder();
+      if(rs.next())
+      {
+        for(;;)
+        {
+          buf.append(rs.getString(1)); // key
+          buf.append(": ");
+          String foldedValue = MimeUtility.fold(0, rs.getString(2));
+          buf.append(foldedValue); // value
+          if(rs.next())
+          {
+            buf.append("\r\n");
+          }
+          else
+          {
+            break;
+          }
+        }
+      }
+      
+      return buf.toString();
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getArticleHeaders(articleID);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+
+  @Override
+  public long getArticleIndex(Article article, Group group)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+
+    try
+    {
+      this.pstmtGetArticleIndex.setString(1, article.getMessageID());
+      this.pstmtGetArticleIndex.setLong(2, group.getInternalID());
+      
+      rs = this.pstmtGetArticleIndex.executeQuery();
+      if(rs.next())
+      {
+        return rs.getLong(1);
+      }
+      else
+      {
+        return -1;
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getArticleIndex(article, group);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+  
+  /**
+   * Returns a list of Long/Article Pairs.
+   * @throws java.sql.SQLException
+   */
+  @Override
+  public List<Pair<Long, ArticleHead>> getArticleHeads(Group group, long first,
+    long last)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+
+    try
+    {
+      this.pstmtGetArticleHeads.setLong(1, group.getInternalID());
+      this.pstmtGetArticleHeads.setLong(2, first);
+      this.pstmtGetArticleHeads.setLong(3, last);
+      rs = pstmtGetArticleHeads.executeQuery();
+
+      List<Pair<Long, ArticleHead>> articles 
+        = new ArrayList<Pair<Long, ArticleHead>>();
+
+      while (rs.next())
+      {
+        long aid  = rs.getLong("article_id");
+        long aidx = rs.getLong("article_index");
+        String headers = getArticleHeaders(aid);
+        articles.add(new Pair<Long, ArticleHead>(aidx, 
+                        new ArticleHead(headers)));
+      }
+
+      return articles;
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getArticleHeads(group, first, last);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+
+  @Override
+  public List<Long> getArticleNumbers(long gid)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+    try
+    {
+      List<Long> ids = new ArrayList<Long>();
+      this.pstmtGetArticleIDs.setLong(1, gid);
+      rs = this.pstmtGetArticleIDs.executeQuery();
+      while(rs.next())
+      {
+        ids.add(rs.getLong(1));
+      }
+      return ids;
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getArticleNumbers(gid);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+          restarts = 0; // Clear the restart count after successful request
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+
+  @Override
+  public String getConfigValue(String key)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+    try
+    {
+      this.pstmtGetConfigValue.setString(1, key);
+
+      rs = this.pstmtGetConfigValue.executeQuery();
+      if(rs.next())
+      {
+        return rs.getString(1); // First data on index 1 not 0
+      }
+      else
+      {
+        return null;
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getConfigValue(key);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+        restarts = 0; // Clear the restart count after successful request
+      }
+    }
+  }
+
+  @Override
+  public int getEventsCount(int type, long start, long end, Channel channel)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+    
+    try
+    {
+      if(channel == null)
+      {
+        this.pstmtGetEventsCount0.setInt(1, type);
+        this.pstmtGetEventsCount0.setLong(2, start);
+        this.pstmtGetEventsCount0.setLong(3, end);
+        rs = this.pstmtGetEventsCount0.executeQuery();
+      }
+      else
+      {
+        this.pstmtGetEventsCount1.setInt(1, type);
+        this.pstmtGetEventsCount1.setLong(2, start);
+        this.pstmtGetEventsCount1.setLong(3, end);
+        this.pstmtGetEventsCount1.setLong(4, channel.getInternalID());
+        rs = this.pstmtGetEventsCount1.executeQuery();
+      }
+      
+      if(rs.next())
+      {
+        return rs.getInt(1);
+      }
+      else
+      {
+        return -1;
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getEventsCount(type, start, end, channel);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+  
+  /**
+   * Reads all Groups from the JDBCDatabase.
+   * @return
+   * @throws StorageBackendException
+   */
+  @Override
+  public List<Channel> getGroups()
+    throws StorageBackendException
+  {
+    ResultSet   rs;
+    List<Channel> buffer = new ArrayList<Channel>();
+    Statement   stmt   = null;
+
+    try
+    {
+      stmt = conn.createStatement();
+      rs = stmt.executeQuery("SELECT * FROM groups ORDER BY name");
+
+      while(rs.next())
+      {
+        String name  = rs.getString("name");
+        long   id    = rs.getLong("group_id");
+        int    flags = rs.getInt("flags");
+        
+        Group group = new Group(name, id, flags);
+        buffer.add(group);
+      }
+
+      return buffer;
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getGroups();
+    }
+    finally
+    {
+      if(stmt != null)
+      {
+        try
+        {
+          stmt.close(); // Implicitely closes ResultSets
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+
+  @Override
+  public List<String> getGroupsForList(String listAddress)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+    
+    try
+    {
+      this.pstmtGetGroupForList.setString(1, listAddress);
+
+      rs = this.pstmtGetGroupForList.executeQuery();
+      List<String> groups = new ArrayList<String>();
+      while(rs.next())
+      {
+        String group = rs.getString(1);
+        groups.add(group);
+      }
+      return groups;
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getGroupsForList(listAddress);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+  
+  /**
+   * Returns the Group that is identified by the name.
+   * @param name
+   * @return
+   * @throws StorageBackendException
+   */
+  @Override
+  public Group getGroup(String name)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+    
+    try
+    {
+      this.pstmtGetGroup0.setString(1, name);
+      rs = this.pstmtGetGroup0.executeQuery();
+
+      if (!rs.next())
+      {
+        return null;
+      }
+      else
+      {
+        long id = rs.getLong("group_id");
+        int flags = rs.getInt("flags");
+        return new Group(name, id, flags);
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getGroup(name);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+
+  @Override
+  public List<String> getListsForGroup(String group)
+    throws StorageBackendException
+  {
+    ResultSet     rs    = null;
+    List<String>  lists = new ArrayList<String>();
+
+    try
+    {
+      this.pstmtGetListForGroup.setString(1, group);
+      rs = this.pstmtGetListForGroup.executeQuery();
+
+      while(rs.next())
+      {
+        lists.add(rs.getString(1));
+      }
+      return lists;
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getListsForGroup(group);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+  
+  private int getMaxArticleIndex(long groupID)
+    throws StorageBackendException
+  {
+    ResultSet rs    = null;
+
+    try
+    {
+      this.pstmtGetMaxArticleIndex.setLong(1, groupID);
+      rs = this.pstmtGetMaxArticleIndex.executeQuery();
+
+      int maxIndex = 0;
+      if (rs.next())
+      {
+        maxIndex = rs.getInt(1);
+      }
+
+      return maxIndex;
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getMaxArticleIndex(groupID);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+  
+  private int getMaxArticleID()
+    throws StorageBackendException
+  {
+    ResultSet rs    = null;
+
+    try
+    {
+      rs = this.pstmtGetMaxArticleID.executeQuery();
+
+      int maxIndex = 0;
+      if (rs.next())
+      {
+        maxIndex = rs.getInt(1);
+      }
+
+      return maxIndex;
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getMaxArticleID();
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+
+  @Override
+  public int getLastArticleNumber(Group group)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+
+    try
+    {
+      this.pstmtGetLastArticleNumber.setLong(1, group.getInternalID());
+      rs = this.pstmtGetLastArticleNumber.executeQuery();
+      if (rs.next())
+      {
+        return rs.getInt(1);
+      }
+      else
+      {
+        return 0;
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getLastArticleNumber(group);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+
+  @Override
+  public int getFirstArticleNumber(Group group)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+    try
+    {
+      this.pstmtGetFirstArticleNumber.setLong(1, group.getInternalID());
+      rs = this.pstmtGetFirstArticleNumber.executeQuery();
+      if(rs.next())
+      {
+        return rs.getInt(1);
+      }
+      else
+      {
+        return 0;
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getFirstArticleNumber(group);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+  
+  /**
+   * Returns a group name identified by the given id.
+   * @param id
+   * @return
+   * @throws StorageBackendException
+   */
+  public String getGroup(int id)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+
+    try
+    {
+      this.pstmtGetGroup1.setInt(1, id);
+      rs = this.pstmtGetGroup1.executeQuery();
+
+      if (rs.next())
+      {
+        return rs.getString(1);
+      }
+      else
+      {
+        return null;
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getGroup(id);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+
+  @Override
+  public double getEventsPerHour(int key, long gid)
+    throws StorageBackendException
+  {
+    String gidquery = "";
+    if(gid >= 0)
+    {
+      gidquery = " AND group_id = " + gid;
+    }
+    
+    Statement stmt = null;
+    ResultSet rs   = null;
+    
+    try
+    {
+      stmt = this.conn.createStatement();
+      rs = stmt.executeQuery("SELECT Count(*) / (Max(event_time) - Min(event_time))" +
+        " * 1000 * 60 * 60 FROM events WHERE event_key = " + key + gidquery);
+      
+      if(rs.next())
+      {
+        restarts = 0; // reset error count
+        return rs.getDouble(1);
+      }
+      else
+      {
+        return Double.NaN;
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getEventsPerHour(key, gid);
+    }
+    finally
+    {
+      try
+      {
+        if(stmt != null)
+        {
+          stmt.close(); // Implicitely closes the result sets
+        }
+      }
+      catch(SQLException ex)
+      {
+        ex.printStackTrace();
+      }
+    }
+  }
+
+  @Override
+  public String getOldestArticle()
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+
+    try
+    {
+      rs = this.pstmtGetOldestArticle.executeQuery();
+      if(rs.next())
+      {
+        return rs.getString(1);
+      }
+      else
+      {
+        return null;
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getOldestArticle();
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+
+  @Override
+  public int getPostingsCount(String groupname)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+    
+    try
+    {
+      this.pstmtGetPostingsCount.setString(1, groupname);
+      rs = this.pstmtGetPostingsCount.executeQuery();
+      if(rs.next())
+      {
+        return rs.getInt(1);
+      }
+      else
+      {
+        Log.get().warning("Count on postings return nothing!");
+        return 0;
+      }
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getPostingsCount(groupname);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+
+  @Override
+  public List<Subscription> getSubscriptions(int feedtype)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+    
+    try
+    {
+      List<Subscription> subs = new ArrayList<Subscription>();
+      this.pstmtGetSubscriptions.setInt(1, feedtype);
+      rs = this.pstmtGetSubscriptions.executeQuery();
+      
+      while(rs.next())
+      {
+        String host  = rs.getString("host");
+        String group = rs.getString("name");
+        int    port  = rs.getInt("port");
+        subs.add(new Subscription(host, port, feedtype, group));
+      }
+      
+      return subs;
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return getSubscriptions(feedtype);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+
+  /**
+   * Checks if there is an article with the given messageid in the JDBCDatabase.
+   * @param name
+   * @return
+   * @throws StorageBackendException
+   */
+  @Override
+  public boolean isArticleExisting(String messageID)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+    
+    try
+    {
+      this.pstmtIsArticleExisting.setString(1, messageID);
+      rs = this.pstmtIsArticleExisting.executeQuery();
+      return rs.next() && rs.getInt(1) == 1;
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return isArticleExisting(messageID);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+  
+  /**
+   * Checks if there is a group with the given name in the JDBCDatabase.
+   * @param name
+   * @return
+   * @throws StorageBackendException
+   */
+  @Override
+  public boolean isGroupExisting(String name)
+    throws StorageBackendException
+  {
+    ResultSet rs = null;
+    
+    try
+    {
+      this.pstmtIsGroupExisting.setString(1, name);
+      rs = this.pstmtIsGroupExisting.executeQuery();
+      return rs.next();
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return isGroupExisting(name);
+    }
+    finally
+    {
+      if(rs != null)
+      {
+        try
+        {
+          rs.close();
+        }
+        catch(SQLException ex)
+        {
+          ex.printStackTrace();
+        }
+      }
+    }
+  }
+
+  @Override
+  public void setConfigValue(String key, String value)
+    throws StorageBackendException
+  {
+    try
+    {
+      conn.setAutoCommit(false);
+      this.pstmtSetConfigValue0.setString(1, key);
+      this.pstmtSetConfigValue0.execute();
+      this.pstmtSetConfigValue1.setString(1, key);
+      this.pstmtSetConfigValue1.setString(2, value);
+      this.pstmtSetConfigValue1.execute();
+      conn.commit();
+      conn.setAutoCommit(true);
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      setConfigValue(key, value);
+    }
+  }
+  
+  /**
+   * Closes the JDBCDatabase connection.
+   */
+  public void shutdown()
+    throws StorageBackendException
+  {
+    try
+    {
+      if(this.conn != null)
+      {
+        this.conn.close();
+      }
+    }
+    catch(SQLException ex)
+    {
+      throw new StorageBackendException(ex);
+    }
+  }
+
+  @Override
+  public void purgeGroup(Group group)
+    throws StorageBackendException
+  {
+    try
+    {
+      this.pstmtPurgeGroup0.setLong(1, group.getInternalID());
+      this.pstmtPurgeGroup0.executeUpdate();
+
+      this.pstmtPurgeGroup1.setLong(1, group.getInternalID());
+      this.pstmtPurgeGroup1.executeUpdate();
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      purgeGroup(group);
+    }
+  }
+  
+  private void restartConnection(SQLException cause)
+    throws StorageBackendException
+  {
+    restarts++;
+    Log.get().severe(Thread.currentThread()
+      + ": Database connection was closed (restart " + restarts + ").");
+    
+    if(restarts >= MAX_RESTARTS)
+    {
+      // Delete the current, probably broken JDBCDatabase instance.
+      // So no one can use the instance any more.
+      JDBCDatabaseProvider.instances.remove(Thread.currentThread());
+      
+      // Throw the exception upwards
+      throw new StorageBackendException(cause);
+    }
+    
+    try
+    {
+      Thread.sleep(1500L * restarts);
+    }
+    catch(InterruptedException ex)
+    {
+      Log.get().warning("Interrupted: " + ex.getMessage());
+    }
+    
+    // Try to properly close the old database connection
+    try
+    {
+      if(this.conn != null)
+      {
+        this.conn.close();
+      }
+    }
+    catch(SQLException ex)
+    {
+      Log.get().warning(ex.getMessage());
+    }
+    
+    try
+    {
+      // Try to reinitialize database connection
+      arise();
+    }
+    catch(SQLException ex)
+    {
+      Log.get().warning(ex.getMessage());
+      restartConnection(ex);
+    }
+  }
+
+  @Override
+  public boolean update(Article article)
+    throws StorageBackendException
+  {
+    // DELETE FROM headers WHERE article_id = ?
+
+    // INSERT INTO headers ...
+
+    // SELECT * FROM postings WHERE article_id = ? AND group_id = ?
+    return false;
+  }
+
+  /**
+   * Writes the flags and the name of the given group to the database.
+   * @param group
+   * @throws StorageBackendException
+   */
+  @Override
+  public boolean update(Group group)
+    throws StorageBackendException
+  {
+    try
+    {
+      this.pstmtUpdateGroup.setInt(1, group.getFlags());
+      this.pstmtUpdateGroup.setString(2, group.getName());
+      this.pstmtUpdateGroup.setLong(3, group.getInternalID());
+      int rs = this.pstmtUpdateGroup.executeUpdate();
+      return rs == 1;
+    }
+    catch(SQLException ex)
+    {
+      restartConnection(ex);
+      return update(group);
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/impl/JDBCDatabaseProvider.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/storage/impl/JDBCDatabaseProvider.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,69 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.storage.impl;
+
+import java.sql.SQLException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.sonews.storage.Storage;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.storage.StorageProvider;
+
+/**
+ *
+ * @author Christian Lins
+ * @since sonews/1.0
+ */
+public class JDBCDatabaseProvider implements StorageProvider
+{
+
+  protected static final Map<Thread, JDBCDatabase> instances
+    = new ConcurrentHashMap<Thread, JDBCDatabase>();
+
+  @Override
+  public boolean isSupported(String uri)
+  {
+    throw new UnsupportedOperationException("Not supported yet.");
+  }
+
+  @Override
+  public Storage storage(Thread thread)
+    throws StorageBackendException
+  {
+    try
+    {
+    if(!instances.containsKey(Thread.currentThread()))
+    {
+      JDBCDatabase db = new JDBCDatabase();
+      db.arise();
+      instances.put(Thread.currentThread(), db);
+      return db;
+    }
+    else
+    {
+      return instances.get(Thread.currentThread());
+    }
+    }
+    catch(SQLException ex)
+    {
+      throw new StorageBackendException(ex);
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/package.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/storage/package.html	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,2 @@
+Contains classes of the storage backend and the Group and Article
+abstraction.
\ No newline at end of file
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/DatabaseSetup.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/DatabaseSetup.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,127 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.util;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import java.util.HashMap;
+import java.util.Map;
+import org.sonews.config.Config;
+import org.sonews.util.io.Resource;
+
+/**
+ * Database setup utility class.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public final class DatabaseSetup 
+{
+
+  private static final Map<String, String> templateMap 
+    = new HashMap<String, String>();
+  private static final Map<String, StringTemplate> urlMap
+    = new HashMap<String, StringTemplate>();
+  private static final Map<String, String> driverMap
+    = new HashMap<String, String>();
+  
+  static
+  {
+    templateMap.put("1", "helpers/database_mysql5_tmpl.sql");
+    templateMap.put("2", "helpers/database_postgresql8_tmpl.sql");
+    
+    urlMap.put("1", new StringTemplate("jdbc:mysql://%HOSTNAME/%DB"));
+    urlMap.put("2", new StringTemplate("jdbc:postgresql://%HOSTNAME/%DB"));
+    
+    driverMap.put("1", "com.mysql.jdbc.Driver");
+    driverMap.put("2", "org.postgresql.Driver");
+  }
+  
+  public static void main(String[] args)
+    throws Exception
+  {
+    System.out.println("sonews Database setup helper");
+    System.out.println("This program will create a initial database table structure");
+    System.out.println("for the sonews Newsserver.");
+    System.out.println("You need to create a database and a db user manually before!");
+    
+    System.out.println("Select DBMS type:");
+    System.out.println("[1] MySQL 5.x or higher");
+    System.out.println("[2] PostgreSQL 8.x or higher");
+    System.out.print("Your choice: ");
+    
+    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
+    String dbmsType = in.readLine();
+    String tmplName = templateMap.get(dbmsType);
+    if(tmplName == null)
+    {
+      System.err.println("Invalid choice. Try again you fool!");
+      main(args);
+      return;
+    }
+    
+    // Load JDBC Driver class
+    Class.forName(driverMap.get(dbmsType));
+    
+    String tmpl = Resource.getAsString(tmplName, true);
+    
+    System.out.print("Database server hostname (e.g. localhost): ");
+    String dbHostname = in.readLine();
+    
+    System.out.print("Database name: ");
+    String dbName = in.readLine();
+
+    System.out.print("Give name of DB user that can create tables: ");
+    String dbUser = in.readLine();
+
+    System.out.print("Password: ");
+    String dbPassword = in.readLine();
+    
+    String url = urlMap.get(dbmsType)
+      .set("HOSTNAME", dbHostname)
+      .set("DB", dbName).toString();
+    
+    Connection conn = 
+      DriverManager.getConnection(url, dbUser, dbPassword);
+    conn.setAutoCommit(false);
+    
+    String[] tmplChunks = tmpl.split(";");
+    
+    for(String chunk : tmplChunks)
+    {
+      if(chunk.trim().equals(""))
+      {
+        continue;
+      }
+      
+      Statement stmt = conn.createStatement();
+      stmt.execute(chunk);
+    }
+    
+    conn.commit();
+    conn.setAutoCommit(true);
+    
+    // Create config file
+    
+    System.out.println("Ok");
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/Log.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/Log.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,57 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.util;
+
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+import java.util.logging.SimpleFormatter;
+import java.util.logging.StreamHandler;
+import org.sonews.config.Config;
+
+/**
+ * Provides logging and debugging methods.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class Log extends Logger
+{
+
+  private static Log instance = new Log();
+
+  private Log()
+  {
+    super("org.sonews", null);
+
+    StreamHandler handler = new StreamHandler(System.out, new SimpleFormatter());
+    Level level = Level.parse(Config.inst().get(Config.LOGLEVEL, "INFO"));
+    handler.setLevel(level);
+    addHandler(handler);
+    setLevel(level);
+    LogManager.getLogManager().addLogger(this);
+  }
+
+  public static Logger get()
+  {
+    Level level = Level.parse(Config.inst().get(Config.LOGLEVEL, "INFO"));
+    instance.setLevel(level);
+    return instance;
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/Pair.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/Pair.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,48 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.util;
+
+/**
+ * A pair of two objects.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class Pair<T1, T2> 
+{
+ 
+  private T1 a;
+  private T2 b;
+  
+  public Pair(T1 a, T2 b)
+  {
+    this.a = a;
+    this.b = b;
+  }
+
+  public T1 getA()
+  {
+    return a;
+  }
+
+  public T2 getB()
+  {
+    return b;
+  } 
+ 
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/Purger.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/Purger.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,149 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.util;
+
+import org.sonews.daemon.AbstractDaemon;
+import org.sonews.config.Config;
+import org.sonews.storage.Article;
+import org.sonews.storage.Headers;
+import java.util.Date;
+import java.util.List;
+import org.sonews.storage.Channel;
+import org.sonews.storage.Group;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.storage.StorageManager;
+
+/**
+ * The purger is started in configurable intervals to search
+ * for messages that can be purged. A message must be deleted if its lifetime
+ * has exceeded, if it was marked as deleted or if the maximum number of
+ * articles in the database is reached.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class Purger extends AbstractDaemon
+{
+
+  /**
+   * Loops through all messages and deletes them if their time
+   * has come.
+   */
+  @Override
+  public void run()
+  {
+    try
+    {
+      while(isRunning())
+      {
+        purgeDeleted();
+        purgeOutdated();
+
+        Thread.sleep(120000); // Sleep for two minutes
+      }
+    }
+    catch(StorageBackendException ex)
+    {
+      ex.printStackTrace();
+    }
+    catch(InterruptedException ex)
+    {
+      Log.get().warning("Purger interrupted: " + ex);
+    }
+  }
+
+  private void purgeDeleted()
+    throws StorageBackendException
+  {
+    List<Channel> groups = StorageManager.current().getGroups();
+    for(Channel channel : groups)
+    {
+      if(!(channel instanceof Group))
+        continue;
+      
+      Group group = (Group)channel;
+      // Look for groups that are marked as deleted
+      if(group.isDeleted())
+      {
+        List<Long> ids = StorageManager.current().getArticleNumbers(group.getInternalID());
+        if(ids.size() == 0)
+        {
+          StorageManager.current().purgeGroup(group);
+          Log.get().info("Group " + group.getName() + " purged.");
+        }
+
+        for(int n = 0; n < ids.size() && n < 10; n++)
+        {
+          Article art = StorageManager.current().getArticle(ids.get(n), group.getInternalID());
+          StorageManager.current().delete(art.getMessageID());
+          Log.get().info("Article " + art.getMessageID() + " purged.");
+        }
+      }
+    }
+  }
+
+  private void purgeOutdated()
+    throws InterruptedException, StorageBackendException
+  {
+    long articleMaximum =
+      Config.inst().get("sonews.article.maxnum", Long.MAX_VALUE);
+    long lifetime =
+      Config.inst().get("sonews.article.lifetime", -1);
+
+    if(lifetime > 0 || articleMaximum < Stats.getInstance().getNumberOfNews())
+    {
+      Log.get().info("Purging old messages...");
+      String mid = StorageManager.current().getOldestArticle();
+      if (mid == null) // No articles in the database
+      {
+        return;
+      }
+
+      Article art = StorageManager.current().getArticle(mid);
+      long artDate = 0;
+      String dateStr = art.getHeader(Headers.DATE)[0];
+      try
+      {
+        artDate = Date.parse(dateStr) / 1000 / 60 / 60 / 24;
+      }
+      catch (IllegalArgumentException ex)
+      {
+        Log.get().warning("Could not parse date string: " + dateStr + " " + ex);
+      }
+
+      // Should we delete the message because of its age or because the
+      // article maximum was reached?
+      if (lifetime < 0 || artDate < (new Date().getTime() + lifetime))
+      {
+        StorageManager.current().delete(mid);
+        System.out.println("Deleted: " + mid);
+      }
+      else
+      {
+        Thread.sleep(1000 * 60); // Wait 60 seconds
+        return;
+      }
+    }
+    else
+    {
+      Log.get().info("Lifetime purger is disabled");
+      Thread.sleep(1000 * 60 * 30); // Wait 30 minutes
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/Stats.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/Stats.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,206 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.util;
+
+import java.util.Calendar;
+import org.sonews.config.Config;
+import org.sonews.storage.Channel;
+import org.sonews.storage.StorageBackendException;
+import org.sonews.storage.StorageManager;
+
+/**
+ * Class that capsulates statistical data gathering.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public final class Stats 
+{
+      
+  public static final byte CONNECTIONS    = 1;
+  public static final byte POSTED_NEWS    = 2;
+  public static final byte GATEWAYED_NEWS = 3;
+  public static final byte FEEDED_NEWS    = 4;
+  public static final byte MLGW_RUNSTART  = 5;
+  public static final byte MLGW_RUNEND    = 6;
+
+  private static Stats instance = new Stats();
+  
+  public static Stats getInstance()
+  {
+    return Stats.instance;
+  }
+  
+  private Stats() {}
+  
+  private volatile int connectedClients = 0;
+
+  /**
+   * A generic method that writes event data to the storage backend.
+   * If event logging is disabled with sonews.eventlog=false this method
+   * simply does nothing.
+   * @param type
+   * @param groupname
+   */
+  private void addEvent(byte type, String groupname)
+  {
+    try
+    {
+      if (Config.inst().get(Config.EVENTLOG, true))
+      {
+
+        Channel group = Channel.getByName(groupname);
+        if (group != null)
+        {
+          StorageManager.current().addEvent(
+                  System.currentTimeMillis(), type, group.getInternalID());
+        }
+      } 
+      else
+      {
+        Log.get().info("Group " + groupname + " does not exist.");
+      }
+    } 
+    catch (StorageBackendException ex)
+    {
+      ex.printStackTrace();
+    }
+  }
+  
+  public void clientConnect()
+  {
+    this.connectedClients++;
+  }
+  
+  public void clientDisconnect()
+  {
+    this.connectedClients--;
+  }
+  
+  public int connectedClients()
+  {
+    return this.connectedClients;
+  }
+  
+  public int getNumberOfGroups()
+  {
+    try
+    {
+      return StorageManager.current().countGroups();
+    }
+    catch(StorageBackendException ex)
+    {
+      ex.printStackTrace();
+      return -1;
+    }
+  }
+  
+  public int getNumberOfNews()
+  {
+    try
+    {
+      return StorageManager.current().countArticles();
+    }
+    catch(StorageBackendException ex)
+    {
+      ex.printStackTrace();
+      return -1;
+    }
+  }
+  
+  public int getYesterdaysEvents(final byte eventType, final int hour,
+    final Channel group)
+  {
+    // Determine the timestamp values for yesterday and the given hour
+    Calendar cal = Calendar.getInstance();
+    int year  = cal.get(Calendar.YEAR);
+    int month = cal.get(Calendar.MONTH);
+    int dayom = cal.get(Calendar.DAY_OF_MONTH) - 1; // Yesterday
+    
+    cal.set(year, month, dayom, hour, 0, 0);
+    long startTimestamp = cal.getTimeInMillis();
+    
+    cal.set(year, month, dayom, hour + 1, 0, 0);
+    long endTimestamp = cal.getTimeInMillis();
+    
+    try
+    {
+      return StorageManager.current()
+        .getEventsCount(eventType, startTimestamp, endTimestamp, group);
+    }
+    catch(StorageBackendException ex)
+    {
+      ex.printStackTrace();
+      return -1;
+    }
+  }
+  
+  public void mailPosted(String groupname)
+  {
+    addEvent(POSTED_NEWS, groupname);
+  }
+  
+  public void mailGatewayed(String groupname)
+  {
+    addEvent(GATEWAYED_NEWS, groupname);
+  }
+  
+  public void mailFeeded(String groupname)
+  {
+    addEvent(FEEDED_NEWS, groupname);
+  }
+  
+  public void mlgwRunStart()
+  {
+    addEvent(MLGW_RUNSTART, "control");
+  }
+  
+  public void mlgwRunEnd()
+  {
+    addEvent(MLGW_RUNEND, "control");
+  }
+  
+  private double perHour(int key, long gid)
+  {
+    try
+    {
+      return StorageManager.current().getEventsPerHour(key, gid);
+    }
+    catch(StorageBackendException ex)
+    {
+      ex.printStackTrace();
+      return -1;
+    }
+  }
+  
+  public double postedPerHour(long gid)
+  {
+    return perHour(POSTED_NEWS, gid);
+  }
+  
+  public double gatewayedPerHour(long gid)
+  {
+    return perHour(GATEWAYED_NEWS, gid);
+  }
+  
+  public double feededPerHour(long gid)
+  {
+    return perHour(FEEDED_NEWS, gid);
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/StringTemplate.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/StringTemplate.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,97 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.util;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Class that allows simple String template handling.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class StringTemplate 
+{
+
+  private String              str               = null;
+  private String              templateDelimiter = "%";
+  private Map<String, String> templateValues    = new HashMap<String, String>();
+  
+  public StringTemplate(String str, final String templateDelimiter)
+  {
+    if(str == null || templateDelimiter == null)
+    {
+      throw new IllegalArgumentException("null arguments not allowed");
+    }
+
+    this.str               = str;
+    this.templateDelimiter = templateDelimiter;
+  }
+  
+  public StringTemplate(String str)
+  {
+    this(str, "%");
+  }
+  
+  public StringTemplate set(String template, String value)
+  {
+    if(template == null || value == null)
+    {
+      throw new IllegalArgumentException("null arguments not allowed");
+    }
+    
+    this.templateValues.put(template, value);
+    return this;
+  }
+  
+  public StringTemplate set(String template, long value)
+  {
+    return set(template, Long.toString(value));
+  }
+  
+  public StringTemplate set(String template, double value)
+  {
+    return set(template, Double.toString(value));
+  }
+  
+  public StringTemplate set(String template, Object obj)
+  {
+    if(template == null || obj == null)
+    {
+      throw new IllegalArgumentException("null arguments not allowed");
+    }
+
+    return set(template, obj.toString());
+  }
+  
+  @Override
+  public String toString()
+  {
+    String ret = str;
+
+    for(String key : this.templateValues.keySet())
+    {
+      String value = this.templateValues.get(key);
+      ret = ret.replace(templateDelimiter + key, value);
+    }
+    
+    return ret;
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/TimeoutMap.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/TimeoutMap.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,145 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.util;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Implementation of a Map that will loose its stored values after a 
+ * configurable amount of time.
+ * This class may be used to cache config values for example.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class TimeoutMap<K,V> extends ConcurrentHashMap<K, V>
+{
+  
+  private static final long serialVersionUID = 453453467700345L;
+
+  private int                    timeout     = 60000; // 60 sec
+  private transient Map<K, Long> timeoutMap  = new HashMap<K, Long>();
+  
+  /**
+   * Constructor.
+   * @param timeout Timeout in milliseconds
+   */
+  public TimeoutMap(final int timeout)
+  {
+    this.timeout = timeout;
+  }
+  
+  /**
+   * Uses default timeout (60 sec).
+   */
+  public TimeoutMap()
+  {
+  }
+  
+  /**
+   * 
+   * @param key
+   * @return true if key is still valid.
+   */
+  protected boolean checkTimeOut(Object key)
+  {
+    synchronized(this.timeoutMap)
+    {
+      if(this.timeoutMap.containsKey(key))
+      {
+        long keytime = this.timeoutMap.get(key);
+        if((System.currentTimeMillis() - keytime) < this.timeout)
+        {
+          return true;
+        }
+        else
+        {
+          remove(key);
+          return false;
+        }
+      }
+      else
+      {
+        return false;
+      }
+    }
+  }
+  
+  @Override
+  public boolean containsKey(Object key)
+  {
+    return checkTimeOut(key);
+  }
+
+  @Override
+  public synchronized V get(Object key)
+  {
+    if(checkTimeOut(key))
+    {
+      return super.get(key);
+    }
+    else
+    {
+      return null;
+    }
+  }
+
+  @Override
+  public V put(K key, V value)
+  {
+    synchronized(this.timeoutMap)
+    {
+      removeStaleKeys();
+      this.timeoutMap.put(key, System.currentTimeMillis());
+      return super.put(key, value);
+    }
+  }
+
+  /**
+   * @param arg0
+   * @return
+   */
+  @Override
+  public V remove(Object arg0)
+  {
+    synchronized(this.timeoutMap)
+    {
+      this.timeoutMap.remove(arg0);
+      V val = super.remove(arg0);
+      return val;
+    }
+  }
+
+  protected void removeStaleKeys()
+  {
+    synchronized(this.timeoutMap)
+    {
+      Set<Object> keySet = new HashSet<Object>(this.timeoutMap.keySet());
+      for(Object key : keySet)
+      {
+        // The key/value is removed by the checkTimeOut() method if true
+        checkTimeOut(key);
+      }
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/io/ArticleInputStream.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/io/ArticleInputStream.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,71 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.util.io;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import org.sonews.storage.Article;
+
+/**
+ * Capsulates an Article to provide a raw InputStream.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class ArticleInputStream extends InputStream
+{
+
+  private byte[] buf;
+  private int    pos = 0;
+  
+  public ArticleInputStream(final Article art)
+    throws IOException, UnsupportedEncodingException
+  {
+    final ByteArrayOutputStream out = new ByteArrayOutputStream();
+    out.write(art.getHeaderSource().getBytes("UTF-8"));
+    out.write("\r\n\r\n".getBytes());
+    out.write(art.getBody()); // Without CRLF
+    out.flush();
+    this.buf = out.toByteArray();
+  }
+
+  /**
+   * This method reads one byte from the stream.  The <code>pos</code>
+   * counter is advanced to the next byte to be read.  The byte read is
+   * returned as an int in the range of 0-255.  If the stream position
+   * is already at the end of the buffer, no byte is read and a -1 is
+   * returned in order to indicate the end of the stream.
+   *
+   * @return The byte read, or -1 if end of stream
+   */
+  @Override
+  public synchronized int read()
+  {
+    if(pos < buf.length)
+    {
+      return ((int)buf[pos++]) & 0xFF;
+    }
+    else
+    {
+      return -1;
+    }
+  }
+  
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/io/ArticleReader.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/io/ArticleReader.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,135 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.util.io;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import org.sonews.config.Config;
+import org.sonews.util.Log;
+
+/**
+ * Reads an news article from a NNTP server.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class ArticleReader 
+{
+
+  private BufferedOutputStream out;
+  private BufferedInputStream  in;
+  private String               messageID;
+  
+  public ArticleReader(String host, int port, String messageID)
+    throws IOException, UnknownHostException
+  {
+    this.messageID = messageID;
+
+    // Connect to NNTP server
+    Socket socket = new Socket(host, port);
+    this.out = new BufferedOutputStream(socket.getOutputStream());
+    this.in  = new BufferedInputStream(socket.getInputStream());
+    String line = readln(this.in);
+    if(!line.startsWith("200 "))
+    {
+      throw new IOException("Invalid hello from server: " + line);
+    }
+  }
+  
+  private boolean eofArticle(byte[] buf)
+  {
+    if(buf.length < 4)
+    {
+      return false;
+    }
+    
+    int l = buf.length - 1;
+    return buf[l-3] == 10 // '*\n'
+        && buf[l-2] == '.'                   // '.'
+        && buf[l-1] == 13 && buf[l] == 10;  // '\r\n'
+  }
+  
+  public byte[] getArticleData()
+    throws IOException, UnsupportedEncodingException
+  {
+    long maxSize = Config.inst().get(Config.ARTICLE_MAXSIZE, 1024) * 1024L;
+
+    try
+    {
+      this.out.write(("ARTICLE " + this.messageID + "\r\n").getBytes("UTF-8"));
+      this.out.flush();
+
+      String line = readln(this.in);
+      if(line.startsWith("220 "))
+      {
+        ByteArrayOutputStream buf = new ByteArrayOutputStream();
+        
+        while(!eofArticle(buf.toByteArray()))
+        {
+          for(int b = in.read(); b != 10; b = in.read())
+          {
+            buf.write(b);
+          }
+
+          buf.write(10);
+          if(buf.size() > maxSize)
+          {
+            Log.get().warning("Skipping message that is too large: " + buf.size());
+            return null;
+          }
+        }
+        
+        return buf.toByteArray();
+      }
+      else
+      {
+        Log.get().warning("ArticleReader: " + line);
+        return null;
+      }
+    }
+    catch(IOException ex)
+    {
+      throw ex;
+    }
+    finally
+    {
+      this.out.write("QUIT\r\n".getBytes("UTF-8"));
+      this.out.flush();
+      this.out.close();
+    }
+  }
+  
+  private String readln(InputStream in)
+    throws IOException
+  {
+    ByteArrayOutputStream buf = new ByteArrayOutputStream();
+    for(int b = in.read(); b != 10 /* \n */; b = in.read())
+    {
+      buf.write(b);
+    }
+    
+    return new String(buf.toByteArray());
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/io/ArticleWriter.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/io/ArticleWriter.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,133 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.util.io;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import org.sonews.storage.Article;
+
+/**
+ * Posts an Article to a NNTP server using the POST command.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public class ArticleWriter 
+{
+  
+  private BufferedOutputStream out;
+  private BufferedReader       inr;
+
+  public ArticleWriter(String host, int port)
+    throws IOException, UnknownHostException
+  {
+    // Connect to NNTP server
+    Socket socket = new Socket(host, port);
+    this.out = new BufferedOutputStream(socket.getOutputStream());
+    this.inr = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+    String line = inr.readLine();
+    if(line == null || !line.startsWith("200 "))
+    {
+      throw new IOException("Invalid hello from server: " + line);
+    }
+  }
+  
+  public void close()
+    throws IOException, UnsupportedEncodingException
+  {
+    this.out.write("QUIT\r\n".getBytes("UTF-8"));
+    this.out.flush();
+  }
+
+  protected void finishPOST()
+    throws IOException
+  {
+    this.out.write("\r\n.\r\n".getBytes());
+    this.out.flush();
+    String line = inr.readLine();
+    if(line == null || !line.startsWith("240 ") || !line.startsWith("441 "))
+    {
+      throw new IOException(line);
+    }
+  }
+
+  protected void preparePOST()
+    throws IOException
+  {
+    this.out.write("POST\r\n".getBytes("UTF-8"));
+    this.out.flush();
+
+    String line = this.inr.readLine();
+    if(line == null || !line.startsWith("340 "))
+    {
+      throw new IOException(line);
+    }
+  }
+
+  public void writeArticle(Article article)
+    throws IOException, UnsupportedEncodingException
+  {
+    byte[] buf = new byte[512];
+    ArticleInputStream in = new ArticleInputStream(article);
+
+    preparePOST();
+    
+    int len = in.read(buf);
+    while(len != -1)
+    {
+      writeLine(buf, len);
+      len = in.read(buf);
+    }
+
+    finishPOST();
+  }
+
+  /**
+   * Writes the raw content of an article to the remote server. This method
+   * does no charset conversion/handling of any kind so its the preferred
+   * method for sending an article to remote peers.
+   * @param rawArticle
+   * @throws IOException
+   */
+  public void writeArticle(byte[] rawArticle)
+    throws IOException
+  {
+    preparePOST();
+    writeLine(rawArticle, rawArticle.length);
+    finishPOST();
+  }
+
+  /**
+   * Writes the given buffer to the connect remote server.
+   * @param buffer
+   * @param len
+   * @throws IOException
+   */
+  protected void writeLine(byte[] buffer, int len)
+    throws IOException
+  {
+    this.out.write(buffer, 0, len);
+    this.out.flush();
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/io/Resource.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/io/Resource.java	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,132 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sonews.util.io;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.nio.charset.Charset;
+
+/**
+ * Provides method for loading of resources.
+ * @author Christian Lins
+ * @since sonews/0.5.0
+ */
+public final class Resource
+{
+  
+  /**
+   * Loads a resource and returns it as URL reference.
+   * The Resource's classloader is used to load the resource, not
+   * the System's ClassLoader so it may be safe to use this method
+   * in a sandboxed environment.
+   * @return
+   */
+  public static URL getAsURL(final String name)
+  {
+    if(name == null)
+    {
+      return null;
+    }
+
+    return Resource.class.getClassLoader().getResource(name);
+  }
+  
+  /**
+   * Loads a resource and returns an InputStream to it.
+   * @param name
+   * @return
+   */
+  public static InputStream getAsStream(String name)
+  {
+    try
+    {
+      URL url = getAsURL(name);
+      if(url == null)
+      {
+        return null;
+      }
+      else
+      {
+        return url.openStream();
+      }
+    }
+    catch(IOException e)
+    {
+      e.printStackTrace();
+      return null;
+    }
+  }
+
+  /**
+   * Loads a plain text resource.
+   * @param withNewline If false all newlines are removed from the 
+   * return String
+   */
+  public static String getAsString(String name, boolean withNewline)
+  {
+    if(name == null)
+      return null;
+
+    BufferedReader in = null;
+    try
+    {
+      InputStream ins = getAsStream(name);
+      if(ins == null)
+        return null;
+
+      in = new BufferedReader(
+        new InputStreamReader(ins, Charset.forName("UTF-8")));
+      StringBuffer buf = new StringBuffer();
+
+      for(;;)
+      {
+        String line = in.readLine();
+        if(line == null)
+          break;
+
+        buf.append(line);
+        if(withNewline)
+          buf.append('\n');
+      }
+
+      return buf.toString();
+    }
+    catch(Exception e)
+    {
+      e.printStackTrace();
+      return null;
+    }
+    finally
+    {
+      try
+      {
+        if(in != null)
+          in.close();
+      }
+      catch(IOException ex)
+      {
+        ex.printStackTrace();
+      }
+    }
+  }
+
+}
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/io/package.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/io/package.html	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,1 @@
+Contains I/O utilitiy classes.
\ No newline at end of file
diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/package.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/package.html	Sun Aug 29 17:28:58 2010 +0200
@@ -0,0 +1,1 @@
+Contains various utility classes.
\ No newline at end of file