# HG changeset patch # User cli # Date 1283098657 -7200 # Node ID 74139325d3051a756dc438d794d1c789bb85c674 # Parent c404a87db5b7483e418e175170066fd01faebf40 Switch intent style to Original K&R / Linux / Kernel. diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/Main.java --- a/src/org/sonews/Main.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/Main.java Sun Aug 29 18:17:37 2010 +0200 @@ -44,155 +44,123 @@ */ 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"); + /** Version information of the sonews daemon */ + public static final String VERSION = "sonews/1.1.0"; + public static final Date STARTDATE = new Date(); - // 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 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(); - } + /** + * The main entrypoint. + * @param args + * @throws Exception + */ + public static void main(String[] args) throws Exception + { + System.out.println(VERSION); + Thread.currentThread().setName("Mainthread"); - 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); - } + // 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 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); + } + + private Main() + { + } } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/ShutdownHook.java --- a/src/org/sonews/ShutdownHook.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/ShutdownHook.java Sun Aug 29 18:17:37 2010 +0200 @@ -30,55 +30,44 @@ class ShutdownHook extends Thread { - /** - * Called when the JVM exits. - */ - @Override - public void run() - { - System.out.println("sonews: Trying to shutdown all threads..."); + /** + * Called when the JVM exits. + */ + @Override + public void run() + { + System.out.println("sonews: Trying to shutdown all threads..."); - Map 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."); - } - + Map 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 c404a87db5b7 -r 74139325d305 src/org/sonews/acl/AccessControl.java --- a/src/org/sonews/acl/AccessControl.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/acl/AccessControl.java Sun Aug 29 18:17:37 2010 +0200 @@ -26,6 +26,5 @@ public interface AccessControl { - boolean hasPermission(String user, char[] secret, String permission); - + boolean hasPermission(String user, char[] secret, String permission); } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/acl/AuthInfoCommand.java --- a/src/org/sonews/acl/AuthInfoCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/acl/AuthInfoCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -31,34 +31,34 @@ public class AuthInfoCommand implements Command { - @Override - public String[] getSupportedCommandStrings() - { - throw new UnsupportedOperationException("Not supported yet."); - } + @Override + public String[] getSupportedCommandStrings() + { + throw new UnsupportedOperationException("Not supported yet."); + } - @Override - public boolean hasFinished() - { - 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 String impliedCapability() + { + throw new UnsupportedOperationException("Not supported yet."); + } - @Override - public boolean isStateful() - { - 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."); - } - + @Override + public void processLine(NNTPConnection conn, String line, byte[] rawLine) + throws IOException, StorageBackendException + { + throw new UnsupportedOperationException("Not supported yet."); + } } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/config/AbstractConfig.java --- a/src/org/sonews/config/AbstractConfig.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/config/AbstractConfig.java Sun Aug 29 18:17:37 2010 +0200 @@ -23,35 +23,34 @@ * @author Christian Lins * @since sonews/0.5.0 */ -public abstract class AbstractConfig +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); - } + public abstract String get(String key, String defVal); - protected abstract void set(String key, String val); - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/config/BackendConfig.java --- a/src/org/sonews/config/BackendConfig.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/config/BackendConfig.java Sun Aug 29 18:17:37 2010 +0200 @@ -33,83 +33,67 @@ class BackendConfig extends AbstractConfig { - private static BackendConfig instance = new BackendConfig(); - - public static BackendConfig getInstance() - { - return instance; - } - - private final TimeoutMap values - = new TimeoutMap(); - - 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; - } + private static BackendConfig instance = new BackendConfig(); - 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(); - } - } - + public static BackendConfig getInstance() + { + return instance; + } + private final TimeoutMap values = new TimeoutMap(); + + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/config/CommandLineConfig.java --- a/src/org/sonews/config/CommandLineConfig.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/config/CommandLineConfig.java Sun Aug 29 18:17:37 2010 +0200 @@ -28,37 +28,34 @@ class CommandLineConfig extends AbstractConfig { - private static final CommandLineConfig instance = new CommandLineConfig(); + private static final CommandLineConfig instance = new CommandLineConfig(); - public static CommandLineConfig getInstance() - { - return instance; - } + public static CommandLineConfig getInstance() + { + return instance; + } + private final Map values = new HashMap(); - private final Map values = new HashMap(); - - private CommandLineConfig() {} + 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 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); - } - } - + @Override + public void set(String key, String val) + { + synchronized (this.values) { + this.values.put(key, val); + } + } } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/config/Config.java --- a/src/org/sonews/config/Config.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/config/Config.java Sun Aug 29 18:17:37 2010 +0200 @@ -25,151 +25,132 @@ */ 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"; + 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(); - /** 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"; + public static Config inst() + { + return instance; + } - /** Key constant. Value is classname of the JDBC driver */ - public static final String STORAGE_DBMSDRIVER = "sonews.storage.dbmsdriver"; + private Config() + { + } - /** Key constant. Value is JDBC connect String to the database. */ - public static final String STORAGE_DATABASE = "sonews.storage.database"; + @Override + public String get(String key, String def) + { + String val = CommandLineConfig.getInstance().get(key, null); - /** Key constant. Value is the username for the DBMS. */ - public static final String STORAGE_USER = "sonews.storage.user"; + if (val == null) { + val = FileConfig.getInstance().get(key, null); + } - /** Key constant. Value is the password for the DBMS. */ - public static final String STORAGE_PASSWORD = "sonews.storage.password"; + if (val == null) { + val = BackendConfig.getInstance().get(key, def); + } - /** 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"; + return val; + } - /** The config key for the filename of the logfile */ - public static final String LOGFILE = "sonews.log"; + public String get(int maxLevel, String key, String def) + { + String val = CommandLineConfig.getInstance().get(key, null); - 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 - }; + if (val == null && maxLevel >= LEVEL_FILE) { + val = FileConfig.getInstance().get(key, null); + if (val == null && maxLevel >= LEVEL_BACKEND) { + val = BackendConfig.getInstance().get(key, def); + } + } - private static Config instance = new Config(); - - public static Config inst() - { - return instance; - } - - private Config(){} + return val != null ? val : def; + } - @Override - public String get(String key, String def) - { - String val = CommandLineConfig.getInstance().get(key, null); - - if(val == null) - { - val = FileConfig.getInstance().get(key, null); - } + @Override + public void set(String key, String val) + { + set(LEVEL_BACKEND, key, val); + } - 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; - } - } - } - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/config/FileConfig.java --- a/src/org/sonews/config/FileConfig.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/config/FileConfig.java Sun Aug 29 18:17:37 2010 +0200 @@ -35,136 +35,120 @@ 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; - } + private static final Properties defaultConfig = new Properties(); + private static FileConfig instance = null; - // Every config instance is initialized with the default values. - private final Properties settings = (Properties)defaultConfig.clone(); + 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"); + } - /** - * 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(); - } - } + /** + * 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(); - /** - * 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(); - } - } + /** + * 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(); + } + } - /** - * 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); - } + /** + * 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; - /** - * 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); - } + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/AbstractDaemon.java --- a/src/org/sonews/daemon/AbstractDaemon.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/AbstractDaemon.java Sun Aug 29 18:17:37 2010 +0200 @@ -32,70 +32,63 @@ public abstract class AbstractDaemon extends Thread { - /** This variable is write synchronized through setRunning */ - private boolean isRunning = false; + /** 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(); - } - + /** + * 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/ChannelLineBuffers.java --- a/src/org/sonews/daemon/ChannelLineBuffers.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/ChannelLineBuffers.java Sun Aug 29 18:17:37 2010 +0200 @@ -30,254 +30,223 @@ * @author Christian Lins * @since sonews/0.5.0 */ -public class ChannelLineBuffers +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 freeSmallBuffers - = new ArrayList(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 outputBuffers = new ArrayList(); - - /** - * 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; + /** + * 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 freeSmallBuffers = new ArrayList(maxCachedBuffers); - // Mark the current write position - int mark = buffer.position(); + /** + * 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 outputBuffers = new ArrayList(); - // Set position to 0 and limit to current position - buffer.flip(); + /** + * 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(); + } - ByteBuffer lineBuffer = newLineBuffer(); + synchronized (outputBuffers) { + outputBuffers.add(buffer); + } + } - 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(); + /** + * 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; + } - lineBuffer.flip(); // limit to position, position to 0 - return lineBuffer; - } - else - { - lineBuffer.put(b); - } - } + /** + * 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; + } + } + } - buffer.limit(BUFFER_SIZE); - buffer.position(mark); + /** + * @return false if there are output buffers pending to be written to the + * client. + */ + boolean isOutputBufferEmpty() + { + synchronized (outputBuffers) { + return outputBuffers.isEmpty(); + } + } - 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; + /** + * 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; + } - 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; - } - } - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/ChannelReader.java --- a/src/org/sonews/daemon/ChannelReader.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/ChannelReader.java Sun Aug 29 18:17:37 2010 +0200 @@ -37,166 +37,141 @@ class ChannelReader extends AbstractDaemon { - private static ChannelReader instance = new ChannelReader(); + 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; + /** + * @return Active ChannelReader instance. + */ + public static ChannelReader getInstance() + { + return instance; + } + private Selector 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(); + protected ChannelReader() + { + } - // 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 selKeys = selector.selectedKeys(); - SelectionKey selKey = null; + /** + * 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; + } - synchronized (selKeys) - { - Iterator it = selKeys.iterator(); + /** + * Run loop. Blocks until some data is available in a channel. + */ + @Override + public void run() + { + assert selector != null; - // Process the first pending event - while (it.hasNext()) - { - selKey = (SelectionKey) it.next(); - channel = (SocketChannel) selKey.channel(); - conn = Connections.getInstance().get(channel); + 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(); - // 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; - } - } - } + // 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 selKeys = selector.selectedKeys(); + SelectionKey selKey = 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(); - } + synchronized (selKeys) { + Iterator it = selKeys.iterator(); - } - 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()); - } - } - + // 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/ChannelWriter.java --- a/src/org/sonews/daemon/ChannelWriter.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/ChannelWriter.java Sun Aug 29 18:17:37 2010 +0200 @@ -35,176 +35,151 @@ class ChannelWriter extends AbstractDaemon { - private static ChannelWriter instance = new ChannelWriter(); + 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; + /** + * @return Returns the active ChannelWriter instance. + */ + public static ChannelWriter getInstance() + { + return instance; + } + private Selector selector = null; - while(isRunning()) - { - try - { - SelectionKey selKey = null; - SocketChannel socketChannel = null; - NNTPConnection connection = null; + protected ChannelWriter() + { + } - // 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 + /** + * @return Selector associated with this instance. + */ + public Selector getSelector() + { + return this.selector; + } - // 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(); + /** + * Sets the selector that is used by this ChannelWriter. + * @param selector + */ + public void setSelector(final Selector selector) + { + this.selector = selector; + } - 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); + /** + * Run loop. + */ + @Override + public void run() + { + assert selector != null; - 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); + while (isRunning()) { + try { + SelectionKey selKey = null; + SocketChannel socketChannel = null; + NNTPConnection connection = null; - // 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(); + // 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 - // 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); + // 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(); - if(socketChannel.socket().isClosed()) - { - connection.shutdownInput(); - connection.shutdownOutput(); - socketChannel.close(); - Log.get().info("Connection closed."); - } - } - } - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/CommandSelector.java --- a/src/org/sonews/daemon/CommandSelector.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/CommandSelector.java Sun Aug 29 18:17:37 2010 +0200 @@ -35,107 +35,84 @@ public class CommandSelector { - private static Map instances - = new ConcurrentHashMap(); - private static Map> commandClassesMapping - = new ConcurrentHashMap>(); + private static Map instances = new ConcurrentHashMap(); + private static Map> commandClassesMapping = new ConcurrentHashMap>(); - static - { - String[] classes = Resource.getAsString("helpers/commands.list", true).split("\n"); - for(String className : classes) - { - if(className.charAt(0) == '#') - { - // Skip comments - continue; - } + 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); - } - } - } + 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 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 getCommandNames() - { - return commandClassesMapping.keySet(); - } + public static Set 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; - } + 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 commandMapping = new HashMap(); + private Command unsupportedCmd = new UnsupportedCommand(); - private Map commandMapping = new HashMap(); - private Command unsupportedCmd = new UnsupportedCommand(); + private CommandSelector() + { + } - private CommandSelector() - {} + public Command get(String commandName) + { + try { + commandName = commandName.toUpperCase(); + Command cmd = this.commandMapping.get(commandName); - 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(); + } - 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; - } - } - + return cmd; + } catch (Exception ex) { + ex.printStackTrace(); + return this.unsupportedCmd; + } + } } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/ConnectionWorker.java --- a/src/org/sonews/daemon/ConnectionWorker.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/ConnectionWorker.java Sun Aug 29 18:17:37 2010 +0200 @@ -31,72 +31,60 @@ class ConnectionWorker extends AbstractDaemon { - // 256 pending events should be enough - private static ArrayBlockingQueue pendingChannels - = new ArrayBlockingQueue(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(); + // 256 pending events should be enough + private static ArrayBlockingQueue pendingChannels = new ArrayBlockingQueue(256, true); - 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); + /** + * Registers the given channel for further event processing. + * @param channel + */ + public static void addChannel(SocketChannel channel) + throws InterruptedException + { + pendingChannels.put(channel); + } - // 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()) - } - + /** + * 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/Connections.java --- a/src/org/sonews/daemon/Connections.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/Connections.java Sun Aug 29 18:17:37 2010 +0200 @@ -41,141 +41,120 @@ 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 connections - = new ArrayList(); - private final Map connByChannel - = new HashMap(); - - 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); - } - } + private static final Connections instance = new Connections(); - int getConnectionCount(String remote) - { - int cnt = 0; - synchronized(this.connections) - { - for(NNTPConnection conn : this.connections) - { - assert conn != null; - assert conn.getSocketChannel() != null; + /** + * @return Active Connections instance. + */ + public static Connections getInstance() + { + return Connections.instance; + } + private final List connections = new ArrayList(); + private final Map connByChannel = new HashMap(); - 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 iter = this.connections.listIterator(); - NNTPConnection conn; + private Connections() + { + setName("Connections"); + } - 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(); + /** + * 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); + } + } - // 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); - } + /** + * @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); + } + } - // Recycle the used buffers - conn.getBuffers().recycleBuffers(); - - Stats.getInstance().clientDisconnect(); - } - } - } + int getConnectionCount(String remote) + { + int cnt = 0; + synchronized (this.connections) { + for (NNTPConnection conn : this.connections) { + assert conn != null; + assert conn.getSocketChannel() != null; - try - { - Thread.sleep(10000); // Sleep ten seconds - } - catch(InterruptedException ex) - { - Log.get().warning("Connections Thread was interrupted: " + ex.getMessage()); - } - } - } - + 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 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/LineEncoder.java --- a/src/org/sonews/daemon/LineEncoder.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/LineEncoder.java Sun Aug 29 18:17:37 2010 +0200 @@ -33,48 +33,46 @@ 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; + private CharBuffer characters; + private Charset charset; - CoderResult res = encoder.encode(characters, buf, true); + /** + * Constructs new LineEncoder. + * @param characters + * @param charset + */ + public LineEncoder(CharBuffer characters, Charset charset) + { + this.characters = characters; + this.charset = charset; + } - // Set limit to current position and current position to 0; - // means make ready for read from buffer - buf.flip(); - buffer.addOutputBuffer(buf); + /** + * 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; - if (res.isUnderflow()) // All input processed - { - break; - } - } - } - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/NNTPConnection.java --- a/src/org/sonews/daemon/NNTPConnection.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/NNTPConnection.java Sun Aug 29 18:17:37 2010 +0200 @@ -46,383 +46,341 @@ 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"); - } + 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; - 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"); - } + public NNTPConnection(final SocketChannel channel) + throws IOException + { + if (channel == null) { + throw new IllegalArgumentException("channel is null"); + } - void setWriteSelectionKey(SelectionKey selKey) - { - this.writeSelKey = selKey; - } + this.channel = channel; + Stats.getInstance().clientConnect(); + } - 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; - } + /** + * 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; + } + } + } - /** - * @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"); - } + /** + * 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(); + } + } + } - 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; - } + /** + * @return Current input buffer of this NNTPConnection instance. + */ + public ByteBuffer getInputBuffer() + { + return this.lineBuffers.getInputBuffer(); + } - 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"); + /** + * @return Output buffer of this NNTPConnection which has at least one byte + * free storage. + */ + public ByteBuffer getOutputBuffer() + { + return this.lineBuffers.getOutputBuffer(); + } - // 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(); - } - } + /** + * @return ChannelLineBuffers instance associated with this NNTPConnection. + */ + public ChannelLineBuffers getBuffers() + { + return this.lineBuffers; + } - 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); - } + /** + * @return true if this connection comes from a local remote address. + */ + public boolean isLocalConnection() + { + return ((InetSocketAddress) this.channel.socket().getRemoteSocketAddress()).getHostName().equalsIgnoreCase("localhost"); + } - /** - * 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); - } + void setWriteSelectionKey(SelectionKey selKey) + { + this.writeSelKey = selKey; + } - 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; - } + 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); + } + } - // 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; - } + public void shutdownOutput() + { + cancelTimer.schedule(new TimerTask() + { - void setLastActivity(long timestamp) - { - this.lastActivity = timestamp; - } - + @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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/NNTPDaemon.java --- a/src/org/sonews/daemon/NNTPDaemon.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/NNTPDaemon.java Sun Aug 29 18:17:37 2010 +0200 @@ -40,158 +40,130 @@ 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; - } + public static final Object RegisterGate = new Object(); + private static NNTPDaemon instance = null; - @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()); + 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; - // 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); - } - } - } - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/ArticleCommand.java --- a/src/org/sonews/daemon/command/ArticleCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/ArticleCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -33,142 +33,120 @@ public class ArticleCommand implements Command { - @Override - public String[] getSupportedCommandStrings() - { - return new String[] {"ARTICLE", "BODY", "HEAD"}; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"ARTICLE", "BODY", "HEAD"}; + } - @Override - public boolean hasFinished() - { - return true; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public String impliedCapability() - { - return null; - } + @Override + public String impliedCapability() + { + return null; + } - @Override - public boolean isStateful() - { - return false; - } + @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(); - } + // 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(" "); - if (article == null) - { - conn.println("423 no such article number in this group"); - return; - } - conn.setCurrentArticle(article); - } + 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; + } - 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("."); - } - } - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/CapabilitiesCommand.java --- a/src/org/sonews/daemon/command/CapabilitiesCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/CapabilitiesCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -42,52 +42,49 @@ 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 - }; + 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"}; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"CAPABILITIES"}; + } - /** - * First called after one call to processLine(). - * @return - */ - @Override - public boolean hasFinished() - { - return true; - } + /** + * 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 String impliedCapability() + { + return null; + } - @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("."); - } + @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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/Command.java --- a/src/org/sonews/daemon/command/Command.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/Command.java Sun Aug 29 18:17:37 2010 +0200 @@ -30,22 +30,21 @@ public interface Command { - /** - * @return true if this instance can be reused. - */ - boolean hasFinished(); + /** + * @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(); + /** + * 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(); + boolean isStateful(); - String[] getSupportedCommandStrings(); + String[] getSupportedCommandStrings(); - void processLine(NNTPConnection conn, String line, byte[] rawLine) - throws IOException, StorageBackendException; - + void processLine(NNTPConnection conn, String line, byte[] rawLine) + throws IOException, StorageBackendException; } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/GroupCommand.java --- a/src/org/sonews/daemon/command/GroupCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/GroupCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -48,55 +48,48 @@ public class GroupCommand implements Command { - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"GROUP"}; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"GROUP"}; + } - @Override - public boolean hasFinished() - { - return true; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public String impliedCapability() - { - return null; - } + @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(" "); + @Override + public boolean isStateful() + { + return true; + } - 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"); - } - } + @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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/HelpCommand.java --- a/src/org/sonews/daemon/command/HelpCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/HelpCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -35,66 +35,56 @@ public class HelpCommand implements Command { - @Override - public boolean hasFinished() - { - return true; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public String impliedCapability() - { - return null; - } + @Override + public String impliedCapability() + { + return null; + } - @Override - public boolean isStateful() - { - return true; - } + @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"); + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"HELP"}; + } - if(line.length() <= 1) - { - final String[] help = Resource - .getAsString("helpers/helptext", true).split("\n"); - for(String hstr : help) - { - conn.println(hstr); - } + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException + { + final String[] command = line.split(" "); + conn.println("100 help text follows"); - Set 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("."); - } - + if (line.length() <= 1) { + final String[] help = Resource.getAsString("helpers/helptext", true).split("\n"); + for (String hstr : help) { + conn.println(hstr); + } + + Set 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/HelpfulCommand.java --- a/src/org/sonews/daemon/command/HelpfulCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/HelpfulCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -26,10 +26,9 @@ public interface HelpfulCommand extends Command { - /** - * @return A short description of this command, that is - * used within the output of the HELP command. - */ - String getHelpString(); - + /** + * @return A short description of this command, that is + * used within the output of the HELP command. + */ + String getHelpString(); } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/ListCommand.java --- a/src/org/sonews/daemon/command/ListCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/ListCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -37,117 +37,93 @@ public class ListCommand implements Command { - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"LIST"}; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"LIST"}; + } - @Override - public boolean hasFinished() - { - return true; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public String impliedCapability() - { - return null; - } + @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 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); - } - } + @Override + public boolean isStateful() + { + return false; + } - private void printGroupInfo(NNTPConnection conn, String pattern) - throws IOException, StorageBackendException - { - final List 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"); - } - } + @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 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 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/ListGroupCommand.java --- a/src/org/sonews/daemon/command/ListGroupCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/ListGroupCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -33,62 +33,56 @@ public class ListGroupCommand implements Command { - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"LISTGROUP"}; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"LISTGROUP"}; + } - @Override - public boolean hasFinished() - { - return true; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public String impliedCapability() - { - return null; - } + @Override + public String impliedCapability() + { + return null; + } - @Override - public boolean isStateful() - { - return false; - } + @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(" "); + @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(); - } + 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 command"); - return; - } + if (group == null) { + conn.println("412 no group selected; use GROUP command"); + return; + } - List 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("."); - } - + List 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/ModeReaderCommand.java --- a/src/org/sonews/daemon/command/ModeReaderCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/ModeReaderCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -30,43 +30,39 @@ */ public class ModeReaderCommand implements Command { - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"MODE"}; - } - @Override - public boolean hasFinished() - { - return true; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"MODE"}; + } - @Override - public String impliedCapability() - { - return null; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public boolean isStateful() - { - return false; - } + @Override + public String impliedCapability() + { + return null; + } - @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"); - } - } + @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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/NewGroupsCommand.java --- a/src/org/sonews/daemon/command/NewGroupsCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/NewGroupsCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -31,48 +31,44 @@ public class NewGroupsCommand implements Command { - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"NEWGROUPS"}; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"NEWGROUPS"}; + } - @Override - public boolean hasFinished() - { - return true; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public String impliedCapability() - { - return null; - } + @Override + public String impliedCapability() + { + return null; + } - @Override - public boolean isStateful() - { - return false; - } + @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(" "); + @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"); + 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"); - } - } - + // 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/NextPrevCommand.java --- a/src/org/sonews/daemon/command/NextPrevCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/NextPrevCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -33,84 +33,73 @@ public class NextPrevCommand implements Command { - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"NEXT", "PREV"}; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"NEXT", "PREV"}; + } - @Override - public boolean hasFinished() - { - return true; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public String impliedCapability() - { - return null; - } + @Override + public String impliedCapability() + { + return null; + } - @Override - public boolean isStateful() - { - return false; - } + @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(" "); + @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(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; + if (currA == null) { + conn.println("420 no current article has been selected"); + return; + } - article = grp.getArticle(grp.getIndexOf(article) + delta); + if (currG == null) { + conn.println("412 no newsgroup selected"); + return; + } - 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"); - } - } + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/OverCommand.java --- a/src/org/sonews/daemon/command/OverCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/OverCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -109,186 +109,153 @@ public class OverCommand implements Command { - public static final int MAX_LINES_PER_DBREQUEST = 200; + public static final int MAX_LINES_PER_DBREQUEST = 200; - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"OVER", "XOVER"}; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"OVER", "XOVER"}; + } - @Override - public boolean hasFinished() - { - return true; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public String impliedCapability() - { - return null; - } + @Override + public String impliedCapability() + { + return null; + } - @Override - public boolean isStateful() - { - return false; - } + @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(" "); + @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; - } + // 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(); - } + 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 (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> 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"); - } + 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> 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 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'); + for (Pair article : articleHeads) { + String overview = buildOverview(article.getB(), article.getA()); + conn.println(overview); + } + } // for + conn.println("."); + } + } + } + } - String subject = art.getHeader(Headers.SUBJECT)[0]; - if("".equals(subject)) - { - subject = ""; - } - overview.append(escapeString(subject)); - overview.append('\t'); + private String buildOverview(ArticleHead art, long nr) + { + StringBuilder overview = new StringBuilder(); + overview.append(nr); + 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 subject = art.getHeader(Headers.SUBJECT)[0]; + if ("".equals(subject)) { + subject = ""; + } + overview.append(escapeString(subject)); + overview.append('\t'); - String bytes = art.getHeader(Headers.BYTES)[0]; - if("".equals(bytes)) - { - bytes = "0"; - } - overview.append(escapeString(bytes)); - 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 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])); + String bytes = art.getHeader(Headers.BYTES)[0]; + if ("".equals(bytes)) { + bytes = "0"; + } + overview.append(escapeString(bytes)); + overview.append('\t'); - // 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(); - } - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/PostCommand.java --- a/src/org/sonews/daemon/command/PostCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/PostCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -47,286 +47,237 @@ */ 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"}; - } + 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 boolean hasFinished() - { - return this.state == PostState.Finished; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"POST"}; + } - @Override - public String impliedCapability() - { - return null; - } + @Override + public boolean hasFinished() + { + return this.state == PostState.Finished; + } - @Override - public boolean isStateful() - { - return true; - } + @Override + public String impliedCapability() + { + return null; + } - /** - * 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 ."); - 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()))); + @Override + public boolean isStateful() + { + return true; + } - // 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; - } + /** + * 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 ."); + state = PostState.ReadingHeaders; + } else { + conn.println("500 invalid command usage"); + } + break; + } + case ReadingHeaders: { + strHead.append(line); + strHead.append(NNTPConnection.NEWLINE); - // 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)); + if ("".equals(line) || ".".equals(line)) { + // we finally met the blank line + // separating headers from body - 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 " - { - 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 { + // Parse the header using the InternetHeader class from JavaMail API + headers = new InternetHeaders( + new ByteArrayInputStream(strHead.toString().trim().getBytes(conn.getCurrentCharset()))); - // 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); + // 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; + } - // Log this posting to statistics - Stats.getInstance().mailPosted( - article.getHeader(Headers.NEWSGROUPS)[0]); - } - success = true; - } - } - } // end for + // Change charset for reading body; + // for multipart messages UTF-8 is returned + //conn.setCurrentCharset(article.getBodyCharset()); - 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"); - } - } - } + 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 " + { + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/PostState.java --- a/src/org/sonews/daemon/command/PostState.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/PostState.java Sun Aug 29 18:17:37 2010 +0200 @@ -25,5 +25,6 @@ */ enum PostState { - WaitForLineOne, ReadingHeaders, ReadingBody, Finished + + WaitForLineOne, ReadingHeaders, ReadingBody, Finished } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/QuitCommand.java --- a/src/org/sonews/daemon/command/QuitCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/QuitCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -30,38 +30,37 @@ public class QuitCommand implements Command { - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"QUIT"}; - } - - @Override - public boolean hasFinished() - { - return true; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"QUIT"}; + } - @Override - public String impliedCapability() - { - return null; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public boolean isStateful() - { - return false; - } + @Override + public String impliedCapability() + { + return null; + } - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException, StorageBackendException - { - conn.println("205 cya"); - - conn.shutdownInput(); - conn.shutdownOutput(); - } + @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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/StatCommand.java --- a/src/org/sonews/daemon/command/StatCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/StatCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -31,84 +31,70 @@ public class StatCommand implements Command { - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"STAT"}; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"STAT"}; + } - @Override - public boolean hasFinished() - { - return true; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public String impliedCapability() - { - return null; - } + @Override + public String impliedCapability() + { + return null; + } - @Override - public boolean isStateful() - { - return false; - } + @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(" "); + // 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"); - } - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/UnsupportedCommand.java --- a/src/org/sonews/daemon/command/UnsupportedCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/UnsupportedCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -29,39 +29,38 @@ */ public class UnsupportedCommand implements Command { - - /** - * @return Always returns null. - */ - @Override - public String[] getSupportedCommandStrings() - { - return null; - } - @Override - public boolean hasFinished() - { - return true; - } + /** + * @return Always returns null. + */ + @Override + public String[] getSupportedCommandStrings() + { + return null; + } - @Override - public String impliedCapability() - { - return null; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public boolean isStateful() - { - return false; - } + @Override + public String impliedCapability() + { + return null; + } - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException - { - conn.println("500 command not supported"); - } - + @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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/XDaemonCommand.java --- a/src/org/sonews/daemon/command/XDaemonCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/XDaemonCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -43,228 +43,162 @@ public class XDaemonCommand implements Command { - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"XDAEMON"}; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"XDAEMON"}; + } - @Override - public boolean hasFinished() - { - return true; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public String impliedCapability() - { - return null; - } + @Override + public String impliedCapability() + { + return null; + } - @Override - public boolean isStateful() - { - return false; - } + @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"); - } - } + 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 pull = - StorageManager.current().getSubscriptions(FeedManager.TYPE_PULL); - List 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]); - } + // 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 pull = + StorageManager.current().getSubscriptions(FeedManager.TYPE_PULL); + List 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"); - } - } - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/daemon/command/XPatCommand.java --- a/src/org/sonews/daemon/command/XPatCommand.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/daemon/command/XPatCommand.java Sun Aug 29 18:17:37 2010 +0200 @@ -78,93 +78,79 @@ public class XPatCommand implements Command { - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"XPAT"}; - } - - @Override - public boolean hasFinished() - { - return true; - } + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"XPAT"}; + } - @Override - public String impliedCapability() - { - return null; - } + @Override + public boolean hasFinished() + { + return true; + } - @Override - public boolean isStateful() - { - return false; - } + @Override + public String impliedCapability() + { + return null; + } - @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; - } + @Override + public boolean isStateful() + { + return false; + } - String[] command = line.split("\\p{Space}+"); + @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; + } - // 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]; + String[] command = line.split("\\p{Space}+"); - 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); - } + // 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]; - try - { - List> heads = StorageManager.current(). - getArticleHeaders(conn.getCurrentChannel(), start, end, header, pattern); - - conn.println("221 header follows"); - for(Pair 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"); - } - } + 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> heads = StorageManager.current(). + getArticleHeaders(conn.getCurrentChannel(), start, end, header, pattern); + + conn.println("221 header follows"); + for (Pair 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 c404a87db5b7 -r 74139325d305 src/org/sonews/feed/FeedManager.java --- a/src/org/sonews/feed/FeedManager.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/feed/FeedManager.java Sun Aug 29 18:17:37 2010 +0200 @@ -25,30 +25,30 @@ * @author Christian Lins * @since sonews/0.5.0 */ -public final class FeedManager +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() {} - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/feed/PullFeeder.java --- a/src/org/sonews/feed/PullFeeder.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/feed/PullFeeder.java Sun Aug 29 18:17:37 2010 +0200 @@ -49,228 +49,192 @@ */ class PullFeeder extends AbstractDaemon { - - private Map highMarks = new HashMap(); - private BufferedReader in; - private PrintWriter out; - private Set subscriptions = new HashSet(); - - 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())); + private Map highMarks = new HashMap(); + private BufferedReader in; + private PrintWriter out; + private Set subscriptions = new HashSet(); - String line = in.readLine(); - if(!(line.charAt(0) == '2')) // Could be 200 or 2xx if posting is not allowed - { - throw new IOException(line); - } + private void addSubscription(final Subscription sub) + { + subscriptions.add(sub); - // 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 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 messages = new ArrayList(); - 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..."); + if (!highMarks.containsKey(sub)) { + // Set a initial highMark + this.highMarks.put(sub, 0); + } + } - try - { - this.subscriptions.clear(); - List subsPull = StorageManager.current() - .getSubscriptions(FeedManager.TYPE_PULL); - for(Subscription sub : subsPull) - { - addSubscription(sub); - } - } - catch(StorageBackendException ex) - { - Log.get().log(Level.SEVERE, host, ex); - } + /** + * 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(); - try - { - for(Subscription sub : this.subscriptions) - { - host = sub.getHost(); - port = sub.getPort(); + 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); + } + } - 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 messageIDs = over(oldMark, newMark); + 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())); - 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()); - } - } - } - + 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 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 messages = new ArrayList(); + 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 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 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 c404a87db5b7 -r 74139325d305 src/org/sonews/feed/PushFeeder.java --- a/src/org/sonews/feed/PushFeeder.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/feed/PushFeeder.java Sun Aug 29 18:17:37 2010 +0200 @@ -37,82 +37,65 @@ */ class PushFeeder extends AbstractDaemon { - - private ConcurrentLinkedQueue
articleQueue = - new ConcurrentLinkedQueue
(); - - @Override - public void run() - { - while(isRunning()) - { - try - { - synchronized(this) - { - this.wait(); - } - - List 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; - } + private ConcurrentLinkedQueue
articleQueue = + new ConcurrentLinkedQueue
(); - 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(); - } - } - + @Override + public void run() + { + while (isRunning()) { + try { + synchronized (this) { + this.wait(); + } + + List 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 c404a87db5b7 -r 74139325d305 src/org/sonews/feed/Subscription.java --- a/src/org/sonews/feed/Subscription.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/feed/Subscription.java Sun Aug 29 18:17:37 2010 +0200 @@ -24,61 +24,57 @@ * @author Christian Lins * @since sonews/0.5.0 */ -public class Subscription +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; - } + private String host; + private int port; + private int feedtype; + private String 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; - } - } + public Subscription(String host, int port, int feedtype, String group) + { + this.host = host; + this.port = port; + this.feedtype = feedtype; + this.group = group; + } - @Override - public int hashCode() - { - return host.hashCode() + port + feedtype + group.hashCode(); - } + @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; + } + } - public int getFeedtype() - { - return feedtype; - } + @Override + public int hashCode() + { + return host.hashCode() + port + feedtype + group.hashCode(); + } - public String getGroup() - { - return group; - } + public int getFeedtype() + { + return feedtype; + } - public String getHost() - { - return host; - } + public String getGroup() + { + return group; + } - public int getPort() - { - return port; - } - + public String getHost() + { + return host; + } + + public int getPort() + { + return port; + } } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/mlgw/Dispatcher.java --- a/src/org/sonews/mlgw/Dispatcher.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/mlgw/Dispatcher.java Sun Aug 29 18:17:37 2010 +0200 @@ -43,267 +43,233 @@ * @author Christian Lins * @since sonews/0.5.0 */ -public class Dispatcher +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"); + static class PasswordAuthenticator extends Authenticator + { - return new PasswordAuthentication(username, password); - } - - } + @Override + public PasswordAuthentication getPasswordAuthentication() + { + final String username = + Config.inst().get(Config.MLSEND_USER, "user"); + final String password = + Config.inst().get(Config.MLSEND_PASSWORD, "mysecret"); - /** - * 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 "" - 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; - } - } + return new PasswordAuthentication(username, password); + } + } - /** - * 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 getGroupFor(final Message msg, final boolean fallback) - throws MessagingException, StorageBackendException - { - List groups = null; + /** + * 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 "" + 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; + } + } - // Is there a List-Post header? - String[] listPost = msg.getHeader(Headers.LIST_POST); - InternetAddress listPostAddr; + /** + * 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 getGroupFor(final Message msg, final boolean fallback) + throws MessagingException, StorageBackendException + { + List groups = null; - if(listPost == null || listPost.length == 0 || "".equals(listPost[0])) - { - // Is there a X-List-Post header? - listPost = msg.getHeader(Headers.X_LIST_POST); - } + // Is there a List-Post header? + String[] listPost = msg.getHeader(Headers.LIST_POST); + InternetAddress listPostAddr; - if(listPost != null && listPost.length > 0 - && !"".equals(listPost[0]) && chunkListPost(listPost[0]) != null) - { - // listPost[0] is of form "" - 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(); - // Fallback to TO/CC/BCC addresses - Address[] to = msg.getAllRecipients(); - for(Address toa : to) // Address can have '<' '>' around - { - if(toa instanceof InternetAddress) - { - List 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) - { - if(msg == null) + 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 "" + 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(); + // Fallback to TO/CC/BCC addresses + Address[] to = msg.getAllRecipients(); + for (Address toa : to) // Address can have '<' '>' around + { + if (toa instanceof InternetAddress) { + List 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) { - throw new IllegalArgumentException("Argument 'msg' must not be null!"); - } + if (msg == null) { + throw new IllegalArgumentException("Argument 'msg' must not be null!"); + } - try - { - // Create new Article object - Article article = new Article(msg); - boolean posted = false; + 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()); + // Check if this mail is already existing the storage + boolean updateReq = + StorageManager.current().isArticleExisting(article.getMessageID()); - List newsgroups = getGroupFor(msg, !updateReq); - List oldgroups = new ArrayList(); - if(updateReq) - { - // Check for duplicate entries of the same group - Article oldArticle = StorageManager.current().getArticle(article.getMessageID()); - if(oldArticle != null) - { - List oldGroups = oldArticle.getGroups(); - for(Group oldGroup : oldGroups) - { - if(!newsgroups.contains(oldGroup.getName())) - { - oldgroups.add(oldGroup.getName()); - } - } + List newsgroups = getGroupFor(msg, !updateReq); + List oldgroups = new ArrayList(); + if (updateReq) { + // Check for duplicate entries of the same group + Article oldArticle = StorageManager.current().getArticle(article.getMessageID()); + if (oldArticle != null) { + List 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; } - } + } - 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()); + /** + * 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 rcptAddresses = StorageManager.current().getListsForGroup(group); - article.setGroup(groups.toString()); - //article.removeHeader(Headers.REPLY_TO); - //article.removeHeader(Headers.TO); + if (rcptAddresses == null || rcptAddresses.size() == 0) { + Log.get().warning("No ML-address for " + group + " found."); + return; + } - // 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 rcptAddresses = StorageManager.current().getListsForGroup(group); + 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]); - if(rcptAddresses == null || rcptAddresses.size() == 0) - { - Log.get().warning("No ML-address for " + group + " found."); - return; - } + // 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); - 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]); + article.setHeader("To", rcptAddress); + //article.setHeader("Reply-To", listAddress); - // 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); + if (Config.inst().get(Config.MLSEND_RW_SENDER, false)) { + rewriteSenderAddress(article); // Set the SENDER address + } - article.setHeader("To", rcptAddress); - //article.setHeader("Reply-To", listAddress); + SMTPTransport smtpTransport = new SMTPTransport(smtpHost, smtpPort); + smtpTransport.send(article, smtpFrom, rcptAddress); + smtpTransport.close(); - if (Config.inst().get(Config.MLSEND_RW_SENDER, false)) - { - rewriteSenderAddress(article); // Set the SENDER address - } + Stats.getInstance().mailGatewayed(group); + Log.get().info("MLGateway: Mail " + article.getHeader("Subject")[0] + + " was delivered to " + rcptAddress + "."); + } + } - SMTPTransport smtpTransport = new SMTPTransport(smtpHost, smtpPort); - smtpTransport.send(article, smtpFrom, rcptAddress); - smtpTransport.close(); + /** + * 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); - 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!"); - } - } - + if (mlAddress != null) { + msg.setHeader(Headers.SENDER, mlAddress); + } else { + throw new MessagingException("Cannot rewrite SENDER header!"); + } + } } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/mlgw/MailPoller.java --- a/src/org/sonews/mlgw/MailPoller.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/mlgw/MailPoller.java Sun Aug 29 18:17:37 2010 +0200 @@ -42,110 +42,94 @@ 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"); + static class PasswordAuthenticator extends Authenticator + { - 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"); + @Override + public PasswordAuthentication getPasswordAuthentication() + { + final String username = + Config.inst().get(Config.MLPOLL_USER, "user"); + final String password = + Config.inst().get(Config.MLPOLL_PASSWORD, "mysecret"); - // Get session - Session session = Session.getInstance(props); + return new PasswordAuthentication(username, password); + } + } - // Get the store - Store store = session.getStore("pop3"); - store.connect(host, 110, 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 - // Get folder - Folder folder = store.getFolder("INBOX"); - folder.open(Folder.READ_WRITE); + 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"); - // Get directory - Message[] messages = folder.getMessages(); + Stats.getInstance().mlgwRunStart(); - // 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); - } - } + // Create empty properties + Properties props = System.getProperties(); + props.put("mail.pop3.host", host); + props.put("mail.mime.address.strict", "false"); - // 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."); - } - + // 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 c404a87db5b7 -r 74139325d305 src/org/sonews/mlgw/SMTPTransport.java --- a/src/org/sonews/mlgw/SMTPTransport.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/mlgw/SMTPTransport.java Sun Aug 29 18:17:37 2010 +0200 @@ -36,98 +36,90 @@ class SMTPTransport { - protected BufferedReader in; - protected BufferedOutputStream out; - protected Socket socket; + 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()); + 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); - } + // 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); - } - } + // 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 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(); + public void close() + throws IOException + { + this.out.write("QUIT".getBytes("UTF-8")); + this.out.flush(); + this.in.readLine(); - this.socket.close(); - } + this.socket.close(); + } - public void send(Article article, String mailFrom, String rcptTo) - throws IOException - { - assert(article != null); - assert(mailFrom != null); - assert(rcptTo != null); + 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(("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(("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); - } + 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); - } + 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); - } - } - + // 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 c404a87db5b7 -r 74139325d305 src/org/sonews/plugin/Plugin.java --- a/src/org/sonews/plugin/Plugin.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/plugin/Plugin.java Sun Aug 29 18:17:37 2010 +0200 @@ -28,15 +28,14 @@ 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 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(); - + /** + * Called when the Plugin is unloaded by sonews. + */ + void unload(); } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/storage/Article.java --- a/src/org/sonews/storage/Article.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/storage/Article.java Sun Aug 29 18:17:37 2010 +0200 @@ -41,213 +41,196 @@ */ 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(); - } - } + /** + * 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]; - /** - * 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(); + /** + * Default constructor. + */ + public Article() + { + } - for(Enumeration e = msg.getAllHeaders() ; e.hasMoreElements();) - { - final Header header = (Header)e.nextElement(); - this.headers.addHeader(header.getName(), header.getValue()); - } + /** + * Creates a new Article object using the date from the given + * raw data. + */ + public Article(String headers, byte[] body) + { + try { + this.body = body; - // Reads the raw byte body using Message.writeTo(OutputStream out) - this.body = readContent(msg); - - // Validate headers - validateHeaders(); - } + // Parse the header + this.headers = new InternetHeaders( + new ByteArrayInputStream(headers.getBytes())); - /** - * 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(); - } + this.headerSrc = headers; + } catch (MessagingException ex) { + ex.printStackTrace(); + } + } - /** - * Removes the header identified by the given key. - * @param headerKey - */ - public void removeHeader(final String headerKey) - { - this.headers.removeHeader(headerKey); - this.headerSrc = null; - } + /** + * 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(); - /** - * 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; - } + for (Enumeration e = msg.getAllHeaders(); e.hasMoreElements();) { + final Header header = (Header) e.nextElement(); + this.headers.addHeader(header.getName(), header.getValue()); + } - /** - * Returns the body string. - */ - public byte[] getBody() - { - return body; - } - - /** - * @return Numerical IDs of the newsgroups this Article belongs to. - */ - public List getGroups() - { - String[] groupnames = getHeader(Headers.NEWSGROUPS)[0].split(","); - ArrayList groups = new ArrayList(); + // Reads the raw byte body using Message.writeTo(OutputStream out) + this.body = readContent(msg); - 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; - } + // Validate headers + validateHeaders(); + } - 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); - } + /** + * 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(); + } - /** - * 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(); - } + /** + * 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 getGroups() + { + String[] groupnames = getHeader(Headers.NEWSGROUPS)[0].split(","); + ArrayList groups = new ArrayList(); + + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/storage/ArticleHead.java --- a/src/org/sonews/storage/ArticleHead.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/storage/ArticleHead.java Sun Aug 29 18:17:37 2010 +0200 @@ -31,131 +31,122 @@ * @author Christian Lins * @since sonews/0.5.0 */ -public class ArticleHead +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; - } + protected InternetHeaders headers = null; + protected String headerSrc = null; - 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; - } + protected ArticleHead() + { + } - public Enumeration getAllHeaders() - { - return this.headers.getAllHeaders(); - } + public ArticleHead(String headers) + { + try { + // Parse the header + this.headers = new InternetHeaders( + new ByteArrayInputStream(headers.getBytes())); + } catch (MessagingException ex) { + ex.printStackTrace(); + } + } - /** - * @return Header source code of this Article. - */ - public String getHeaderSource() - { - if(this.headerSrc != null) - { - return this.headerSrc; - } + /** + * 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; + } - StringBuffer buf = new StringBuffer(); + public String[] getHeader(String name) + { + return getHeader(name, false); + } - for(Enumeration en = this.headers.getAllHeaders(); en.hasMoreElements();) - { - Header entry = (Header)en.nextElement(); + /** + * 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; + } - String value = entry.getValue().replaceAll("[\r\n]", " "); - buf.append(entry.getName()); - buf.append(": "); - buf.append(MimeUtility.fold(entry.getName().length() + 2, value)); + public Enumeration getAllHeaders() + { + return this.headers.getAllHeaders(); + } - if(en.hasMoreElements()) - { - buf.append("\r\n"); - } - } + /** + * @return Header source code of this Article. + */ + public String getHeaderSource() + { + if (this.headerSrc != null) { + return this.headerSrc; + } - this.headerSrc = buf.toString(); - return this.headerSrc; - } + StringBuffer buf = new StringBuffer(); - /** - * 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(); - } + for (Enumeration en = this.headers.getAllHeaders(); en.hasMoreElements();) { + Header entry = (Header) en.nextElement(); - /** - * 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()); - } - } - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/storage/Channel.java --- a/src/org/sonews/storage/Channel.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/storage/Channel.java Sun Aug 29 18:17:37 2010 +0200 @@ -33,79 +33,75 @@ 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 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; - /** - * 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; + public static List getAll() + { + List all = new ArrayList(); - /** - * 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; + /*List agroups = AggregatedGroup.getAll(); + if(agroups != null) + { + all.addAll(agroups); + }*/ - public static List getAll() - { - List all = new ArrayList(); + List groups = Group.getAll(); + if (groups != null) { + all.addAll(groups); + } - /*List agroups = AggregatedGroup.getAll(); - if(agroups != null) - { - all.addAll(agroups); - }*/ + return all; + } - List groups = Group.getAll(); - if(groups != null) - { - all.addAll(groups); - } + public static Channel getByName(String name) + throws StorageBackendException + { + return StorageManager.current().getGroup(name); + } - return all; - } + public abstract Article getArticle(long idx) + throws StorageBackendException; - public static Channel getByName(String name) - throws StorageBackendException - { - return StorageManager.current().getGroup(name); - } + public abstract List> getArticleHeads( + final long first, final long last) + throws StorageBackendException; - public abstract Article getArticle(long idx) - throws StorageBackendException; + public abstract List getArticleNumbers() + throws StorageBackendException; - public abstract List> getArticleHeads( - final long first, final long last) - throws StorageBackendException; + public abstract long getFirstArticleNumber() + throws StorageBackendException; - public abstract List getArticleNumbers() - throws StorageBackendException; + public abstract long getIndexOf(Article art) + throws StorageBackendException; - public abstract long getFirstArticleNumber() - throws StorageBackendException; + public abstract long getInternalID(); - public abstract long getIndexOf(Article art) - throws StorageBackendException; + public abstract long getLastArticleNumber() + throws StorageBackendException; - public abstract long getInternalID(); + public abstract String getName(); - public abstract long getLastArticleNumber() - throws StorageBackendException; + public abstract long getPostingsCount() + throws StorageBackendException; - public abstract String getName(); - - public abstract long getPostingsCount() - throws StorageBackendException; + public abstract boolean isDeleted(); - public abstract boolean isDeleted(); - - public abstract boolean isWriteable(); - + public abstract boolean isWriteable(); } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/storage/Group.java --- a/src/org/sonews/storage/Group.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/storage/Group.java Sun Aug 29 18:17:37 2010 +0200 @@ -31,154 +31,147 @@ // 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 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; - } + private long id = 0; + private int flags = -1; + private String name = null; - @Override - public boolean equals(Object obj) - { - if(obj instanceof Group) - { - return ((Group)obj).id == this.id; - } - else - { - return false; - } - } + /** + * @return List of all groups this server handles. + */ + public static List getAll() + { + try { + return StorageManager.current().getGroups(); + } catch (StorageBackendException ex) { + Log.get().severe(ex.getMessage()); + return null; + } + } - public Article getArticle(long idx) - throws StorageBackendException - { - return StorageManager.current().getArticle(idx, this.id); - } + /** + * @param name + * @param id + */ + public Group(final String name, final long id, final int flags) + { + this.id = id; + this.flags = flags; + this.name = name; + } - public List> getArticleHeads(final long first, final long last) - throws StorageBackendException - { - return StorageManager.current().getArticleHeads(this, first, last); - } - - public List getArticleNumbers() - throws StorageBackendException - { - return StorageManager.current().getArticleNumbers(id); - } + @Override + public boolean equals(Object obj) + { + if (obj instanceof Group) { + return ((Group) obj).id == this.id; + } else { + return false; + } + } - public long getFirstArticleNumber() - throws StorageBackendException - { - return StorageManager.current().getFirstArticleNumber(this); - } + public Article getArticle(long idx) + throws StorageBackendException + { + return StorageManager.current().getArticle(idx, this.id); + } - public int getFlags() - { - return this.flags; - } + public List> getArticleHeads(final long first, final long last) + throws StorageBackendException + { + return StorageManager.current().getArticleHeads(this, first, last); + } - public long getIndexOf(Article art) - throws StorageBackendException - { - return StorageManager.current().getArticleIndex(art, this); - } + public List getArticleNumbers() + throws StorageBackendException + { + return StorageManager.current().getArticleNumbers(id); + } - /** - * Returns the group id. - */ - public long getInternalID() - { - assert id > 0; + public long getFirstArticleNumber() + throws StorageBackendException + { + return StorageManager.current().getFirstArticleNumber(this); + } - return id; - } + public int getFlags() + { + return this.flags; + } - public boolean isDeleted() - { - return (this.flags & DELETED) != 0; - } + public long getIndexOf(Article art) + throws StorageBackendException + { + return StorageManager.current().getArticleIndex(art, this); + } - public boolean isMailingList() - { - return (this.flags & MAILINGLIST) != 0; - } + /** + * Returns the group id. + */ + public long getInternalID() + { + assert id > 0; - public boolean isWriteable() - { - return true; - } + return id; + } - public long getLastArticleNumber() - throws StorageBackendException - { - return StorageManager.current().getLastArticleNumber(this); - } + public boolean isDeleted() + { + return (this.flags & DELETED) != 0; + } - public String getName() - { - return name; - } + public boolean isMailingList() + { + return (this.flags & MAILINGLIST) != 0; + } - /** - * 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 boolean isWriteable() + { + return true; + } - public void setName(final String name) - { - this.name = name; - } + public long getLastArticleNumber() + throws StorageBackendException + { + return StorageManager.current().getLastArticleNumber(this); + } - /** - * @return Number of posted articles in this group. - * @throws java.sql.SQLException - */ - public long getPostingsCount() - throws StorageBackendException - { - return StorageManager.current().getPostingsCount(this.name); - } + public String getName() + { + return name; + } - /** - * Updates flags and name in the backend. - */ - public void update() - throws StorageBackendException - { - StorageManager.current().update(this); - } + /** + * 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 c404a87db5b7 -r 74139325d305 src/org/sonews/storage/Headers.java --- a/src/org/sonews/storage/Headers.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/storage/Headers.java Sun Aug 29 18:17:37 2010 +0200 @@ -27,30 +27,30 @@ 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"; + 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() - {} - + private Headers() + { + } } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/storage/Storage.java --- a/src/org/sonews/storage/Storage.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/storage/Storage.java Sun Aug 29 18:17:37 2010 +0200 @@ -30,121 +30,120 @@ public interface Storage { - /** - * Stores the given Article in the storage. - * @param art - * @throws StorageBackendException - */ - void addArticle(Article art) - throws StorageBackendException; + /** + * 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 addEvent(long timestamp, int type, long groupID) + throws StorageBackendException; - void addGroup(String groupname, int flags) - throws StorageBackendException; + void addGroup(String groupname, int flags) + throws StorageBackendException; - int countArticles() - throws StorageBackendException; + int countArticles() + throws StorageBackendException; - int countGroups() - throws StorageBackendException; + int countGroups() + throws StorageBackendException; - void delete(String messageID) - throws StorageBackendException; + void delete(String messageID) + throws StorageBackendException; - Article getArticle(String messageID) - throws StorageBackendException; + Article getArticle(String messageID) + throws StorageBackendException; - Article getArticle(long articleIndex, long groupID) - throws StorageBackendException; + Article getArticle(long articleIndex, long groupID) + throws StorageBackendException; - List> getArticleHeads(Group group, long first, long last) - throws StorageBackendException; + List> getArticleHeads(Group group, long first, long last) + throws StorageBackendException; - List> getArticleHeaders(Channel channel, long start, long end, - String header, String pattern) - throws StorageBackendException; + List> getArticleHeaders(Channel channel, long start, long end, + String header, String pattern) + throws StorageBackendException; - long getArticleIndex(Article art, Group group) - throws StorageBackendException; + long getArticleIndex(Article art, Group group) + throws StorageBackendException; - List getArticleNumbers(long groupID) - throws StorageBackendException; + List getArticleNumbers(long groupID) + throws StorageBackendException; - String getConfigValue(String key) - throws StorageBackendException; + String getConfigValue(String key) + throws StorageBackendException; - int getEventsCount(int eventType, long startTimestamp, long endTimestamp, - Channel channel) - throws StorageBackendException; + int getEventsCount(int eventType, long startTimestamp, long endTimestamp, + Channel channel) + throws StorageBackendException; - double getEventsPerHour(int key, long gid) - throws StorageBackendException; + double getEventsPerHour(int key, long gid) + throws StorageBackendException; - int getFirstArticleNumber(Group group) - throws StorageBackendException; + int getFirstArticleNumber(Group group) + throws StorageBackendException; - Group getGroup(String name) - throws StorageBackendException; + Group getGroup(String name) + throws StorageBackendException; - List getGroups() - throws StorageBackendException; + List getGroups() + throws StorageBackendException; - /** - * Retrieves the collection of groupnames that are associated with the - * given list address. - * @param inetaddress - * @return - * @throws StorageBackendException - */ - List getGroupsForList(String listAddress) - throws StorageBackendException; + /** + * Retrieves the collection of groupnames that are associated with the + * given list address. + * @param inetaddress + * @return + * @throws StorageBackendException + */ + List getGroupsForList(String listAddress) + throws StorageBackendException; - int getLastArticleNumber(Group group) - 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 getListsForGroup(String groupname) - 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 getListsForGroup(String groupname) + throws StorageBackendException; - String getOldestArticle() - throws StorageBackendException; + String getOldestArticle() + throws StorageBackendException; - int getPostingsCount(String groupname) - throws StorageBackendException; + int getPostingsCount(String groupname) + throws StorageBackendException; - List getSubscriptions(int type) - throws StorageBackendException; + List getSubscriptions(int type) + throws StorageBackendException; - boolean isArticleExisting(String messageID) - throws StorageBackendException; + boolean isArticleExisting(String messageID) + throws StorageBackendException; - boolean isGroupExisting(String groupname) - throws StorageBackendException; + boolean isGroupExisting(String groupname) + throws StorageBackendException; - void purgeGroup(Group group) - throws StorageBackendException; + void purgeGroup(Group group) + throws StorageBackendException; - void setConfigValue(String key, String value) - 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; + /** + * 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; - + boolean update(Group group) + throws StorageBackendException; } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/storage/StorageBackendException.java --- a/src/org/sonews/storage/StorageBackendException.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/storage/StorageBackendException.java Sun Aug 29 18:17:37 2010 +0200 @@ -19,21 +19,20 @@ package org.sonews.storage; /** - * + * Exception caused by the storage backend. * @author Christian Lins * @since sonews/1.0 */ public class StorageBackendException extends Exception { - public StorageBackendException(Throwable cause) - { - super(cause); - } + public StorageBackendException(Throwable cause) + { + super(cause); + } - public StorageBackendException(String msg) - { - super(msg); - } - + public StorageBackendException(String msg) + { + super(msg); + } } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/storage/StorageManager.java --- a/src/org/sonews/storage/StorageManager.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/storage/StorageManager.java Sun Aug 29 18:17:37 2010 +0200 @@ -26,64 +26,53 @@ public final class StorageManager { - private static StorageProvider provider; + 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 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; - } - } + 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; - } - } + /** + * 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; - } - } - + /** + * Disables the current provider. + */ + public static void disableProvider() + { + synchronized (StorageManager.class) { + provider = null; + } + } } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/storage/StorageProvider.java --- a/src/org/sonews/storage/StorageProvider.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/storage/StorageProvider.java Sun Aug 29 18:17:37 2010 +0200 @@ -26,15 +26,14 @@ public interface StorageProvider { - public boolean isSupported(String uri); + 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; - + /** + * 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 c404a87db5b7 -r 74139325d305 src/org/sonews/storage/impl/JDBCDatabase.java --- a/src/org/sonews/storage/impl/JDBCDatabase.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/storage/impl/JDBCDatabase.java Sun Aug 29 18:17:37 2010 +0200 @@ -52,1731 +52,1378 @@ 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")); + 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; - // Establish database connection - this.conn = DriverManager.getConnection( - Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DATABASE, ""), - Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_USER, "root"), - Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_PASSWORD, "")); + /** + * 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")); - this.conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); - if(this.conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE) - { - Log.get().warning("Database is NOT fully serializable!"); - } + // Establish database connection + this.conn = DriverManager.getConnection( + Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DATABASE, ""), + Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_USER, "root"), + Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_PASSWORD, "")); - // 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 (?, ?)"); + this.conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); + if (this.conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE) { + Log.get().warning("Database is NOT fully serializable!"); + } - // 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 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 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 addStatValue() + this.pstmtAddEvent = conn.prepareStatement( + "INSERT INTO events VALUES (?, ?, ?)"); - // 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"); + // Prepare statement for method addGroup() + this.pstmtAddGroup0 = conn.prepareStatement( + "INSERT INTO groups (name, flags) VALUES (?, ?)"); - 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 statement for method countArticles() + this.pstmtCountArticles = conn.prepareStatement( + "SELECT Count(article_id) FROM article_ids"); - // 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 statement for method countGroups() + this.pstmtCountGroups = conn.prepareStatement( + "SELECT Count(group_id) FROM groups WHERE " + + "flags & " + Channel.DELETED + " = 0"); - // Prepare statements for method getConfigValue() - this.pstmtGetConfigValue = conn.prepareStatement( - "SELECT config_value FROM config WHERE config_key = ?"); + // 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 method getEventsCount() - this.pstmtGetEventsCount0 = conn.prepareStatement( - "SELECT Count(*) FROM events WHERE event_key = ? AND " + - "event_time >= ? AND event_time < ?"); + // 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 = ?)"); - 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 getArticleHeaders() + this.pstmtGetArticleHeaders0 = conn.prepareStatement( + "SELECT header_key, header_value FROM headers WHERE article_id = ? " + + "ORDER BY header_index ASC"); - // 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 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"); - // Prepare statement for method getLastArticleNumber() - this.pstmtGetLastArticleNumber = conn.prepareStatement( - "SELECT Max(article_index) FROM postings WHERE group_id = ?"); + this.pstmtGetArticleIDs = conn.prepareStatement( + "SELECT 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 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 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 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 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 getConfigValue() + this.pstmtGetConfigValue = conn.prepareStatement( + "SELECT config_value FROM config WHERE config_key = ?"); - // 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 statements for method getEventsCount() + this.pstmtGetEventsCount0 = conn.prepareStatement( + "SELECT Count(*) FROM events WHERE event_key = ? AND " + + "event_time >= ? AND event_time < ?"); - // 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); + this.pstmtGetEventsCount1 = conn.prepareStatement( + "SELECT Count(*) FROM events WHERE event_key = ? AND " + + "event_time >= ? AND event_time < ? AND group_id = ?"); - int newArticleID = getMaxArticleID() + 1; + // 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 = ?"); - // Fill prepared statement with values; - // writes body to article table - pstmtAddArticle1.setInt(1, newArticleID); - pstmtAddArticle1.setBytes(2, article.getBody()); - pstmtAddArticle1.execute(); + // 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 = ?"); - // 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 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(); + // Prepare statement for method getLastArticleNumber() + this.pstmtGetLastArticleNumber = conn.prepareStatement( + "SELECT Max(article_index) FROM postings WHERE group_id = ?"); - this.conn.commit(); - this.conn.setAutoCommit(true); + // 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 = ?"); - 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); - } + // Prepare statement for method getMaxArticleID() + this.pstmtGetMaxArticleID = conn.prepareStatement( + "SELECT Max(article_id) FROM articles"); - 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); + // Prepare statement for method getMaxArticleIndex() + this.pstmtGetMaxArticleIndex = conn.prepareStatement( + "SELECT Max(article_index) FROM postings WHERE group_id = ?"); - 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(); - } + // 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)"); - restartConnection(ex); - addGroup(name, flags); - } - } + // Prepare statement for method getFirstArticleNumber() + this.pstmtGetFirstArticleNumber = conn.prepareStatement( + "SELECT Min(article_index) FROM postings WHERE group_id = ?"); - @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(); - } + // Prepare statement for method getPostingsCount() + this.pstmtGetPostingsCount = conn.prepareStatement( + "SELECT Count(*) FROM postings NATURAL JOIN groups " + + "WHERE groups.name = ?"); - restartConnection(ex); - addEvent(time, type, gid); - } - } + // Prepare statement for method getSubscriptions() + this.pstmtGetSubscriptions = conn.prepareStatement( + "SELECT host, port, name FROM peers NATURAL JOIN " + + "peer_subscriptions NATURAL JOIN groups WHERE feedtype = ?"); - @Override - public int countArticles() - throws StorageBackendException - { - ResultSet rs = null; + // Prepare statement for method isArticleExisting() + this.pstmtIsArticleExisting = conn.prepareStatement( + "SELECT Count(article_id) FROM article_ids WHERE message_id = ?"); - 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; - } - } - } + // Prepare statement for method isGroupExisting() + this.pstmtIsGroupExisting = conn.prepareStatement( + "SELECT * FROM groups WHERE name = ?"); - @Override - public int countGroups() - throws StorageBackendException - { - ResultSet rs = null; + // Prepare statement for method setConfigValue() + this.pstmtSetConfigValue0 = conn.prepareStatement( + "DELETE FROM config WHERE config_key = ?"); + this.pstmtSetConfigValue1 = conn.prepareStatement( + "INSERT INTO config VALUES(?, ?)"); - 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; - } - } - } + // 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 = ?"); - @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(); + // 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); + } + } - this.pstmtDeleteArticle2.setString(1, messageID); - rs = this.pstmtDeleteArticle2.executeUpdate(); + /** + * 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); - this.pstmtDeleteArticle3.setString(1, messageID); - rs = this.pstmtDeleteArticle3.executeUpdate(); - - this.conn.commit(); - this.conn.setAutoCommit(true); - } - catch(SQLException ex) - { - throw new StorageBackendException(ex); - } - } + int newArticleID = getMaxArticleID() + 1; - @Override - public Article getArticle(String messageID) - throws StorageBackendException - { - ResultSet rs = null; - try - { - pstmtGetArticle0.setString(1, messageID); - rs = pstmtGetArticle0.executeQuery(); + // Fill prepared statement with values; + // writes body to article table + pstmtAddArticle1.setInt(1, newArticleID); + pstmtAddArticle1.setBytes(2, article.getBody()); + pstmtAddArticle1.execute(); - 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; + // 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(); + } - try - { - this.pstmtGetArticle1.setLong(1, articleIndex); - this.pstmtGetArticle1.setLong(2, gid); + // For each newsgroup add a reference + List 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(); + } - rs = this.pstmtGetArticle1.executeQuery(); + // Write message-id to article_ids table + this.pstmtAddArticle4.setInt(1, newArticleID); + this.pstmtAddArticle4.setString(2, article.getMessageID()); + this.pstmtAddArticle4.execute(); - 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; - } - } - } + this.conn.commit(); + this.conn.setAutoCommit(true); - /** - * 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> getArticleHeaders(Channel group, long start, - long end, String headerKey, String patStr) - throws StorageBackendException, PatternSyntaxException - { - ResultSet rs = null; - List> heads = new ArrayList>(); + 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.pstmtGetArticleHeaders1.setString(1, group.getName()); - this.pstmtGetArticleHeaders1.setString(2, headerKey); - this.pstmtGetArticleHeaders1.setLong(3, start); + try { + this.conn.setAutoCommit(true); // and release locks + } catch (SQLException ex2) { + Log.get().severe("setAutoCommit(true) of addArticle() failed: " + ex2); + } - rs = this.pstmtGetArticleHeaders1.executeQuery(); + restartConnection(ex); + addArticle(article); + } + } - // Convert the "NNTP" regex to Java regex - patStr = patStr.replace("*", ".*"); - Pattern pattern = Pattern.compile(patStr); + /** + * 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); - 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(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(); - } - } - } + 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(); + } - return heads; - } + restartConnection(ex); + addGroup(name, flags); + } + } - 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 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(); + } - @Override - public long getArticleIndex(Article article, Group group) - throws StorageBackendException - { - ResultSet rs = null; + restartConnection(ex); + addEvent(time, type, gid); + } + } - 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> getArticleHeads(Group group, long first, - long last) - throws StorageBackendException - { - ResultSet rs = null; + @Override + public int countArticles() + 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(); + 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; + } + } + } - List> articles - = new ArrayList>(); + @Override + public int countGroups() + throws StorageBackendException + { + ResultSet rs = null; - while (rs.next()) - { - long aid = rs.getLong("article_id"); - long aidx = rs.getLong("article_index"); - String headers = getArticleHeaders(aid); - articles.add(new Pair(aidx, - new ArticleHead(headers))); - } + 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; + } + } + } - 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 void delete(final String messageID) + throws StorageBackendException + { + try { + this.conn.setAutoCommit(false); - @Override - public List getArticleNumbers(long gid) - throws StorageBackendException - { - ResultSet rs = null; - try - { - List ids = new ArrayList(); - 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(); - } - } - } - } + this.pstmtDeleteArticle0.setString(1, messageID); + int rs = this.pstmtDeleteArticle0.executeUpdate(); - @Override - public String getConfigValue(String key) - throws StorageBackendException - { - ResultSet rs = null; - try - { - this.pstmtGetConfigValue.setString(1, key); + // We do not trust the ON DELETE CASCADE functionality to delete + // orphaned references... + this.pstmtDeleteArticle1.setString(1, messageID); + rs = this.pstmtDeleteArticle1.executeUpdate(); - 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 - } - } - } + this.pstmtDeleteArticle2.setString(1, messageID); + rs = this.pstmtDeleteArticle2.executeUpdate(); - @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 getGroups() - throws StorageBackendException - { - ResultSet rs; - List buffer = new ArrayList(); - Statement stmt = null; + this.pstmtDeleteArticle3.setString(1, messageID); + rs = this.pstmtDeleteArticle3.executeUpdate(); - try - { - stmt = conn.createStatement(); - rs = stmt.executeQuery("SELECT * FROM groups ORDER BY name"); + this.conn.commit(); + this.conn.setAutoCommit(true); + } catch (SQLException ex) { + throw new StorageBackendException(ex); + } + } - 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); - } + @Override + public Article getArticle(String messageID) + throws StorageBackendException + { + ResultSet rs = null; + try { + pstmtGetArticle0.setString(1, messageID); + rs = pstmtGetArticle0.executeQuery(); - return buffer; - } - catch(SQLException ex) - { - restartConnection(ex); - return getGroups(); - } - finally - { - if(stmt != null) - { - try - { - stmt.close(); // Implicitely closes ResultSets - } - catch(SQLException ex) - { - ex.printStackTrace(); - } - } - } - } + 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 + } + } + } - @Override - public List getGroupsForList(String listAddress) - throws StorageBackendException - { - ResultSet rs = null; - - try - { - this.pstmtGetGroupForList.setString(1, listAddress); + /** + * Retrieves an article by its ID. + * @param articleID + * @return + * @throws StorageBackendException + */ + @Override + public Article getArticle(long articleIndex, long gid) + throws StorageBackendException + { + ResultSet rs = null; - rs = this.pstmtGetGroupForList.executeQuery(); - List groups = new ArrayList(); - 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(); + try { + this.pstmtGetArticle1.setLong(1, articleIndex); + this.pstmtGetArticle1.setLong(2, gid); - 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(); - } - } - } - } + rs = this.pstmtGetArticle1.executeQuery(); - @Override - public List getListsForGroup(String group) - throws StorageBackendException - { - ResultSet rs = null; - List lists = new ArrayList(); + 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; + } + } + } - try - { - this.pstmtGetListForGroup.setString(1, group); - rs = this.pstmtGetListForGroup.executeQuery(); + /** + * 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> getArticleHeaders(Channel group, long start, + long end, String headerKey, String patStr) + throws StorageBackendException, PatternSyntaxException + { + ResultSet rs = null; + List> heads = new ArrayList>(); - 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.pstmtGetArticleHeaders1.setString(1, group.getName()); + this.pstmtGetArticleHeaders1.setString(2, headerKey); + this.pstmtGetArticleHeaders1.setLong(3, start); - try - { - this.pstmtGetMaxArticleIndex.setLong(1, groupID); - rs = this.pstmtGetMaxArticleIndex.executeQuery(); + rs = this.pstmtGetArticleHeaders1.executeQuery(); - int maxIndex = 0; - if (rs.next()) - { - maxIndex = rs.getInt(1); - } + // Convert the "NNTP" regex to Java regex + patStr = patStr.replace("*", ".*"); + Pattern pattern = Pattern.compile(patStr); - 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; + 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(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(); + } + } + } - try - { - rs = this.pstmtGetMaxArticleID.executeQuery(); + return heads; + } - int maxIndex = 0; - if (rs.next()) - { - maxIndex = rs.getInt(1); - } + private String getArticleHeaders(long articleID) + throws StorageBackendException + { + ResultSet rs = null; - return maxIndex; - } - catch(SQLException ex) - { - restartConnection(ex); - return getMaxArticleID(); - } - finally - { - if(rs != null) - { - try - { - rs.close(); - } - catch(SQLException ex) - { - ex.printStackTrace(); - } - } - } - } + try { + this.pstmtGetArticleHeaders0.setLong(1, articleID); + rs = this.pstmtGetArticleHeaders0.executeQuery(); - @Override - public int getLastArticleNumber(Group group) - throws StorageBackendException - { - ResultSet rs = null; + 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; + } + } + } - 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(); - } - } - } - } + 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 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; + @Override + public long getArticleIndex(Article article, Group group) + throws StorageBackendException + { + ResultSet rs = null; - try - { - this.pstmtGetGroup1.setInt(1, id); - rs = this.pstmtGetGroup1.executeQuery(); + try { + this.pstmtGetArticleIndex.setString(1, article.getMessageID()); + this.pstmtGetArticleIndex.setLong(2, group.getInternalID()); - 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(); - } - } - } - } + 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(); + } + } + } + } - @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(); - } - } - } + /** + * Returns a list of Long/Article Pairs. + * @throws java.sql.SQLException + */ + @Override + public List> getArticleHeads(Group group, long first, + long last) + throws StorageBackendException + { + ResultSet rs = null; - @Override - public String getOldestArticle() - 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(); - 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(); - } - } - } - } + List> articles = new ArrayList>(); - @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(); - } - } - } - } + while (rs.next()) { + long aid = rs.getLong("article_id"); + long aidx = rs.getLong("article_index"); + String headers = getArticleHeaders(aid); + articles.add(new Pair(aidx, + new ArticleHead(headers))); + } - @Override - public List getSubscriptions(int feedtype) - throws StorageBackendException - { - ResultSet rs = null; - - try - { - List subs = new ArrayList(); - 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(); - } - } - } - } + return articles; + } catch (SQLException ex) { + restartConnection(ex); + return getArticleHeads(group, first, last); + } 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 List getArticleNumbers(long gid) + throws StorageBackendException + { + ResultSet rs = null; + try { + List ids = new ArrayList(); + 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 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 String getConfigValue(String key) + throws StorageBackendException + { + ResultSet rs = null; + try { + this.pstmtGetConfigValue.setString(1, key); - @Override - public void purgeGroup(Group group) - throws StorageBackendException - { - try - { - this.pstmtPurgeGroup0.setLong(1, group.getInternalID()); - this.pstmtPurgeGroup0.executeUpdate(); + 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 + } + } + } - 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 int getEventsCount(int type, long start, long end, Channel channel) + throws StorageBackendException + { + ResultSet rs = null; - @Override - public boolean update(Article article) - throws StorageBackendException - { - // DELETE FROM headers WHERE article_id = ? + 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(); + } - // INSERT INTO headers ... + 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(); + } + } + } + } - // SELECT * FROM postings WHERE article_id = ? AND group_id = ? - return false; - } + /** + * Reads all Groups from the JDBCDatabase. + * @return + * @throws StorageBackendException + */ + @Override + public List getGroups() + throws StorageBackendException + { + ResultSet rs; + List buffer = new ArrayList(); + Statement stmt = null; - /** - * 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); - } - } + 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 getGroupsForList(String listAddress) + throws StorageBackendException + { + ResultSet rs = null; + + try { + this.pstmtGetGroupForList.setString(1, listAddress); + + rs = this.pstmtGetGroupForList.executeQuery(); + List groups = new ArrayList(); + 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 getListsForGroup(String group) + throws StorageBackendException + { + ResultSet rs = null; + List lists = new ArrayList(); + + 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 getSubscriptions(int feedtype) + throws StorageBackendException + { + ResultSet rs = null; + + try { + List subs = new ArrayList(); + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/storage/impl/JDBCDatabaseProvider.java --- a/src/org/sonews/storage/impl/JDBCDatabaseProvider.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/storage/impl/JDBCDatabaseProvider.java Sun Aug 29 18:17:37 2010 +0200 @@ -33,37 +33,29 @@ public class JDBCDatabaseProvider implements StorageProvider { - protected static final Map instances - = new ConcurrentHashMap(); + protected static final Map instances = new ConcurrentHashMap(); - @Override - public boolean isSupported(String uri) - { - throw new UnsupportedOperationException("Not supported yet."); - } + @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); - } - } - + @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 c404a87db5b7 -r 74139325d305 src/org/sonews/util/DatabaseSetup.java --- a/src/org/sonews/util/DatabaseSetup.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/util/DatabaseSetup.java Sun Aug 29 18:17:37 2010 +0200 @@ -25,7 +25,6 @@ import java.sql.Statement; import java.util.HashMap; import java.util.Map; -import org.sonews.config.Config; import org.sonews.util.io.Resource; /** @@ -33,95 +32,85 @@ * @author Christian Lins * @since sonews/0.5.0 */ -public final class DatabaseSetup +public final class DatabaseSetup { - private static final Map templateMap - = new HashMap(); - private static final Map urlMap - = new HashMap(); - private static final Map driverMap - = new HashMap(); - - 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(); + private static final Map templateMap = new HashMap(); + private static final Map urlMap = new HashMap(); + private static final Map driverMap = new HashMap(); - System.out.print("Give name of DB user that can create tables: "); - String dbUser = in.readLine(); + static { + templateMap.put("1", "helpers/database_mysql5_tmpl.sql"); + templateMap.put("2", "helpers/database_postgresql8_tmpl.sql"); - 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"); - } - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/util/Log.java --- a/src/org/sonews/util/Log.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/util/Log.java Sun Aug 29 18:17:37 2010 +0200 @@ -33,25 +33,24 @@ public class Log extends Logger { - private static Log instance = new Log(); + private static Log instance = new Log(); - private Log() - { - super("org.sonews", null); + 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); - } + 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; - } - + public static Logger get() + { + Level level = Level.parse(Config.inst().get(Config.LOGLEVEL, "INFO")); + instance.setLevel(level); + return instance; + } } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/util/Pair.java --- a/src/org/sonews/util/Pair.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/util/Pair.java Sun Aug 29 18:17:37 2010 +0200 @@ -23,26 +23,25 @@ * @author Christian Lins * @since sonews/0.5.0 */ -public class Pair +public class Pair { - - private T1 a; - private T2 b; - - public Pair(T1 a, T2 b) - { - this.a = a; - this.b = b; - } - public T1 getA() - { - return a; - } + private T1 a; + private T2 b; - public T2 getB() - { - return 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 c404a87db5b7 -r 74139325d305 src/org/sonews/util/Purger.java --- a/src/org/sonews/util/Purger.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/util/Purger.java Sun Aug 29 18:17:37 2010 +0200 @@ -40,110 +40,91 @@ 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(); + /** + * 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); - } - } + 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 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 ids = StorageManager.current().getArticleNumbers(group.getInternalID()); - if(ids.size() == 0) - { - StorageManager.current().purgeGroup(group); - Log.get().info("Group " + group.getName() + " purged."); - } + private void purgeDeleted() + throws StorageBackendException + { + List groups = StorageManager.current().getGroups(); + for (Channel channel : groups) { + if (!(channel instanceof Group)) { + continue; + } - 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."); - } - } - } - } + Group group = (Group) channel; + // Look for groups that are marked as deleted + if (group.isDeleted()) { + List ids = StorageManager.current().getArticleNumbers(group.getInternalID()); + if (ids.size() == 0) { + StorageManager.current().purgeGroup(group); + Log.get().info("Group " + group.getName() + " 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); + 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."); + } + } + } + } - 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; - } + 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); - 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); - } + 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; + } - // 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 - } - } + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/util/Stats.java --- a/src/org/sonews/util/Stats.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/util/Stats.java Sun Aug 29 18:17:37 2010 +0200 @@ -29,178 +29,157 @@ * @author Christian Lins * @since sonews/0.5.0 */ -public final class Stats +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; + 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(); - /** - * 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)) - { + public static Stats getInstance() + { + return Stats.instance; + } - 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); - } - + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/util/StringTemplate.java --- a/src/org/sonews/util/StringTemplate.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/util/StringTemplate.java Sun Aug 29 18:17:37 2010 +0200 @@ -26,72 +26,67 @@ * @author Christian Lins * @since sonews/0.5.0 */ -public class StringTemplate +public class StringTemplate { - private String str = null; - private String templateDelimiter = "%"; - private Map templateValues = new HashMap(); - - public StringTemplate(String str, final String templateDelimiter) - { - if(str == null || templateDelimiter == null) - { - throw new IllegalArgumentException("null arguments not allowed"); - } + private String str = null; + private String templateDelimiter = "%"; + private Map templateValues = new HashMap(); - 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"); - } + public StringTemplate(String str, final String templateDelimiter) + { + if (str == null || templateDelimiter == null) { + throw new IllegalArgumentException("null arguments not allowed"); + } - return set(template, obj.toString()); - } - - @Override - public String toString() - { - String ret = str; + this.str = str; + this.templateDelimiter = templateDelimiter; + } - for(String key : this.templateValues.keySet()) - { - String value = this.templateValues.get(key); - ret = ret.replace(templateDelimiter + key, value); - } - - return ret; - } + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/util/TimeoutMap.java --- a/src/org/sonews/util/TimeoutMap.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/util/TimeoutMap.java Sun Aug 29 18:17:37 2010 +0200 @@ -31,115 +31,99 @@ * @author Christian Lins * @since sonews/0.5.0 */ -public class TimeoutMap extends ConcurrentHashMap +public class TimeoutMap extends ConcurrentHashMap { - - private static final long serialVersionUID = 453453467700345L; - private int timeout = 60000; // 60 sec - private transient Map timeoutMap = new HashMap(); - - /** - * 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); - } + private static final long serialVersionUID = 453453467700345L; + private int timeout = 60000; // 60 sec + private transient Map timeoutMap = new HashMap(); - @Override - public synchronized V get(Object key) - { - if(checkTimeOut(key)) - { - return super.get(key); - } - else - { - return null; - } - } + /** + * Constructor. + * @param timeout Timeout in milliseconds + */ + public TimeoutMap(final int timeout) + { + this.timeout = timeout; + } - @Override - public V put(K key, V value) - { - synchronized(this.timeoutMap) - { - removeStaleKeys(); - this.timeoutMap.put(key, System.currentTimeMillis()); - return super.put(key, value); - } - } + /** + * Uses default timeout (60 sec). + */ + public TimeoutMap() + { + } - /** - * @param arg0 - * @return - */ - @Override - public V remove(Object arg0) - { - synchronized(this.timeoutMap) - { - this.timeoutMap.remove(arg0); - V val = super.remove(arg0); - return val; - } - } + /** + * + * @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; + } + } + } - protected void removeStaleKeys() - { - synchronized(this.timeoutMap) - { - Set keySet = new HashSet(this.timeoutMap.keySet()); - for(Object key : keySet) - { - // The key/value is removed by the checkTimeOut() method if true - checkTimeOut(key); - } - } - } - + @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 keySet = new HashSet(this.timeoutMap.keySet()); + for (Object key : keySet) { + // The key/value is removed by the checkTimeOut() method if true + checkTimeOut(key); + } + } + } } diff -r c404a87db5b7 -r 74139325d305 src/org/sonews/util/io/ArticleInputStream.java --- a/src/org/sonews/util/io/ArticleInputStream.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/util/io/ArticleInputStream.java Sun Aug 29 18:17:37 2010 +0200 @@ -32,40 +32,36 @@ 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(); - } + private byte[] buf; + private int pos = 0; - /** - * This method reads one byte from the stream. The pos - * 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; - } - } - + 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 pos + * 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 c404a87db5b7 -r 74139325d305 src/org/sonews/util/io/ArticleReader.java --- a/src/org/sonews/util/io/ArticleReader.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/util/io/ArticleReader.java Sun Aug 29 18:17:37 2010 +0200 @@ -34,102 +34,87 @@ * @author Christian Lins * @since sonews/0.5.0 */ -public class ArticleReader +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; + private BufferedOutputStream out; + private BufferedInputStream in; + private String 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; + public ArticleReader(String host, int port, String messageID) + throws IOException, UnknownHostException + { + this.messageID = messageID; - try - { - this.out.write(("ARTICLE " + this.messageID + "\r\n").getBytes("UTF-8")); - this.out.flush(); + // 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); + } + } - 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); - } + private boolean eofArticle(byte[] buf) + { + if (buf.length < 4) { + return false; + } - 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()); - } + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/util/io/ArticleWriter.java --- a/src/org/sonews/util/io/ArticleWriter.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/util/io/ArticleWriter.java Sun Aug 29 18:17:37 2010 +0200 @@ -32,102 +32,97 @@ * @author Christian Lins * @since sonews/0.5.0 */ -public class ArticleWriter +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(); - } + private BufferedOutputStream out; + private BufferedReader inr; - 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); - } - } + 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); + } + } - protected void preparePOST() - throws IOException - { - this.out.write("POST\r\n".getBytes("UTF-8")); - this.out.flush(); + public void close() + throws IOException, UnsupportedEncodingException + { + this.out.write("QUIT\r\n".getBytes("UTF-8")); + this.out.flush(); + } - String line = this.inr.readLine(); - if(line == null || !line.startsWith("340 ")) - { - throw new IOException(line); - } - } + 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); + } + } - public void writeArticle(Article article) - throws IOException, UnsupportedEncodingException - { - byte[] buf = new byte[512]; - ArticleInputStream in = new ArticleInputStream(article); + protected void preparePOST() + throws IOException + { + this.out.write("POST\r\n".getBytes("UTF-8")); + this.out.flush(); - preparePOST(); - - int len = in.read(buf); - while(len != -1) - { - writeLine(buf, len); - len = in.read(buf); - } + String line = this.inr.readLine(); + if (line == null || !line.startsWith("340 ")) { + throw new IOException(line); + } + } - finishPOST(); - } + public void writeArticle(Article article) + throws IOException, UnsupportedEncodingException + { + byte[] buf = new byte[512]; + ArticleInputStream in = new ArticleInputStream(article); - /** - * 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(); - } + preparePOST(); - /** - * 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(); - } + 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 c404a87db5b7 -r 74139325d305 src/org/sonews/util/io/Resource.java --- a/src/org/sonews/util/io/Resource.java Sun Aug 29 17:43:58 2010 +0200 +++ b/src/org/sonews/util/io/Resource.java Sun Aug 29 18:17:37 2010 +0200 @@ -32,101 +32,89 @@ */ 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 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; + } - /** - * 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; + return Resource.class.getClassLoader().getResource(name); + } - BufferedReader in = null; - try - { - InputStream ins = getAsStream(name); - if(ins == null) - return null; + /** + * 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; + } + } - in = new BufferedReader( - new InputStreamReader(ins, Charset.forName("UTF-8"))); - StringBuffer buf = new StringBuffer(); + /** + * 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; + } - for(;;) - { - String line = in.readLine(); - if(line == null) - break; + BufferedReader in = null; + try { + InputStream ins = getAsStream(name); + if (ins == null) { + return null; + } - buf.append(line); - if(withNewline) - buf.append('\n'); - } + in = new BufferedReader( + new InputStreamReader(ins, Charset.forName("UTF-8"))); + StringBuffer buf = new StringBuffer(); - return buf.toString(); - } - catch(Exception e) - { - e.printStackTrace(); - return null; - } - finally - { - try - { - if(in != null) - in.close(); - } - catch(IOException ex) - { - ex.printStackTrace(); - } - } - } + 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(); + } + } + } }