# HG changeset patch # User cli # Date 1283095738 -7200 # Node ID ed84c8bdd87b32c4e8a75b894a8a5f62cb9c9fee # Parent 9f0b95aafaa3e861622d4cc8e90dde01f1c4bb54 Moving source files into src/-subdir. diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/Main.java --- a/org/sonews/Main.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,198 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews; - -import java.sql.Driver; -import java.sql.DriverManager; -import java.util.Enumeration; -import java.util.Date; -import java.util.logging.Level; -import org.sonews.config.Config; -import org.sonews.daemon.ChannelLineBuffers; -import org.sonews.daemon.CommandSelector; -import org.sonews.daemon.Connections; -import org.sonews.daemon.NNTPDaemon; -import org.sonews.feed.FeedManager; -import org.sonews.mlgw.MailPoller; -import org.sonews.storage.StorageBackendException; -import org.sonews.storage.StorageManager; -import org.sonews.storage.StorageProvider; -import org.sonews.util.Log; -import org.sonews.util.Purger; -import org.sonews.util.io.Resource; - -/** - * Startup class of the daemon. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public final class Main -{ - - private Main() - { - } - - /** Version information of the sonews daemon */ - public static final String VERSION = "sonews/1.1.0"; - public static final Date STARTDATE = new Date(); - - /** - * The main entrypoint. - * @param args - * @throws Exception - */ - public static void main(String[] args) throws Exception - { - System.out.println(VERSION); - Thread.currentThread().setName("Mainthread"); - - // Command line arguments - boolean feed = false; // Enable feeding? - boolean mlgw = false; // Enable Mailinglist gateway? - int port = -1; - - for(int n = 0; n < args.length; n++) - { - if(args[n].equals("-c") || args[n].equals("-config")) - { - Config.inst().set(Config.LEVEL_CLI, Config.CONFIGFILE, args[++n]); - System.out.println("Using config file " + args[n]); - } - else if(args[n].equals("-dumpjdbcdriver")) - { - System.out.println("Available JDBC drivers:"); - Enumeration drvs = DriverManager.getDrivers(); - while(drvs.hasMoreElements()) - { - System.out.println(drvs.nextElement()); - } - return; - } - else if(args[n].equals("-feed")) - { - feed = true; - } - else if(args[n].equals("-h") || args[n].equals("-help")) - { - printArguments(); - return; - } - else if(args[n].equals("-mlgw")) - { - mlgw = true; - } - else if(args[n].equals("-p")) - { - port = Integer.parseInt(args[++n]); - } - else if(args[n].equals("-plugin")) - { - System.out.println("Warning: -plugin-storage is not implemented!"); - } - else if(args[n].equals("-plugin-command")) - { - try - { - CommandSelector.addCommandHandler(args[++n]); - } - catch(Exception ex) - { - Log.get().warning("Could not load command plugin: " + args[n]); - Log.get().log(Level.INFO, "Main.java", ex); - } - } - else if(args[n].equals("-plugin-storage")) - { - System.out.println("Warning: -plugin-storage is not implemented!"); - } - else if(args[n].equals("-v") || args[n].equals("-version")) - { - // Simply return as the version info is already printed above - return; - } - } - - // Try to load the JDBCDatabase; - // Do NOT USE BackendConfig or Log classes before this point because they require - // a working JDBCDatabase connection. - try - { - StorageProvider sprov = - StorageManager.loadProvider("org.sonews.storage.impl.JDBCDatabaseProvider"); - StorageManager.enableProvider(sprov); - - // Make sure some elementary groups are existing - if(!StorageManager.current().isGroupExisting("control")) - { - StorageManager.current().addGroup("control", 0); - Log.get().info("Group 'control' created."); - } - } - catch(StorageBackendException ex) - { - ex.printStackTrace(); - System.err.println("Database initialization failed with " + ex.toString()); - System.err.println("Make sure you have specified the correct database" + - " settings in sonews.conf!"); - return; - } - - ChannelLineBuffers.allocateDirect(); - - // Add shutdown hook - Runtime.getRuntime().addShutdownHook(new ShutdownHook()); - - // Start the listening daemon - if(port <= 0) - { - port = Config.inst().get(Config.PORT, 119); - } - final NNTPDaemon daemon = NNTPDaemon.createInstance(port); - daemon.start(); - - // Start Connections purger thread... - Connections.getInstance().start(); - - // Start mailinglist gateway... - if(mlgw) - { - new MailPoller().start(); - } - - // Start feeds - if(feed) - { - FeedManager.startFeeding(); - } - - Purger purger = new Purger(); - purger.start(); - - // Wait for main thread to exit (setDaemon(false)) - daemon.join(); - } - - private static void printArguments() - { - String usage = Resource.getAsString("helpers/usage", true); - System.out.println(usage); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/ShutdownHook.java --- a/org/sonews/ShutdownHook.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,84 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews; - -import java.sql.SQLException; -import java.util.Map; -import org.sonews.daemon.AbstractDaemon; - -/** - * Will force all other threads to shutdown cleanly. - * @author Christian Lins - * @since sonews/0.5.0 - */ -class ShutdownHook extends Thread -{ - - /** - * Called when the JVM exits. - */ - @Override - public void run() - { - System.out.println("sonews: Trying to shutdown all threads..."); - - Map threadsMap = Thread.getAllStackTraces(); - for(Thread thread : threadsMap.keySet()) - { - // Interrupt the thread if it's a AbstractDaemon - AbstractDaemon daemon; - if(thread instanceof AbstractDaemon && thread.isAlive()) - { - try - { - daemon = (AbstractDaemon)thread; - daemon.shutdownNow(); - } - catch(SQLException ex) - { - System.out.println("sonews: " + ex); - } - } - } - - for(Thread thread : threadsMap.keySet()) - { - AbstractDaemon daemon; - if(thread instanceof AbstractDaemon && thread.isAlive()) - { - daemon = (AbstractDaemon)thread; - System.out.println("sonews: Waiting for " + daemon + " to exit..."); - try - { - daemon.join(500); - } - catch(InterruptedException ex) - { - System.out.println(ex.getLocalizedMessage()); - } - } - } - - // We have notified all not-sleeping AbstractDaemons of the shutdown; - // all other threads can be simply purged on VM shutdown - - System.out.println("sonews: Clean shutdown."); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/acl/AccessControl.java --- a/org/sonews/acl/AccessControl.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,31 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.acl; - -/** - * - * @author Christian Lins - * @since sonews/1.1 - */ -public interface AccessControl -{ - - boolean hasPermission(String user, char[] secret, String permission); - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/acl/AuthInfoCommand.java --- a/org/sonews/acl/AuthInfoCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.acl; - -import java.io.IOException; -import org.sonews.daemon.NNTPConnection; -import org.sonews.daemon.command.Command; -import org.sonews.storage.StorageBackendException; - -/** - * - * @author Christian Lins - * @since sonews/1.1 - */ -public class AuthInfoCommand implements Command -{ - - @Override - public String[] getSupportedCommandStrings() - { - throw new UnsupportedOperationException("Not supported yet."); - } - - @Override - public boolean hasFinished() - { - throw new UnsupportedOperationException("Not supported yet."); - } - - @Override - public String impliedCapability() - { - throw new UnsupportedOperationException("Not supported yet."); - } - - @Override - public boolean isStateful() - { - throw new UnsupportedOperationException("Not supported yet."); - } - - @Override - public void processLine(NNTPConnection conn, String line, byte[] rawLine) throws IOException, StorageBackendException - { - throw new UnsupportedOperationException("Not supported yet."); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/config/AbstractConfig.java --- a/org/sonews/config/AbstractConfig.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.config; - -/** - * Base class for Config and BootstrapConfig. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public abstract class AbstractConfig -{ - - public abstract String get(String key, String defVal); - - public int get(final String key, final int defVal) - { - return Integer.parseInt( - get(key, Integer.toString(defVal))); - } - - public boolean get(String key, boolean defVal) - { - String val = get(key, Boolean.toString(defVal)); - return Boolean.parseBoolean(val); - } - - /** - * Returns a long config value specified via the given key. - * @param key - * @param defVal - * @return - */ - public long get(String key, long defVal) - { - String val = get(key, Long.toString(defVal)); - return Long.parseLong(val); - } - - protected abstract void set(String key, String val); - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/config/BackendConfig.java --- a/org/sonews/config/BackendConfig.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,115 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.config; - -import java.util.logging.Level; -import org.sonews.util.Log; -import org.sonews.storage.StorageBackendException; -import org.sonews.storage.StorageManager; -import org.sonews.util.TimeoutMap; - -/** - * Provides access to the program wide configuration that is stored within - * the server's database. - * @author Christian Lins - * @since sonews/0.5.0 - */ -class BackendConfig extends AbstractConfig -{ - - private static BackendConfig instance = new BackendConfig(); - - public static BackendConfig getInstance() - { - return instance; - } - - private final TimeoutMap 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 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/config/CommandLineConfig.java --- a/org/sonews/config/CommandLineConfig.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.config; - -import java.util.Map; -import java.util.HashMap; - -/** - * - * @author Christian Lins - */ -class CommandLineConfig extends AbstractConfig -{ - - private static final CommandLineConfig instance = new CommandLineConfig(); - - public static CommandLineConfig getInstance() - { - return instance; - } - - private final Map values = new HashMap(); - - private CommandLineConfig() {} - - @Override - public String get(String key, String def) - { - synchronized(this.values) - { - if(this.values.containsKey(key)) - { - def = this.values.get(key); - } - } - return def; - } - - @Override - public void set(String key, String val) - { - synchronized(this.values) - { - this.values.put(key, val); - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/config/Config.java --- a/org/sonews/config/Config.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,175 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.config; - -/** - * Configuration facade class. - * @author Christian Lins - * @since sonews/1.0 - */ -public class Config extends AbstractConfig -{ - - public static final int LEVEL_CLI = 1; - public static final int LEVEL_FILE = 2; - public static final int LEVEL_BACKEND = 3; - - public static final String CONFIGFILE = "sonews.configfile"; - - /** BackendConfig key constant. Value is the maximum article size in kilobytes. */ - public static final String ARTICLE_MAXSIZE = "sonews.article.maxsize"; - - /** BackendConfig key constant. Value: Amount of news that are feeded per run. */ - public static final String EVENTLOG = "sonews.eventlog"; - public static final String FEED_NEWSPERRUN = "sonews.feed.newsperrun"; - public static final String FEED_PULLINTERVAL = "sonews.feed.pullinterval"; - public static final String HOSTNAME = "sonews.hostname"; - public static final String PORT = "sonews.port"; - public static final String TIMEOUT = "sonews.timeout"; - public static final String LOGLEVEL = "sonews.loglevel"; - public static final String MLPOLL_DELETEUNKNOWN = "sonews.mlpoll.deleteunknown"; - public static final String MLPOLL_HOST = "sonews.mlpoll.host"; - public static final String MLPOLL_PASSWORD = "sonews.mlpoll.password"; - public static final String MLPOLL_USER = "sonews.mlpoll.user"; - public static final String MLSEND_ADDRESS = "sonews.mlsend.address"; - public static final String MLSEND_RW_FROM = "sonews.mlsend.rewrite.from"; - public static final String MLSEND_RW_SENDER = "sonews.mlsend.rewrite.sender"; - public static final String MLSEND_HOST = "sonews.mlsend.host"; - public static final String MLSEND_PASSWORD = "sonews.mlsend.password"; - public static final String MLSEND_PORT = "sonews.mlsend.port"; - public static final String MLSEND_USER = "sonews.mlsend.user"; - - /** Key constant. If value is "true" every I/O is written to logfile - * (which is a lot!) - */ - public static final String DEBUG = "sonews.debug"; - - /** Key constant. Value is classname of the JDBC driver */ - public static final String STORAGE_DBMSDRIVER = "sonews.storage.dbmsdriver"; - - /** Key constant. Value is JDBC connect String to the database. */ - public static final String STORAGE_DATABASE = "sonews.storage.database"; - - /** Key constant. Value is the username for the DBMS. */ - public static final String STORAGE_USER = "sonews.storage.user"; - - /** Key constant. Value is the password for the DBMS. */ - public static final String STORAGE_PASSWORD = "sonews.storage.password"; - - /** Key constant. Value is the name of the host which is allowed to use the - * XDAEMON command; default: "localhost" */ - public static final String XDAEMON_HOST = "sonews.xdaemon.host"; - - /** The config key for the filename of the logfile */ - public static final String LOGFILE = "sonews.log"; - - public static final String[] AVAILABLE_KEYS = { - ARTICLE_MAXSIZE, - EVENTLOG, - FEED_NEWSPERRUN, - FEED_PULLINTERVAL, - HOSTNAME, - MLPOLL_DELETEUNKNOWN, - MLPOLL_HOST, - MLPOLL_PASSWORD, - MLPOLL_USER, - MLSEND_ADDRESS, - MLSEND_HOST, - MLSEND_PASSWORD, - MLSEND_PORT, - MLSEND_RW_FROM, - MLSEND_RW_SENDER, - MLSEND_USER, - PORT, - TIMEOUT, - XDAEMON_HOST - }; - - private static Config instance = new Config(); - - public static Config inst() - { - return instance; - } - - private Config(){} - - @Override - public String get(String key, String def) - { - String val = CommandLineConfig.getInstance().get(key, null); - - if(val == null) - { - val = FileConfig.getInstance().get(key, null); - } - - if(val == null) - { - val = BackendConfig.getInstance().get(key, def); - } - - return val; - } - - public String get(int maxLevel, String key, String def) - { - String val = CommandLineConfig.getInstance().get(key, null); - - if(val == null && maxLevel >= LEVEL_FILE) - { - val = FileConfig.getInstance().get(key, null); - if(val == null && maxLevel >= LEVEL_BACKEND) - { - val = BackendConfig.getInstance().get(key, def); - } - } - - return val != null ? val : def; - } - - @Override - public void set(String key, String val) - { - set(LEVEL_BACKEND, key, val); - } - - public void set(int level, String key, String val) - { - switch(level) - { - case LEVEL_CLI: - { - CommandLineConfig.getInstance().set(key, val); - break; - } - case LEVEL_FILE: - { - FileConfig.getInstance().set(key, val); - break; - } - case LEVEL_BACKEND: - { - BackendConfig.getInstance().set(key, val); - break; - } - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/config/FileConfig.java --- a/org/sonews/config/FileConfig.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,170 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.config; - -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Properties; - -/** - * Manages the bootstrap configuration. It MUST contain all config values - * that are needed to establish a database connection. - * For further configuration values use the Config class instead as that class - * stores its values within the database. - * @author Christian Lins - * @since sonews/0.5.0 - */ -class FileConfig extends AbstractConfig -{ - - private static final Properties defaultConfig = new Properties(); - - private static FileConfig instance = null; - - static - { - // Set some default values - defaultConfig.setProperty(Config.STORAGE_DATABASE, "jdbc:mysql://localhost/sonews"); - defaultConfig.setProperty(Config.STORAGE_DBMSDRIVER, "com.mysql.jdbc.Driver"); - defaultConfig.setProperty(Config.STORAGE_USER, "sonews_user"); - defaultConfig.setProperty(Config.STORAGE_PASSWORD, "mysecret"); - defaultConfig.setProperty(Config.DEBUG, "false"); - } - - /** - * Note: this method is not thread-safe - * @return A Config instance - */ - public static synchronized FileConfig getInstance() - { - if(instance == null) - { - instance = new FileConfig(); - } - return instance; - } - - // Every config instance is initialized with the default values. - private final Properties settings = (Properties)defaultConfig.clone(); - - /** - * Config is a singelton class with only one instance at time. - * So the constructor is private to prevent the creation of more - * then one Config instance. - * @see Config.getInstance() to retrieve an instance of Config - */ - private FileConfig() - { - try - { - // Load settings from file - load(); - } - catch(IOException ex) - { - ex.printStackTrace(); - } - } - - /** - * Loads the configuration from the config file. By default this is done - * by the (private) constructor but it can be useful to reload the config - * by invoking this method. - * @throws IOException - */ - public void load() - throws IOException - { - FileInputStream in = null; - - try - { - in = new FileInputStream( - Config.inst().get(Config.LEVEL_CLI, Config.CONFIGFILE, "sonews.conf")); - settings.load(in); - } - catch (FileNotFoundException e) - { - // MUST NOT use Log otherwise endless loop - System.err.println(e.getMessage()); - save(); - } - finally - { - if(in != null) - in.close(); - } - } - - /** - * Saves this Config to the config file. By default this is done - * at program end. - * @throws FileNotFoundException - * @throws IOException - */ - public void save() throws FileNotFoundException, IOException - { - FileOutputStream out = null; - try - { - out = new FileOutputStream( - Config.inst().get(Config.LEVEL_CLI, Config.CONFIGFILE, "sonews.conf")); - settings.store(out, "SONEWS Config File"); - out.flush(); - } - catch(IOException ex) - { - throw ex; - } - finally - { - if(out != null) - out.close(); - } - } - - /** - * Returns the value that is stored within this config - * identified by the given key. If the key cannot be found - * the default value is returned. - * @param key Key to identify the value. - * @param def The default value that is returned if the key - * is not found in this Config. - * @return - */ - @Override - public String get(String key, String def) - { - return settings.getProperty(key, def); - } - - /** - * Sets the value for a given key. - * @param key - * @param value - */ - @Override - public void set(final String key, final String value) - { - settings.setProperty(key, value); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/AbstractDaemon.java --- a/org/sonews/daemon/AbstractDaemon.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,101 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon; - -import java.sql.SQLException; -import org.sonews.storage.StorageManager; -import org.sonews.util.Log; - -/** - * Base class of all sonews threads. - * Instances of this class will be automatically registered at the ShutdownHook - * to be cleanly exited when the server is forced to exit. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public abstract class AbstractDaemon extends Thread -{ - - /** This variable is write synchronized through setRunning */ - private boolean isRunning = false; - - /** - * Protected constructor. Will be called by derived classes. - */ - protected AbstractDaemon() - { - setDaemon(true); // VM will exit when all threads are daemons - setName(getClass().getSimpleName()); - } - - /** - * @return true if shutdown() was not yet called. - */ - public boolean isRunning() - { - synchronized(this) - { - return this.isRunning; - } - } - - /** - * Marks this thread to exit soon. Closes the associated JDBCDatabase connection - * if available. - * @throws java.sql.SQLException - */ - public void shutdownNow() - throws SQLException - { - synchronized(this) - { - this.isRunning = false; - StorageManager.disableProvider(); - } - } - - /** - * Calls shutdownNow() but catches SQLExceptions if occurring. - */ - public void shutdown() - { - try - { - shutdownNow(); - } - catch(SQLException ex) - { - Log.get().warning(ex.toString()); - } - } - - /** - * Starts this daemon. - */ - @Override - public void start() - { - synchronized(this) - { - this.isRunning = true; - } - super.start(); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/ChannelLineBuffers.java --- a/org/sonews/daemon/ChannelLineBuffers.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,283 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon; - -import java.nio.ByteBuffer; -import java.nio.channels.ClosedChannelException; -import java.util.ArrayList; -import java.util.List; - -/** - * Class holding ByteBuffers for SocketChannels/NNTPConnection. - * Due to the complex nature of AIO/NIO we must properly handle the line - * buffers for the input and output of the SocketChannels. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class ChannelLineBuffers -{ - - /** - * Size of one small buffer; - * per default this is 512 bytes to fit one standard line. - */ - public static final int BUFFER_SIZE = 512; - - private static int maxCachedBuffers = 2048; // Cached buffers maximum - - private static final List 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; - - // Mark the current write position - int mark = buffer.position(); - - // Set position to 0 and limit to current position - buffer.flip(); - - ByteBuffer lineBuffer = newLineBuffer(); - - while (buffer.position() < buffer.limit()) - { - byte b = buffer.get(); - if (b == 10) // '\n' - { - // The bytes between the buffer's current position and its limit, - // if any, are copied to the beginning of the buffer. That is, the - // byte at index p = position() is copied to index zero, the byte at - // index p + 1 is copied to index one, and so forth until the byte - // at index limit() - 1 is copied to index n = limit() - 1 - p. - // The buffer's position is then set to n+1 and its limit is set to - // its capacity. - buffer.compact(); - - lineBuffer.flip(); // limit to position, position to 0 - return lineBuffer; - } - else - { - lineBuffer.put(b); - } - } - - buffer.limit(BUFFER_SIZE); - buffer.position(mark); - - if(buffer.hasRemaining()) - { - return null; - } - else - { - // In the first 512 was no newline found, so the input is not standard - // compliant. We return the current buffer as new line and add a space - // to the beginning of the next line which corrects some overlong header - // lines. - inputBuffer = newLineBuffer(); - inputBuffer.put((byte)' '); - buffer.flip(); - return buffer; - } - } - } - - /** - * Returns a at least 512 bytes long ByteBuffer ready for usage. - * The method first try to reuse an already allocated (cached) buffer but - * if that fails returns a newly allocated direct buffer. - * Use recycleBuffer() method when you do not longer use the allocated buffer. - */ - static ByteBuffer newLineBuffer() - { - ByteBuffer buf = null; - synchronized(freeSmallBuffers) - { - if(!freeSmallBuffers.isEmpty()) - { - buf = freeSmallBuffers.remove(0); - } - } - - if(buf == null) - { - // Allocate a non-direct buffer - buf = ByteBuffer.allocate(BUFFER_SIZE); - } - - assert buf.position() == 0; - assert buf.limit() >= BUFFER_SIZE; - - return buf; - } - - /** - * Adds the given buffer to the list of free buffers if it is a valuable - * direct allocated buffer. - * @param buffer - */ - public static void recycleBuffer(ByteBuffer buffer) - { - assert buffer != null; - - if(buffer.isDirect()) - { - assert buffer.capacity() >= BUFFER_SIZE; - - // Add old buffers to the list of free buffers - synchronized(freeSmallBuffers) - { - buffer.clear(); // Set position to 0 and limit to capacity - freeSmallBuffers.add(buffer); - } - } // if(buffer.isDirect()) - } - - /** - * Recycles all buffers of this ChannelLineBuffers object. - */ - public void recycleBuffers() - { - synchronized(inputBuffer) - { - recycleBuffer(inputBuffer); - this.inputBuffer = null; - } - - synchronized(outputBuffers) - { - for(ByteBuffer buf : outputBuffers) - { - recycleBuffer(buf); - } - outputBuffers = null; - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/ChannelReader.java --- a/org/sonews/daemon/ChannelReader.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,202 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.CancelledKeyException; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.SocketChannel; -import java.util.Iterator; -import java.util.Set; -import java.util.logging.Level; -import org.sonews.util.Log; - -/** - * A Thread task listening for OP_READ events from SocketChannels. - * @author Christian Lins - * @since sonews/0.5.0 - */ -class ChannelReader extends AbstractDaemon -{ - - private static ChannelReader instance = new ChannelReader(); - - /** - * @return Active ChannelReader instance. - */ - public static ChannelReader getInstance() - { - return instance; - } - - private Selector selector = null; - - protected ChannelReader() - { - } - - /** - * Sets the selector which is used by this reader to determine the channel - * to read from. - * @param selector - */ - public void setSelector(final Selector selector) - { - this.selector = selector; - } - - /** - * Run loop. Blocks until some data is available in a channel. - */ - @Override - public void run() - { - assert selector != null; - - while(isRunning()) - { - try - { - // select() blocks until some SelectableChannels are ready for - // processing. There is no need to lock the selector as we have only - // one thread per selector. - selector.select(); - - // Get list of selection keys with pending events. - // Note: the selected key set is not thread-safe - SocketChannel channel = null; - NNTPConnection conn = null; - final Set selKeys = selector.selectedKeys(); - SelectionKey selKey = null; - - synchronized (selKeys) - { - Iterator it = selKeys.iterator(); - - // Process the first pending event - while (it.hasNext()) - { - selKey = (SelectionKey) it.next(); - channel = (SocketChannel) selKey.channel(); - conn = Connections.getInstance().get(channel); - - // Because we cannot lock the selKey as that would cause a deadlock - // we lock the connection. To preserve the order of the received - // byte blocks a selection key for a connection that has pending - // read events is skipped. - if (conn == null || conn.tryReadLock()) - { - // Remove from set to indicate that it's being processed - it.remove(); - if (conn != null) - { - break; // End while loop - } - } - else - { - selKey = null; - channel = null; - conn = null; - } - } - } - - // Do not lock the selKeys while processing because this causes - // a deadlock in sun.nio.ch.SelectorImpl.lockAndDoSelect() - if (selKey != null && channel != null && conn != null) - { - processSelectionKey(conn, channel, selKey); - conn.unlockReadLock(); - } - - } - catch(CancelledKeyException ex) - { - Log.get().warning("ChannelReader.run(): " + ex); - Log.get().log(Level.INFO, "", ex); - } - catch(Exception ex) - { - ex.printStackTrace(); - } - - // Eventually wait for a register operation - synchronized (NNTPDaemon.RegisterGate) - { - // Do nothing; FindBugs may warn about an empty synchronized - // statement, but we cannot use a wait()/notify() mechanism here. - // If we used something like RegisterGate.wait() we block here - // until the NNTPDaemon calls notify(). But the daemon only - // calls notify() if itself is NOT blocked in the listening socket. - } - } // while(isRunning()) - } - - private void processSelectionKey(final NNTPConnection connection, - final SocketChannel socketChannel, final SelectionKey selKey) - throws InterruptedException, IOException - { - assert selKey != null; - assert selKey.isReadable(); - - // Some bytes are available for reading - if(selKey.isValid()) - { - // Lock the channel - //synchronized(socketChannel) - { - // Read the data into the appropriate buffer - ByteBuffer buf = connection.getInputBuffer(); - int read = -1; - try - { - read = socketChannel.read(buf); - } - catch(IOException ex) - { - // The connection was probably closed by the remote host - // in a non-clean fashion - Log.get().info("ChannelReader.processSelectionKey(): " + ex); - } - catch(Exception ex) - { - Log.get().warning("ChannelReader.processSelectionKey(): " + ex); - } - - if(read == -1) // End of stream - { - selKey.cancel(); - } - else if(read > 0) // If some data was read - { - ConnectionWorker.addChannel(socketChannel); - } - } - } - else - { - // Should not happen - Log.get().severe("Should not happen: " + selKey.toString()); - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/ChannelWriter.java --- a/org/sonews/daemon/ChannelWriter.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,210 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon; - -import org.sonews.util.Log; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.CancelledKeyException; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.SocketChannel; -import java.util.Iterator; - -/** - * A Thread task that processes OP_WRITE events for SocketChannels. - * @author Christian Lins - * @since sonews/0.5.0 - */ -class ChannelWriter extends AbstractDaemon -{ - - private static ChannelWriter instance = new ChannelWriter(); - - /** - * @return Returns the active ChannelWriter instance. - */ - public static ChannelWriter getInstance() - { - return instance; - } - - private Selector selector = null; - - protected ChannelWriter() - { - } - - /** - * @return Selector associated with this instance. - */ - public Selector getSelector() - { - return this.selector; - } - - /** - * Sets the selector that is used by this ChannelWriter. - * @param selector - */ - public void setSelector(final Selector selector) - { - this.selector = selector; - } - - /** - * Run loop. - */ - @Override - public void run() - { - assert selector != null; - - while(isRunning()) - { - try - { - SelectionKey selKey = null; - SocketChannel socketChannel = null; - NNTPConnection connection = null; - - // select() blocks until some SelectableChannels are ready for - // processing. There is no need to synchronize the selector as we - // have only one thread per selector. - selector.select(); // The return value of select can be ignored - - // Get list of selection keys with pending OP_WRITE events. - // The keySET is not thread-safe whereas the keys itself are. - Iterator it = selector.selectedKeys().iterator(); - - while (it.hasNext()) - { - // We remove the first event from the set and store it for - // later processing. - selKey = (SelectionKey) it.next(); - socketChannel = (SocketChannel) selKey.channel(); - connection = Connections.getInstance().get(socketChannel); - - it.remove(); - if (connection != null) - { - break; - } - else - { - selKey = null; - } - } - - if (selKey != null) - { - try - { - // Process the selected key. - // As there is only one OP_WRITE key for a given channel, we need - // not to synchronize this processing to retain the order. - processSelectionKey(connection, socketChannel, selKey); - } - catch (IOException ex) - { - Log.get().warning("Error writing to channel: " + ex); - - // Cancel write events for this channel - selKey.cancel(); - connection.shutdownInput(); - connection.shutdownOutput(); - } - } - - // Eventually wait for a register operation - synchronized(NNTPDaemon.RegisterGate) { /* do nothing */ } - } - catch(CancelledKeyException ex) - { - Log.get().info("ChannelWriter.run(): " + ex); - } - catch(Exception ex) - { - ex.printStackTrace(); - } - } // while(isRunning()) - } - - private void processSelectionKey(final NNTPConnection connection, - final SocketChannel socketChannel, final SelectionKey selKey) - throws InterruptedException, IOException - { - assert connection != null; - assert socketChannel != null; - assert selKey != null; - assert selKey.isWritable(); - - // SocketChannel is ready for writing - if(selKey.isValid()) - { - // Lock the socket channel - synchronized(socketChannel) - { - // Get next output buffer - ByteBuffer buf = connection.getOutputBuffer(); - if(buf == null) - { - // Currently we have nothing to write, so we stop the writeable - // events until we have something to write to the socket channel - //selKey.cancel(); - selKey.interestOps(0); - // Update activity timestamp to prevent too early disconnects - // on slow client connections - connection.setLastActivity(System.currentTimeMillis()); - return; - } - - while(buf != null) // There is data to be send - { - // Write buffer to socket channel; this method does not block - if(socketChannel.write(buf) <= 0) - { - // Perhaps there is data to be written, but the SocketChannel's - // buffer is full, so we stop writing to until the next event. - break; - } - else - { - // Retrieve next buffer if available; method may return the same - // buffer instance if it still have some bytes remaining - buf = connection.getOutputBuffer(); - } - } - } - } - else - { - Log.get().warning("Invalid OP_WRITE key: " + selKey); - - if(socketChannel.socket().isClosed()) - { - connection.shutdownInput(); - connection.shutdownOutput(); - socketChannel.close(); - Log.get().info("Connection closed."); - } - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/CommandSelector.java --- a/org/sonews/daemon/CommandSelector.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,141 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import org.sonews.daemon.command.Command; -import org.sonews.daemon.command.UnsupportedCommand; -import org.sonews.util.Log; -import org.sonews.util.io.Resource; - -/** - * Selects the correct command processing class. - * @author Christian Lins - * @since sonews/1.0 - */ -public class CommandSelector -{ - - private static Map 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; - } - - try - { - addCommandHandler(className); - } - catch(ClassNotFoundException ex) - { - Log.get().warning("Could not load command class: " + ex); - } - catch(InstantiationException ex) - { - Log.get().severe("Could not instantiate command class: " + ex); - } - catch(IllegalAccessException ex) - { - Log.get().severe("Could not access command class: " + ex); - } - } - } - - public static void addCommandHandler(String className) - throws ClassNotFoundException, InstantiationException, IllegalAccessException - { - Class clazz = Class.forName(className); - Command cmd = (Command)clazz.newInstance(); - String[] cmdStrs = cmd.getSupportedCommandStrings(); - for (String cmdStr : cmdStrs) - { - commandClassesMapping.put(cmdStr, clazz); - } - } - - public static Set getCommandNames() - { - return commandClassesMapping.keySet(); - } - - public static CommandSelector getInstance() - { - CommandSelector csel = instances.get(Thread.currentThread()); - if(csel == null) - { - csel = new CommandSelector(); - instances.put(Thread.currentThread(), csel); - } - return csel; - } - - private Map commandMapping = new HashMap(); - private Command unsupportedCmd = new UnsupportedCommand(); - - private CommandSelector() - {} - - public Command get(String commandName) - { - try - { - commandName = commandName.toUpperCase(); - Command cmd = this.commandMapping.get(commandName); - - if(cmd == null) - { - Class clazz = commandClassesMapping.get(commandName); - if(clazz == null) - { - cmd = this.unsupportedCmd; - } - else - { - cmd = (Command)clazz.newInstance(); - this.commandMapping.put(commandName, cmd); - } - } - else if(cmd.isStateful()) - { - cmd = cmd.getClass().newInstance(); - } - - return cmd; - } - catch(Exception ex) - { - ex.printStackTrace(); - return this.unsupportedCmd; - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/ConnectionWorker.java --- a/org/sonews/daemon/ConnectionWorker.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,102 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon; - -import org.sonews.util.Log; -import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; -import java.util.concurrent.ArrayBlockingQueue; - -/** - * Does most of the work: parsing input, talking to client and Database. - * @author Christian Lins - * @since sonews/0.5.0 - */ -class ConnectionWorker extends AbstractDaemon -{ - - // 256 pending events should be enough - private static ArrayBlockingQueue 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(); - - if(channel != null) - { - // Connections.getInstance().get() MAY return null - NNTPConnection conn = Connections.getInstance().get(channel); - - // Try to lock the connection object - if(conn != null && conn.tryReadLock()) - { - ByteBuffer buf = conn.getBuffers().nextInputLine(); - while(buf != null) // Complete line was received - { - final byte[] line = new byte[buf.limit()]; - buf.get(line); - ChannelLineBuffers.recycleBuffer(buf); - - // Here is the actual work done - conn.lineReceived(line); - - // Read next line as we could have already received the next line - buf = conn.getBuffers().nextInputLine(); - } - conn.unlockReadLock(); - } - else - { - addChannel(channel); - } - } - } - catch(InterruptedException ex) - { - Log.get().info("ConnectionWorker interrupted: " + ex); - } - catch(Exception ex) - { - Log.get().severe("Exception in ConnectionWorker: " + ex); - ex.printStackTrace(); - } - } // end while(isRunning()) - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/Connections.java --- a/org/sonews/daemon/Connections.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,181 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon; - -import org.sonews.config.Config; -import org.sonews.util.Log; -import org.sonews.util.Stats; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.nio.channels.SocketChannel; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; - -/** - * Daemon thread collecting all NNTPConnection instances. The thread - * checks periodically if there are stale/timed out connections and - * removes and purges them properly. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public final class Connections extends AbstractDaemon -{ - - private static final Connections instance = new Connections(); - - /** - * @return Active Connections instance. - */ - public static Connections getInstance() - { - return Connections.instance; - } - - private final List 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); - } - } - - int getConnectionCount(String remote) - { - int cnt = 0; - synchronized(this.connections) - { - for(NNTPConnection conn : this.connections) - { - assert conn != null; - assert conn.getSocketChannel() != null; - - Socket socket = conn.getSocketChannel().socket(); - if(socket != null) - { - InetSocketAddress sockAddr = (InetSocketAddress)socket.getRemoteSocketAddress(); - if(sockAddr != null) - { - if(sockAddr.getHostName().equals(remote)) - { - cnt++; - } - } - } // if(socket != null) - } - } - return cnt; - } - - /** - * Run loops. Checks periodically for timed out connections and purged them - * from the lists. - */ - @Override - public void run() - { - while(isRunning()) - { - int timeoutMillis = 1000 * Config.inst().get(Config.TIMEOUT, 180); - - synchronized (this.connections) - { - final ListIterator iter = this.connections.listIterator(); - NNTPConnection conn; - - while (iter.hasNext()) - { - conn = iter.next(); - if((System.currentTimeMillis() - conn.getLastActivity()) > timeoutMillis - && conn.getBuffers().isOutputBufferEmpty()) - { - // A connection timeout has occurred so purge the connection - iter.remove(); - - // Close and remove the channel - SocketChannel channel = conn.getSocketChannel(); - connByChannel.remove(channel); - - try - { - assert channel != null; - assert channel.socket() != null; - - // Close the channel; implicitely cancels all selectionkeys - channel.close(); - Log.get().info("Disconnected: " + channel.socket().getRemoteSocketAddress() + - " (timeout)"); - } - catch(IOException ex) - { - Log.get().warning("Connections.run(): " + ex); - } - - // Recycle the used buffers - conn.getBuffers().recycleBuffers(); - - Stats.getInstance().clientDisconnect(); - } - } - } - - try - { - Thread.sleep(10000); // Sleep ten seconds - } - catch(InterruptedException ex) - { - Log.get().warning("Connections Thread was interrupted: " + ex.getMessage()); - } - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/LineEncoder.java --- a/org/sonews/daemon/LineEncoder.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,80 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon; - -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.channels.ClosedChannelException; -import java.nio.charset.Charset; -import java.nio.charset.CharsetEncoder; -import java.nio.charset.CoderResult; - -/** - * Encodes a line to buffers using the correct charset. - * @author Christian Lins - * @since sonews/0.5.0 - */ -class LineEncoder -{ - - private CharBuffer characters; - private Charset charset; - - /** - * Constructs new LineEncoder. - * @param characters - * @param charset - */ - public LineEncoder(CharBuffer characters, Charset charset) - { - this.characters = characters; - this.charset = charset; - } - - /** - * Encodes the characters of this instance to the given ChannelLineBuffers - * using the Charset of this instance. - * @param buffer - * @throws java.nio.channels.ClosedChannelException - */ - public void encode(ChannelLineBuffers buffer) - throws ClosedChannelException - { - CharsetEncoder encoder = charset.newEncoder(); - while (characters.hasRemaining()) - { - ByteBuffer buf = ChannelLineBuffers.newLineBuffer(); - assert buf.position() == 0; - assert buf.capacity() >= 512; - - CoderResult res = encoder.encode(characters, buf, true); - - // Set limit to current position and current position to 0; - // means make ready for read from buffer - buf.flip(); - buffer.addOutputBuffer(buf); - - if (res.isUnderflow()) // All input processed - { - break; - } - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/NNTPConnection.java --- a/org/sonews/daemon/NNTPConnection.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,428 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketException; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.SelectionKey; -import java.nio.channels.SocketChannel; -import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.Timer; -import java.util.TimerTask; -import org.sonews.daemon.command.Command; -import org.sonews.storage.Article; -import org.sonews.storage.Channel; -import org.sonews.storage.StorageBackendException; -import org.sonews.util.Log; -import org.sonews.util.Stats; - -/** - * For every SocketChannel (so TCP/IP connection) there is an instance of - * this class. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public final class NNTPConnection -{ - - public static final String NEWLINE = "\r\n"; // RFC defines this as newline - public static final String MESSAGE_ID_PATTERN = "<[^>]+>"; - - private static final Timer cancelTimer = new Timer(true); // Thread-safe? True for run as daemon - - /** SocketChannel is generally thread-safe */ - private SocketChannel channel = null; - private Charset charset = Charset.forName("UTF-8"); - private Command command = null; - private Article currentArticle = null; - private Channel currentGroup = null; - private volatile long lastActivity = System.currentTimeMillis(); - private ChannelLineBuffers lineBuffers = new ChannelLineBuffers(); - private int readLock = 0; - private final Object readLockGate = new Object(); - private SelectionKey writeSelKey = null; - - public NNTPConnection(final SocketChannel channel) - throws IOException - { - if(channel == null) - { - throw new IllegalArgumentException("channel is null"); - } - - this.channel = channel; - Stats.getInstance().clientConnect(); - } - - /** - * Tries to get the read lock for this NNTPConnection. This method is Thread- - * safe and returns true of the read lock was successfully set. If the lock - * is still hold by another Thread the method returns false. - */ - boolean tryReadLock() - { - // As synchronizing simple types may cause deadlocks, - // we use a gate object. - synchronized(readLockGate) - { - if(readLock != 0) - { - return false; - } - else - { - readLock = Thread.currentThread().hashCode(); - return true; - } - } - } - - /** - * Releases the read lock in a Thread-safe way. - * @throws IllegalMonitorStateException if a Thread not holding the lock - * tries to release it. - */ - void unlockReadLock() - { - synchronized(readLockGate) - { - if(readLock == Thread.currentThread().hashCode()) - { - readLock = 0; - } - else - { - throw new IllegalMonitorStateException(); - } - } - } - - /** - * @return Current input buffer of this NNTPConnection instance. - */ - public ByteBuffer getInputBuffer() - { - return this.lineBuffers.getInputBuffer(); - } - - /** - * @return Output buffer of this NNTPConnection which has at least one byte - * free storage. - */ - public ByteBuffer getOutputBuffer() - { - return this.lineBuffers.getOutputBuffer(); - } - - /** - * @return ChannelLineBuffers instance associated with this NNTPConnection. - */ - public ChannelLineBuffers getBuffers() - { - return this.lineBuffers; - } - - /** - * @return true if this connection comes from a local remote address. - */ - public boolean isLocalConnection() - { - return ((InetSocketAddress)this.channel.socket().getRemoteSocketAddress()) - .getHostName().equalsIgnoreCase("localhost"); - } - - void setWriteSelectionKey(SelectionKey selKey) - { - this.writeSelKey = selKey; - } - - public void shutdownInput() - { - try - { - // Closes the input line of the channel's socket, so no new data - // will be received and a timeout can be triggered. - this.channel.socket().shutdownInput(); - } - catch(IOException ex) - { - Log.get().warning("Exception in NNTPConnection.shutdownInput(): " + ex); - } - } - - public void shutdownOutput() - { - cancelTimer.schedule(new TimerTask() - { - @Override - public void run() - { - try - { - // Closes the output line of the channel's socket. - channel.socket().shutdownOutput(); - channel.close(); - } - catch(SocketException ex) - { - // Socket was already disconnected - Log.get().info("NNTPConnection.shutdownOutput(): " + ex); - } - catch(Exception ex) - { - Log.get().warning("NNTPConnection.shutdownOutput(): " + ex); - } - } - }, 3000); - } - - public SocketChannel getSocketChannel() - { - return this.channel; - } - - public Article getCurrentArticle() - { - return this.currentArticle; - } - - public Charset getCurrentCharset() - { - return this.charset; - } - - /** - * @return The currently selected communication channel (not SocketChannel) - */ - public Channel getCurrentChannel() - { - return this.currentGroup; - } - - public void setCurrentArticle(final Article article) - { - this.currentArticle = article; - } - - public void setCurrentGroup(final Channel group) - { - this.currentGroup = group; - } - - public long getLastActivity() - { - return this.lastActivity; - } - - /** - * Due to the readLockGate there is no need to synchronize this method. - * @param raw - * @throws IllegalArgumentException if raw is null. - * @throws IllegalStateException if calling thread does not own the readLock. - */ - void lineReceived(byte[] raw) - { - if(raw == null) - { - throw new IllegalArgumentException("raw is null"); - } - - if(readLock == 0 || readLock != Thread.currentThread().hashCode()) - { - throw new IllegalStateException("readLock not properly set"); - } - - this.lastActivity = System.currentTimeMillis(); - - String line = new String(raw, this.charset); - - // There might be a trailing \r, but trim() is a bad idea - // as it removes also leading spaces from long header lines. - if(line.endsWith("\r")) - { - line = line.substring(0, line.length() - 1); - raw = Arrays.copyOf(raw, raw.length - 1); - } - - Log.get().fine("<< " + line); - - if(command == null) - { - command = parseCommandLine(line); - assert command != null; - } - - try - { - // The command object will process the line we just received - try - { - command.processLine(this, line, raw); - } - catch(StorageBackendException ex) - { - Log.get().info("Retry command processing after StorageBackendException"); - - // Try it a second time, so that the backend has time to recover - command.processLine(this, line, raw); - } - } - catch(ClosedChannelException ex0) - { - try - { - Log.get().info("Connection to " + channel.socket().getRemoteSocketAddress() - + " closed: " + ex0); - } - catch(Exception ex0a) - { - ex0a.printStackTrace(); - } - } - catch(Exception ex1) // This will catch a second StorageBackendException - { - try - { - command = null; - ex1.printStackTrace(); - println("500 Internal server error"); - } - catch(Exception ex2) - { - ex2.printStackTrace(); - } - } - - if(command == null || command.hasFinished()) - { - command = null; - charset = Charset.forName("UTF-8"); // Reset to default - } - } - - /** - * This method determines the fitting command processing class. - * @param line - * @return - */ - private Command parseCommandLine(String line) - { - String cmdStr = line.split(" ")[0]; - return CommandSelector.getInstance().get(cmdStr); - } - - /** - * Puts the given line into the output buffer, adds a newline character - * and returns. The method returns immediately and does not block until - * the line was sent. If line is longer than 510 octets it is split up in - * several lines. Each line is terminated by \r\n (NNTPConnection.NEWLINE). - * @param line - */ - public void println(final CharSequence line, final Charset charset) - throws IOException - { - writeToChannel(CharBuffer.wrap(line), charset, line); - writeToChannel(CharBuffer.wrap(NEWLINE), charset, null); - } - - /** - * Writes the given raw lines to the output buffers and finishes with - * a newline character (\r\n). - * @param rawLines - */ - public void println(final byte[] rawLines) - throws IOException - { - this.lineBuffers.addOutputBuffer(ByteBuffer.wrap(rawLines)); - writeToChannel(CharBuffer.wrap(NEWLINE), charset, null); - } - - /** - * Encodes the given CharBuffer using the given Charset to a bunch of - * ByteBuffers (each 512 bytes large) and enqueues them for writing at the - * connected SocketChannel. - * @throws java.io.IOException - */ - private void writeToChannel(CharBuffer characters, final Charset charset, - CharSequence debugLine) - throws IOException - { - if(!charset.canEncode()) - { - Log.get().severe("FATAL: Charset " + charset + " cannot encode!"); - return; - } - - // Write characters to output buffers - LineEncoder lenc = new LineEncoder(characters, charset); - lenc.encode(lineBuffers); - - enableWriteEvents(debugLine); - } - - private void enableWriteEvents(CharSequence debugLine) - { - // Enable OP_WRITE events so that the buffers are processed - try - { - this.writeSelKey.interestOps(SelectionKey.OP_WRITE); - ChannelWriter.getInstance().getSelector().wakeup(); - } - catch(Exception ex) // CancelledKeyException and ChannelCloseException - { - Log.get().warning("NNTPConnection.writeToChannel(): " + ex); - return; - } - - // Update last activity timestamp - this.lastActivity = System.currentTimeMillis(); - if(debugLine != null) - { - Log.get().fine(">> " + debugLine); - } - } - - public void println(final CharSequence line) - throws IOException - { - println(line, charset); - } - - public void print(final String line) - throws IOException - { - writeToChannel(CharBuffer.wrap(line), charset, line); - } - - public void setCurrentCharset(final Charset charset) - { - this.charset = charset; - } - - void setLastActivity(long timestamp) - { - this.lastActivity = timestamp; - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/NNTPDaemon.java --- a/org/sonews/daemon/NNTPDaemon.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,197 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon; - -import org.sonews.config.Config; -import org.sonews.Main; -import org.sonews.util.Log; -import java.io.IOException; -import java.net.BindException; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.nio.channels.CancelledKeyException; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; - -/** - * NNTP daemon using SelectableChannels. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public final class NNTPDaemon extends AbstractDaemon -{ - - public static final Object RegisterGate = new Object(); - - private static NNTPDaemon instance = null; - - public static synchronized NNTPDaemon createInstance(int port) - { - if(instance == null) - { - instance = new NNTPDaemon(port); - return instance; - } - else - { - throw new RuntimeException("NNTPDaemon.createInstance() called twice"); - } - } - - private int port; - - private NNTPDaemon(final int port) - { - Log.get().info("Server listening on port " + port); - this.port = port; - } - - @Override - public void run() - { - try - { - // Create a Selector that handles the SocketChannel multiplexing - final Selector readSelector = Selector.open(); - final Selector writeSelector = Selector.open(); - - // Start working threads - final int workerThreads = Runtime.getRuntime().availableProcessors() * 4; - ConnectionWorker[] cworkers = new ConnectionWorker[workerThreads]; - for(int n = 0; n < workerThreads; n++) - { - cworkers[n] = new ConnectionWorker(); - cworkers[n].start(); - } - - ChannelWriter.getInstance().setSelector(writeSelector); - ChannelReader.getInstance().setSelector(readSelector); - ChannelWriter.getInstance().start(); - ChannelReader.getInstance().start(); - - final ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); - serverSocketChannel.configureBlocking(true); // Set to blocking mode - - // Configure ServerSocket; bind to socket... - final ServerSocket serverSocket = serverSocketChannel.socket(); - serverSocket.bind(new InetSocketAddress(this.port)); - - while(isRunning()) - { - SocketChannel socketChannel; - - try - { - // As we set the server socket channel to blocking mode the accept() - // method will block. - socketChannel = serverSocketChannel.accept(); - socketChannel.configureBlocking(false); - assert socketChannel.isConnected(); - assert socketChannel.finishConnect(); - } - catch(IOException ex) - { - // Under heavy load an IOException "Too many open files may - // be thrown. It most cases we should slow down the connection - // accepting, to give the worker threads some time to process work. - Log.get().severe("IOException while accepting connection: " + ex.getMessage()); - Log.get().info("Connection accepting sleeping for seconds..."); - Thread.sleep(5000); // 5 seconds - continue; - } - - final NNTPConnection conn; - try - { - conn = new NNTPConnection(socketChannel); - Connections.getInstance().add(conn); - } - catch(IOException ex) - { - Log.get().warning(ex.toString()); - socketChannel.close(); - continue; - } - - try - { - SelectionKey selKeyWrite = - registerSelector(writeSelector, socketChannel, SelectionKey.OP_WRITE); - registerSelector(readSelector, socketChannel, SelectionKey.OP_READ); - - Log.get().info("Connected: " + socketChannel.socket().getRemoteSocketAddress()); - - // Set write selection key and send hello to client - conn.setWriteSelectionKey(selKeyWrite); - conn.println("200 " + Config.inst().get(Config.HOSTNAME, "localhost") - + " " + Main.VERSION + " news server ready - (posting ok)."); - } - catch(CancelledKeyException cke) - { - Log.get().warning("CancelledKeyException " + cke.getMessage() + " was thrown: " - + socketChannel.socket()); - } - catch(ClosedChannelException cce) - { - Log.get().warning("ClosedChannelException " + cce.getMessage() + " was thrown: " - + socketChannel.socket()); - } - } - } - catch(BindException ex) - { - // Could not bind to socket; this is a fatal problem; so perform shutdown - ex.printStackTrace(); - System.exit(1); - } - catch(IOException ex) - { - ex.printStackTrace(); - } - catch(Exception ex) - { - ex.printStackTrace(); - } - } - - public static SelectionKey registerSelector(final Selector selector, - final SocketChannel channel, final int op) - throws CancelledKeyException, ClosedChannelException - { - // Register the selector at the channel, so that it will be notified - // on the socket's events - synchronized(RegisterGate) - { - // Wakeup the currently blocking reader/writer thread; we have locked - // the RegisterGate to prevent the awakened thread to block again - selector.wakeup(); - - // Lock the selector to prevent the waiting worker threads going into - // selector.select() which would block the selector. - synchronized (selector) - { - return channel.register(selector, op, null); - } - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/ArticleCommand.java --- a/org/sonews/daemon/command/ArticleCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,174 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import org.sonews.storage.Article; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.Channel; -import org.sonews.storage.StorageBackendException; - -/** - * Class handling the ARTICLE, BODY and HEAD commands. - * @author Christian Lins - * @author Dennis Schwerdel - * @since n3tpd/0.1 - */ -public class ArticleCommand implements Command -{ - - @Override - public String[] getSupportedCommandStrings() - { - return new String[] {"ARTICLE", "BODY", "HEAD"}; - } - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return false; - } - - // TODO: Refactor this method to reduce its complexity! - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException - { - final String[] command = line.split(" "); - - Article article = null; - long artIndex = -1; - if (command.length == 1) - { - article = conn.getCurrentArticle(); - if (article == null) - { - conn.println("420 no current article has been selected"); - return; - } - } - else if (command[1].matches(NNTPConnection.MESSAGE_ID_PATTERN)) - { - // Message-ID - article = Article.getByMessageID(command[1]); - if (article == null) - { - conn.println("430 no such article found"); - return; - } - } - else - { - // Message Number - try - { - Channel currentGroup = conn.getCurrentChannel(); - if(currentGroup == null) - { - conn.println("400 no group selected"); - return; - } - - artIndex = Long.parseLong(command[1]); - article = currentGroup.getArticle(artIndex); - } - catch(NumberFormatException ex) - { - ex.printStackTrace(); - } - catch(StorageBackendException ex) - { - ex.printStackTrace(); - } - - if (article == null) - { - conn.println("423 no such article number in this group"); - return; - } - conn.setCurrentArticle(article); - } - - if(command[0].equalsIgnoreCase("ARTICLE")) - { - conn.println("220 " + artIndex + " " + article.getMessageID() - + " article retrieved - head and body follow"); - conn.println(article.getHeaderSource()); - conn.println(""); - conn.println(article.getBody()); - conn.println("."); - } - else if(command[0].equalsIgnoreCase("BODY")) - { - conn.println("222 " + artIndex + " " + article.getMessageID() + " body"); - conn.println(article.getBody()); - conn.println("."); - } - - /* - * HEAD: This command is mandatory. - * - * Syntax - * HEAD message-id - * HEAD number - * HEAD - * - * Responses - * - * First form (message-id specified) - * 221 0|n message-id Headers follow (multi-line) - * 430 No article with that message-id - * - * Second form (article number specified) - * 221 n message-id Headers follow (multi-line) - * 412 No newsgroup selected - * 423 No article with that number - * - * Third form (current article number used) - * 221 n message-id Headers follow (multi-line) - * 412 No newsgroup selected - * 420 Current article number is invalid - * - * Parameters - * number Requested article number - * n Returned article number - * message-id Article message-id - */ - else if(command[0].equalsIgnoreCase("HEAD")) - { - conn.println("221 " + artIndex + " " + article.getMessageID() - + " Headers follow (multi-line)"); - conn.println(article.getHeaderSource()); - conn.println("."); - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/CapabilitiesCommand.java --- a/org/sonews/daemon/command/CapabilitiesCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,93 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import org.sonews.daemon.NNTPConnection; - -/** - *
- *  The CAPABILITIES command allows a client to determine the
- *  capabilities of the server at any given time.
- *
- *  This command MAY be issued at any time; the server MUST NOT require
- *  it to be issued in order to make use of any capability. The response
- *  generated by this command MAY change during a session because of
- *  other state information (which, in turn, may be changed by the
- *  effects of other commands or by external events).  An NNTP client is
- *  only able to get the current and correct information concerning
- *  available capabilities at any point during a session by issuing a
- *  CAPABILITIES command at that point of that session and processing the
- *  response.
- * 
- * @author Christian Lins - * @since sonews/0.5.0 - */ -public class CapabilitiesCommand implements Command -{ - - static final String[] CAPABILITIES = new String[] - { - "VERSION 2", // MUST be the first one; VERSION 2 refers to RFC3977 - "READER", // Server implements commands for reading - "POST", // Server implements POST command - "OVER" // Server implements OVER command - }; - - @Override - public String[] getSupportedCommandStrings() - { - return new String[] {"CAPABILITIES"}; - } - - /** - * First called after one call to processLine(). - * @return - */ - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return false; - } - - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException - { - conn.println("101 Capabilities list:"); - for(String cap : CAPABILITIES) - { - conn.println(cap); - } - conn.println("."); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/Command.java --- a/org/sonews/daemon/command/Command.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,51 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.StorageBackendException; - -/** - * Interface for pluggable NNTP commands handling classes. - * @author Christian Lins - * @since sonews/0.6.0 - */ -public interface Command -{ - - /** - * @return true if this instance can be reused. - */ - boolean hasFinished(); - - /** - * Returns capability string that is implied by this command class. - * MAY return null if the command is required by the NNTP standard. - */ - String impliedCapability(); - - boolean isStateful(); - - String[] getSupportedCommandStrings(); - - void processLine(NNTPConnection conn, String line, byte[] rawLine) - throws IOException, StorageBackendException; - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/GroupCommand.java --- a/org/sonews/daemon/command/GroupCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,102 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.Channel; -import org.sonews.storage.StorageBackendException; - -/** - * Class handling the GROUP command. - *
- *  Syntax
- *    GROUP group
- *
- *  Responses
- *    211 number low high group     Group successfully selected
- *    411                           No such newsgroup
- *
- *  Parameters
- *    group     Name of newsgroup
- *    number    Estimated number of articles in the group
- *    low       Reported low water mark
- *    high      Reported high water mark
- * 
- * (from RFC 3977) - * - * @author Christian Lins - * @author Dennis Schwerdel - * @since n3tpd/0.1 - */ -public class GroupCommand implements Command -{ - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"GROUP"}; - } - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return true; - } - - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException, StorageBackendException - { - final String[] command = line.split(" "); - - Channel group; - if(command.length >= 2) - { - group = Channel.getByName(command[1]); - if(group == null || group.isDeleted()) - { - conn.println("411 no such news group"); - } - else - { - conn.setCurrentGroup(group); - conn.println("211 " + group.getPostingsCount() + " " + group.getFirstArticleNumber() - + " " + group.getLastArticleNumber() + " " + group.getName() + " group selected"); - } - } - else - { - conn.println("500 no group name given"); - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/HelpCommand.java --- a/org/sonews/daemon/command/HelpCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import java.util.Set; -import org.sonews.daemon.CommandSelector; -import org.sonews.daemon.NNTPConnection; -import org.sonews.util.io.Resource; - -/** - * This command provides a short summary of the commands that are - * understood by this implementation of the server. The help text will - * be presented as a multi-line data block following the 100 response - * code (taken from RFC). - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class HelpCommand implements Command -{ - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return true; - } - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"HELP"}; - } - - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException - { - final String[] command = line.split(" "); - conn.println("100 help text follows"); - - if(line.length() <= 1) - { - final String[] help = Resource - .getAsString("helpers/helptext", true).split("\n"); - for(String hstr : help) - { - conn.println(hstr); - } - - Set commandNames = CommandSelector.getCommandNames(); - for(String cmdName : commandNames) - { - conn.println(cmdName); - } - } - else - { - Command cmd = CommandSelector.getInstance().get(command[1]); - if(cmd instanceof HelpfulCommand) - { - conn.println(((HelpfulCommand)cmd).getHelpString()); - } - else - { - conn.println("No further help information available."); - } - } - - conn.println("."); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/HelpfulCommand.java --- a/org/sonews/daemon/command/HelpfulCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,35 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -/** - * - * @since sonews/1.1 - * @author Christian Lins - */ -public interface HelpfulCommand extends Command -{ - - /** - * @return A short description of this command, that is - * used within the output of the HELP command. - */ - String getHelpString(); - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/ListCommand.java --- a/org/sonews/daemon/command/ListCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,153 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.Channel; -import org.sonews.storage.StorageBackendException; -import org.sonews.util.Log; - -/** - * Class handling the LIST command. - * @author Christian Lins - * @author Dennis Schwerdel - * @since n3tpd/0.1 - */ -public class ListCommand implements Command -{ - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"LIST"}; - } - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return false; - } - - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException, StorageBackendException - { - final String[] command = line.split(" "); - - if(command.length >= 2) - { - if(command[1].equalsIgnoreCase("OVERVIEW.FMT")) - { - conn.println("215 information follows"); - conn.println("Subject:\nFrom:\nDate:\nMessage-ID:\nReferences:\nBytes:\nLines:\nXref"); - conn.println("."); - } - else if(command[1].equalsIgnoreCase("NEWSGROUPS")) - { - conn.println("215 information follows"); - final List 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 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/ListGroupCommand.java --- a/org/sonews/daemon/command/ListGroupCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,94 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import java.util.List; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.Channel; -import org.sonews.storage.StorageBackendException; - -/** - * Class handling the LISTGROUP command. - * @author Christian Lins - * @author Dennis Schwerdel - * @since n3tpd/0.1 - */ -public class ListGroupCommand implements Command -{ - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"LISTGROUP"}; - } - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return false; - } - - @Override - public void processLine(NNTPConnection conn, final String commandName, byte[] raw) - throws IOException, StorageBackendException - { - final String[] command = commandName.split(" "); - - Channel group; - if(command.length >= 2) - { - group = Channel.getByName(command[1]); - } - else - { - group = conn.getCurrentChannel(); - } - - if (group == null) - { - conn.println("412 no group selected; use GROUP 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("."); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/ModeReaderCommand.java --- a/org/sonews/daemon/command/ModeReaderCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,72 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.StorageBackendException; - -/** - * Class handling the MODE READER command. This command actually does nothing - * but returning a success status code. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class ModeReaderCommand implements Command -{ - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"MODE"}; - } - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return false; - } - - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException, StorageBackendException - { - if(line.equalsIgnoreCase("MODE READER")) - { - conn.println("200 hello you can post"); - } - else - { - conn.println("500 I do not know this mode command"); - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/NewGroupsCommand.java --- a/org/sonews/daemon/command/NewGroupsCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,78 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.StorageBackendException; - -/** - * Class handling the NEWGROUPS command. - * @author Christian Lins - * @author Dennis Schwerdel - * @since n3tpd/0.1 - */ -public class NewGroupsCommand implements Command -{ - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"NEWGROUPS"}; - } - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return false; - } - - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException, StorageBackendException - { - final String[] command = line.split(" "); - - if(command.length == 3) - { - conn.println("231 list of new newsgroups follows"); - - // Currently we do not store a group's creation date; - // so we return an empty list which is a valid response - conn.println("."); - } - else - { - conn.println("500 invalid command usage"); - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/NextPrevCommand.java --- a/org/sonews/daemon/command/NextPrevCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,116 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.Article; -import org.sonews.storage.Channel; -import org.sonews.storage.StorageBackendException; - -/** - * Class handling the NEXT and LAST command. - * @author Christian Lins - * @author Dennis Schwerdel - * @since n3tpd/0.1 - */ -public class NextPrevCommand implements Command -{ - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"NEXT", "PREV"}; - } - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return false; - } - - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException, StorageBackendException - { - final Article currA = conn.getCurrentArticle(); - final Channel currG = conn.getCurrentChannel(); - - if (currA == null) - { - conn.println("420 no current article has been selected"); - return; - } - - if (currG == null) - { - conn.println("412 no newsgroup selected"); - return; - } - - final String[] command = line.split(" "); - - if(command[0].equalsIgnoreCase("NEXT")) - { - selectNewArticle(conn, currA, currG, 1); - } - else if(command[0].equalsIgnoreCase("PREV")) - { - selectNewArticle(conn, currA, currG, -1); - } - else - { - conn.println("500 internal server error"); - } - } - - private void selectNewArticle(NNTPConnection conn, Article article, Channel grp, - final int delta) - throws IOException, StorageBackendException - { - assert article != null; - - article = grp.getArticle(grp.getIndexOf(article) + delta); - - if(article == null) - { - conn.println("421 no next article in this group"); - } - else - { - conn.setCurrentArticle(article); - conn.println("223 " + conn.getCurrentChannel().getIndexOf(article) - + " " + article.getMessageID() - + " article retrieved - request text separately"); - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/OverCommand.java --- a/org/sonews/daemon/command/OverCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,294 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import java.util.List; -import org.sonews.util.Log; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.Article; -import org.sonews.storage.ArticleHead; -import org.sonews.storage.Headers; -import org.sonews.storage.StorageBackendException; -import org.sonews.util.Pair; - -/** - * Class handling the OVER/XOVER command. - * - * Description of the XOVER command: - *
- * XOVER [range]
- *
- * The XOVER command returns information from the overview
- * database for the article(s) specified.
- *
- * The optional range argument may be any of the following:
- *              an article number
- *              an article number followed by a dash to indicate
- *                 all following
- *              an article number followed by a dash followed by
- *                 another article number
- *
- * If no argument is specified, then information from the
- * current article is displayed. Successful responses start
- * with a 224 response followed by the overview information
- * for all matched messages. Once the output is complete, a
- * period is sent on a line by itself. If no argument is
- * specified, the information for the current article is
- * returned.  A news group must have been selected earlier,
- * else a 412 error response is returned. If no articles are
- * in the range specified, a 420 error response is returned
- * by the server. A 502 response will be returned if the
- * client only has permission to transfer articles.
- *
- * Each line of output will be formatted with the article number,
- * followed by each of the headers in the overview database or the
- * article itself (when the data is not available in the overview
- * database) for that article separated by a tab character.  The
- * sequence of fields must be in this order: subject, author,
- * date, message-id, references, byte count, and line count. Other
- * optional fields may follow line count. Other optional fields may
- * follow line count. These fields are specified by examining the
- * response to the LIST OVERVIEW.FMT command. Where no data exists,
- * a null field must be provided (i.e. the output will have two tab
- * characters adjacent to each other). Servers should not output
- * fields for articles that have been removed since the XOVER database
- * was created.
- *
- * The LIST OVERVIEW.FMT command should be implemented if XOVER
- * is implemented. A client can use LIST OVERVIEW.FMT to determine
- * what optional fields  and in which order all fields will be
- * supplied by the XOVER command. 
- *
- * Note that any tab and end-of-line characters in any header
- * data that is returned will be converted to a space character.
- *
- * Responses:
- *
- *   224 Overview information follows
- *   412 No news group current selected
- *   420 No article(s) selected
- *   502 no permission
- *
- * OVER defines additional responses:
- *
- *  First form (message-id specified)
- *    224    Overview information follows (multi-line)
- *    430    No article with that message-id
- *
- *  Second form (range specified)
- *    224    Overview information follows (multi-line)
- *    412    No newsgroup selected
- *    423    No articles in that range
- *
- *  Third form (current article number used)
- *    224    Overview information follows (multi-line)
- *    412    No newsgroup selected
- *    420    Current article number is invalid
- *
- * 
- * @author Christian Lins - * @since sonews/0.5.0 - */ -public class OverCommand implements Command -{ - - public static final int MAX_LINES_PER_DBREQUEST = 200; - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"OVER", "XOVER"}; - } - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return false; - } - - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException, StorageBackendException - { - if(conn.getCurrentChannel() == null) - { - conn.println("412 no newsgroup selected"); - } - else - { - String[] command = line.split(" "); - - // If no parameter was specified, show information about - // the currently selected article(s) - if(command.length == 1) - { - final Article art = conn.getCurrentArticle(); - if(art == null) - { - conn.println("420 no article(s) selected"); - return; - } - - conn.println(buildOverview(art, -1)); - } - // otherwise print information about the specified range - else - { - long artStart; - long artEnd = conn.getCurrentChannel().getLastArticleNumber(); - String[] nums = command[1].split("-"); - if(nums.length >= 1) - { - try - { - artStart = Integer.parseInt(nums[0]); - } - catch(NumberFormatException e) - { - Log.get().info(e.getMessage()); - artStart = Integer.parseInt(command[1]); - } - } - else - { - artStart = conn.getCurrentChannel().getFirstArticleNumber(); - } - - if(nums.length >=2) - { - try - { - artEnd = Integer.parseInt(nums[1]); - } - catch(NumberFormatException e) - { - e.printStackTrace(); - } - } - - if(artStart > artEnd) - { - if(command[0].equalsIgnoreCase("OVER")) - { - conn.println("423 no articles in that range"); - } - else - { - conn.println("224 (empty) overview information follows:"); - conn.println("."); - } - } - else - { - for(long n = artStart; n <= artEnd; n += MAX_LINES_PER_DBREQUEST) - { - long nEnd = Math.min(n + MAX_LINES_PER_DBREQUEST - 1, artEnd); - List> 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'); - - String subject = art.getHeader(Headers.SUBJECT)[0]; - if("".equals(subject)) - { - subject = ""; - } - overview.append(escapeString(subject)); - overview.append('\t'); - - overview.append(escapeString(art.getHeader(Headers.FROM)[0])); - overview.append('\t'); - overview.append(escapeString(art.getHeader(Headers.DATE)[0])); - overview.append('\t'); - overview.append(escapeString(art.getHeader(Headers.MESSAGE_ID)[0])); - overview.append('\t'); - overview.append(escapeString(art.getHeader(Headers.REFERENCES)[0])); - overview.append('\t'); - - String bytes = art.getHeader(Headers.BYTES)[0]; - if("".equals(bytes)) - { - bytes = "0"; - } - overview.append(escapeString(bytes)); - overview.append('\t'); - - String lines = art.getHeader(Headers.LINES)[0]; - if("".equals(lines)) - { - lines = "0"; - } - overview.append(escapeString(lines)); - overview.append('\t'); - overview.append(escapeString(art.getHeader(Headers.XREF)[0])); - - // Remove trailing tabs if some data is empty - return overview.toString().trim(); - } - - private String escapeString(String str) - { - String nstr = str.replace("\r", ""); - nstr = nstr.replace('\n', ' '); - nstr = nstr.replace('\t', ' '); - return nstr.trim(); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/PostCommand.java --- a/org/sonews/daemon/command/PostCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,332 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.sql.SQLException; -import java.util.Arrays; -import javax.mail.MessagingException; -import javax.mail.internet.AddressException; -import javax.mail.internet.InternetHeaders; -import org.sonews.config.Config; -import org.sonews.util.Log; -import org.sonews.mlgw.Dispatcher; -import org.sonews.storage.Article; -import org.sonews.storage.Group; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.Headers; -import org.sonews.storage.StorageBackendException; -import org.sonews.storage.StorageManager; -import org.sonews.feed.FeedManager; -import org.sonews.util.Stats; - -/** - * Implementation of the POST command. This command requires multiple lines - * from the client, so the handling of asynchronous reading is a little tricky - * to handle. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class PostCommand implements Command -{ - - private final Article article = new Article(); - private int lineCount = 0; - private long bodySize = 0; - private InternetHeaders headers = null; - private long maxBodySize = - Config.inst().get(Config.ARTICLE_MAXSIZE, 128) * 1024L; // Size in bytes - private PostState state = PostState.WaitForLineOne; - private final ByteArrayOutputStream bufBody = new ByteArrayOutputStream(); - private final StringBuilder strHead = new StringBuilder(); - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"POST"}; - } - - @Override - public boolean hasFinished() - { - return this.state == PostState.Finished; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return true; - } - - /** - * Process the given line String. line.trim() was called by NNTPConnection. - * @param line - * @throws java.io.IOException - * @throws java.sql.SQLException - */ - @Override // TODO: Refactor this method to reduce complexity! - public void processLine(NNTPConnection conn, String line, byte[] raw) - throws IOException, StorageBackendException - { - switch(state) - { - case WaitForLineOne: - { - if(line.equalsIgnoreCase("POST")) - { - conn.println("340 send article to be posted. End with ."); - state = PostState.ReadingHeaders; - } - else - { - conn.println("500 invalid command usage"); - } - break; - } - case ReadingHeaders: - { - strHead.append(line); - strHead.append(NNTPConnection.NEWLINE); - - if("".equals(line) || ".".equals(line)) - { - // we finally met the blank line - // separating headers from body - - try - { - // Parse the header using the InternetHeader class from JavaMail API - headers = new InternetHeaders( - new ByteArrayInputStream(strHead.toString().trim() - .getBytes(conn.getCurrentCharset()))); - - // add the header entries for the article - article.setHeaders(headers); - } - catch (MessagingException e) - { - e.printStackTrace(); - conn.println("500 posting failed - invalid header"); - state = PostState.Finished; - break; - } - - // Change charset for reading body; - // for multipart messages UTF-8 is returned - //conn.setCurrentCharset(article.getBodyCharset()); - - state = PostState.ReadingBody; - - if(".".equals(line)) - { - // Post an article without body - postArticle(conn, article); - state = PostState.Finished; - } - } - break; - } - case ReadingBody: - { - if(".".equals(line)) - { - // Set some headers needed for Over command - headers.setHeader(Headers.LINES, Integer.toString(lineCount)); - headers.setHeader(Headers.BYTES, Long.toString(bodySize)); - - byte[] body = bufBody.toByteArray(); - if(body.length >= 2) - { - // Remove trailing CRLF - body = Arrays.copyOf(body, body.length - 2); - } - article.setBody(body); // set the article body - - postArticle(conn, article); - state = PostState.Finished; - } - else - { - bodySize += line.length() + 1; - lineCount++; - - // Add line to body buffer - bufBody.write(raw, 0, raw.length); - bufBody.write(NNTPConnection.NEWLINE.getBytes()); - - if(bodySize > maxBodySize) - { - conn.println("500 article is too long"); - state = PostState.Finished; - break; - } - } - break; - } - default: - { - // Should never happen - Log.get().severe("PostCommand::processLine(): already finished..."); - } - } - } - - /** - * Article is a control message and needs special handling. - * @param article - */ - private void controlMessage(NNTPConnection conn, Article article) - throws IOException - { - String[] ctrl = article.getHeader(Headers.CONTROL)[0].split(" "); - if(ctrl.length == 2) // "cancel " - { - try - { - StorageManager.current().delete(ctrl[1]); - - // Move cancel message to "control" group - article.setHeader(Headers.NEWSGROUPS, "control"); - StorageManager.current().addArticle(article); - conn.println("240 article cancelled"); - } - catch(StorageBackendException ex) - { - Log.get().severe(ex.toString()); - conn.println("500 internal server error"); - } - } - else - { - conn.println("441 unknown control header"); - } - } - - private void supersedeMessage(NNTPConnection conn, Article article) - throws IOException - { - try - { - String oldMsg = article.getHeader(Headers.SUPERSEDES)[0]; - StorageManager.current().delete(oldMsg); - StorageManager.current().addArticle(article); - conn.println("240 article replaced"); - } - catch(StorageBackendException ex) - { - Log.get().severe(ex.toString()); - conn.println("500 internal server error"); - } - } - - private void postArticle(NNTPConnection conn, Article article) - throws IOException - { - if(article.getHeader(Headers.CONTROL)[0].length() > 0) - { - controlMessage(conn, article); - } - else if(article.getHeader(Headers.SUPERSEDES)[0].length() > 0) - { - supersedeMessage(conn, article); - } - else // Post the article regularily - { - // Circle check; note that Path can already contain the hostname here - String host = Config.inst().get(Config.HOSTNAME, "localhost"); - if(article.getHeader(Headers.PATH)[0].indexOf(host + "!", 1) > 0) - { - Log.get().info(article.getMessageID() + " skipped for host " + host); - conn.println("441 I know this article already"); - return; - } - - // Try to create the article in the database or post it to - // appropriate mailing list - try - { - boolean success = false; - String[] groupnames = article.getHeader(Headers.NEWSGROUPS)[0].split(","); - for(String groupname : groupnames) - { - Group group = StorageManager.current().getGroup(groupname); - if(group != null && !group.isDeleted()) - { - if(group.isMailingList() && !conn.isLocalConnection()) - { - // Send to mailing list; the Dispatcher writes - // statistics to database - Dispatcher.toList(article, group.getName()); - success = true; - } - else - { - // Store in database - if(!StorageManager.current().isArticleExisting(article.getMessageID())) - { - StorageManager.current().addArticle(article); - - // Log this posting to statistics - Stats.getInstance().mailPosted( - article.getHeader(Headers.NEWSGROUPS)[0]); - } - success = true; - } - } - } // end for - - if(success) - { - conn.println("240 article posted ok"); - FeedManager.queueForPush(article); - } - else - { - conn.println("441 newsgroup not found"); - } - } - catch(AddressException ex) - { - Log.get().warning(ex.getMessage()); - conn.println("441 invalid sender address"); - } - catch(MessagingException ex) - { - // A MessageException is thrown when the sender email address is - // invalid or something is wrong with the SMTP server. - System.err.println(ex.getLocalizedMessage()); - conn.println("441 " + ex.getClass().getCanonicalName() + ": " + ex.getLocalizedMessage()); - } - catch(StorageBackendException ex) - { - ex.printStackTrace(); - conn.println("500 internal server error"); - } - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/PostState.java --- a/org/sonews/daemon/command/PostState.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,29 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -/** - * States of the POST command's finite state machine. - * @author Christian Lins - * @since sonews/0.5.0 - */ -enum PostState -{ - WaitForLineOne, ReadingHeaders, ReadingBody, Finished -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/QuitCommand.java --- a/org/sonews/daemon/command/QuitCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,67 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.StorageBackendException; - -/** - * Implementation of the QUIT command; client wants to shutdown the connection. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class QuitCommand implements Command -{ - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"QUIT"}; - } - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return false; - } - - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException, StorageBackendException - { - conn.println("205 cya"); - - conn.shutdownInput(); - conn.shutdownOutput(); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/StatCommand.java --- a/org/sonews/daemon/command/StatCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,114 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import org.sonews.storage.Article; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.StorageBackendException; - -/** - * Implementation of the STAT command. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class StatCommand implements Command -{ - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"STAT"}; - } - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return false; - } - - // TODO: Method has various exit points => Refactor! - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException, StorageBackendException - { - final String[] command = line.split(" "); - - Article article = null; - if(command.length == 1) - { - article = conn.getCurrentArticle(); - if(article == null) - { - conn.println("420 no current article has been selected"); - return; - } - } - else if(command[1].matches(NNTPConnection.MESSAGE_ID_PATTERN)) - { - // Message-ID - article = Article.getByMessageID(command[1]); - if (article == null) - { - conn.println("430 no such article found"); - return; - } - } - else - { - // Message Number - try - { - long aid = Long.parseLong(command[1]); - article = conn.getCurrentChannel().getArticle(aid); - } - catch(NumberFormatException ex) - { - ex.printStackTrace(); - } - catch(StorageBackendException ex) - { - ex.printStackTrace(); - } - if (article == null) - { - conn.println("423 no such article number in this group"); - return; - } - conn.setCurrentArticle(article); - } - - conn.println("223 " + conn.getCurrentChannel().getIndexOf(article) + " " - + article.getMessageID() - + " article retrieved - request text separately"); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/UnsupportedCommand.java --- a/org/sonews/daemon/command/UnsupportedCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,67 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import org.sonews.daemon.NNTPConnection; - -/** - * A default "Unsupported Command". Simply returns error code 500 and a - * "command not supported" message. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class UnsupportedCommand implements Command -{ - - /** - * @return Always returns null. - */ - @Override - public String[] getSupportedCommandStrings() - { - return null; - } - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return false; - } - - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException - { - conn.println("500 command not supported"); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/XDaemonCommand.java --- a/org/sonews/daemon/command/XDaemonCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,270 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.util.List; -import org.sonews.config.Config; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.StorageBackendException; -import org.sonews.storage.StorageManager; -import org.sonews.feed.FeedManager; -import org.sonews.feed.Subscription; -import org.sonews.storage.Channel; -import org.sonews.storage.Group; -import org.sonews.util.Stats; - -/** - * The XDAEMON command allows a client to get/set properties of the - * running server daemon. Only locally connected clients are allowed to - * use this command. - * The restriction to localhost connection can be suppressed by overriding - * the sonews.xdaemon.host bootstrap config property. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class XDaemonCommand implements Command -{ - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"XDAEMON"}; - } - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return false; - } - - private void channelAdd(String[] commands, NNTPConnection conn) - throws IOException, StorageBackendException - { - String groupName = commands[2]; - if(StorageManager.current().isGroupExisting(groupName)) - { - conn.println("400 group " + groupName + " already existing!"); - } - else - { - StorageManager.current().addGroup(groupName, Integer.parseInt(commands[3])); - conn.println("200 group " + groupName + " created"); - } - } - - // TODO: Refactor this method to reduce complexity! - @Override - public void processLine(NNTPConnection conn, String line, byte[] raw) - throws IOException, StorageBackendException - { - InetSocketAddress addr = (InetSocketAddress)conn.getSocketChannel().socket() - .getRemoteSocketAddress(); - if(addr.getHostName().equals( - Config.inst().get(Config.XDAEMON_HOST, "localhost"))) - { - String[] commands = line.split(" ", 4); - if(commands.length == 3 && commands[1].equalsIgnoreCase("LIST")) - { - if(commands[2].equalsIgnoreCase("CONFIGKEYS")) - { - conn.println("100 list of available config keys follows"); - for(String key : Config.AVAILABLE_KEYS) - { - conn.println(key); - } - conn.println("."); - } - else if(commands[2].equalsIgnoreCase("PEERINGRULES")) - { - List 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"); - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/XPatCommand.java --- a/org/sonews/daemon/command/XPatCommand.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,170 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.daemon.command; - -import java.io.IOException; -import java.util.List; -import java.util.Locale; -import java.util.regex.PatternSyntaxException; -import org.sonews.daemon.NNTPConnection; -import org.sonews.storage.StorageBackendException; -import org.sonews.storage.StorageManager; -import org.sonews.util.Pair; - -/** - *
- *   XPAT header range| pat [pat...]
- *
- *   The XPAT command is used to retrieve specific headers from
- *   specific articles, based on pattern matching on the contents of
- *   the header. This command was first available in INN.
- *
- *   The required header parameter is the name of a header line (e.g.
- *   "subject") in a news group article. See RFC-1036 for a list
- *   of valid header lines. The required range argument may be
- *   any of the following:
- *               an article number
- *               an article number followed by a dash to indicate
- *                  all following
- *               an article number followed by a dash followed by
- *                  another article number
- *
- *   The required message-id argument indicates a specific
- *   article. The range and message-id arguments are mutually
- *   exclusive. At least one pattern in wildmat must be specified
- *   as well. If there are additional arguments the are joined
- *   together separated by a single space to form one complete
- *   pattern. Successful responses start with a 221 response
- *   followed by a the headers from all messages in which the
- *   pattern matched the contents of the specified header line. This
- *   includes an empty list. Once the output is complete, a period
- *   is sent on a line by itself. If the optional argument is a
- *   message-id and no such article exists, the 430 error response
- *   is returned. A 502 response will be returned if the client only
- *   has permission to transfer articles.
- *
- *   Responses
- *
- *       221 Header follows
- *       430 no such article
- *       502 no permission
- *
- *   Response Data:
- *
- *       art_nr fitting_header_value
- * 
- * 
- * [Source:"draft-ietf-nntp-imp-02.txt"] [Copyright: 1998 S. Barber] - * - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class XPatCommand implements Command -{ - - @Override - public String[] getSupportedCommandStrings() - { - return new String[]{"XPAT"}; - } - - @Override - public boolean hasFinished() - { - return true; - } - - @Override - public String impliedCapability() - { - return null; - } - - @Override - public boolean isStateful() - { - return false; - } - - @Override - public void processLine(NNTPConnection conn, final String line, byte[] raw) - throws IOException, StorageBackendException - { - if(conn.getCurrentChannel() == null) - { - conn.println("430 no group selected"); - return; - } - - String[] command = line.split("\\p{Space}+"); - - // There may be multiple patterns and Thunderbird produces - // additional spaces between range and pattern - if(command.length >= 4) - { - String header = command[1].toLowerCase(Locale.US); - String range = command[2]; - String pattern = command[3]; - - long start = -1; - long end = -1; - if(range.contains("-")) - { - String[] rsplit = range.split("-", 2); - start = Long.parseLong(rsplit[0]); - if(rsplit[1].length() > 0) - { - end = Long.parseLong(rsplit[1]); - } - } - else // TODO: Handle Message-IDs - { - start = Long.parseLong(range); - } - - try - { - List> 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 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/command/package.html --- a/org/sonews/daemon/command/package.html Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -Contains a class for every NNTP command. \ No newline at end of file diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/daemon/package.html --- a/org/sonews/daemon/package.html Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -Contains basic classes of the daemon. \ No newline at end of file diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/feed/FeedManager.java --- a/org/sonews/feed/FeedManager.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.feed; - -import org.sonews.storage.Article; - -/** - * Controlls push and pull feeder. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public final class FeedManager -{ - - public static final int TYPE_PULL = 0; - public static final int TYPE_PUSH = 1; - - private static PullFeeder pullFeeder = new PullFeeder(); - private static PushFeeder pushFeeder = new PushFeeder(); - - /** - * Reads the peer subscriptions from database and starts the appropriate - * PullFeeder or PushFeeder. - */ - public static synchronized void startFeeding() - { - pullFeeder.start(); - pushFeeder.start(); - } - - public static void queueForPush(Article article) - { - pushFeeder.queueForPush(article); - } - - private FeedManager() {} - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/feed/PullFeeder.java --- a/org/sonews/feed/PullFeeder.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,276 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.feed; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.net.Socket; -import java.net.SocketException; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.logging.Level; -import org.sonews.config.Config; -import org.sonews.daemon.AbstractDaemon; -import org.sonews.util.Log; -import org.sonews.storage.StorageBackendException; -import org.sonews.storage.StorageManager; -import org.sonews.util.Stats; -import org.sonews.util.io.ArticleReader; -import org.sonews.util.io.ArticleWriter; - -/** - * The PullFeeder class regularily checks another Newsserver for new - * messages. - * @author Christian Lins - * @since sonews/0.5.0 - */ -class PullFeeder extends AbstractDaemon -{ - - private Map 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())); - - 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 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/feed/PushFeeder.java --- a/org/sonews/feed/PushFeeder.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,118 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.feed; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.ConcurrentLinkedQueue; -import org.sonews.daemon.AbstractDaemon; -import org.sonews.storage.Article; -import org.sonews.storage.Headers; -import org.sonews.storage.StorageBackendException; -import org.sonews.storage.StorageManager; -import org.sonews.util.Log; -import org.sonews.util.io.ArticleWriter; - -/** - * Pushes new articles to remote newsservers. This feeder sleeps until a new - * message is posted to the sonews instance. - * @author Christian Lins - * @since sonews/0.5.0 - */ -class PushFeeder extends AbstractDaemon -{ - - private ConcurrentLinkedQueue
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; - } - - try - { - for(String group : groups) - { - if(sub.getGroup().equals(group)) - { - // Delete headers that may cause problems - article.removeHeader(Headers.NNTP_POSTING_DATE); - article.removeHeader(Headers.NNTP_POSTING_HOST); - article.removeHeader(Headers.X_COMPLAINTS_TO); - article.removeHeader(Headers.X_TRACE); - article.removeHeader(Headers.XREF); - - // POST the message to remote server - ArticleWriter awriter = new ArticleWriter(sub.getHost(), sub.getPort()); - awriter.writeArticle(article); - break; - } - } - } - catch(IOException ex) - { - Log.get().warning(ex.toString()); - } - } - } - catch(StorageBackendException ex) - { - Log.get().severe(ex.toString()); - } - catch(InterruptedException ex) - { - Log.get().warning("PushFeeder interrupted: " + ex); - } - } - } - - public void queueForPush(Article article) - { - this.articleQueue.add(article); - synchronized(this) - { - this.notifyAll(); - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/feed/Subscription.java --- a/org/sonews/feed/Subscription.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,84 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.feed; - -/** - * For every group that is synchronized with or from a remote newsserver - * a Subscription instance exists. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class Subscription -{ - - private String host; - private int port; - private int feedtype; - private String group; - - public Subscription(String host, int port, int feedtype, String group) - { - this.host = host; - this.port = port; - this.feedtype = feedtype; - this.group = group; - } - - @Override - public boolean equals(Object obj) - { - if(obj instanceof Subscription) - { - Subscription sub = (Subscription)obj; - return sub.host.equals(host) && sub.group.equals(group) - && sub.port == port && sub.feedtype == feedtype; - } - else - { - return false; - } - } - - @Override - public int hashCode() - { - return host.hashCode() + port + feedtype + group.hashCode(); - } - - public int getFeedtype() - { - return feedtype; - } - - public String getGroup() - { - return group; - } - - public String getHost() - { - return host; - } - - public int getPort() - { - return port; - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/feed/package.html --- a/org/sonews/feed/package.html Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,2 +0,0 @@ -Contains classes for the peering functionality, e.g. pulling and pushing -mails from and to remote newsservers. \ No newline at end of file diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/mlgw/Dispatcher.java --- a/org/sonews/mlgw/Dispatcher.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,301 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.mlgw; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.mail.Address; -import javax.mail.Authenticator; -import javax.mail.Message; -import javax.mail.MessagingException; -import javax.mail.PasswordAuthentication; -import javax.mail.internet.InternetAddress; -import org.sonews.config.Config; -import org.sonews.storage.Article; -import org.sonews.storage.Group; -import org.sonews.storage.Headers; -import org.sonews.storage.StorageBackendException; -import org.sonews.storage.StorageManager; -import org.sonews.util.Log; -import org.sonews.util.Stats; - -/** - * Dispatches messages from mailing list to newsserver or vice versa. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class Dispatcher -{ - - static class PasswordAuthenticator extends Authenticator - { - - @Override - public PasswordAuthentication getPasswordAuthentication() - { - final String username = - Config.inst().get(Config.MLSEND_USER, "user"); - final String password = - Config.inst().get(Config.MLSEND_PASSWORD, "mysecret"); - - return new PasswordAuthentication(username, password); - } - - } - - /** - * Chunks out the email address of the full List-Post header field. - * @param listPostValue - * @return The matching email address or null - */ - private static String chunkListPost(String listPostValue) - { - // listPostValue is of form "" - Pattern mailPattern = Pattern.compile("(\\w+[-|.])*\\w+@(\\w+.)+\\w+"); - Matcher mailMatcher = mailPattern.matcher(listPostValue); - if(mailMatcher.find()) - { - return listPostValue.substring(mailMatcher.start(), mailMatcher.end()); - } - else - { - return null; - } - } - - /** - * This method inspects the header of the given message, trying - * to find the most appropriate recipient. - * @param msg - * @param fallback If this is false only List-Post and X-List-Post headers - * are examined. - * @return null or fitting group name for the given message. - */ - private static List getGroupFor(final Message msg, final boolean fallback) - throws MessagingException, StorageBackendException - { - List groups = null; - - // Is there a List-Post header? - String[] listPost = msg.getHeader(Headers.LIST_POST); - InternetAddress listPostAddr; - - if(listPost == null || listPost.length == 0 || "".equals(listPost[0])) - { - // Is there a X-List-Post header? - listPost = msg.getHeader(Headers.X_LIST_POST); - } - - if(listPost != null && listPost.length > 0 - && !"".equals(listPost[0]) && chunkListPost(listPost[0]) != null) - { - // listPost[0] is of form "" - 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) - { - try - { - // Create new Article object - Article article = new Article(msg); - boolean posted = false; - - // Check if this mail is already existing the storage - boolean updateReq = - StorageManager.current().isArticleExisting(article.getMessageID()); - - List 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()); - 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; - } - } - - /** - * 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); - - if(rcptAddresses == null || rcptAddresses.size() == 0) - { - Log.get().warning("No ML-address for " + group + " found."); - return; - } - - for(String rcptAddress : rcptAddresses) - { - // Compose message and send it via given SMTP-Host - String smtpHost = Config.inst().get(Config.MLSEND_HOST, "localhost"); - int smtpPort = Config.inst().get(Config.MLSEND_PORT, 25); - String smtpUser = Config.inst().get(Config.MLSEND_USER, "user"); - String smtpPw = Config.inst().get(Config.MLSEND_PASSWORD, "mysecret"); - String smtpFrom = Config.inst().get( - Config.MLSEND_ADDRESS, article.getHeader(Headers.FROM)[0]); - - // TODO: Make Article cloneable() - article.getMessageID(); // Make sure an ID is existing - article.removeHeader(Headers.NEWSGROUPS); - article.removeHeader(Headers.PATH); - article.removeHeader(Headers.LINES); - article.removeHeader(Headers.BYTES); - - article.setHeader("To", rcptAddress); - //article.setHeader("Reply-To", listAddress); - - if (Config.inst().get(Config.MLSEND_RW_SENDER, false)) - { - rewriteSenderAddress(article); // Set the SENDER address - } - - SMTPTransport smtpTransport = new SMTPTransport(smtpHost, smtpPort); - smtpTransport.send(article, smtpFrom, rcptAddress); - smtpTransport.close(); - - Stats.getInstance().mailGatewayed(group); - Log.get().info("MLGateway: Mail " + article.getHeader("Subject")[0] - + " was delivered to " + rcptAddress + "."); - } - } - - /** - * Sets the SENDER header of the given MimeMessage. This might be necessary - * for moderated groups that does not allow the "normal" FROM sender. - * @param msg - * @throws javax.mail.MessagingException - */ - private static void rewriteSenderAddress(Article msg) - throws MessagingException - { - String mlAddress = Config.inst().get(Config.MLSEND_ADDRESS, null); - - if(mlAddress != null) - { - msg.setHeader(Headers.SENDER, mlAddress); - } - else - { - throw new MessagingException("Cannot rewrite SENDER header!"); - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/mlgw/MailPoller.java --- a/org/sonews/mlgw/MailPoller.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,151 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.mlgw; - -import java.util.Properties; -import javax.mail.AuthenticationFailedException; -import javax.mail.Authenticator; -import javax.mail.Flags.Flag; -import javax.mail.Folder; -import javax.mail.Message; -import javax.mail.MessagingException; -import javax.mail.NoSuchProviderException; -import javax.mail.PasswordAuthentication; -import javax.mail.Session; -import javax.mail.Store; -import org.sonews.config.Config; -import org.sonews.daemon.AbstractDaemon; -import org.sonews.util.Log; -import org.sonews.util.Stats; - -/** - * Daemon polling for new mails in a POP3 account to be delivered to newsgroups. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class MailPoller extends AbstractDaemon -{ - - static class PasswordAuthenticator extends Authenticator - { - - @Override - public PasswordAuthentication getPasswordAuthentication() - { - final String username = - Config.inst().get(Config.MLPOLL_USER, "user"); - final String password = - Config.inst().get(Config.MLPOLL_PASSWORD, "mysecret"); - - return new PasswordAuthentication(username, password); - } - - } - - @Override - public void run() - { - Log.get().info("Starting Mailinglist Poller..."); - int errors = 0; - while(isRunning()) - { - try - { - // Wait some time between runs. At the beginning has advantages, - // because the wait is not skipped if an exception occurs. - Thread.sleep(60000 * (errors + 1)); // one minute * errors - - final String host = - Config.inst().get(Config.MLPOLL_HOST, "samplehost"); - final String username = - Config.inst().get(Config.MLPOLL_USER, "user"); - final String password = - Config.inst().get(Config.MLPOLL_PASSWORD, "mysecret"); - - Stats.getInstance().mlgwRunStart(); - - // Create empty properties - Properties props = System.getProperties(); - props.put("mail.pop3.host", host); - props.put("mail.mime.address.strict", "false"); - - // Get session - Session session = Session.getInstance(props); - - // Get the store - Store store = session.getStore("pop3"); - store.connect(host, 110, username, password); - - // Get folder - Folder folder = store.getFolder("INBOX"); - folder.open(Folder.READ_WRITE); - - // Get directory - Message[] messages = folder.getMessages(); - - // Dispatch messages and delete it afterwards on the inbox - for(Message message : messages) - { - if(Dispatcher.toGroup(message) - || Config.inst().get(Config.MLPOLL_DELETEUNKNOWN, false)) - { - // Delete the message - message.setFlag(Flag.DELETED, true); - } - } - - // Close connection - folder.close(true); // true to expunge deleted messages - store.close(); - errors = 0; - - Stats.getInstance().mlgwRunEnd(); - } - catch(NoSuchProviderException ex) - { - Log.get().severe(ex.toString()); - shutdown(); - } - catch(AuthenticationFailedException ex) - { - // AuthentificationFailedException may be thrown if credentials are - // bad or if the Mailbox is in use (locked). - ex.printStackTrace(); - errors = errors < 5 ? errors + 1 : errors; - } - catch(InterruptedException ex) - { - System.out.println("sonews: " + this + " returns: " + ex); - return; - } - catch(MessagingException ex) - { - ex.printStackTrace(); - errors = errors < 5 ? errors + 1 : errors; - } - catch(Exception ex) - { - ex.printStackTrace(); - errors = errors < 5 ? errors + 1 : errors; - } - } - Log.get().severe("MailPoller exited."); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/mlgw/SMTPTransport.java --- a/org/sonews/mlgw/SMTPTransport.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,133 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.mlgw; - -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.Socket; -import java.net.UnknownHostException; -import org.sonews.config.Config; -import org.sonews.storage.Article; -import org.sonews.util.io.ArticleInputStream; - -/** - * Connects to a SMTP server and sends a given Article to it. - * @author Christian Lins - * @since sonews/1.0 - */ -class SMTPTransport -{ - - protected BufferedReader in; - protected BufferedOutputStream out; - protected Socket socket; - - public SMTPTransport(String host, int port) - throws IOException, UnknownHostException - { - socket = new Socket(host, port); - this.in = new BufferedReader(new InputStreamReader(socket.getInputStream())); - this.out = new BufferedOutputStream(socket.getOutputStream()); - - // Read helo from server - String line = this.in.readLine(); - if(line == null || !line.startsWith("220 ")) - { - throw new IOException("Invalid helo from server: " + line); - } - - // Send HELO to server - this.out.write( - ("HELO " + Config.inst().get(Config.HOSTNAME, "localhost") + "\r\n").getBytes("UTF-8")); - this.out.flush(); - line = this.in.readLine(); - if(line == null || !line.startsWith("250 ")) - { - throw new IOException("Unexpected reply: " + line); - } - } - - public SMTPTransport(String host) - throws IOException - { - this(host, 25); - } - - public void close() - throws IOException - { - this.out.write("QUIT".getBytes("UTF-8")); - this.out.flush(); - this.in.readLine(); - - this.socket.close(); - } - - public void send(Article article, String mailFrom, String rcptTo) - throws IOException - { - assert(article != null); - assert(mailFrom != null); - assert(rcptTo != null); - - this.out.write(("MAIL FROM: " + mailFrom).getBytes("UTF-8")); - this.out.flush(); - String line = this.in.readLine(); - if(line == null || !line.startsWith("250 ")) - { - throw new IOException("Unexpected reply: " + line); - } - - this.out.write(("RCPT TO: " + rcptTo).getBytes("UTF-8")); - this.out.flush(); - line = this.in.readLine(); - if(line == null || !line.startsWith("250 ")) - { - throw new IOException("Unexpected reply: " + line); - } - - this.out.write("DATA".getBytes("UTF-8")); - this.out.flush(); - line = this.in.readLine(); - if(line == null || !line.startsWith("354 ")) - { - throw new IOException("Unexpected reply: " + line); - } - - ArticleInputStream artStream = new ArticleInputStream(article); - for(int b = artStream.read(); b >= 0; b = artStream.read()) - { - this.out.write(b); - } - - // Flush the binary stream; important because otherwise the output - // will be mixed with the PrintWriter. - this.out.flush(); - this.out.write("\r\n.\r\n".getBytes("UTF-8")); - this.out.flush(); - line = this.in.readLine(); - if(line == null || !line.startsWith("250 ")) - { - throw new IOException("Unexpected reply: " + line); - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/mlgw/package.html --- a/org/sonews/mlgw/package.html Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -Contains classes of the Mailinglist Gateway. \ No newline at end of file diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/plugin/Plugin.java --- a/org/sonews/plugin/Plugin.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.plugin; - -/** - * A generic Plugin for sonews. Implementing classes do not really add new - * functionality to sonews but can use this interface as convenient procedure - * for installing functionality plugins, e.g. Command-Plugins or Storage-Plugins. - * @author Christian Lins - * @since sonews/1.1 - */ -public interface Plugin -{ - - /** - * Called when the Plugin is loaded by sonews. This method can be used - * by implementing classes to install additional or required plugins. - */ - void load(); - - /** - * Called when the Plugin is unloaded by sonews. - */ - void unload(); - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/Article.java --- a/org/sonews/storage/Article.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,253 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.storage; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.UUID; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import javax.mail.Header; -import javax.mail.Message; -import javax.mail.MessagingException; -import javax.mail.internet.InternetHeaders; -import org.sonews.config.Config; - -/** - * Represents a newsgroup article. - * @author Christian Lins - * @author Denis Schwerdel - * @since n3tpd/0.1 - */ -public class Article extends ArticleHead -{ - - /** - * Loads the Article identified by the given ID from the JDBCDatabase. - * @param messageID - * @return null if Article is not found or if an error occurred. - */ - public static Article getByMessageID(final String messageID) - { - try - { - return StorageManager.current().getArticle(messageID); - } - catch(StorageBackendException ex) - { - ex.printStackTrace(); - return null; - } - } - - private byte[] body = new byte[0]; - - /** - * Default constructor. - */ - public Article() - { - } - - /** - * Creates a new Article object using the date from the given - * raw data. - */ - public Article(String headers, byte[] body) - { - try - { - this.body = body; - - // Parse the header - this.headers = new InternetHeaders( - new ByteArrayInputStream(headers.getBytes())); - - this.headerSrc = headers; - } - catch(MessagingException ex) - { - ex.printStackTrace(); - } - } - - /** - * Creates an Article instance using the data from the javax.mail.Message - * object. This constructor is called by the Mailinglist gateway. - * @see javax.mail.Message - * @param msg - * @throws IOException - * @throws MessagingException - */ - public Article(final Message msg) - throws IOException, MessagingException - { - this.headers = new InternetHeaders(); - - for(Enumeration e = msg.getAllHeaders() ; e.hasMoreElements();) - { - final Header header = (Header)e.nextElement(); - this.headers.addHeader(header.getName(), header.getValue()); - } - - // Reads the raw byte body using Message.writeTo(OutputStream out) - this.body = readContent(msg); - - // Validate headers - validateHeaders(); - } - - /** - * Reads from the given Message into a byte array. - * @param in - * @return - * @throws IOException - */ - private byte[] readContent(Message in) - throws IOException, MessagingException - { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - in.writeTo(out); - return out.toByteArray(); - } - - /** - * Removes the header identified by the given key. - * @param headerKey - */ - public void removeHeader(final String headerKey) - { - this.headers.removeHeader(headerKey); - this.headerSrc = null; - } - - /** - * Generates a message id for this article and sets it into - * the header object. You have to update the JDBCDatabase manually to make this - * change persistent. - * Note: a Message-ID should never be changed and only generated once. - */ - private String generateMessageID() - { - String randomString; - MessageDigest md5; - try - { - md5 = MessageDigest.getInstance("MD5"); - md5.reset(); - md5.update(getBody()); - md5.update(getHeader(Headers.SUBJECT)[0].getBytes()); - md5.update(getHeader(Headers.FROM)[0].getBytes()); - byte[] result = md5.digest(); - StringBuffer hexString = new StringBuffer(); - for (int i = 0; i < result.length; i++) - { - hexString.append(Integer.toHexString(0xFF & result[i])); - } - randomString = hexString.toString(); - } - catch (NoSuchAlgorithmException e) - { - e.printStackTrace(); - randomString = UUID.randomUUID().toString(); - } - String msgID = "<" + randomString + "@" - + Config.inst().get(Config.HOSTNAME, "localhost") + ">"; - - this.headers.setHeader(Headers.MESSAGE_ID, msgID); - - return msgID; - } - - /** - * Returns the body string. - */ - public byte[] getBody() - { - return body; - } - - /** - * @return Numerical IDs of the newsgroups this Article belongs to. - */ - public List 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 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/ArticleHead.java --- a/org/sonews/storage/ArticleHead.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,161 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.storage; - -import java.io.ByteArrayInputStream; -import java.util.Enumeration; -import javax.mail.Header; -import javax.mail.MessagingException; -import javax.mail.internet.InternetHeaders; -import javax.mail.internet.MimeUtility; -import org.sonews.config.Config; - -/** - * An article with no body only headers. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class ArticleHead -{ - - protected InternetHeaders headers = null; - protected String headerSrc = null; - - protected ArticleHead() - { - } - - public ArticleHead(String headers) - { - try - { - // Parse the header - this.headers = new InternetHeaders( - new ByteArrayInputStream(headers.getBytes())); - } - catch(MessagingException ex) - { - ex.printStackTrace(); - } - } - - /** - * Returns the header field with given name. - * @param name Name of the header field(s). - * @param returnNull If set to true, this method will return null instead - * of an empty array if there is no header field found. - * @return Header values or empty string. - */ - public String[] getHeader(String name, boolean returnNull) - { - String[] ret = this.headers.getHeader(name); - if(ret == null && !returnNull) - { - ret = new String[]{""}; - } - return ret; - } - - public String[] getHeader(String name) - { - return getHeader(name, false); - } - - /** - * Sets the header value identified through the header name. - * @param name - * @param value - */ - public void setHeader(String name, String value) - { - this.headers.setHeader(name, value); - this.headerSrc = null; - } - - public Enumeration getAllHeaders() - { - return this.headers.getAllHeaders(); - } - - /** - * @return Header source code of this Article. - */ - public String getHeaderSource() - { - if(this.headerSrc != null) - { - return this.headerSrc; - } - - StringBuffer buf = new StringBuffer(); - - for(Enumeration en = this.headers.getAllHeaders(); en.hasMoreElements();) - { - Header entry = (Header)en.nextElement(); - - String value = entry.getValue().replaceAll("[\r\n]", " "); - buf.append(entry.getName()); - buf.append(": "); - buf.append(MimeUtility.fold(entry.getName().length() + 2, value)); - - if(en.hasMoreElements()) - { - buf.append("\r\n"); - } - } - - this.headerSrc = buf.toString(); - return this.headerSrc; - } - - /** - * Sets the headers of this Article. If headers contain no - * Message-Id a new one is created. - * @param headers - */ - public void setHeaders(InternetHeaders headers) - { - this.headers = headers; - this.headerSrc = null; - validateHeaders(); - } - - /** - * Checks some headers for their validity and generates an - * appropriate Path-header for this host if not yet existing. - * This method is called by some Article constructors and the - * method setHeaders(). - * @return true if something on the headers was changed. - */ - protected void validateHeaders() - { - // Check for valid Path-header - final String path = getHeader(Headers.PATH)[0]; - final String host = Config.inst().get(Config.HOSTNAME, "localhost"); - if(!path.startsWith(host)) - { - StringBuffer pathBuf = new StringBuffer(); - pathBuf.append(host); - pathBuf.append('!'); - pathBuf.append(path); - this.headers.setHeader(Headers.PATH, pathBuf.toString()); - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/Channel.java --- a/org/sonews/storage/Channel.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,111 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.storage; - -import java.util.ArrayList; -import java.util.List; -import org.sonews.util.Pair; - -/** - * A logical communication Channel is the a generic structural element for sets - * of messages; e.g. a Newsgroup for a set of Articles. - * A Channel can either be a real set of messages or an aggregated set of - * several subsets. - * @author Christian Lins - * @since sonews/1.0 - */ -public abstract class Channel -{ - - /** - * If this flag is set the Group is no real newsgroup but a mailing list - * mirror. In that case every posting and receiving mails must go through - * the mailing list gateway. - */ - public static final int MAILINGLIST = 0x1; - - /** - * If this flag is set the Group is marked as readonly and the posting - * is prohibited. This can be useful for groups that are synced only in - * one direction. - */ - public static final int READONLY = 0x2; - - /** - * If this flag is set the Group is marked as deleted and must not occur - * in any output. The deletion is done lazily by a low priority daemon. - */ - public static final int DELETED = 0x80; - - public static List getAll() - { - List all = new ArrayList(); - - /*List agroups = AggregatedGroup.getAll(); - if(agroups != null) - { - all.addAll(agroups); - }*/ - - List groups = Group.getAll(); - if(groups != null) - { - all.addAll(groups); - } - - return all; - } - - public static Channel getByName(String name) - throws StorageBackendException - { - return StorageManager.current().getGroup(name); - } - - public abstract Article getArticle(long idx) - throws StorageBackendException; - - public abstract List> getArticleHeads( - final long first, final long last) - throws StorageBackendException; - - public abstract List getArticleNumbers() - throws StorageBackendException; - - public abstract long getFirstArticleNumber() - throws StorageBackendException; - - public abstract long getIndexOf(Article art) - throws StorageBackendException; - - public abstract long getInternalID(); - - public abstract long getLastArticleNumber() - throws StorageBackendException; - - public abstract String getName(); - - public abstract long getPostingsCount() - throws StorageBackendException; - - public abstract boolean isDeleted(); - - public abstract boolean isWriteable(); - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/Group.java --- a/org/sonews/storage/Group.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,184 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.storage; - -import java.sql.SQLException; -import java.util.List; -import org.sonews.util.Log; -import org.sonews.util.Pair; - -/** - * Represents a logical Group within this newsserver. - * @author Christian Lins - * @since sonews/0.5.0 - */ -// TODO: This class should not be public! -public class Group extends Channel -{ - - private long id = 0; - private int flags = -1; - private String name = null; - - /** - * @return List of all groups this server handles. - */ - public static List getAll() - { - try - { - return StorageManager.current().getGroups(); - } - catch(StorageBackendException ex) - { - Log.get().severe(ex.getMessage()); - return null; - } - } - - /** - * @param name - * @param id - */ - public Group(final String name, final long id, final int flags) - { - this.id = id; - this.flags = flags; - this.name = name; - } - - @Override - public boolean equals(Object obj) - { - if(obj instanceof Group) - { - return ((Group)obj).id == this.id; - } - else - { - return false; - } - } - - public Article getArticle(long idx) - throws StorageBackendException - { - return StorageManager.current().getArticle(idx, this.id); - } - - public List> 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); - } - - public long getFirstArticleNumber() - throws StorageBackendException - { - return StorageManager.current().getFirstArticleNumber(this); - } - - public int getFlags() - { - return this.flags; - } - - public long getIndexOf(Article art) - throws StorageBackendException - { - return StorageManager.current().getArticleIndex(art, this); - } - - /** - * Returns the group id. - */ - public long getInternalID() - { - assert id > 0; - - return id; - } - - public boolean isDeleted() - { - return (this.flags & DELETED) != 0; - } - - public boolean isMailingList() - { - return (this.flags & MAILINGLIST) != 0; - } - - public boolean isWriteable() - { - return true; - } - - public long getLastArticleNumber() - throws StorageBackendException - { - return StorageManager.current().getLastArticleNumber(this); - } - - public String getName() - { - return name; - } - - /** - * Performs this.flags |= flag to set a specified flag and updates the data - * in the JDBCDatabase. - * @param flag - */ - public void setFlag(final int flag) - { - this.flags |= flag; - } - - public void setName(final String name) - { - this.name = name; - } - - /** - * @return Number of posted articles in this group. - * @throws java.sql.SQLException - */ - public long getPostingsCount() - throws StorageBackendException - { - return StorageManager.current().getPostingsCount(this.name); - } - - /** - * Updates flags and name in the backend. - */ - public void update() - throws StorageBackendException - { - StorageManager.current().update(this); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/Headers.java --- a/org/sonews/storage/Headers.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.storage; - -/** - * Contains header constants. These header keys are no way complete but all - * headers that are relevant for sonews. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public final class Headers -{ - - public static final String BYTES = "bytes"; - public static final String CONTENT_TYPE = "content-type"; - public static final String CONTROL = "control"; - public static final String DATE = "date"; - public static final String FROM = "from"; - public static final String LINES = "lines"; - public static final String LIST_POST = "list-post"; - public static final String MESSAGE_ID = "message-id"; - public static final String NEWSGROUPS = "newsgroups"; - public static final String NNTP_POSTING_DATE = "nntp-posting-date"; - public static final String NNTP_POSTING_HOST = "nntp-posting-host"; - public static final String PATH = "path"; - public static final String REFERENCES = "references"; - public static final String REPLY_TO = "reply-to"; - public static final String SENDER = "sender"; - public static final String SUBJECT = "subject"; - public static final String SUPERSEDES = "subersedes"; - public static final String TO = "to"; - public static final String X_COMPLAINTS_TO = "x-complaints-to"; - public static final String X_LIST_POST = "x-list-post"; - public static final String X_TRACE = "x-trace"; - public static final String XREF = "xref"; - - private Headers() - {} - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/Storage.java --- a/org/sonews/storage/Storage.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,150 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.storage; - -import java.util.List; -import org.sonews.feed.Subscription; -import org.sonews.util.Pair; - -/** - * A generic storage backend interface. - * @author Christian Lins - * @since sonews/1.0 - */ -public interface Storage -{ - - /** - * Stores the given Article in the storage. - * @param art - * @throws StorageBackendException - */ - void addArticle(Article art) - throws StorageBackendException; - - void addEvent(long timestamp, int type, long groupID) - throws StorageBackendException; - - void addGroup(String groupname, int flags) - throws StorageBackendException; - - int countArticles() - throws StorageBackendException; - - int countGroups() - throws StorageBackendException; - - void delete(String messageID) - throws StorageBackendException; - - Article getArticle(String messageID) - throws StorageBackendException; - - Article getArticle(long articleIndex, long groupID) - throws StorageBackendException; - - List> getArticleHeads(Group group, long first, long last) - throws StorageBackendException; - - List> getArticleHeaders(Channel channel, long start, long end, - String header, String pattern) - throws StorageBackendException; - - long getArticleIndex(Article art, Group group) - throws StorageBackendException; - - List getArticleNumbers(long groupID) - throws StorageBackendException; - - String getConfigValue(String key) - throws StorageBackendException; - - int getEventsCount(int eventType, long startTimestamp, long endTimestamp, - Channel channel) - throws StorageBackendException; - - double getEventsPerHour(int key, long gid) - throws StorageBackendException; - - int getFirstArticleNumber(Group group) - throws StorageBackendException; - - Group getGroup(String name) - throws StorageBackendException; - - List 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; - - 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; - - String getOldestArticle() - throws StorageBackendException; - - int getPostingsCount(String groupname) - throws StorageBackendException; - - List getSubscriptions(int type) - throws StorageBackendException; - - boolean isArticleExisting(String messageID) - throws StorageBackendException; - - boolean isGroupExisting(String groupname) - throws StorageBackendException; - - void purgeGroup(Group group) - throws StorageBackendException; - - void setConfigValue(String key, String value) - throws StorageBackendException; - - /** - * Updates headers and channel references of the given article. - * @param article - * @return - * @throws StorageBackendException - */ - boolean update(Article article) - throws StorageBackendException; - - boolean update(Group group) - throws StorageBackendException; - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/StorageBackendException.java --- a/org/sonews/storage/StorageBackendException.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,39 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.storage; - -/** - * - * @author Christian Lins - * @since sonews/1.0 - */ -public class StorageBackendException extends Exception -{ - - public StorageBackendException(Throwable cause) - { - super(cause); - } - - public StorageBackendException(String msg) - { - super(msg); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/StorageManager.java --- a/org/sonews/storage/StorageManager.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,89 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.storage; - -/** - * - * @author Christian Lins - * @since sonews/1.0 - */ -public final class StorageManager -{ - - private static StorageProvider provider; - - public static Storage current() - throws StorageBackendException - { - synchronized(StorageManager.class) - { - if(provider == null) - { - return null; - } - else - { - return provider.storage(Thread.currentThread()); - } - } - } - - public static StorageProvider loadProvider(String pluginClassName) - { - try - { - Class clazz = Class.forName(pluginClassName); - Object inst = clazz.newInstance(); - return (StorageProvider)inst; - } - catch(Exception ex) - { - System.err.println(ex); - return null; - } - } - - /** - * Sets the current storage provider. - * @param provider - */ - public static void enableProvider(StorageProvider provider) - { - synchronized(StorageManager.class) - { - if(StorageManager.provider != null) - { - disableProvider(); - } - StorageManager.provider = provider; - } - } - - /** - * Disables the current provider. - */ - public static void disableProvider() - { - synchronized(StorageManager.class) - { - provider = null; - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/StorageProvider.java --- a/org/sonews/storage/StorageProvider.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,40 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.storage; - -/** - * Provides access to storage backend instances. - * @author Christian Lins - * @since sonews/1.0 - */ -public interface StorageProvider -{ - - public boolean isSupported(String uri); - - /** - * This method returns the reference to the associated storage. - * The reference MAY be unique for each thread. In any case it MUST be - * thread-safe to use this method. - * @return The reference to the associated Storage. - */ - public Storage storage(Thread thread) - throws StorageBackendException; - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/impl/JDBCDatabase.java --- a/org/sonews/storage/impl/JDBCDatabase.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1782 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.storage.impl; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.sql.PreparedStatement; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; -import javax.mail.Header; -import javax.mail.internet.MimeUtility; -import org.sonews.config.Config; -import org.sonews.util.Log; -import org.sonews.feed.Subscription; -import org.sonews.storage.Article; -import org.sonews.storage.ArticleHead; -import org.sonews.storage.Channel; -import org.sonews.storage.Group; -import org.sonews.storage.Storage; -import org.sonews.storage.StorageBackendException; -import org.sonews.util.Pair; - -/** - * JDBCDatabase facade class. - * @author Christian Lins - * @since sonews/0.5.0 - */ -// TODO: Refactor this class to reduce size (e.g. ArticleDatabase GroupDatabase) -public class JDBCDatabase implements Storage -{ - - public static final int MAX_RESTARTS = 2; - - private Connection conn = null; - private PreparedStatement pstmtAddArticle1 = null; - private PreparedStatement pstmtAddArticle2 = null; - private PreparedStatement pstmtAddArticle3 = null; - private PreparedStatement pstmtAddArticle4 = null; - private PreparedStatement pstmtAddGroup0 = null; - private PreparedStatement pstmtAddEvent = null; - private PreparedStatement pstmtCountArticles = null; - private PreparedStatement pstmtCountGroups = null; - private PreparedStatement pstmtDeleteArticle0 = null; - private PreparedStatement pstmtDeleteArticle1 = null; - private PreparedStatement pstmtDeleteArticle2 = null; - private PreparedStatement pstmtDeleteArticle3 = null; - private PreparedStatement pstmtGetArticle0 = null; - private PreparedStatement pstmtGetArticle1 = null; - private PreparedStatement pstmtGetArticleHeaders0 = null; - private PreparedStatement pstmtGetArticleHeaders1 = null; - private PreparedStatement pstmtGetArticleHeads = null; - private PreparedStatement pstmtGetArticleIDs = null; - private PreparedStatement pstmtGetArticleIndex = null; - private PreparedStatement pstmtGetConfigValue = null; - private PreparedStatement pstmtGetEventsCount0 = null; - private PreparedStatement pstmtGetEventsCount1 = null; - private PreparedStatement pstmtGetGroupForList = null; - private PreparedStatement pstmtGetGroup0 = null; - private PreparedStatement pstmtGetGroup1 = null; - private PreparedStatement pstmtGetFirstArticleNumber = null; - private PreparedStatement pstmtGetListForGroup = null; - private PreparedStatement pstmtGetLastArticleNumber = null; - private PreparedStatement pstmtGetMaxArticleID = null; - private PreparedStatement pstmtGetMaxArticleIndex = null; - private PreparedStatement pstmtGetOldestArticle = null; - private PreparedStatement pstmtGetPostingsCount = null; - private PreparedStatement pstmtGetSubscriptions = null; - private PreparedStatement pstmtIsArticleExisting = null; - private PreparedStatement pstmtIsGroupExisting = null; - private PreparedStatement pstmtPurgeGroup0 = null; - private PreparedStatement pstmtPurgeGroup1 = null; - private PreparedStatement pstmtSetConfigValue0 = null; - private PreparedStatement pstmtSetConfigValue1 = null; - private PreparedStatement pstmtUpdateGroup = null; - - /** How many times the database connection was reinitialized */ - private int restarts = 0; - - /** - * Rises the database: reconnect and recreate all prepared statements. - * @throws java.lang.SQLException - */ - protected void arise() - throws SQLException - { - try - { - // Load database driver - Class.forName( - Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DBMSDRIVER, "java.lang.Object")); - - // Establish database connection - this.conn = DriverManager.getConnection( - Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DATABASE, ""), - Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_USER, "root"), - Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_PASSWORD, "")); - - this.conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); - if(this.conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE) - { - Log.get().warning("Database is NOT fully serializable!"); - } - - // Prepare statements for method addArticle() - this.pstmtAddArticle1 = conn.prepareStatement( - "INSERT INTO articles (article_id, body) VALUES(?, ?)"); - this.pstmtAddArticle2 = conn.prepareStatement( - "INSERT INTO headers (article_id, header_key, header_value, header_index) " + - "VALUES (?, ?, ?, ?)"); - this.pstmtAddArticle3 = conn.prepareStatement( - "INSERT INTO postings (group_id, article_id, article_index)" + - "VALUES (?, ?, ?)"); - this.pstmtAddArticle4 = conn.prepareStatement( - "INSERT INTO article_ids (article_id, message_id) VALUES (?, ?)"); - - // Prepare statement for method addStatValue() - this.pstmtAddEvent = conn.prepareStatement( - "INSERT INTO events VALUES (?, ?, ?)"); - - // Prepare statement for method addGroup() - this.pstmtAddGroup0 = conn.prepareStatement( - "INSERT INTO groups (name, flags) VALUES (?, ?)"); - - // Prepare statement for method countArticles() - this.pstmtCountArticles = conn.prepareStatement( - "SELECT Count(article_id) FROM article_ids"); - - // Prepare statement for method countGroups() - this.pstmtCountGroups = conn.prepareStatement( - "SELECT Count(group_id) FROM groups WHERE " + - "flags & " + Channel.DELETED + " = 0"); - - // Prepare statements for method delete(article) - this.pstmtDeleteArticle0 = conn.prepareStatement( - "DELETE FROM articles WHERE article_id = " + - "(SELECT article_id FROM article_ids WHERE message_id = ?)"); - this.pstmtDeleteArticle1 = conn.prepareStatement( - "DELETE FROM headers WHERE article_id = " + - "(SELECT article_id FROM article_ids WHERE message_id = ?)"); - this.pstmtDeleteArticle2 = conn.prepareStatement( - "DELETE FROM postings WHERE article_id = " + - "(SELECT article_id FROM article_ids WHERE message_id = ?)"); - this.pstmtDeleteArticle3 = conn.prepareStatement( - "DELETE FROM article_ids WHERE message_id = ?"); - - // Prepare statements for methods getArticle() - this.pstmtGetArticle0 = conn.prepareStatement( - "SELECT * FROM articles WHERE article_id = " + - "(SELECT article_id FROM article_ids WHERE message_id = ?)"); - this.pstmtGetArticle1 = conn.prepareStatement( - "SELECT * FROM articles WHERE article_id = " + - "(SELECT article_id FROM postings WHERE " + - "article_index = ? AND group_id = ?)"); - - // Prepare statement for method getArticleHeaders() - this.pstmtGetArticleHeaders0 = conn.prepareStatement( - "SELECT header_key, header_value FROM headers WHERE article_id = ? " + - "ORDER BY header_index ASC"); - - // Prepare statement for method getArticleHeaders(regular expr pattern) - this.pstmtGetArticleHeaders1 = conn.prepareStatement( - "SELECT p.article_index, h.header_value FROM headers h " + - "INNER JOIN postings p ON h.article_id = p.article_id " + - "INNER JOIN groups g ON p.group_id = g.group_id " + - "WHERE g.name = ? AND " + - "h.header_key = ? AND " + - "p.article_index >= ? " + - "ORDER BY p.article_index ASC"); - - this.pstmtGetArticleIDs = conn.prepareStatement( - "SELECT article_index FROM postings WHERE group_id = ?"); - - // Prepare statement for method getArticleIndex - this.pstmtGetArticleIndex = conn.prepareStatement( - "SELECT article_index FROM postings WHERE " + - "article_id = (SELECT article_id FROM article_ids " + - "WHERE message_id = ?) " + - " AND group_id = ?"); - - // Prepare statements for method getArticleHeads() - this.pstmtGetArticleHeads = conn.prepareStatement( - "SELECT article_id, article_index FROM postings WHERE " + - "postings.group_id = ? AND article_index >= ? AND " + - "article_index <= ?"); - - // Prepare statements for method getConfigValue() - this.pstmtGetConfigValue = conn.prepareStatement( - "SELECT config_value FROM config WHERE config_key = ?"); - - // Prepare statements for method getEventsCount() - this.pstmtGetEventsCount0 = conn.prepareStatement( - "SELECT Count(*) FROM events WHERE event_key = ? AND " + - "event_time >= ? AND event_time < ?"); - - this.pstmtGetEventsCount1 = conn.prepareStatement( - "SELECT Count(*) FROM events WHERE event_key = ? AND " + - "event_time >= ? AND event_time < ? AND group_id = ?"); - - // Prepare statement for method getGroupForList() - this.pstmtGetGroupForList = conn.prepareStatement( - "SELECT name FROM groups INNER JOIN groups2list " + - "ON groups.group_id = groups2list.group_id " + - "WHERE groups2list.listaddress = ?"); - - // Prepare statement for method getGroup() - this.pstmtGetGroup0 = conn.prepareStatement( - "SELECT group_id, flags FROM groups WHERE Name = ?"); - this.pstmtGetGroup1 = conn.prepareStatement( - "SELECT name FROM groups WHERE group_id = ?"); - - // Prepare statement for method getLastArticleNumber() - this.pstmtGetLastArticleNumber = conn.prepareStatement( - "SELECT Max(article_index) FROM postings WHERE group_id = ?"); - - // Prepare statement for method getListForGroup() - this.pstmtGetListForGroup = conn.prepareStatement( - "SELECT listaddress FROM groups2list INNER JOIN groups " + - "ON groups.group_id = groups2list.group_id WHERE name = ?"); - - // Prepare statement for method getMaxArticleID() - this.pstmtGetMaxArticleID = conn.prepareStatement( - "SELECT Max(article_id) FROM articles"); - - // Prepare statement for method getMaxArticleIndex() - this.pstmtGetMaxArticleIndex = conn.prepareStatement( - "SELECT Max(article_index) FROM postings WHERE group_id = ?"); - - // Prepare statement for method getOldestArticle() - this.pstmtGetOldestArticle = conn.prepareStatement( - "SELECT message_id FROM article_ids WHERE article_id = " + - "(SELECT Min(article_id) FROM article_ids)"); - - // Prepare statement for method getFirstArticleNumber() - this.pstmtGetFirstArticleNumber = conn.prepareStatement( - "SELECT Min(article_index) FROM postings WHERE group_id = ?"); - - // Prepare statement for method getPostingsCount() - this.pstmtGetPostingsCount = conn.prepareStatement( - "SELECT Count(*) FROM postings NATURAL JOIN groups " + - "WHERE groups.name = ?"); - - // Prepare statement for method getSubscriptions() - this.pstmtGetSubscriptions = conn.prepareStatement( - "SELECT host, port, name FROM peers NATURAL JOIN " + - "peer_subscriptions NATURAL JOIN groups WHERE feedtype = ?"); - - // Prepare statement for method isArticleExisting() - this.pstmtIsArticleExisting = conn.prepareStatement( - "SELECT Count(article_id) FROM article_ids WHERE message_id = ?"); - - // Prepare statement for method isGroupExisting() - this.pstmtIsGroupExisting = conn.prepareStatement( - "SELECT * FROM groups WHERE name = ?"); - - // Prepare statement for method setConfigValue() - this.pstmtSetConfigValue0 = conn.prepareStatement( - "DELETE FROM config WHERE config_key = ?"); - this.pstmtSetConfigValue1 = conn.prepareStatement( - "INSERT INTO config VALUES(?, ?)"); - - // Prepare statements for method purgeGroup() - this.pstmtPurgeGroup0 = conn.prepareStatement( - "DELETE FROM peer_subscriptions WHERE group_id = ?"); - this.pstmtPurgeGroup1 = conn.prepareStatement( - "DELETE FROM groups WHERE group_id = ?"); - - // Prepare statement for method update(Group) - this.pstmtUpdateGroup = conn.prepareStatement( - "UPDATE groups SET flags = ?, name = ? WHERE group_id = ?"); - } - catch(ClassNotFoundException ex) - { - throw new Error("JDBC Driver not found!", ex); - } - } - - /** - * Adds an article to the database. - * @param article - * @return - * @throws java.sql.SQLException - */ - @Override - public void addArticle(final Article article) - throws StorageBackendException - { - try - { - this.conn.setAutoCommit(false); - - int newArticleID = getMaxArticleID() + 1; - - // Fill prepared statement with values; - // writes body to article table - pstmtAddArticle1.setInt(1, newArticleID); - pstmtAddArticle1.setBytes(2, article.getBody()); - pstmtAddArticle1.execute(); - - // Add headers - Enumeration headers = article.getAllHeaders(); - for(int n = 0; headers.hasMoreElements(); n++) - { - Header header = (Header)headers.nextElement(); - pstmtAddArticle2.setInt(1, newArticleID); - pstmtAddArticle2.setString(2, header.getName().toLowerCase()); - pstmtAddArticle2.setString(3, - header.getValue().replaceAll("[\r\n]", "")); - pstmtAddArticle2.setInt(4, n); - pstmtAddArticle2.execute(); - } - - // For each newsgroup add a reference - List groups = article.getGroups(); - for(Group group : groups) - { - pstmtAddArticle3.setLong(1, group.getInternalID()); - pstmtAddArticle3.setInt(2, newArticleID); - pstmtAddArticle3.setLong(3, getMaxArticleIndex(group.getInternalID()) + 1); - pstmtAddArticle3.execute(); - } - - // Write message-id to article_ids table - this.pstmtAddArticle4.setInt(1, newArticleID); - this.pstmtAddArticle4.setString(2, article.getMessageID()); - this.pstmtAddArticle4.execute(); - - this.conn.commit(); - this.conn.setAutoCommit(true); - - this.restarts = 0; // Reset error count - } - catch(SQLException ex) - { - try - { - this.conn.rollback(); // Rollback changes - } - catch(SQLException ex2) - { - Log.get().severe("Rollback of addArticle() failed: " + ex2); - } - - try - { - this.conn.setAutoCommit(true); // and release locks - } - catch(SQLException ex2) - { - Log.get().severe("setAutoCommit(true) of addArticle() failed: " + ex2); - } - - restartConnection(ex); - addArticle(article); - } - } - - /** - * Adds a group to the JDBCDatabase. This method is not accessible via NNTP. - * @param name - * @throws java.sql.SQLException - */ - @Override - public void addGroup(String name, int flags) - throws StorageBackendException - { - try - { - this.conn.setAutoCommit(false); - pstmtAddGroup0.setString(1, name); - pstmtAddGroup0.setInt(2, flags); - - pstmtAddGroup0.executeUpdate(); - this.conn.commit(); - this.conn.setAutoCommit(true); - this.restarts = 0; // Reset error count - } - catch(SQLException ex) - { - try - { - this.conn.rollback(); - this.conn.setAutoCommit(true); - } - catch(SQLException ex2) - { - ex2.printStackTrace(); - } - - restartConnection(ex); - addGroup(name, flags); - } - } - - @Override - public void addEvent(long time, int type, long gid) - throws StorageBackendException - { - try - { - this.conn.setAutoCommit(false); - this.pstmtAddEvent.setLong(1, time); - this.pstmtAddEvent.setInt(2, type); - this.pstmtAddEvent.setLong(3, gid); - this.pstmtAddEvent.executeUpdate(); - this.conn.commit(); - this.conn.setAutoCommit(true); - this.restarts = 0; - } - catch(SQLException ex) - { - try - { - this.conn.rollback(); - this.conn.setAutoCommit(true); - } - catch(SQLException ex2) - { - ex2.printStackTrace(); - } - - restartConnection(ex); - addEvent(time, type, gid); - } - } - - @Override - public int countArticles() - throws StorageBackendException - { - ResultSet rs = null; - - try - { - rs = this.pstmtCountArticles.executeQuery(); - if(rs.next()) - { - return rs.getInt(1); - } - else - { - return -1; - } - } - catch(SQLException ex) - { - restartConnection(ex); - return countArticles(); - } - finally - { - if(rs != null) - { - try - { - rs.close(); - } - catch(SQLException ex) - { - ex.printStackTrace(); - } - restarts = 0; - } - } - } - - @Override - public int countGroups() - throws StorageBackendException - { - ResultSet rs = null; - - try - { - rs = this.pstmtCountGroups.executeQuery(); - if(rs.next()) - { - return rs.getInt(1); - } - else - { - return -1; - } - } - catch(SQLException ex) - { - restartConnection(ex); - return countGroups(); - } - finally - { - if(rs != null) - { - try - { - rs.close(); - } - catch(SQLException ex) - { - ex.printStackTrace(); - } - restarts = 0; - } - } - } - - @Override - public void delete(final String messageID) - throws StorageBackendException - { - try - { - this.conn.setAutoCommit(false); - - this.pstmtDeleteArticle0.setString(1, messageID); - int rs = this.pstmtDeleteArticle0.executeUpdate(); - - // We do not trust the ON DELETE CASCADE functionality to delete - // orphaned references... - this.pstmtDeleteArticle1.setString(1, messageID); - rs = this.pstmtDeleteArticle1.executeUpdate(); - - this.pstmtDeleteArticle2.setString(1, messageID); - rs = this.pstmtDeleteArticle2.executeUpdate(); - - this.pstmtDeleteArticle3.setString(1, messageID); - rs = this.pstmtDeleteArticle3.executeUpdate(); - - this.conn.commit(); - this.conn.setAutoCommit(true); - } - catch(SQLException ex) - { - throw new StorageBackendException(ex); - } - } - - @Override - public Article getArticle(String messageID) - throws StorageBackendException - { - ResultSet rs = null; - try - { - pstmtGetArticle0.setString(1, messageID); - rs = pstmtGetArticle0.executeQuery(); - - if(!rs.next()) - { - return null; - } - else - { - byte[] body = rs.getBytes("body"); - String headers = getArticleHeaders(rs.getInt("article_id")); - return new Article(headers, body); - } - } - catch(SQLException ex) - { - restartConnection(ex); - return getArticle(messageID); - } - finally - { - if(rs != null) - { - try - { - rs.close(); - } - catch(SQLException ex) - { - ex.printStackTrace(); - } - restarts = 0; // Reset error count - } - } - } - - /** - * Retrieves an article by its ID. - * @param articleID - * @return - * @throws StorageBackendException - */ - @Override - public Article getArticle(long articleIndex, long gid) - throws StorageBackendException - { - ResultSet rs = null; - - try - { - this.pstmtGetArticle1.setLong(1, articleIndex); - this.pstmtGetArticle1.setLong(2, gid); - - rs = this.pstmtGetArticle1.executeQuery(); - - if(rs.next()) - { - byte[] body = rs.getBytes("body"); - String headers = getArticleHeaders(rs.getInt("article_id")); - return new Article(headers, body); - } - else - { - return null; - } - } - catch(SQLException ex) - { - restartConnection(ex); - return getArticle(articleIndex, gid); - } - finally - { - if(rs != null) - { - try - { - rs.close(); - } - catch(SQLException ex) - { - ex.printStackTrace(); - } - restarts = 0; - } - } - } - - /** - * Searches for fitting header values using the given regular expression. - * @param group - * @param start - * @param end - * @param headerKey - * @param pattern - * @return - * @throws StorageBackendException - */ - @Override - public List> getArticleHeaders(Channel group, long start, - long end, String headerKey, String patStr) - throws StorageBackendException, PatternSyntaxException - { - ResultSet rs = null; - List> heads = new ArrayList>(); - - try - { - this.pstmtGetArticleHeaders1.setString(1, group.getName()); - this.pstmtGetArticleHeaders1.setString(2, headerKey); - this.pstmtGetArticleHeaders1.setLong(3, start); - - rs = this.pstmtGetArticleHeaders1.executeQuery(); - - // Convert the "NNTP" regex to Java regex - patStr = patStr.replace("*", ".*"); - Pattern pattern = Pattern.compile(patStr); - - while(rs.next()) - { - Long articleIndex = rs.getLong(1); - if(end < 0 || articleIndex <= end) // Match start is done via SQL - { - String headerValue = rs.getString(2); - Matcher matcher = pattern.matcher(headerValue); - if(matcher.matches()) - { - heads.add(new Pair(articleIndex, headerValue)); - } - } - } - } - catch(SQLException ex) - { - restartConnection(ex); - return getArticleHeaders(group, start, end, headerKey, patStr); - } - finally - { - if(rs != null) - { - try - { - rs.close(); - } - catch(SQLException ex) - { - ex.printStackTrace(); - } - } - } - - return heads; - } - - private String getArticleHeaders(long articleID) - throws StorageBackendException - { - ResultSet rs = null; - - try - { - this.pstmtGetArticleHeaders0.setLong(1, articleID); - rs = this.pstmtGetArticleHeaders0.executeQuery(); - - StringBuilder buf = new StringBuilder(); - if(rs.next()) - { - for(;;) - { - buf.append(rs.getString(1)); // key - buf.append(": "); - String foldedValue = MimeUtility.fold(0, rs.getString(2)); - buf.append(foldedValue); // value - if(rs.next()) - { - buf.append("\r\n"); - } - else - { - break; - } - } - } - - return buf.toString(); - } - catch(SQLException ex) - { - restartConnection(ex); - return getArticleHeaders(articleID); - } - finally - { - if(rs != null) - { - try - { - rs.close(); - } - catch(SQLException ex) - { - ex.printStackTrace(); - } - } - } - } - - @Override - public long getArticleIndex(Article article, Group group) - throws StorageBackendException - { - ResultSet rs = null; - - try - { - this.pstmtGetArticleIndex.setString(1, article.getMessageID()); - this.pstmtGetArticleIndex.setLong(2, group.getInternalID()); - - rs = this.pstmtGetArticleIndex.executeQuery(); - if(rs.next()) - { - return rs.getLong(1); - } - else - { - return -1; - } - } - catch(SQLException ex) - { - restartConnection(ex); - return getArticleIndex(article, group); - } - finally - { - if(rs != null) - { - try - { - rs.close(); - } - catch(SQLException ex) - { - ex.printStackTrace(); - } - } - } - } - - /** - * Returns a list of Long/Article Pairs. - * @throws java.sql.SQLException - */ - @Override - public List> getArticleHeads(Group group, long first, - long last) - throws StorageBackendException - { - ResultSet rs = null; - - try - { - this.pstmtGetArticleHeads.setLong(1, group.getInternalID()); - this.pstmtGetArticleHeads.setLong(2, first); - this.pstmtGetArticleHeads.setLong(3, last); - rs = pstmtGetArticleHeads.executeQuery(); - - List> articles - = new ArrayList>(); - - 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))); - } - - return articles; - } - catch(SQLException ex) - { - restartConnection(ex); - return getArticleHeads(group, first, last); - } - finally - { - if(rs != null) - { - try - { - rs.close(); - } - catch(SQLException ex) - { - ex.printStackTrace(); - } - } - } - } - - @Override - public List 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 String getConfigValue(String key) - throws StorageBackendException - { - ResultSet rs = null; - try - { - this.pstmtGetConfigValue.setString(1, key); - - rs = this.pstmtGetConfigValue.executeQuery(); - if(rs.next()) - { - return rs.getString(1); // First data on index 1 not 0 - } - else - { - return null; - } - } - catch(SQLException ex) - { - restartConnection(ex); - return getConfigValue(key); - } - finally - { - if(rs != null) - { - try - { - rs.close(); - } - catch(SQLException ex) - { - ex.printStackTrace(); - } - restarts = 0; // Clear the restart count after successful request - } - } - } - - @Override - public int getEventsCount(int type, long start, long end, Channel channel) - throws StorageBackendException - { - ResultSet rs = null; - - try - { - if(channel == null) - { - this.pstmtGetEventsCount0.setInt(1, type); - this.pstmtGetEventsCount0.setLong(2, start); - this.pstmtGetEventsCount0.setLong(3, end); - rs = this.pstmtGetEventsCount0.executeQuery(); - } - else - { - this.pstmtGetEventsCount1.setInt(1, type); - this.pstmtGetEventsCount1.setLong(2, start); - this.pstmtGetEventsCount1.setLong(3, end); - this.pstmtGetEventsCount1.setLong(4, channel.getInternalID()); - rs = this.pstmtGetEventsCount1.executeQuery(); - } - - if(rs.next()) - { - return rs.getInt(1); - } - else - { - return -1; - } - } - catch(SQLException ex) - { - restartConnection(ex); - return getEventsCount(type, start, end, channel); - } - finally - { - if(rs != null) - { - try - { - rs.close(); - } - catch(SQLException ex) - { - ex.printStackTrace(); - } - } - } - } - - /** - * Reads all Groups from the JDBCDatabase. - * @return - * @throws StorageBackendException - */ - @Override - public List getGroups() - throws StorageBackendException - { - ResultSet rs; - List buffer = new ArrayList(); - Statement stmt = null; - - try - { - stmt = conn.createStatement(); - rs = stmt.executeQuery("SELECT * FROM groups ORDER BY name"); - - while(rs.next()) - { - String name = rs.getString("name"); - long id = rs.getLong("group_id"); - int flags = rs.getInt("flags"); - - Group group = new Group(name, id, flags); - buffer.add(group); - } - - return buffer; - } - catch(SQLException ex) - { - restartConnection(ex); - return getGroups(); - } - finally - { - if(stmt != null) - { - try - { - stmt.close(); // Implicitely closes ResultSets - } - catch(SQLException ex) - { - ex.printStackTrace(); - } - } - } - } - - @Override - public List 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 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/impl/JDBCDatabaseProvider.java --- a/org/sonews/storage/impl/JDBCDatabaseProvider.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,69 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.storage.impl; - -import java.sql.SQLException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import org.sonews.storage.Storage; -import org.sonews.storage.StorageBackendException; -import org.sonews.storage.StorageProvider; - -/** - * - * @author Christian Lins - * @since sonews/1.0 - */ -public class JDBCDatabaseProvider implements StorageProvider -{ - - protected static final Map instances - = new ConcurrentHashMap(); - - @Override - public boolean isSupported(String uri) - { - throw new UnsupportedOperationException("Not supported yet."); - } - - @Override - public Storage storage(Thread thread) - throws StorageBackendException - { - try - { - if(!instances.containsKey(Thread.currentThread())) - { - JDBCDatabase db = new JDBCDatabase(); - db.arise(); - instances.put(Thread.currentThread(), db); - return db; - } - else - { - return instances.get(Thread.currentThread()); - } - } - catch(SQLException ex) - { - throw new StorageBackendException(ex); - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/storage/package.html --- a/org/sonews/storage/package.html Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,2 +0,0 @@ -Contains classes of the storage backend and the Group and Article -abstraction. \ No newline at end of file diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/DatabaseSetup.java --- a/org/sonews/util/DatabaseSetup.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,127 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.util; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.Statement; -import java.util.HashMap; -import java.util.Map; -import org.sonews.config.Config; -import org.sonews.util.io.Resource; - -/** - * Database setup utility class. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public final class DatabaseSetup -{ - - private static final Map 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(); - - System.out.print("Give name of DB user that can create tables: "); - String dbUser = in.readLine(); - - System.out.print("Password: "); - String dbPassword = in.readLine(); - - String url = urlMap.get(dbmsType) - .set("HOSTNAME", dbHostname) - .set("DB", dbName).toString(); - - Connection conn = - DriverManager.getConnection(url, dbUser, dbPassword); - conn.setAutoCommit(false); - - String[] tmplChunks = tmpl.split(";"); - - for(String chunk : tmplChunks) - { - if(chunk.trim().equals("")) - { - continue; - } - - Statement stmt = conn.createStatement(); - stmt.execute(chunk); - } - - conn.commit(); - conn.setAutoCommit(true); - - // Create config file - - System.out.println("Ok"); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/Log.java --- a/org/sonews/util/Log.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.util; - -import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; -import java.util.logging.StreamHandler; -import org.sonews.config.Config; - -/** - * Provides logging and debugging methods. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class Log extends Logger -{ - - private static Log instance = new Log(); - - private Log() - { - super("org.sonews", null); - - StreamHandler handler = new StreamHandler(System.out, new SimpleFormatter()); - Level level = Level.parse(Config.inst().get(Config.LOGLEVEL, "INFO")); - handler.setLevel(level); - addHandler(handler); - setLevel(level); - LogManager.getLogManager().addLogger(this); - } - - public static Logger get() - { - Level level = Level.parse(Config.inst().get(Config.LOGLEVEL, "INFO")); - instance.setLevel(level); - return instance; - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/Pair.java --- a/org/sonews/util/Pair.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,48 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.util; - -/** - * A pair of two objects. - * @author Christian Lins - * @since sonews/0.5.0 - */ -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; - } - - public T2 getB() - { - return b; - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/Purger.java --- a/org/sonews/util/Purger.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,149 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.util; - -import org.sonews.daemon.AbstractDaemon; -import org.sonews.config.Config; -import org.sonews.storage.Article; -import org.sonews.storage.Headers; -import java.util.Date; -import java.util.List; -import org.sonews.storage.Channel; -import org.sonews.storage.Group; -import org.sonews.storage.StorageBackendException; -import org.sonews.storage.StorageManager; - -/** - * The purger is started in configurable intervals to search - * for messages that can be purged. A message must be deleted if its lifetime - * has exceeded, if it was marked as deleted or if the maximum number of - * articles in the database is reached. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class Purger extends AbstractDaemon -{ - - /** - * Loops through all messages and deletes them if their time - * has come. - */ - @Override - public void run() - { - try - { - while(isRunning()) - { - purgeDeleted(); - purgeOutdated(); - - Thread.sleep(120000); // Sleep for two minutes - } - } - catch(StorageBackendException ex) - { - ex.printStackTrace(); - } - catch(InterruptedException ex) - { - Log.get().warning("Purger interrupted: " + ex); - } - } - - private void purgeDeleted() - throws StorageBackendException - { - List 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."); - } - - for(int n = 0; n < ids.size() && n < 10; n++) - { - Article art = StorageManager.current().getArticle(ids.get(n), group.getInternalID()); - StorageManager.current().delete(art.getMessageID()); - Log.get().info("Article " + art.getMessageID() + " purged."); - } - } - } - } - - private void purgeOutdated() - throws InterruptedException, StorageBackendException - { - long articleMaximum = - Config.inst().get("sonews.article.maxnum", Long.MAX_VALUE); - long lifetime = - Config.inst().get("sonews.article.lifetime", -1); - - if(lifetime > 0 || articleMaximum < Stats.getInstance().getNumberOfNews()) - { - Log.get().info("Purging old messages..."); - String mid = StorageManager.current().getOldestArticle(); - if (mid == null) // No articles in the database - { - return; - } - - Article art = StorageManager.current().getArticle(mid); - long artDate = 0; - String dateStr = art.getHeader(Headers.DATE)[0]; - try - { - artDate = Date.parse(dateStr) / 1000 / 60 / 60 / 24; - } - catch (IllegalArgumentException ex) - { - Log.get().warning("Could not parse date string: " + dateStr + " " + ex); - } - - // Should we delete the message because of its age or because the - // article maximum was reached? - if (lifetime < 0 || artDate < (new Date().getTime() + lifetime)) - { - StorageManager.current().delete(mid); - System.out.println("Deleted: " + mid); - } - else - { - Thread.sleep(1000 * 60); // Wait 60 seconds - return; - } - } - else - { - Log.get().info("Lifetime purger is disabled"); - Thread.sleep(1000 * 60 * 30); // Wait 30 minutes - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/Stats.java --- a/org/sonews/util/Stats.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,206 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.util; - -import java.util.Calendar; -import org.sonews.config.Config; -import org.sonews.storage.Channel; -import org.sonews.storage.StorageBackendException; -import org.sonews.storage.StorageManager; - -/** - * Class that capsulates statistical data gathering. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public final class Stats -{ - - public static final byte CONNECTIONS = 1; - public static final byte POSTED_NEWS = 2; - public static final byte GATEWAYED_NEWS = 3; - public static final byte FEEDED_NEWS = 4; - public static final byte MLGW_RUNSTART = 5; - public static final byte MLGW_RUNEND = 6; - - private static Stats instance = new Stats(); - - public static Stats getInstance() - { - return Stats.instance; - } - - private Stats() {} - - private volatile int connectedClients = 0; - - /** - * A generic method that writes event data to the storage backend. - * If event logging is disabled with sonews.eventlog=false this method - * simply does nothing. - * @param type - * @param groupname - */ - private void addEvent(byte type, String groupname) - { - try - { - if (Config.inst().get(Config.EVENTLOG, true)) - { - - Channel group = Channel.getByName(groupname); - if (group != null) - { - StorageManager.current().addEvent( - System.currentTimeMillis(), type, group.getInternalID()); - } - } - else - { - Log.get().info("Group " + groupname + " does not exist."); - } - } - catch (StorageBackendException ex) - { - ex.printStackTrace(); - } - } - - public void clientConnect() - { - this.connectedClients++; - } - - public void clientDisconnect() - { - this.connectedClients--; - } - - public int connectedClients() - { - return this.connectedClients; - } - - public int getNumberOfGroups() - { - try - { - return StorageManager.current().countGroups(); - } - catch(StorageBackendException ex) - { - ex.printStackTrace(); - return -1; - } - } - - public int getNumberOfNews() - { - try - { - return StorageManager.current().countArticles(); - } - catch(StorageBackendException ex) - { - ex.printStackTrace(); - return -1; - } - } - - public int getYesterdaysEvents(final byte eventType, final int hour, - final Channel group) - { - // Determine the timestamp values for yesterday and the given hour - Calendar cal = Calendar.getInstance(); - int year = cal.get(Calendar.YEAR); - int month = cal.get(Calendar.MONTH); - int dayom = cal.get(Calendar.DAY_OF_MONTH) - 1; // Yesterday - - cal.set(year, month, dayom, hour, 0, 0); - long startTimestamp = cal.getTimeInMillis(); - - cal.set(year, month, dayom, hour + 1, 0, 0); - long endTimestamp = cal.getTimeInMillis(); - - try - { - return StorageManager.current() - .getEventsCount(eventType, startTimestamp, endTimestamp, group); - } - catch(StorageBackendException ex) - { - ex.printStackTrace(); - return -1; - } - } - - public void mailPosted(String groupname) - { - addEvent(POSTED_NEWS, groupname); - } - - public void mailGatewayed(String groupname) - { - addEvent(GATEWAYED_NEWS, groupname); - } - - public void mailFeeded(String groupname) - { - addEvent(FEEDED_NEWS, groupname); - } - - public void mlgwRunStart() - { - addEvent(MLGW_RUNSTART, "control"); - } - - public void mlgwRunEnd() - { - addEvent(MLGW_RUNEND, "control"); - } - - private double perHour(int key, long gid) - { - try - { - return StorageManager.current().getEventsPerHour(key, gid); - } - catch(StorageBackendException ex) - { - ex.printStackTrace(); - return -1; - } - } - - public double postedPerHour(long gid) - { - return perHour(POSTED_NEWS, gid); - } - - public double gatewayedPerHour(long gid) - { - return perHour(GATEWAYED_NEWS, gid); - } - - public double feededPerHour(long gid) - { - return perHour(FEEDED_NEWS, gid); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/StringTemplate.java --- a/org/sonews/util/StringTemplate.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,97 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.util; - -import java.util.HashMap; -import java.util.Map; - -/** - * Class that allows simple String template handling. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class StringTemplate -{ - - private String str = null; - private String templateDelimiter = "%"; - private Map templateValues = new HashMap(); - - public StringTemplate(String str, final String templateDelimiter) - { - if(str == null || templateDelimiter == null) - { - throw new IllegalArgumentException("null arguments not allowed"); - } - - this.str = str; - this.templateDelimiter = templateDelimiter; - } - - public StringTemplate(String str) - { - this(str, "%"); - } - - public StringTemplate set(String template, String value) - { - if(template == null || value == null) - { - throw new IllegalArgumentException("null arguments not allowed"); - } - - this.templateValues.put(template, value); - return this; - } - - public StringTemplate set(String template, long value) - { - return set(template, Long.toString(value)); - } - - public StringTemplate set(String template, double value) - { - return set(template, Double.toString(value)); - } - - public StringTemplate set(String template, Object obj) - { - if(template == null || obj == null) - { - throw new IllegalArgumentException("null arguments not allowed"); - } - - return set(template, obj.toString()); - } - - @Override - public String toString() - { - String ret = str; - - for(String key : this.templateValues.keySet()) - { - String value = this.templateValues.get(key); - ret = ret.replace(templateDelimiter + key, value); - } - - return ret; - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/TimeoutMap.java --- a/org/sonews/util/TimeoutMap.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,145 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.util; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Implementation of a Map that will loose its stored values after a - * configurable amount of time. - * This class may be used to cache config values for example. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class TimeoutMap 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); - } - - @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 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/io/ArticleInputStream.java --- a/org/sonews/util/io/ArticleInputStream.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,71 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.util.io; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import org.sonews.storage.Article; - -/** - * Capsulates an Article to provide a raw InputStream. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class ArticleInputStream extends InputStream -{ - - private byte[] buf; - private int pos = 0; - - public ArticleInputStream(final Article art) - throws IOException, UnsupportedEncodingException - { - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(art.getHeaderSource().getBytes("UTF-8")); - out.write("\r\n\r\n".getBytes()); - out.write(art.getBody()); // Without CRLF - out.flush(); - this.buf = out.toByteArray(); - } - - /** - * This method reads one byte from the stream. The 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 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/io/ArticleReader.java --- a/org/sonews/util/io/ArticleReader.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,135 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.util.io; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.Socket; -import java.net.UnknownHostException; -import org.sonews.config.Config; -import org.sonews.util.Log; - -/** - * Reads an news article from a NNTP server. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class ArticleReader -{ - - private BufferedOutputStream out; - private BufferedInputStream in; - private String messageID; - - public ArticleReader(String host, int port, String messageID) - throws IOException, UnknownHostException - { - this.messageID = messageID; - - // Connect to NNTP server - Socket socket = new Socket(host, port); - this.out = new BufferedOutputStream(socket.getOutputStream()); - this.in = new BufferedInputStream(socket.getInputStream()); - String line = readln(this.in); - if(!line.startsWith("200 ")) - { - throw new IOException("Invalid hello from server: " + line); - } - } - - private boolean eofArticle(byte[] buf) - { - if(buf.length < 4) - { - return false; - } - - int l = buf.length - 1; - return buf[l-3] == 10 // '*\n' - && buf[l-2] == '.' // '.' - && buf[l-1] == 13 && buf[l] == 10; // '\r\n' - } - - public byte[] getArticleData() - throws IOException, UnsupportedEncodingException - { - long maxSize = Config.inst().get(Config.ARTICLE_MAXSIZE, 1024) * 1024L; - - try - { - this.out.write(("ARTICLE " + this.messageID + "\r\n").getBytes("UTF-8")); - this.out.flush(); - - String line = readln(this.in); - if(line.startsWith("220 ")) - { - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - - while(!eofArticle(buf.toByteArray())) - { - for(int b = in.read(); b != 10; b = in.read()) - { - buf.write(b); - } - - buf.write(10); - if(buf.size() > maxSize) - { - Log.get().warning("Skipping message that is too large: " + buf.size()); - return null; - } - } - - return buf.toByteArray(); - } - else - { - Log.get().warning("ArticleReader: " + line); - return null; - } - } - catch(IOException ex) - { - throw ex; - } - finally - { - this.out.write("QUIT\r\n".getBytes("UTF-8")); - this.out.flush(); - this.out.close(); - } - } - - private String readln(InputStream in) - throws IOException - { - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - for(int b = in.read(); b != 10 /* \n */; b = in.read()) - { - buf.write(b); - } - - return new String(buf.toByteArray()); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/io/ArticleWriter.java --- a/org/sonews/util/io/ArticleWriter.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,133 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.util.io; - -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; -import java.net.Socket; -import java.net.UnknownHostException; -import org.sonews.storage.Article; - -/** - * Posts an Article to a NNTP server using the POST command. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public class ArticleWriter -{ - - private BufferedOutputStream out; - private BufferedReader inr; - - public ArticleWriter(String host, int port) - throws IOException, UnknownHostException - { - // Connect to NNTP server - Socket socket = new Socket(host, port); - this.out = new BufferedOutputStream(socket.getOutputStream()); - this.inr = new BufferedReader(new InputStreamReader(socket.getInputStream())); - String line = inr.readLine(); - if(line == null || !line.startsWith("200 ")) - { - throw new IOException("Invalid hello from server: " + line); - } - } - - public void close() - throws IOException, UnsupportedEncodingException - { - this.out.write("QUIT\r\n".getBytes("UTF-8")); - this.out.flush(); - } - - protected void finishPOST() - throws IOException - { - this.out.write("\r\n.\r\n".getBytes()); - this.out.flush(); - String line = inr.readLine(); - if(line == null || !line.startsWith("240 ") || !line.startsWith("441 ")) - { - throw new IOException(line); - } - } - - protected void preparePOST() - throws IOException - { - this.out.write("POST\r\n".getBytes("UTF-8")); - this.out.flush(); - - String line = this.inr.readLine(); - if(line == null || !line.startsWith("340 ")) - { - throw new IOException(line); - } - } - - public void writeArticle(Article article) - throws IOException, UnsupportedEncodingException - { - byte[] buf = new byte[512]; - ArticleInputStream in = new ArticleInputStream(article); - - preparePOST(); - - int len = in.read(buf); - while(len != -1) - { - writeLine(buf, len); - len = in.read(buf); - } - - finishPOST(); - } - - /** - * Writes the raw content of an article to the remote server. This method - * does no charset conversion/handling of any kind so its the preferred - * method for sending an article to remote peers. - * @param rawArticle - * @throws IOException - */ - public void writeArticle(byte[] rawArticle) - throws IOException - { - preparePOST(); - writeLine(rawArticle, rawArticle.length); - finishPOST(); - } - - /** - * Writes the given buffer to the connect remote server. - * @param buffer - * @param len - * @throws IOException - */ - protected void writeLine(byte[] buffer, int len) - throws IOException - { - this.out.write(buffer, 0, len); - this.out.flush(); - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/io/Resource.java --- a/org/sonews/util/io/Resource.java Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,132 +0,0 @@ -/* - * SONEWS News Server - * see AUTHORS for the list of contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sonews.util.io; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URL; -import java.nio.charset.Charset; - -/** - * Provides method for loading of resources. - * @author Christian Lins - * @since sonews/0.5.0 - */ -public final class Resource -{ - - /** - * Loads a resource and returns it as URL reference. - * The Resource's classloader is used to load the resource, not - * the System's ClassLoader so it may be safe to use this method - * in a sandboxed environment. - * @return - */ - public static URL getAsURL(final String name) - { - if(name == null) - { - return null; - } - - return Resource.class.getClassLoader().getResource(name); - } - - /** - * Loads a resource and returns an InputStream to it. - * @param name - * @return - */ - public static InputStream getAsStream(String name) - { - try - { - URL url = getAsURL(name); - if(url == null) - { - return null; - } - else - { - return url.openStream(); - } - } - catch(IOException e) - { - e.printStackTrace(); - return null; - } - } - - /** - * Loads a plain text resource. - * @param withNewline If false all newlines are removed from the - * return String - */ - public static String getAsString(String name, boolean withNewline) - { - if(name == null) - return null; - - BufferedReader in = null; - try - { - InputStream ins = getAsStream(name); - if(ins == null) - return null; - - in = new BufferedReader( - new InputStreamReader(ins, Charset.forName("UTF-8"))); - StringBuffer buf = new StringBuffer(); - - for(;;) - { - String line = in.readLine(); - if(line == null) - break; - - buf.append(line); - if(withNewline) - buf.append('\n'); - } - - return buf.toString(); - } - catch(Exception e) - { - e.printStackTrace(); - return null; - } - finally - { - try - { - if(in != null) - in.close(); - } - catch(IOException ex) - { - ex.printStackTrace(); - } - } - } - -} diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/io/package.html --- a/org/sonews/util/io/package.html Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -Contains I/O utilitiy classes. \ No newline at end of file diff -r 9f0b95aafaa3 -r ed84c8bdd87b org/sonews/util/package.html --- a/org/sonews/util/package.html Sun Aug 29 17:04:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -Contains various utility classes. \ No newline at end of file diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/Main.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/Main.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,198 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews; + +import java.sql.Driver; +import java.sql.DriverManager; +import java.util.Enumeration; +import java.util.Date; +import java.util.logging.Level; +import org.sonews.config.Config; +import org.sonews.daemon.ChannelLineBuffers; +import org.sonews.daemon.CommandSelector; +import org.sonews.daemon.Connections; +import org.sonews.daemon.NNTPDaemon; +import org.sonews.feed.FeedManager; +import org.sonews.mlgw.MailPoller; +import org.sonews.storage.StorageBackendException; +import org.sonews.storage.StorageManager; +import org.sonews.storage.StorageProvider; +import org.sonews.util.Log; +import org.sonews.util.Purger; +import org.sonews.util.io.Resource; + +/** + * Startup class of the daemon. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public final class Main +{ + + private Main() + { + } + + /** Version information of the sonews daemon */ + public static final String VERSION = "sonews/1.1.0"; + public static final Date STARTDATE = new Date(); + + /** + * The main entrypoint. + * @param args + * @throws Exception + */ + public static void main(String[] args) throws Exception + { + System.out.println(VERSION); + Thread.currentThread().setName("Mainthread"); + + // Command line arguments + boolean feed = false; // Enable feeding? + boolean mlgw = false; // Enable Mailinglist gateway? + int port = -1; + + for(int n = 0; n < args.length; n++) + { + if(args[n].equals("-c") || args[n].equals("-config")) + { + Config.inst().set(Config.LEVEL_CLI, Config.CONFIGFILE, args[++n]); + System.out.println("Using config file " + args[n]); + } + else if(args[n].equals("-dumpjdbcdriver")) + { + System.out.println("Available JDBC drivers:"); + Enumeration drvs = DriverManager.getDrivers(); + while(drvs.hasMoreElements()) + { + System.out.println(drvs.nextElement()); + } + return; + } + else if(args[n].equals("-feed")) + { + feed = true; + } + else if(args[n].equals("-h") || args[n].equals("-help")) + { + printArguments(); + return; + } + else if(args[n].equals("-mlgw")) + { + mlgw = true; + } + else if(args[n].equals("-p")) + { + port = Integer.parseInt(args[++n]); + } + else if(args[n].equals("-plugin")) + { + System.out.println("Warning: -plugin-storage is not implemented!"); + } + else if(args[n].equals("-plugin-command")) + { + try + { + CommandSelector.addCommandHandler(args[++n]); + } + catch(Exception ex) + { + Log.get().warning("Could not load command plugin: " + args[n]); + Log.get().log(Level.INFO, "Main.java", ex); + } + } + else if(args[n].equals("-plugin-storage")) + { + System.out.println("Warning: -plugin-storage is not implemented!"); + } + else if(args[n].equals("-v") || args[n].equals("-version")) + { + // Simply return as the version info is already printed above + return; + } + } + + // Try to load the JDBCDatabase; + // Do NOT USE BackendConfig or Log classes before this point because they require + // a working JDBCDatabase connection. + try + { + StorageProvider sprov = + StorageManager.loadProvider("org.sonews.storage.impl.JDBCDatabaseProvider"); + StorageManager.enableProvider(sprov); + + // Make sure some elementary groups are existing + if(!StorageManager.current().isGroupExisting("control")) + { + StorageManager.current().addGroup("control", 0); + Log.get().info("Group 'control' created."); + } + } + catch(StorageBackendException ex) + { + ex.printStackTrace(); + System.err.println("Database initialization failed with " + ex.toString()); + System.err.println("Make sure you have specified the correct database" + + " settings in sonews.conf!"); + return; + } + + ChannelLineBuffers.allocateDirect(); + + // Add shutdown hook + Runtime.getRuntime().addShutdownHook(new ShutdownHook()); + + // Start the listening daemon + if(port <= 0) + { + port = Config.inst().get(Config.PORT, 119); + } + final NNTPDaemon daemon = NNTPDaemon.createInstance(port); + daemon.start(); + + // Start Connections purger thread... + Connections.getInstance().start(); + + // Start mailinglist gateway... + if(mlgw) + { + new MailPoller().start(); + } + + // Start feeds + if(feed) + { + FeedManager.startFeeding(); + } + + Purger purger = new Purger(); + purger.start(); + + // Wait for main thread to exit (setDaemon(false)) + daemon.join(); + } + + private static void printArguments() + { + String usage = Resource.getAsString("helpers/usage", true); + System.out.println(usage); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/ShutdownHook.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/ShutdownHook.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,84 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews; + +import java.sql.SQLException; +import java.util.Map; +import org.sonews.daemon.AbstractDaemon; + +/** + * Will force all other threads to shutdown cleanly. + * @author Christian Lins + * @since sonews/0.5.0 + */ +class ShutdownHook extends Thread +{ + + /** + * Called when the JVM exits. + */ + @Override + public void run() + { + System.out.println("sonews: Trying to shutdown all threads..."); + + Map threadsMap = Thread.getAllStackTraces(); + for(Thread thread : threadsMap.keySet()) + { + // Interrupt the thread if it's a AbstractDaemon + AbstractDaemon daemon; + if(thread instanceof AbstractDaemon && thread.isAlive()) + { + try + { + daemon = (AbstractDaemon)thread; + daemon.shutdownNow(); + } + catch(SQLException ex) + { + System.out.println("sonews: " + ex); + } + } + } + + for(Thread thread : threadsMap.keySet()) + { + AbstractDaemon daemon; + if(thread instanceof AbstractDaemon && thread.isAlive()) + { + daemon = (AbstractDaemon)thread; + System.out.println("sonews: Waiting for " + daemon + " to exit..."); + try + { + daemon.join(500); + } + catch(InterruptedException ex) + { + System.out.println(ex.getLocalizedMessage()); + } + } + } + + // We have notified all not-sleeping AbstractDaemons of the shutdown; + // all other threads can be simply purged on VM shutdown + + System.out.println("sonews: Clean shutdown."); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/acl/AccessControl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/acl/AccessControl.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,31 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.acl; + +/** + * + * @author Christian Lins + * @since sonews/1.1 + */ +public interface AccessControl +{ + + boolean hasPermission(String user, char[] secret, String permission); + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/acl/AuthInfoCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/acl/AuthInfoCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,64 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.acl; + +import java.io.IOException; +import org.sonews.daemon.NNTPConnection; +import org.sonews.daemon.command.Command; +import org.sonews.storage.StorageBackendException; + +/** + * + * @author Christian Lins + * @since sonews/1.1 + */ +public class AuthInfoCommand implements Command +{ + + @Override + public String[] getSupportedCommandStrings() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean hasFinished() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String impliedCapability() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isStateful() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void processLine(NNTPConnection conn, String line, byte[] rawLine) throws IOException, StorageBackendException + { + throw new UnsupportedOperationException("Not supported yet."); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/config/AbstractConfig.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/config/AbstractConfig.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,57 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.config; + +/** + * Base class for Config and BootstrapConfig. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public abstract class AbstractConfig +{ + + public abstract String get(String key, String defVal); + + public int get(final String key, final int defVal) + { + return Integer.parseInt( + get(key, Integer.toString(defVal))); + } + + public boolean get(String key, boolean defVal) + { + String val = get(key, Boolean.toString(defVal)); + return Boolean.parseBoolean(val); + } + + /** + * Returns a long config value specified via the given key. + * @param key + * @param defVal + * @return + */ + public long get(String key, long defVal) + { + String val = get(key, Long.toString(defVal)); + return Long.parseLong(val); + } + + protected abstract void set(String key, String val); + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/config/BackendConfig.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/config/BackendConfig.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,115 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.config; + +import java.util.logging.Level; +import org.sonews.util.Log; +import org.sonews.storage.StorageBackendException; +import org.sonews.storage.StorageManager; +import org.sonews.util.TimeoutMap; + +/** + * Provides access to the program wide configuration that is stored within + * the server's database. + * @author Christian Lins + * @since sonews/0.5.0 + */ +class BackendConfig extends AbstractConfig +{ + + private static BackendConfig instance = new BackendConfig(); + + public static BackendConfig getInstance() + { + return instance; + } + + private final TimeoutMap 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 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/config/CommandLineConfig.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/config/CommandLineConfig.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,64 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.config; + +import java.util.Map; +import java.util.HashMap; + +/** + * + * @author Christian Lins + */ +class CommandLineConfig extends AbstractConfig +{ + + private static final CommandLineConfig instance = new CommandLineConfig(); + + public static CommandLineConfig getInstance() + { + return instance; + } + + private final Map values = new HashMap(); + + private CommandLineConfig() {} + + @Override + public String get(String key, String def) + { + synchronized(this.values) + { + if(this.values.containsKey(key)) + { + def = this.values.get(key); + } + } + return def; + } + + @Override + public void set(String key, String val) + { + synchronized(this.values) + { + this.values.put(key, val); + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/config/Config.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/config/Config.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,175 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.config; + +/** + * Configuration facade class. + * @author Christian Lins + * @since sonews/1.0 + */ +public class Config extends AbstractConfig +{ + + public static final int LEVEL_CLI = 1; + public static final int LEVEL_FILE = 2; + public static final int LEVEL_BACKEND = 3; + + public static final String CONFIGFILE = "sonews.configfile"; + + /** BackendConfig key constant. Value is the maximum article size in kilobytes. */ + public static final String ARTICLE_MAXSIZE = "sonews.article.maxsize"; + + /** BackendConfig key constant. Value: Amount of news that are feeded per run. */ + public static final String EVENTLOG = "sonews.eventlog"; + public static final String FEED_NEWSPERRUN = "sonews.feed.newsperrun"; + public static final String FEED_PULLINTERVAL = "sonews.feed.pullinterval"; + public static final String HOSTNAME = "sonews.hostname"; + public static final String PORT = "sonews.port"; + public static final String TIMEOUT = "sonews.timeout"; + public static final String LOGLEVEL = "sonews.loglevel"; + public static final String MLPOLL_DELETEUNKNOWN = "sonews.mlpoll.deleteunknown"; + public static final String MLPOLL_HOST = "sonews.mlpoll.host"; + public static final String MLPOLL_PASSWORD = "sonews.mlpoll.password"; + public static final String MLPOLL_USER = "sonews.mlpoll.user"; + public static final String MLSEND_ADDRESS = "sonews.mlsend.address"; + public static final String MLSEND_RW_FROM = "sonews.mlsend.rewrite.from"; + public static final String MLSEND_RW_SENDER = "sonews.mlsend.rewrite.sender"; + public static final String MLSEND_HOST = "sonews.mlsend.host"; + public static final String MLSEND_PASSWORD = "sonews.mlsend.password"; + public static final String MLSEND_PORT = "sonews.mlsend.port"; + public static final String MLSEND_USER = "sonews.mlsend.user"; + + /** Key constant. If value is "true" every I/O is written to logfile + * (which is a lot!) + */ + public static final String DEBUG = "sonews.debug"; + + /** Key constant. Value is classname of the JDBC driver */ + public static final String STORAGE_DBMSDRIVER = "sonews.storage.dbmsdriver"; + + /** Key constant. Value is JDBC connect String to the database. */ + public static final String STORAGE_DATABASE = "sonews.storage.database"; + + /** Key constant. Value is the username for the DBMS. */ + public static final String STORAGE_USER = "sonews.storage.user"; + + /** Key constant. Value is the password for the DBMS. */ + public static final String STORAGE_PASSWORD = "sonews.storage.password"; + + /** Key constant. Value is the name of the host which is allowed to use the + * XDAEMON command; default: "localhost" */ + public static final String XDAEMON_HOST = "sonews.xdaemon.host"; + + /** The config key for the filename of the logfile */ + public static final String LOGFILE = "sonews.log"; + + public static final String[] AVAILABLE_KEYS = { + ARTICLE_MAXSIZE, + EVENTLOG, + FEED_NEWSPERRUN, + FEED_PULLINTERVAL, + HOSTNAME, + MLPOLL_DELETEUNKNOWN, + MLPOLL_HOST, + MLPOLL_PASSWORD, + MLPOLL_USER, + MLSEND_ADDRESS, + MLSEND_HOST, + MLSEND_PASSWORD, + MLSEND_PORT, + MLSEND_RW_FROM, + MLSEND_RW_SENDER, + MLSEND_USER, + PORT, + TIMEOUT, + XDAEMON_HOST + }; + + private static Config instance = new Config(); + + public static Config inst() + { + return instance; + } + + private Config(){} + + @Override + public String get(String key, String def) + { + String val = CommandLineConfig.getInstance().get(key, null); + + if(val == null) + { + val = FileConfig.getInstance().get(key, null); + } + + if(val == null) + { + val = BackendConfig.getInstance().get(key, def); + } + + return val; + } + + public String get(int maxLevel, String key, String def) + { + String val = CommandLineConfig.getInstance().get(key, null); + + if(val == null && maxLevel >= LEVEL_FILE) + { + val = FileConfig.getInstance().get(key, null); + if(val == null && maxLevel >= LEVEL_BACKEND) + { + val = BackendConfig.getInstance().get(key, def); + } + } + + return val != null ? val : def; + } + + @Override + public void set(String key, String val) + { + set(LEVEL_BACKEND, key, val); + } + + public void set(int level, String key, String val) + { + switch(level) + { + case LEVEL_CLI: + { + CommandLineConfig.getInstance().set(key, val); + break; + } + case LEVEL_FILE: + { + FileConfig.getInstance().set(key, val); + break; + } + case LEVEL_BACKEND: + { + BackendConfig.getInstance().set(key, val); + break; + } + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/config/FileConfig.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/config/FileConfig.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,170 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.config; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Properties; + +/** + * Manages the bootstrap configuration. It MUST contain all config values + * that are needed to establish a database connection. + * For further configuration values use the Config class instead as that class + * stores its values within the database. + * @author Christian Lins + * @since sonews/0.5.0 + */ +class FileConfig extends AbstractConfig +{ + + private static final Properties defaultConfig = new Properties(); + + private static FileConfig instance = null; + + static + { + // Set some default values + defaultConfig.setProperty(Config.STORAGE_DATABASE, "jdbc:mysql://localhost/sonews"); + defaultConfig.setProperty(Config.STORAGE_DBMSDRIVER, "com.mysql.jdbc.Driver"); + defaultConfig.setProperty(Config.STORAGE_USER, "sonews_user"); + defaultConfig.setProperty(Config.STORAGE_PASSWORD, "mysecret"); + defaultConfig.setProperty(Config.DEBUG, "false"); + } + + /** + * Note: this method is not thread-safe + * @return A Config instance + */ + public static synchronized FileConfig getInstance() + { + if(instance == null) + { + instance = new FileConfig(); + } + return instance; + } + + // Every config instance is initialized with the default values. + private final Properties settings = (Properties)defaultConfig.clone(); + + /** + * Config is a singelton class with only one instance at time. + * So the constructor is private to prevent the creation of more + * then one Config instance. + * @see Config.getInstance() to retrieve an instance of Config + */ + private FileConfig() + { + try + { + // Load settings from file + load(); + } + catch(IOException ex) + { + ex.printStackTrace(); + } + } + + /** + * Loads the configuration from the config file. By default this is done + * by the (private) constructor but it can be useful to reload the config + * by invoking this method. + * @throws IOException + */ + public void load() + throws IOException + { + FileInputStream in = null; + + try + { + in = new FileInputStream( + Config.inst().get(Config.LEVEL_CLI, Config.CONFIGFILE, "sonews.conf")); + settings.load(in); + } + catch (FileNotFoundException e) + { + // MUST NOT use Log otherwise endless loop + System.err.println(e.getMessage()); + save(); + } + finally + { + if(in != null) + in.close(); + } + } + + /** + * Saves this Config to the config file. By default this is done + * at program end. + * @throws FileNotFoundException + * @throws IOException + */ + public void save() throws FileNotFoundException, IOException + { + FileOutputStream out = null; + try + { + out = new FileOutputStream( + Config.inst().get(Config.LEVEL_CLI, Config.CONFIGFILE, "sonews.conf")); + settings.store(out, "SONEWS Config File"); + out.flush(); + } + catch(IOException ex) + { + throw ex; + } + finally + { + if(out != null) + out.close(); + } + } + + /** + * Returns the value that is stored within this config + * identified by the given key. If the key cannot be found + * the default value is returned. + * @param key Key to identify the value. + * @param def The default value that is returned if the key + * is not found in this Config. + * @return + */ + @Override + public String get(String key, String def) + { + return settings.getProperty(key, def); + } + + /** + * Sets the value for a given key. + * @param key + * @param value + */ + @Override + public void set(final String key, final String value) + { + settings.setProperty(key, value); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/AbstractDaemon.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/AbstractDaemon.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,101 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon; + +import java.sql.SQLException; +import org.sonews.storage.StorageManager; +import org.sonews.util.Log; + +/** + * Base class of all sonews threads. + * Instances of this class will be automatically registered at the ShutdownHook + * to be cleanly exited when the server is forced to exit. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public abstract class AbstractDaemon extends Thread +{ + + /** This variable is write synchronized through setRunning */ + private boolean isRunning = false; + + /** + * Protected constructor. Will be called by derived classes. + */ + protected AbstractDaemon() + { + setDaemon(true); // VM will exit when all threads are daemons + setName(getClass().getSimpleName()); + } + + /** + * @return true if shutdown() was not yet called. + */ + public boolean isRunning() + { + synchronized(this) + { + return this.isRunning; + } + } + + /** + * Marks this thread to exit soon. Closes the associated JDBCDatabase connection + * if available. + * @throws java.sql.SQLException + */ + public void shutdownNow() + throws SQLException + { + synchronized(this) + { + this.isRunning = false; + StorageManager.disableProvider(); + } + } + + /** + * Calls shutdownNow() but catches SQLExceptions if occurring. + */ + public void shutdown() + { + try + { + shutdownNow(); + } + catch(SQLException ex) + { + Log.get().warning(ex.toString()); + } + } + + /** + * Starts this daemon. + */ + @Override + public void start() + { + synchronized(this) + { + this.isRunning = true; + } + super.start(); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/ChannelLineBuffers.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/ChannelLineBuffers.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,283 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon; + +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.List; + +/** + * Class holding ByteBuffers for SocketChannels/NNTPConnection. + * Due to the complex nature of AIO/NIO we must properly handle the line + * buffers for the input and output of the SocketChannels. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class ChannelLineBuffers +{ + + /** + * Size of one small buffer; + * per default this is 512 bytes to fit one standard line. + */ + public static final int BUFFER_SIZE = 512; + + private static int maxCachedBuffers = 2048; // Cached buffers maximum + + private static final List 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; + + // Mark the current write position + int mark = buffer.position(); + + // Set position to 0 and limit to current position + buffer.flip(); + + ByteBuffer lineBuffer = newLineBuffer(); + + while (buffer.position() < buffer.limit()) + { + byte b = buffer.get(); + if (b == 10) // '\n' + { + // The bytes between the buffer's current position and its limit, + // if any, are copied to the beginning of the buffer. That is, the + // byte at index p = position() is copied to index zero, the byte at + // index p + 1 is copied to index one, and so forth until the byte + // at index limit() - 1 is copied to index n = limit() - 1 - p. + // The buffer's position is then set to n+1 and its limit is set to + // its capacity. + buffer.compact(); + + lineBuffer.flip(); // limit to position, position to 0 + return lineBuffer; + } + else + { + lineBuffer.put(b); + } + } + + buffer.limit(BUFFER_SIZE); + buffer.position(mark); + + if(buffer.hasRemaining()) + { + return null; + } + else + { + // In the first 512 was no newline found, so the input is not standard + // compliant. We return the current buffer as new line and add a space + // to the beginning of the next line which corrects some overlong header + // lines. + inputBuffer = newLineBuffer(); + inputBuffer.put((byte)' '); + buffer.flip(); + return buffer; + } + } + } + + /** + * Returns a at least 512 bytes long ByteBuffer ready for usage. + * The method first try to reuse an already allocated (cached) buffer but + * if that fails returns a newly allocated direct buffer. + * Use recycleBuffer() method when you do not longer use the allocated buffer. + */ + static ByteBuffer newLineBuffer() + { + ByteBuffer buf = null; + synchronized(freeSmallBuffers) + { + if(!freeSmallBuffers.isEmpty()) + { + buf = freeSmallBuffers.remove(0); + } + } + + if(buf == null) + { + // Allocate a non-direct buffer + buf = ByteBuffer.allocate(BUFFER_SIZE); + } + + assert buf.position() == 0; + assert buf.limit() >= BUFFER_SIZE; + + return buf; + } + + /** + * Adds the given buffer to the list of free buffers if it is a valuable + * direct allocated buffer. + * @param buffer + */ + public static void recycleBuffer(ByteBuffer buffer) + { + assert buffer != null; + + if(buffer.isDirect()) + { + assert buffer.capacity() >= BUFFER_SIZE; + + // Add old buffers to the list of free buffers + synchronized(freeSmallBuffers) + { + buffer.clear(); // Set position to 0 and limit to capacity + freeSmallBuffers.add(buffer); + } + } // if(buffer.isDirect()) + } + + /** + * Recycles all buffers of this ChannelLineBuffers object. + */ + public void recycleBuffers() + { + synchronized(inputBuffer) + { + recycleBuffer(inputBuffer); + this.inputBuffer = null; + } + + synchronized(outputBuffers) + { + for(ByteBuffer buf : outputBuffers) + { + recycleBuffer(buf); + } + outputBuffers = null; + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/ChannelReader.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/ChannelReader.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,202 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; +import java.util.logging.Level; +import org.sonews.util.Log; + +/** + * A Thread task listening for OP_READ events from SocketChannels. + * @author Christian Lins + * @since sonews/0.5.0 + */ +class ChannelReader extends AbstractDaemon +{ + + private static ChannelReader instance = new ChannelReader(); + + /** + * @return Active ChannelReader instance. + */ + public static ChannelReader getInstance() + { + return instance; + } + + private Selector selector = null; + + protected ChannelReader() + { + } + + /** + * Sets the selector which is used by this reader to determine the channel + * to read from. + * @param selector + */ + public void setSelector(final Selector selector) + { + this.selector = selector; + } + + /** + * Run loop. Blocks until some data is available in a channel. + */ + @Override + public void run() + { + assert selector != null; + + while(isRunning()) + { + try + { + // select() blocks until some SelectableChannels are ready for + // processing. There is no need to lock the selector as we have only + // one thread per selector. + selector.select(); + + // Get list of selection keys with pending events. + // Note: the selected key set is not thread-safe + SocketChannel channel = null; + NNTPConnection conn = null; + final Set selKeys = selector.selectedKeys(); + SelectionKey selKey = null; + + synchronized (selKeys) + { + Iterator it = selKeys.iterator(); + + // Process the first pending event + while (it.hasNext()) + { + selKey = (SelectionKey) it.next(); + channel = (SocketChannel) selKey.channel(); + conn = Connections.getInstance().get(channel); + + // Because we cannot lock the selKey as that would cause a deadlock + // we lock the connection. To preserve the order of the received + // byte blocks a selection key for a connection that has pending + // read events is skipped. + if (conn == null || conn.tryReadLock()) + { + // Remove from set to indicate that it's being processed + it.remove(); + if (conn != null) + { + break; // End while loop + } + } + else + { + selKey = null; + channel = null; + conn = null; + } + } + } + + // Do not lock the selKeys while processing because this causes + // a deadlock in sun.nio.ch.SelectorImpl.lockAndDoSelect() + if (selKey != null && channel != null && conn != null) + { + processSelectionKey(conn, channel, selKey); + conn.unlockReadLock(); + } + + } + catch(CancelledKeyException ex) + { + Log.get().warning("ChannelReader.run(): " + ex); + Log.get().log(Level.INFO, "", ex); + } + catch(Exception ex) + { + ex.printStackTrace(); + } + + // Eventually wait for a register operation + synchronized (NNTPDaemon.RegisterGate) + { + // Do nothing; FindBugs may warn about an empty synchronized + // statement, but we cannot use a wait()/notify() mechanism here. + // If we used something like RegisterGate.wait() we block here + // until the NNTPDaemon calls notify(). But the daemon only + // calls notify() if itself is NOT blocked in the listening socket. + } + } // while(isRunning()) + } + + private void processSelectionKey(final NNTPConnection connection, + final SocketChannel socketChannel, final SelectionKey selKey) + throws InterruptedException, IOException + { + assert selKey != null; + assert selKey.isReadable(); + + // Some bytes are available for reading + if(selKey.isValid()) + { + // Lock the channel + //synchronized(socketChannel) + { + // Read the data into the appropriate buffer + ByteBuffer buf = connection.getInputBuffer(); + int read = -1; + try + { + read = socketChannel.read(buf); + } + catch(IOException ex) + { + // The connection was probably closed by the remote host + // in a non-clean fashion + Log.get().info("ChannelReader.processSelectionKey(): " + ex); + } + catch(Exception ex) + { + Log.get().warning("ChannelReader.processSelectionKey(): " + ex); + } + + if(read == -1) // End of stream + { + selKey.cancel(); + } + else if(read > 0) // If some data was read + { + ConnectionWorker.addChannel(socketChannel); + } + } + } + else + { + // Should not happen + Log.get().severe("Should not happen: " + selKey.toString()); + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/ChannelWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/ChannelWriter.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,210 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon; + +import org.sonews.util.Log; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.Iterator; + +/** + * A Thread task that processes OP_WRITE events for SocketChannels. + * @author Christian Lins + * @since sonews/0.5.0 + */ +class ChannelWriter extends AbstractDaemon +{ + + private static ChannelWriter instance = new ChannelWriter(); + + /** + * @return Returns the active ChannelWriter instance. + */ + public static ChannelWriter getInstance() + { + return instance; + } + + private Selector selector = null; + + protected ChannelWriter() + { + } + + /** + * @return Selector associated with this instance. + */ + public Selector getSelector() + { + return this.selector; + } + + /** + * Sets the selector that is used by this ChannelWriter. + * @param selector + */ + public void setSelector(final Selector selector) + { + this.selector = selector; + } + + /** + * Run loop. + */ + @Override + public void run() + { + assert selector != null; + + while(isRunning()) + { + try + { + SelectionKey selKey = null; + SocketChannel socketChannel = null; + NNTPConnection connection = null; + + // select() blocks until some SelectableChannels are ready for + // processing. There is no need to synchronize the selector as we + // have only one thread per selector. + selector.select(); // The return value of select can be ignored + + // Get list of selection keys with pending OP_WRITE events. + // The keySET is not thread-safe whereas the keys itself are. + Iterator it = selector.selectedKeys().iterator(); + + while (it.hasNext()) + { + // We remove the first event from the set and store it for + // later processing. + selKey = (SelectionKey) it.next(); + socketChannel = (SocketChannel) selKey.channel(); + connection = Connections.getInstance().get(socketChannel); + + it.remove(); + if (connection != null) + { + break; + } + else + { + selKey = null; + } + } + + if (selKey != null) + { + try + { + // Process the selected key. + // As there is only one OP_WRITE key for a given channel, we need + // not to synchronize this processing to retain the order. + processSelectionKey(connection, socketChannel, selKey); + } + catch (IOException ex) + { + Log.get().warning("Error writing to channel: " + ex); + + // Cancel write events for this channel + selKey.cancel(); + connection.shutdownInput(); + connection.shutdownOutput(); + } + } + + // Eventually wait for a register operation + synchronized(NNTPDaemon.RegisterGate) { /* do nothing */ } + } + catch(CancelledKeyException ex) + { + Log.get().info("ChannelWriter.run(): " + ex); + } + catch(Exception ex) + { + ex.printStackTrace(); + } + } // while(isRunning()) + } + + private void processSelectionKey(final NNTPConnection connection, + final SocketChannel socketChannel, final SelectionKey selKey) + throws InterruptedException, IOException + { + assert connection != null; + assert socketChannel != null; + assert selKey != null; + assert selKey.isWritable(); + + // SocketChannel is ready for writing + if(selKey.isValid()) + { + // Lock the socket channel + synchronized(socketChannel) + { + // Get next output buffer + ByteBuffer buf = connection.getOutputBuffer(); + if(buf == null) + { + // Currently we have nothing to write, so we stop the writeable + // events until we have something to write to the socket channel + //selKey.cancel(); + selKey.interestOps(0); + // Update activity timestamp to prevent too early disconnects + // on slow client connections + connection.setLastActivity(System.currentTimeMillis()); + return; + } + + while(buf != null) // There is data to be send + { + // Write buffer to socket channel; this method does not block + if(socketChannel.write(buf) <= 0) + { + // Perhaps there is data to be written, but the SocketChannel's + // buffer is full, so we stop writing to until the next event. + break; + } + else + { + // Retrieve next buffer if available; method may return the same + // buffer instance if it still have some bytes remaining + buf = connection.getOutputBuffer(); + } + } + } + } + else + { + Log.get().warning("Invalid OP_WRITE key: " + selKey); + + if(socketChannel.socket().isClosed()) + { + connection.shutdownInput(); + connection.shutdownOutput(); + socketChannel.close(); + Log.get().info("Connection closed."); + } + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/CommandSelector.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/CommandSelector.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,141 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.sonews.daemon.command.Command; +import org.sonews.daemon.command.UnsupportedCommand; +import org.sonews.util.Log; +import org.sonews.util.io.Resource; + +/** + * Selects the correct command processing class. + * @author Christian Lins + * @since sonews/1.0 + */ +public class CommandSelector +{ + + private static Map 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; + } + + try + { + addCommandHandler(className); + } + catch(ClassNotFoundException ex) + { + Log.get().warning("Could not load command class: " + ex); + } + catch(InstantiationException ex) + { + Log.get().severe("Could not instantiate command class: " + ex); + } + catch(IllegalAccessException ex) + { + Log.get().severe("Could not access command class: " + ex); + } + } + } + + public static void addCommandHandler(String className) + throws ClassNotFoundException, InstantiationException, IllegalAccessException + { + Class clazz = Class.forName(className); + Command cmd = (Command)clazz.newInstance(); + String[] cmdStrs = cmd.getSupportedCommandStrings(); + for (String cmdStr : cmdStrs) + { + commandClassesMapping.put(cmdStr, clazz); + } + } + + public static Set getCommandNames() + { + return commandClassesMapping.keySet(); + } + + public static CommandSelector getInstance() + { + CommandSelector csel = instances.get(Thread.currentThread()); + if(csel == null) + { + csel = new CommandSelector(); + instances.put(Thread.currentThread(), csel); + } + return csel; + } + + private Map commandMapping = new HashMap(); + private Command unsupportedCmd = new UnsupportedCommand(); + + private CommandSelector() + {} + + public Command get(String commandName) + { + try + { + commandName = commandName.toUpperCase(); + Command cmd = this.commandMapping.get(commandName); + + if(cmd == null) + { + Class clazz = commandClassesMapping.get(commandName); + if(clazz == null) + { + cmd = this.unsupportedCmd; + } + else + { + cmd = (Command)clazz.newInstance(); + this.commandMapping.put(commandName, cmd); + } + } + else if(cmd.isStateful()) + { + cmd = cmd.getClass().newInstance(); + } + + return cmd; + } + catch(Exception ex) + { + ex.printStackTrace(); + return this.unsupportedCmd; + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/ConnectionWorker.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/ConnectionWorker.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,102 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon; + +import org.sonews.util.Log; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.concurrent.ArrayBlockingQueue; + +/** + * Does most of the work: parsing input, talking to client and Database. + * @author Christian Lins + * @since sonews/0.5.0 + */ +class ConnectionWorker extends AbstractDaemon +{ + + // 256 pending events should be enough + private static ArrayBlockingQueue 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(); + + if(channel != null) + { + // Connections.getInstance().get() MAY return null + NNTPConnection conn = Connections.getInstance().get(channel); + + // Try to lock the connection object + if(conn != null && conn.tryReadLock()) + { + ByteBuffer buf = conn.getBuffers().nextInputLine(); + while(buf != null) // Complete line was received + { + final byte[] line = new byte[buf.limit()]; + buf.get(line); + ChannelLineBuffers.recycleBuffer(buf); + + // Here is the actual work done + conn.lineReceived(line); + + // Read next line as we could have already received the next line + buf = conn.getBuffers().nextInputLine(); + } + conn.unlockReadLock(); + } + else + { + addChannel(channel); + } + } + } + catch(InterruptedException ex) + { + Log.get().info("ConnectionWorker interrupted: " + ex); + } + catch(Exception ex) + { + Log.get().severe("Exception in ConnectionWorker: " + ex); + ex.printStackTrace(); + } + } // end while(isRunning()) + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/Connections.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/Connections.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,181 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon; + +import org.sonews.config.Config; +import org.sonews.util.Log; +import org.sonews.util.Stats; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +/** + * Daemon thread collecting all NNTPConnection instances. The thread + * checks periodically if there are stale/timed out connections and + * removes and purges them properly. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public final class Connections extends AbstractDaemon +{ + + private static final Connections instance = new Connections(); + + /** + * @return Active Connections instance. + */ + public static Connections getInstance() + { + return Connections.instance; + } + + private final List 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); + } + } + + int getConnectionCount(String remote) + { + int cnt = 0; + synchronized(this.connections) + { + for(NNTPConnection conn : this.connections) + { + assert conn != null; + assert conn.getSocketChannel() != null; + + Socket socket = conn.getSocketChannel().socket(); + if(socket != null) + { + InetSocketAddress sockAddr = (InetSocketAddress)socket.getRemoteSocketAddress(); + if(sockAddr != null) + { + if(sockAddr.getHostName().equals(remote)) + { + cnt++; + } + } + } // if(socket != null) + } + } + return cnt; + } + + /** + * Run loops. Checks periodically for timed out connections and purged them + * from the lists. + */ + @Override + public void run() + { + while(isRunning()) + { + int timeoutMillis = 1000 * Config.inst().get(Config.TIMEOUT, 180); + + synchronized (this.connections) + { + final ListIterator iter = this.connections.listIterator(); + NNTPConnection conn; + + while (iter.hasNext()) + { + conn = iter.next(); + if((System.currentTimeMillis() - conn.getLastActivity()) > timeoutMillis + && conn.getBuffers().isOutputBufferEmpty()) + { + // A connection timeout has occurred so purge the connection + iter.remove(); + + // Close and remove the channel + SocketChannel channel = conn.getSocketChannel(); + connByChannel.remove(channel); + + try + { + assert channel != null; + assert channel.socket() != null; + + // Close the channel; implicitely cancels all selectionkeys + channel.close(); + Log.get().info("Disconnected: " + channel.socket().getRemoteSocketAddress() + + " (timeout)"); + } + catch(IOException ex) + { + Log.get().warning("Connections.run(): " + ex); + } + + // Recycle the used buffers + conn.getBuffers().recycleBuffers(); + + Stats.getInstance().clientDisconnect(); + } + } + } + + try + { + Thread.sleep(10000); // Sleep ten seconds + } + catch(InterruptedException ex) + { + Log.get().warning("Connections Thread was interrupted: " + ex.getMessage()); + } + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/LineEncoder.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/LineEncoder.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,80 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; + +/** + * Encodes a line to buffers using the correct charset. + * @author Christian Lins + * @since sonews/0.5.0 + */ +class LineEncoder +{ + + private CharBuffer characters; + private Charset charset; + + /** + * Constructs new LineEncoder. + * @param characters + * @param charset + */ + public LineEncoder(CharBuffer characters, Charset charset) + { + this.characters = characters; + this.charset = charset; + } + + /** + * Encodes the characters of this instance to the given ChannelLineBuffers + * using the Charset of this instance. + * @param buffer + * @throws java.nio.channels.ClosedChannelException + */ + public void encode(ChannelLineBuffers buffer) + throws ClosedChannelException + { + CharsetEncoder encoder = charset.newEncoder(); + while (characters.hasRemaining()) + { + ByteBuffer buf = ChannelLineBuffers.newLineBuffer(); + assert buf.position() == 0; + assert buf.capacity() >= 512; + + CoderResult res = encoder.encode(characters, buf, true); + + // Set limit to current position and current position to 0; + // means make ready for read from buffer + buf.flip(); + buffer.addOutputBuffer(buf); + + if (res.isUnderflow()) // All input processed + { + break; + } + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/NNTPConnection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/NNTPConnection.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,428 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Timer; +import java.util.TimerTask; +import org.sonews.daemon.command.Command; +import org.sonews.storage.Article; +import org.sonews.storage.Channel; +import org.sonews.storage.StorageBackendException; +import org.sonews.util.Log; +import org.sonews.util.Stats; + +/** + * For every SocketChannel (so TCP/IP connection) there is an instance of + * this class. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public final class NNTPConnection +{ + + public static final String NEWLINE = "\r\n"; // RFC defines this as newline + public static final String MESSAGE_ID_PATTERN = "<[^>]+>"; + + private static final Timer cancelTimer = new Timer(true); // Thread-safe? True for run as daemon + + /** SocketChannel is generally thread-safe */ + private SocketChannel channel = null; + private Charset charset = Charset.forName("UTF-8"); + private Command command = null; + private Article currentArticle = null; + private Channel currentGroup = null; + private volatile long lastActivity = System.currentTimeMillis(); + private ChannelLineBuffers lineBuffers = new ChannelLineBuffers(); + private int readLock = 0; + private final Object readLockGate = new Object(); + private SelectionKey writeSelKey = null; + + public NNTPConnection(final SocketChannel channel) + throws IOException + { + if(channel == null) + { + throw new IllegalArgumentException("channel is null"); + } + + this.channel = channel; + Stats.getInstance().clientConnect(); + } + + /** + * Tries to get the read lock for this NNTPConnection. This method is Thread- + * safe and returns true of the read lock was successfully set. If the lock + * is still hold by another Thread the method returns false. + */ + boolean tryReadLock() + { + // As synchronizing simple types may cause deadlocks, + // we use a gate object. + synchronized(readLockGate) + { + if(readLock != 0) + { + return false; + } + else + { + readLock = Thread.currentThread().hashCode(); + return true; + } + } + } + + /** + * Releases the read lock in a Thread-safe way. + * @throws IllegalMonitorStateException if a Thread not holding the lock + * tries to release it. + */ + void unlockReadLock() + { + synchronized(readLockGate) + { + if(readLock == Thread.currentThread().hashCode()) + { + readLock = 0; + } + else + { + throw new IllegalMonitorStateException(); + } + } + } + + /** + * @return Current input buffer of this NNTPConnection instance. + */ + public ByteBuffer getInputBuffer() + { + return this.lineBuffers.getInputBuffer(); + } + + /** + * @return Output buffer of this NNTPConnection which has at least one byte + * free storage. + */ + public ByteBuffer getOutputBuffer() + { + return this.lineBuffers.getOutputBuffer(); + } + + /** + * @return ChannelLineBuffers instance associated with this NNTPConnection. + */ + public ChannelLineBuffers getBuffers() + { + return this.lineBuffers; + } + + /** + * @return true if this connection comes from a local remote address. + */ + public boolean isLocalConnection() + { + return ((InetSocketAddress)this.channel.socket().getRemoteSocketAddress()) + .getHostName().equalsIgnoreCase("localhost"); + } + + void setWriteSelectionKey(SelectionKey selKey) + { + this.writeSelKey = selKey; + } + + public void shutdownInput() + { + try + { + // Closes the input line of the channel's socket, so no new data + // will be received and a timeout can be triggered. + this.channel.socket().shutdownInput(); + } + catch(IOException ex) + { + Log.get().warning("Exception in NNTPConnection.shutdownInput(): " + ex); + } + } + + public void shutdownOutput() + { + cancelTimer.schedule(new TimerTask() + { + @Override + public void run() + { + try + { + // Closes the output line of the channel's socket. + channel.socket().shutdownOutput(); + channel.close(); + } + catch(SocketException ex) + { + // Socket was already disconnected + Log.get().info("NNTPConnection.shutdownOutput(): " + ex); + } + catch(Exception ex) + { + Log.get().warning("NNTPConnection.shutdownOutput(): " + ex); + } + } + }, 3000); + } + + public SocketChannel getSocketChannel() + { + return this.channel; + } + + public Article getCurrentArticle() + { + return this.currentArticle; + } + + public Charset getCurrentCharset() + { + return this.charset; + } + + /** + * @return The currently selected communication channel (not SocketChannel) + */ + public Channel getCurrentChannel() + { + return this.currentGroup; + } + + public void setCurrentArticle(final Article article) + { + this.currentArticle = article; + } + + public void setCurrentGroup(final Channel group) + { + this.currentGroup = group; + } + + public long getLastActivity() + { + return this.lastActivity; + } + + /** + * Due to the readLockGate there is no need to synchronize this method. + * @param raw + * @throws IllegalArgumentException if raw is null. + * @throws IllegalStateException if calling thread does not own the readLock. + */ + void lineReceived(byte[] raw) + { + if(raw == null) + { + throw new IllegalArgumentException("raw is null"); + } + + if(readLock == 0 || readLock != Thread.currentThread().hashCode()) + { + throw new IllegalStateException("readLock not properly set"); + } + + this.lastActivity = System.currentTimeMillis(); + + String line = new String(raw, this.charset); + + // There might be a trailing \r, but trim() is a bad idea + // as it removes also leading spaces from long header lines. + if(line.endsWith("\r")) + { + line = line.substring(0, line.length() - 1); + raw = Arrays.copyOf(raw, raw.length - 1); + } + + Log.get().fine("<< " + line); + + if(command == null) + { + command = parseCommandLine(line); + assert command != null; + } + + try + { + // The command object will process the line we just received + try + { + command.processLine(this, line, raw); + } + catch(StorageBackendException ex) + { + Log.get().info("Retry command processing after StorageBackendException"); + + // Try it a second time, so that the backend has time to recover + command.processLine(this, line, raw); + } + } + catch(ClosedChannelException ex0) + { + try + { + Log.get().info("Connection to " + channel.socket().getRemoteSocketAddress() + + " closed: " + ex0); + } + catch(Exception ex0a) + { + ex0a.printStackTrace(); + } + } + catch(Exception ex1) // This will catch a second StorageBackendException + { + try + { + command = null; + ex1.printStackTrace(); + println("500 Internal server error"); + } + catch(Exception ex2) + { + ex2.printStackTrace(); + } + } + + if(command == null || command.hasFinished()) + { + command = null; + charset = Charset.forName("UTF-8"); // Reset to default + } + } + + /** + * This method determines the fitting command processing class. + * @param line + * @return + */ + private Command parseCommandLine(String line) + { + String cmdStr = line.split(" ")[0]; + return CommandSelector.getInstance().get(cmdStr); + } + + /** + * Puts the given line into the output buffer, adds a newline character + * and returns. The method returns immediately and does not block until + * the line was sent. If line is longer than 510 octets it is split up in + * several lines. Each line is terminated by \r\n (NNTPConnection.NEWLINE). + * @param line + */ + public void println(final CharSequence line, final Charset charset) + throws IOException + { + writeToChannel(CharBuffer.wrap(line), charset, line); + writeToChannel(CharBuffer.wrap(NEWLINE), charset, null); + } + + /** + * Writes the given raw lines to the output buffers and finishes with + * a newline character (\r\n). + * @param rawLines + */ + public void println(final byte[] rawLines) + throws IOException + { + this.lineBuffers.addOutputBuffer(ByteBuffer.wrap(rawLines)); + writeToChannel(CharBuffer.wrap(NEWLINE), charset, null); + } + + /** + * Encodes the given CharBuffer using the given Charset to a bunch of + * ByteBuffers (each 512 bytes large) and enqueues them for writing at the + * connected SocketChannel. + * @throws java.io.IOException + */ + private void writeToChannel(CharBuffer characters, final Charset charset, + CharSequence debugLine) + throws IOException + { + if(!charset.canEncode()) + { + Log.get().severe("FATAL: Charset " + charset + " cannot encode!"); + return; + } + + // Write characters to output buffers + LineEncoder lenc = new LineEncoder(characters, charset); + lenc.encode(lineBuffers); + + enableWriteEvents(debugLine); + } + + private void enableWriteEvents(CharSequence debugLine) + { + // Enable OP_WRITE events so that the buffers are processed + try + { + this.writeSelKey.interestOps(SelectionKey.OP_WRITE); + ChannelWriter.getInstance().getSelector().wakeup(); + } + catch(Exception ex) // CancelledKeyException and ChannelCloseException + { + Log.get().warning("NNTPConnection.writeToChannel(): " + ex); + return; + } + + // Update last activity timestamp + this.lastActivity = System.currentTimeMillis(); + if(debugLine != null) + { + Log.get().fine(">> " + debugLine); + } + } + + public void println(final CharSequence line) + throws IOException + { + println(line, charset); + } + + public void print(final String line) + throws IOException + { + writeToChannel(CharBuffer.wrap(line), charset, line); + } + + public void setCurrentCharset(final Charset charset) + { + this.charset = charset; + } + + void setLastActivity(long timestamp) + { + this.lastActivity = timestamp; + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/NNTPDaemon.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/NNTPDaemon.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,197 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon; + +import org.sonews.config.Config; +import org.sonews.Main; +import org.sonews.util.Log; +import java.io.IOException; +import java.net.BindException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; + +/** + * NNTP daemon using SelectableChannels. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public final class NNTPDaemon extends AbstractDaemon +{ + + public static final Object RegisterGate = new Object(); + + private static NNTPDaemon instance = null; + + public static synchronized NNTPDaemon createInstance(int port) + { + if(instance == null) + { + instance = new NNTPDaemon(port); + return instance; + } + else + { + throw new RuntimeException("NNTPDaemon.createInstance() called twice"); + } + } + + private int port; + + private NNTPDaemon(final int port) + { + Log.get().info("Server listening on port " + port); + this.port = port; + } + + @Override + public void run() + { + try + { + // Create a Selector that handles the SocketChannel multiplexing + final Selector readSelector = Selector.open(); + final Selector writeSelector = Selector.open(); + + // Start working threads + final int workerThreads = Runtime.getRuntime().availableProcessors() * 4; + ConnectionWorker[] cworkers = new ConnectionWorker[workerThreads]; + for(int n = 0; n < workerThreads; n++) + { + cworkers[n] = new ConnectionWorker(); + cworkers[n].start(); + } + + ChannelWriter.getInstance().setSelector(writeSelector); + ChannelReader.getInstance().setSelector(readSelector); + ChannelWriter.getInstance().start(); + ChannelReader.getInstance().start(); + + final ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + serverSocketChannel.configureBlocking(true); // Set to blocking mode + + // Configure ServerSocket; bind to socket... + final ServerSocket serverSocket = serverSocketChannel.socket(); + serverSocket.bind(new InetSocketAddress(this.port)); + + while(isRunning()) + { + SocketChannel socketChannel; + + try + { + // As we set the server socket channel to blocking mode the accept() + // method will block. + socketChannel = serverSocketChannel.accept(); + socketChannel.configureBlocking(false); + assert socketChannel.isConnected(); + assert socketChannel.finishConnect(); + } + catch(IOException ex) + { + // Under heavy load an IOException "Too many open files may + // be thrown. It most cases we should slow down the connection + // accepting, to give the worker threads some time to process work. + Log.get().severe("IOException while accepting connection: " + ex.getMessage()); + Log.get().info("Connection accepting sleeping for seconds..."); + Thread.sleep(5000); // 5 seconds + continue; + } + + final NNTPConnection conn; + try + { + conn = new NNTPConnection(socketChannel); + Connections.getInstance().add(conn); + } + catch(IOException ex) + { + Log.get().warning(ex.toString()); + socketChannel.close(); + continue; + } + + try + { + SelectionKey selKeyWrite = + registerSelector(writeSelector, socketChannel, SelectionKey.OP_WRITE); + registerSelector(readSelector, socketChannel, SelectionKey.OP_READ); + + Log.get().info("Connected: " + socketChannel.socket().getRemoteSocketAddress()); + + // Set write selection key and send hello to client + conn.setWriteSelectionKey(selKeyWrite); + conn.println("200 " + Config.inst().get(Config.HOSTNAME, "localhost") + + " " + Main.VERSION + " news server ready - (posting ok)."); + } + catch(CancelledKeyException cke) + { + Log.get().warning("CancelledKeyException " + cke.getMessage() + " was thrown: " + + socketChannel.socket()); + } + catch(ClosedChannelException cce) + { + Log.get().warning("ClosedChannelException " + cce.getMessage() + " was thrown: " + + socketChannel.socket()); + } + } + } + catch(BindException ex) + { + // Could not bind to socket; this is a fatal problem; so perform shutdown + ex.printStackTrace(); + System.exit(1); + } + catch(IOException ex) + { + ex.printStackTrace(); + } + catch(Exception ex) + { + ex.printStackTrace(); + } + } + + public static SelectionKey registerSelector(final Selector selector, + final SocketChannel channel, final int op) + throws CancelledKeyException, ClosedChannelException + { + // Register the selector at the channel, so that it will be notified + // on the socket's events + synchronized(RegisterGate) + { + // Wakeup the currently blocking reader/writer thread; we have locked + // the RegisterGate to prevent the awakened thread to block again + selector.wakeup(); + + // Lock the selector to prevent the waiting worker threads going into + // selector.select() which would block the selector. + synchronized (selector) + { + return channel.register(selector, op, null); + } + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/ArticleCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/ArticleCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,174 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import org.sonews.storage.Article; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.Channel; +import org.sonews.storage.StorageBackendException; + +/** + * Class handling the ARTICLE, BODY and HEAD commands. + * @author Christian Lins + * @author Dennis Schwerdel + * @since n3tpd/0.1 + */ +public class ArticleCommand implements Command +{ + + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"ARTICLE", "BODY", "HEAD"}; + } + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return false; + } + + // TODO: Refactor this method to reduce its complexity! + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException + { + final String[] command = line.split(" "); + + Article article = null; + long artIndex = -1; + if (command.length == 1) + { + article = conn.getCurrentArticle(); + if (article == null) + { + conn.println("420 no current article has been selected"); + return; + } + } + else if (command[1].matches(NNTPConnection.MESSAGE_ID_PATTERN)) + { + // Message-ID + article = Article.getByMessageID(command[1]); + if (article == null) + { + conn.println("430 no such article found"); + return; + } + } + else + { + // Message Number + try + { + Channel currentGroup = conn.getCurrentChannel(); + if(currentGroup == null) + { + conn.println("400 no group selected"); + return; + } + + artIndex = Long.parseLong(command[1]); + article = currentGroup.getArticle(artIndex); + } + catch(NumberFormatException ex) + { + ex.printStackTrace(); + } + catch(StorageBackendException ex) + { + ex.printStackTrace(); + } + + if (article == null) + { + conn.println("423 no such article number in this group"); + return; + } + conn.setCurrentArticle(article); + } + + if(command[0].equalsIgnoreCase("ARTICLE")) + { + conn.println("220 " + artIndex + " " + article.getMessageID() + + " article retrieved - head and body follow"); + conn.println(article.getHeaderSource()); + conn.println(""); + conn.println(article.getBody()); + conn.println("."); + } + else if(command[0].equalsIgnoreCase("BODY")) + { + conn.println("222 " + artIndex + " " + article.getMessageID() + " body"); + conn.println(article.getBody()); + conn.println("."); + } + + /* + * HEAD: This command is mandatory. + * + * Syntax + * HEAD message-id + * HEAD number + * HEAD + * + * Responses + * + * First form (message-id specified) + * 221 0|n message-id Headers follow (multi-line) + * 430 No article with that message-id + * + * Second form (article number specified) + * 221 n message-id Headers follow (multi-line) + * 412 No newsgroup selected + * 423 No article with that number + * + * Third form (current article number used) + * 221 n message-id Headers follow (multi-line) + * 412 No newsgroup selected + * 420 Current article number is invalid + * + * Parameters + * number Requested article number + * n Returned article number + * message-id Article message-id + */ + else if(command[0].equalsIgnoreCase("HEAD")) + { + conn.println("221 " + artIndex + " " + article.getMessageID() + + " Headers follow (multi-line)"); + conn.println(article.getHeaderSource()); + conn.println("."); + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/CapabilitiesCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/CapabilitiesCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,93 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import org.sonews.daemon.NNTPConnection; + +/** + *
+ *  The CAPABILITIES command allows a client to determine the
+ *  capabilities of the server at any given time.
+ *
+ *  This command MAY be issued at any time; the server MUST NOT require
+ *  it to be issued in order to make use of any capability. The response
+ *  generated by this command MAY change during a session because of
+ *  other state information (which, in turn, may be changed by the
+ *  effects of other commands or by external events).  An NNTP client is
+ *  only able to get the current and correct information concerning
+ *  available capabilities at any point during a session by issuing a
+ *  CAPABILITIES command at that point of that session and processing the
+ *  response.
+ * 
+ * @author Christian Lins + * @since sonews/0.5.0 + */ +public class CapabilitiesCommand implements Command +{ + + static final String[] CAPABILITIES = new String[] + { + "VERSION 2", // MUST be the first one; VERSION 2 refers to RFC3977 + "READER", // Server implements commands for reading + "POST", // Server implements POST command + "OVER" // Server implements OVER command + }; + + @Override + public String[] getSupportedCommandStrings() + { + return new String[] {"CAPABILITIES"}; + } + + /** + * First called after one call to processLine(). + * @return + */ + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return false; + } + + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException + { + conn.println("101 Capabilities list:"); + for(String cap : CAPABILITIES) + { + conn.println(cap); + } + conn.println("."); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/Command.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/Command.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,51 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.StorageBackendException; + +/** + * Interface for pluggable NNTP commands handling classes. + * @author Christian Lins + * @since sonews/0.6.0 + */ +public interface Command +{ + + /** + * @return true if this instance can be reused. + */ + boolean hasFinished(); + + /** + * Returns capability string that is implied by this command class. + * MAY return null if the command is required by the NNTP standard. + */ + String impliedCapability(); + + boolean isStateful(); + + String[] getSupportedCommandStrings(); + + void processLine(NNTPConnection conn, String line, byte[] rawLine) + throws IOException, StorageBackendException; + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/GroupCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/GroupCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,102 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.Channel; +import org.sonews.storage.StorageBackendException; + +/** + * Class handling the GROUP command. + *
+ *  Syntax
+ *    GROUP group
+ *
+ *  Responses
+ *    211 number low high group     Group successfully selected
+ *    411                           No such newsgroup
+ *
+ *  Parameters
+ *    group     Name of newsgroup
+ *    number    Estimated number of articles in the group
+ *    low       Reported low water mark
+ *    high      Reported high water mark
+ * 
+ * (from RFC 3977) + * + * @author Christian Lins + * @author Dennis Schwerdel + * @since n3tpd/0.1 + */ +public class GroupCommand implements Command +{ + + @Override + public String[] getSupportedCommandStrings() + { + return new String[]{"GROUP"}; + } + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return true; + } + + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException, StorageBackendException + { + final String[] command = line.split(" "); + + Channel group; + if(command.length >= 2) + { + group = Channel.getByName(command[1]); + if(group == null || group.isDeleted()) + { + conn.println("411 no such news group"); + } + else + { + conn.setCurrentGroup(group); + conn.println("211 " + group.getPostingsCount() + " " + group.getFirstArticleNumber() + + " " + group.getLastArticleNumber() + " " + group.getName() + " group selected"); + } + } + else + { + conn.println("500 no group name given"); + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/HelpCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/HelpCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,100 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import java.util.Set; +import org.sonews.daemon.CommandSelector; +import org.sonews.daemon.NNTPConnection; +import org.sonews.util.io.Resource; + +/** + * This command provides a short summary of the commands that are + * understood by this implementation of the server. The help text will + * be presented as a multi-line data block following the 100 response + * code (taken from RFC). + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class HelpCommand implements Command +{ + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return true; + } + + @Override + public String[] getSupportedCommandStrings() + { + return new String[]{"HELP"}; + } + + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException + { + final String[] command = line.split(" "); + conn.println("100 help text follows"); + + if(line.length() <= 1) + { + final String[] help = Resource + .getAsString("helpers/helptext", true).split("\n"); + for(String hstr : help) + { + conn.println(hstr); + } + + Set commandNames = CommandSelector.getCommandNames(); + for(String cmdName : commandNames) + { + conn.println(cmdName); + } + } + else + { + Command cmd = CommandSelector.getInstance().get(command[1]); + if(cmd instanceof HelpfulCommand) + { + conn.println(((HelpfulCommand)cmd).getHelpString()); + } + else + { + conn.println("No further help information available."); + } + } + + conn.println("."); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/HelpfulCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/HelpfulCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,35 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +/** + * + * @since sonews/1.1 + * @author Christian Lins + */ +public interface HelpfulCommand extends Command +{ + + /** + * @return A short description of this command, that is + * used within the output of the HELP command. + */ + String getHelpString(); + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/ListCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/ListCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,153 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.Channel; +import org.sonews.storage.StorageBackendException; +import org.sonews.util.Log; + +/** + * Class handling the LIST command. + * @author Christian Lins + * @author Dennis Schwerdel + * @since n3tpd/0.1 + */ +public class ListCommand implements Command +{ + + @Override + public String[] getSupportedCommandStrings() + { + return new String[]{"LIST"}; + } + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return false; + } + + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException, StorageBackendException + { + final String[] command = line.split(" "); + + if(command.length >= 2) + { + if(command[1].equalsIgnoreCase("OVERVIEW.FMT")) + { + conn.println("215 information follows"); + conn.println("Subject:\nFrom:\nDate:\nMessage-ID:\nReferences:\nBytes:\nLines:\nXref"); + conn.println("."); + } + else if(command[1].equalsIgnoreCase("NEWSGROUPS")) + { + conn.println("215 information follows"); + final List 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 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/ListGroupCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/ListGroupCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,94 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import java.util.List; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.Channel; +import org.sonews.storage.StorageBackendException; + +/** + * Class handling the LISTGROUP command. + * @author Christian Lins + * @author Dennis Schwerdel + * @since n3tpd/0.1 + */ +public class ListGroupCommand implements Command +{ + + @Override + public String[] getSupportedCommandStrings() + { + return new String[]{"LISTGROUP"}; + } + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return false; + } + + @Override + public void processLine(NNTPConnection conn, final String commandName, byte[] raw) + throws IOException, StorageBackendException + { + final String[] command = commandName.split(" "); + + Channel group; + if(command.length >= 2) + { + group = Channel.getByName(command[1]); + } + else + { + group = conn.getCurrentChannel(); + } + + if (group == null) + { + conn.println("412 no group selected; use GROUP 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("."); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/ModeReaderCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/ModeReaderCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,72 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.StorageBackendException; + +/** + * Class handling the MODE READER command. This command actually does nothing + * but returning a success status code. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class ModeReaderCommand implements Command +{ + + @Override + public String[] getSupportedCommandStrings() + { + return new String[]{"MODE"}; + } + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return false; + } + + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException, StorageBackendException + { + if(line.equalsIgnoreCase("MODE READER")) + { + conn.println("200 hello you can post"); + } + else + { + conn.println("500 I do not know this mode command"); + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/NewGroupsCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/NewGroupsCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,78 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.StorageBackendException; + +/** + * Class handling the NEWGROUPS command. + * @author Christian Lins + * @author Dennis Schwerdel + * @since n3tpd/0.1 + */ +public class NewGroupsCommand implements Command +{ + + @Override + public String[] getSupportedCommandStrings() + { + return new String[]{"NEWGROUPS"}; + } + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return false; + } + + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException, StorageBackendException + { + final String[] command = line.split(" "); + + if(command.length == 3) + { + conn.println("231 list of new newsgroups follows"); + + // Currently we do not store a group's creation date; + // so we return an empty list which is a valid response + conn.println("."); + } + else + { + conn.println("500 invalid command usage"); + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/NextPrevCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/NextPrevCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,116 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.Article; +import org.sonews.storage.Channel; +import org.sonews.storage.StorageBackendException; + +/** + * Class handling the NEXT and LAST command. + * @author Christian Lins + * @author Dennis Schwerdel + * @since n3tpd/0.1 + */ +public class NextPrevCommand implements Command +{ + + @Override + public String[] getSupportedCommandStrings() + { + return new String[]{"NEXT", "PREV"}; + } + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return false; + } + + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException, StorageBackendException + { + final Article currA = conn.getCurrentArticle(); + final Channel currG = conn.getCurrentChannel(); + + if (currA == null) + { + conn.println("420 no current article has been selected"); + return; + } + + if (currG == null) + { + conn.println("412 no newsgroup selected"); + return; + } + + final String[] command = line.split(" "); + + if(command[0].equalsIgnoreCase("NEXT")) + { + selectNewArticle(conn, currA, currG, 1); + } + else if(command[0].equalsIgnoreCase("PREV")) + { + selectNewArticle(conn, currA, currG, -1); + } + else + { + conn.println("500 internal server error"); + } + } + + private void selectNewArticle(NNTPConnection conn, Article article, Channel grp, + final int delta) + throws IOException, StorageBackendException + { + assert article != null; + + article = grp.getArticle(grp.getIndexOf(article) + delta); + + if(article == null) + { + conn.println("421 no next article in this group"); + } + else + { + conn.setCurrentArticle(article); + conn.println("223 " + conn.getCurrentChannel().getIndexOf(article) + + " " + article.getMessageID() + + " article retrieved - request text separately"); + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/OverCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/OverCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,294 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import java.util.List; +import org.sonews.util.Log; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.Article; +import org.sonews.storage.ArticleHead; +import org.sonews.storage.Headers; +import org.sonews.storage.StorageBackendException; +import org.sonews.util.Pair; + +/** + * Class handling the OVER/XOVER command. + * + * Description of the XOVER command: + *
+ * XOVER [range]
+ *
+ * The XOVER command returns information from the overview
+ * database for the article(s) specified.
+ *
+ * The optional range argument may be any of the following:
+ *              an article number
+ *              an article number followed by a dash to indicate
+ *                 all following
+ *              an article number followed by a dash followed by
+ *                 another article number
+ *
+ * If no argument is specified, then information from the
+ * current article is displayed. Successful responses start
+ * with a 224 response followed by the overview information
+ * for all matched messages. Once the output is complete, a
+ * period is sent on a line by itself. If no argument is
+ * specified, the information for the current article is
+ * returned.  A news group must have been selected earlier,
+ * else a 412 error response is returned. If no articles are
+ * in the range specified, a 420 error response is returned
+ * by the server. A 502 response will be returned if the
+ * client only has permission to transfer articles.
+ *
+ * Each line of output will be formatted with the article number,
+ * followed by each of the headers in the overview database or the
+ * article itself (when the data is not available in the overview
+ * database) for that article separated by a tab character.  The
+ * sequence of fields must be in this order: subject, author,
+ * date, message-id, references, byte count, and line count. Other
+ * optional fields may follow line count. Other optional fields may
+ * follow line count. These fields are specified by examining the
+ * response to the LIST OVERVIEW.FMT command. Where no data exists,
+ * a null field must be provided (i.e. the output will have two tab
+ * characters adjacent to each other). Servers should not output
+ * fields for articles that have been removed since the XOVER database
+ * was created.
+ *
+ * The LIST OVERVIEW.FMT command should be implemented if XOVER
+ * is implemented. A client can use LIST OVERVIEW.FMT to determine
+ * what optional fields  and in which order all fields will be
+ * supplied by the XOVER command. 
+ *
+ * Note that any tab and end-of-line characters in any header
+ * data that is returned will be converted to a space character.
+ *
+ * Responses:
+ *
+ *   224 Overview information follows
+ *   412 No news group current selected
+ *   420 No article(s) selected
+ *   502 no permission
+ *
+ * OVER defines additional responses:
+ *
+ *  First form (message-id specified)
+ *    224    Overview information follows (multi-line)
+ *    430    No article with that message-id
+ *
+ *  Second form (range specified)
+ *    224    Overview information follows (multi-line)
+ *    412    No newsgroup selected
+ *    423    No articles in that range
+ *
+ *  Third form (current article number used)
+ *    224    Overview information follows (multi-line)
+ *    412    No newsgroup selected
+ *    420    Current article number is invalid
+ *
+ * 
+ * @author Christian Lins + * @since sonews/0.5.0 + */ +public class OverCommand implements Command +{ + + public static final int MAX_LINES_PER_DBREQUEST = 200; + + @Override + public String[] getSupportedCommandStrings() + { + return new String[]{"OVER", "XOVER"}; + } + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return false; + } + + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException, StorageBackendException + { + if(conn.getCurrentChannel() == null) + { + conn.println("412 no newsgroup selected"); + } + else + { + String[] command = line.split(" "); + + // If no parameter was specified, show information about + // the currently selected article(s) + if(command.length == 1) + { + final Article art = conn.getCurrentArticle(); + if(art == null) + { + conn.println("420 no article(s) selected"); + return; + } + + conn.println(buildOverview(art, -1)); + } + // otherwise print information about the specified range + else + { + long artStart; + long artEnd = conn.getCurrentChannel().getLastArticleNumber(); + String[] nums = command[1].split("-"); + if(nums.length >= 1) + { + try + { + artStart = Integer.parseInt(nums[0]); + } + catch(NumberFormatException e) + { + Log.get().info(e.getMessage()); + artStart = Integer.parseInt(command[1]); + } + } + else + { + artStart = conn.getCurrentChannel().getFirstArticleNumber(); + } + + if(nums.length >=2) + { + try + { + artEnd = Integer.parseInt(nums[1]); + } + catch(NumberFormatException e) + { + e.printStackTrace(); + } + } + + if(artStart > artEnd) + { + if(command[0].equalsIgnoreCase("OVER")) + { + conn.println("423 no articles in that range"); + } + else + { + conn.println("224 (empty) overview information follows:"); + conn.println("."); + } + } + else + { + for(long n = artStart; n <= artEnd; n += MAX_LINES_PER_DBREQUEST) + { + long nEnd = Math.min(n + MAX_LINES_PER_DBREQUEST - 1, artEnd); + List> 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'); + + String subject = art.getHeader(Headers.SUBJECT)[0]; + if("".equals(subject)) + { + subject = ""; + } + overview.append(escapeString(subject)); + overview.append('\t'); + + overview.append(escapeString(art.getHeader(Headers.FROM)[0])); + overview.append('\t'); + overview.append(escapeString(art.getHeader(Headers.DATE)[0])); + overview.append('\t'); + overview.append(escapeString(art.getHeader(Headers.MESSAGE_ID)[0])); + overview.append('\t'); + overview.append(escapeString(art.getHeader(Headers.REFERENCES)[0])); + overview.append('\t'); + + String bytes = art.getHeader(Headers.BYTES)[0]; + if("".equals(bytes)) + { + bytes = "0"; + } + overview.append(escapeString(bytes)); + overview.append('\t'); + + String lines = art.getHeader(Headers.LINES)[0]; + if("".equals(lines)) + { + lines = "0"; + } + overview.append(escapeString(lines)); + overview.append('\t'); + overview.append(escapeString(art.getHeader(Headers.XREF)[0])); + + // Remove trailing tabs if some data is empty + return overview.toString().trim(); + } + + private String escapeString(String str) + { + String nstr = str.replace("\r", ""); + nstr = nstr.replace('\n', ' '); + nstr = nstr.replace('\t', ' '); + return nstr.trim(); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/PostCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/PostCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,332 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.sql.SQLException; +import java.util.Arrays; +import javax.mail.MessagingException; +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetHeaders; +import org.sonews.config.Config; +import org.sonews.util.Log; +import org.sonews.mlgw.Dispatcher; +import org.sonews.storage.Article; +import org.sonews.storage.Group; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.Headers; +import org.sonews.storage.StorageBackendException; +import org.sonews.storage.StorageManager; +import org.sonews.feed.FeedManager; +import org.sonews.util.Stats; + +/** + * Implementation of the POST command. This command requires multiple lines + * from the client, so the handling of asynchronous reading is a little tricky + * to handle. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class PostCommand implements Command +{ + + private final Article article = new Article(); + private int lineCount = 0; + private long bodySize = 0; + private InternetHeaders headers = null; + private long maxBodySize = + Config.inst().get(Config.ARTICLE_MAXSIZE, 128) * 1024L; // Size in bytes + private PostState state = PostState.WaitForLineOne; + private final ByteArrayOutputStream bufBody = new ByteArrayOutputStream(); + private final StringBuilder strHead = new StringBuilder(); + + @Override + public String[] getSupportedCommandStrings() + { + return new String[]{"POST"}; + } + + @Override + public boolean hasFinished() + { + return this.state == PostState.Finished; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return true; + } + + /** + * Process the given line String. line.trim() was called by NNTPConnection. + * @param line + * @throws java.io.IOException + * @throws java.sql.SQLException + */ + @Override // TODO: Refactor this method to reduce complexity! + public void processLine(NNTPConnection conn, String line, byte[] raw) + throws IOException, StorageBackendException + { + switch(state) + { + case WaitForLineOne: + { + if(line.equalsIgnoreCase("POST")) + { + conn.println("340 send article to be posted. End with ."); + state = PostState.ReadingHeaders; + } + else + { + conn.println("500 invalid command usage"); + } + break; + } + case ReadingHeaders: + { + strHead.append(line); + strHead.append(NNTPConnection.NEWLINE); + + if("".equals(line) || ".".equals(line)) + { + // we finally met the blank line + // separating headers from body + + try + { + // Parse the header using the InternetHeader class from JavaMail API + headers = new InternetHeaders( + new ByteArrayInputStream(strHead.toString().trim() + .getBytes(conn.getCurrentCharset()))); + + // add the header entries for the article + article.setHeaders(headers); + } + catch (MessagingException e) + { + e.printStackTrace(); + conn.println("500 posting failed - invalid header"); + state = PostState.Finished; + break; + } + + // Change charset for reading body; + // for multipart messages UTF-8 is returned + //conn.setCurrentCharset(article.getBodyCharset()); + + state = PostState.ReadingBody; + + if(".".equals(line)) + { + // Post an article without body + postArticle(conn, article); + state = PostState.Finished; + } + } + break; + } + case ReadingBody: + { + if(".".equals(line)) + { + // Set some headers needed for Over command + headers.setHeader(Headers.LINES, Integer.toString(lineCount)); + headers.setHeader(Headers.BYTES, Long.toString(bodySize)); + + byte[] body = bufBody.toByteArray(); + if(body.length >= 2) + { + // Remove trailing CRLF + body = Arrays.copyOf(body, body.length - 2); + } + article.setBody(body); // set the article body + + postArticle(conn, article); + state = PostState.Finished; + } + else + { + bodySize += line.length() + 1; + lineCount++; + + // Add line to body buffer + bufBody.write(raw, 0, raw.length); + bufBody.write(NNTPConnection.NEWLINE.getBytes()); + + if(bodySize > maxBodySize) + { + conn.println("500 article is too long"); + state = PostState.Finished; + break; + } + } + break; + } + default: + { + // Should never happen + Log.get().severe("PostCommand::processLine(): already finished..."); + } + } + } + + /** + * Article is a control message and needs special handling. + * @param article + */ + private void controlMessage(NNTPConnection conn, Article article) + throws IOException + { + String[] ctrl = article.getHeader(Headers.CONTROL)[0].split(" "); + if(ctrl.length == 2) // "cancel " + { + try + { + StorageManager.current().delete(ctrl[1]); + + // Move cancel message to "control" group + article.setHeader(Headers.NEWSGROUPS, "control"); + StorageManager.current().addArticle(article); + conn.println("240 article cancelled"); + } + catch(StorageBackendException ex) + { + Log.get().severe(ex.toString()); + conn.println("500 internal server error"); + } + } + else + { + conn.println("441 unknown control header"); + } + } + + private void supersedeMessage(NNTPConnection conn, Article article) + throws IOException + { + try + { + String oldMsg = article.getHeader(Headers.SUPERSEDES)[0]; + StorageManager.current().delete(oldMsg); + StorageManager.current().addArticle(article); + conn.println("240 article replaced"); + } + catch(StorageBackendException ex) + { + Log.get().severe(ex.toString()); + conn.println("500 internal server error"); + } + } + + private void postArticle(NNTPConnection conn, Article article) + throws IOException + { + if(article.getHeader(Headers.CONTROL)[0].length() > 0) + { + controlMessage(conn, article); + } + else if(article.getHeader(Headers.SUPERSEDES)[0].length() > 0) + { + supersedeMessage(conn, article); + } + else // Post the article regularily + { + // Circle check; note that Path can already contain the hostname here + String host = Config.inst().get(Config.HOSTNAME, "localhost"); + if(article.getHeader(Headers.PATH)[0].indexOf(host + "!", 1) > 0) + { + Log.get().info(article.getMessageID() + " skipped for host " + host); + conn.println("441 I know this article already"); + return; + } + + // Try to create the article in the database or post it to + // appropriate mailing list + try + { + boolean success = false; + String[] groupnames = article.getHeader(Headers.NEWSGROUPS)[0].split(","); + for(String groupname : groupnames) + { + Group group = StorageManager.current().getGroup(groupname); + if(group != null && !group.isDeleted()) + { + if(group.isMailingList() && !conn.isLocalConnection()) + { + // Send to mailing list; the Dispatcher writes + // statistics to database + Dispatcher.toList(article, group.getName()); + success = true; + } + else + { + // Store in database + if(!StorageManager.current().isArticleExisting(article.getMessageID())) + { + StorageManager.current().addArticle(article); + + // Log this posting to statistics + Stats.getInstance().mailPosted( + article.getHeader(Headers.NEWSGROUPS)[0]); + } + success = true; + } + } + } // end for + + if(success) + { + conn.println("240 article posted ok"); + FeedManager.queueForPush(article); + } + else + { + conn.println("441 newsgroup not found"); + } + } + catch(AddressException ex) + { + Log.get().warning(ex.getMessage()); + conn.println("441 invalid sender address"); + } + catch(MessagingException ex) + { + // A MessageException is thrown when the sender email address is + // invalid or something is wrong with the SMTP server. + System.err.println(ex.getLocalizedMessage()); + conn.println("441 " + ex.getClass().getCanonicalName() + ": " + ex.getLocalizedMessage()); + } + catch(StorageBackendException ex) + { + ex.printStackTrace(); + conn.println("500 internal server error"); + } + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/PostState.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/PostState.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,29 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +/** + * States of the POST command's finite state machine. + * @author Christian Lins + * @since sonews/0.5.0 + */ +enum PostState +{ + WaitForLineOne, ReadingHeaders, ReadingBody, Finished +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/QuitCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/QuitCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,67 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.StorageBackendException; + +/** + * Implementation of the QUIT command; client wants to shutdown the connection. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class QuitCommand implements Command +{ + + @Override + public String[] getSupportedCommandStrings() + { + return new String[]{"QUIT"}; + } + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return false; + } + + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException, StorageBackendException + { + conn.println("205 cya"); + + conn.shutdownInput(); + conn.shutdownOutput(); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/StatCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/StatCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,114 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import org.sonews.storage.Article; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.StorageBackendException; + +/** + * Implementation of the STAT command. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class StatCommand implements Command +{ + + @Override + public String[] getSupportedCommandStrings() + { + return new String[]{"STAT"}; + } + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return false; + } + + // TODO: Method has various exit points => Refactor! + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException, StorageBackendException + { + final String[] command = line.split(" "); + + Article article = null; + if(command.length == 1) + { + article = conn.getCurrentArticle(); + if(article == null) + { + conn.println("420 no current article has been selected"); + return; + } + } + else if(command[1].matches(NNTPConnection.MESSAGE_ID_PATTERN)) + { + // Message-ID + article = Article.getByMessageID(command[1]); + if (article == null) + { + conn.println("430 no such article found"); + return; + } + } + else + { + // Message Number + try + { + long aid = Long.parseLong(command[1]); + article = conn.getCurrentChannel().getArticle(aid); + } + catch(NumberFormatException ex) + { + ex.printStackTrace(); + } + catch(StorageBackendException ex) + { + ex.printStackTrace(); + } + if (article == null) + { + conn.println("423 no such article number in this group"); + return; + } + conn.setCurrentArticle(article); + } + + conn.println("223 " + conn.getCurrentChannel().getIndexOf(article) + " " + + article.getMessageID() + + " article retrieved - request text separately"); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/UnsupportedCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/UnsupportedCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,67 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import org.sonews.daemon.NNTPConnection; + +/** + * A default "Unsupported Command". Simply returns error code 500 and a + * "command not supported" message. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class UnsupportedCommand implements Command +{ + + /** + * @return Always returns null. + */ + @Override + public String[] getSupportedCommandStrings() + { + return null; + } + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return false; + } + + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException + { + conn.println("500 command not supported"); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/XDaemonCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/XDaemonCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,270 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.List; +import org.sonews.config.Config; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.StorageBackendException; +import org.sonews.storage.StorageManager; +import org.sonews.feed.FeedManager; +import org.sonews.feed.Subscription; +import org.sonews.storage.Channel; +import org.sonews.storage.Group; +import org.sonews.util.Stats; + +/** + * The XDAEMON command allows a client to get/set properties of the + * running server daemon. Only locally connected clients are allowed to + * use this command. + * The restriction to localhost connection can be suppressed by overriding + * the sonews.xdaemon.host bootstrap config property. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class XDaemonCommand implements Command +{ + + @Override + public String[] getSupportedCommandStrings() + { + return new String[]{"XDAEMON"}; + } + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return false; + } + + private void channelAdd(String[] commands, NNTPConnection conn) + throws IOException, StorageBackendException + { + String groupName = commands[2]; + if(StorageManager.current().isGroupExisting(groupName)) + { + conn.println("400 group " + groupName + " already existing!"); + } + else + { + StorageManager.current().addGroup(groupName, Integer.parseInt(commands[3])); + conn.println("200 group " + groupName + " created"); + } + } + + // TODO: Refactor this method to reduce complexity! + @Override + public void processLine(NNTPConnection conn, String line, byte[] raw) + throws IOException, StorageBackendException + { + InetSocketAddress addr = (InetSocketAddress)conn.getSocketChannel().socket() + .getRemoteSocketAddress(); + if(addr.getHostName().equals( + Config.inst().get(Config.XDAEMON_HOST, "localhost"))) + { + String[] commands = line.split(" ", 4); + if(commands.length == 3 && commands[1].equalsIgnoreCase("LIST")) + { + if(commands[2].equalsIgnoreCase("CONFIGKEYS")) + { + conn.println("100 list of available config keys follows"); + for(String key : Config.AVAILABLE_KEYS) + { + conn.println(key); + } + conn.println("."); + } + else if(commands[2].equalsIgnoreCase("PEERINGRULES")) + { + List 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"); + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/XPatCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/XPatCommand.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,170 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.daemon.command; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; +import java.util.regex.PatternSyntaxException; +import org.sonews.daemon.NNTPConnection; +import org.sonews.storage.StorageBackendException; +import org.sonews.storage.StorageManager; +import org.sonews.util.Pair; + +/** + *
+ *   XPAT header range| pat [pat...]
+ *
+ *   The XPAT command is used to retrieve specific headers from
+ *   specific articles, based on pattern matching on the contents of
+ *   the header. This command was first available in INN.
+ *
+ *   The required header parameter is the name of a header line (e.g.
+ *   "subject") in a news group article. See RFC-1036 for a list
+ *   of valid header lines. The required range argument may be
+ *   any of the following:
+ *               an article number
+ *               an article number followed by a dash to indicate
+ *                  all following
+ *               an article number followed by a dash followed by
+ *                  another article number
+ *
+ *   The required message-id argument indicates a specific
+ *   article. The range and message-id arguments are mutually
+ *   exclusive. At least one pattern in wildmat must be specified
+ *   as well. If there are additional arguments the are joined
+ *   together separated by a single space to form one complete
+ *   pattern. Successful responses start with a 221 response
+ *   followed by a the headers from all messages in which the
+ *   pattern matched the contents of the specified header line. This
+ *   includes an empty list. Once the output is complete, a period
+ *   is sent on a line by itself. If the optional argument is a
+ *   message-id and no such article exists, the 430 error response
+ *   is returned. A 502 response will be returned if the client only
+ *   has permission to transfer articles.
+ *
+ *   Responses
+ *
+ *       221 Header follows
+ *       430 no such article
+ *       502 no permission
+ *
+ *   Response Data:
+ *
+ *       art_nr fitting_header_value
+ * 
+ * 
+ * [Source:"draft-ietf-nntp-imp-02.txt"] [Copyright: 1998 S. Barber] + * + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class XPatCommand implements Command +{ + + @Override + public String[] getSupportedCommandStrings() + { + return new String[]{"XPAT"}; + } + + @Override + public boolean hasFinished() + { + return true; + } + + @Override + public String impliedCapability() + { + return null; + } + + @Override + public boolean isStateful() + { + return false; + } + + @Override + public void processLine(NNTPConnection conn, final String line, byte[] raw) + throws IOException, StorageBackendException + { + if(conn.getCurrentChannel() == null) + { + conn.println("430 no group selected"); + return; + } + + String[] command = line.split("\\p{Space}+"); + + // There may be multiple patterns and Thunderbird produces + // additional spaces between range and pattern + if(command.length >= 4) + { + String header = command[1].toLowerCase(Locale.US); + String range = command[2]; + String pattern = command[3]; + + long start = -1; + long end = -1; + if(range.contains("-")) + { + String[] rsplit = range.split("-", 2); + start = Long.parseLong(rsplit[0]); + if(rsplit[1].length() > 0) + { + end = Long.parseLong(rsplit[1]); + } + } + else // TODO: Handle Message-IDs + { + start = Long.parseLong(range); + } + + try + { + List> 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 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/command/package.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/command/package.html Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,1 @@ +Contains a class for every NNTP command. \ No newline at end of file diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/daemon/package.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/daemon/package.html Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,1 @@ +Contains basic classes of the daemon. \ No newline at end of file diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/feed/FeedManager.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/feed/FeedManager.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,54 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.feed; + +import org.sonews.storage.Article; + +/** + * Controlls push and pull feeder. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public final class FeedManager +{ + + public static final int TYPE_PULL = 0; + public static final int TYPE_PUSH = 1; + + private static PullFeeder pullFeeder = new PullFeeder(); + private static PushFeeder pushFeeder = new PushFeeder(); + + /** + * Reads the peer subscriptions from database and starts the appropriate + * PullFeeder or PushFeeder. + */ + public static synchronized void startFeeding() + { + pullFeeder.start(); + pushFeeder.start(); + } + + public static void queueForPush(Article article) + { + pushFeeder.queueForPush(article); + } + + private FeedManager() {} + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/feed/PullFeeder.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/feed/PullFeeder.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,276 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.feed; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import org.sonews.config.Config; +import org.sonews.daemon.AbstractDaemon; +import org.sonews.util.Log; +import org.sonews.storage.StorageBackendException; +import org.sonews.storage.StorageManager; +import org.sonews.util.Stats; +import org.sonews.util.io.ArticleReader; +import org.sonews.util.io.ArticleWriter; + +/** + * The PullFeeder class regularily checks another Newsserver for new + * messages. + * @author Christian Lins + * @since sonews/0.5.0 + */ +class PullFeeder extends AbstractDaemon +{ + + private Map 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())); + + 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 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/feed/PushFeeder.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/feed/PushFeeder.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,118 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.feed; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import org.sonews.daemon.AbstractDaemon; +import org.sonews.storage.Article; +import org.sonews.storage.Headers; +import org.sonews.storage.StorageBackendException; +import org.sonews.storage.StorageManager; +import org.sonews.util.Log; +import org.sonews.util.io.ArticleWriter; + +/** + * Pushes new articles to remote newsservers. This feeder sleeps until a new + * message is posted to the sonews instance. + * @author Christian Lins + * @since sonews/0.5.0 + */ +class PushFeeder extends AbstractDaemon +{ + + private ConcurrentLinkedQueue
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; + } + + try + { + for(String group : groups) + { + if(sub.getGroup().equals(group)) + { + // Delete headers that may cause problems + article.removeHeader(Headers.NNTP_POSTING_DATE); + article.removeHeader(Headers.NNTP_POSTING_HOST); + article.removeHeader(Headers.X_COMPLAINTS_TO); + article.removeHeader(Headers.X_TRACE); + article.removeHeader(Headers.XREF); + + // POST the message to remote server + ArticleWriter awriter = new ArticleWriter(sub.getHost(), sub.getPort()); + awriter.writeArticle(article); + break; + } + } + } + catch(IOException ex) + { + Log.get().warning(ex.toString()); + } + } + } + catch(StorageBackendException ex) + { + Log.get().severe(ex.toString()); + } + catch(InterruptedException ex) + { + Log.get().warning("PushFeeder interrupted: " + ex); + } + } + } + + public void queueForPush(Article article) + { + this.articleQueue.add(article); + synchronized(this) + { + this.notifyAll(); + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/feed/Subscription.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/feed/Subscription.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,84 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.feed; + +/** + * For every group that is synchronized with or from a remote newsserver + * a Subscription instance exists. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class Subscription +{ + + private String host; + private int port; + private int feedtype; + private String group; + + public Subscription(String host, int port, int feedtype, String group) + { + this.host = host; + this.port = port; + this.feedtype = feedtype; + this.group = group; + } + + @Override + public boolean equals(Object obj) + { + if(obj instanceof Subscription) + { + Subscription sub = (Subscription)obj; + return sub.host.equals(host) && sub.group.equals(group) + && sub.port == port && sub.feedtype == feedtype; + } + else + { + return false; + } + } + + @Override + public int hashCode() + { + return host.hashCode() + port + feedtype + group.hashCode(); + } + + public int getFeedtype() + { + return feedtype; + } + + public String getGroup() + { + return group; + } + + public String getHost() + { + return host; + } + + public int getPort() + { + return port; + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/feed/package.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/feed/package.html Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,2 @@ +Contains classes for the peering functionality, e.g. pulling and pushing +mails from and to remote newsservers. \ No newline at end of file diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/mlgw/Dispatcher.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/mlgw/Dispatcher.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,301 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.mlgw; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.mail.Address; +import javax.mail.Authenticator; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.PasswordAuthentication; +import javax.mail.internet.InternetAddress; +import org.sonews.config.Config; +import org.sonews.storage.Article; +import org.sonews.storage.Group; +import org.sonews.storage.Headers; +import org.sonews.storage.StorageBackendException; +import org.sonews.storage.StorageManager; +import org.sonews.util.Log; +import org.sonews.util.Stats; + +/** + * Dispatches messages from mailing list to newsserver or vice versa. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class Dispatcher +{ + + static class PasswordAuthenticator extends Authenticator + { + + @Override + public PasswordAuthentication getPasswordAuthentication() + { + final String username = + Config.inst().get(Config.MLSEND_USER, "user"); + final String password = + Config.inst().get(Config.MLSEND_PASSWORD, "mysecret"); + + return new PasswordAuthentication(username, password); + } + + } + + /** + * Chunks out the email address of the full List-Post header field. + * @param listPostValue + * @return The matching email address or null + */ + private static String chunkListPost(String listPostValue) + { + // listPostValue is of form "" + Pattern mailPattern = Pattern.compile("(\\w+[-|.])*\\w+@(\\w+.)+\\w+"); + Matcher mailMatcher = mailPattern.matcher(listPostValue); + if(mailMatcher.find()) + { + return listPostValue.substring(mailMatcher.start(), mailMatcher.end()); + } + else + { + return null; + } + } + + /** + * This method inspects the header of the given message, trying + * to find the most appropriate recipient. + * @param msg + * @param fallback If this is false only List-Post and X-List-Post headers + * are examined. + * @return null or fitting group name for the given message. + */ + private static List getGroupFor(final Message msg, final boolean fallback) + throws MessagingException, StorageBackendException + { + List groups = null; + + // Is there a List-Post header? + String[] listPost = msg.getHeader(Headers.LIST_POST); + InternetAddress listPostAddr; + + if(listPost == null || listPost.length == 0 || "".equals(listPost[0])) + { + // Is there a X-List-Post header? + listPost = msg.getHeader(Headers.X_LIST_POST); + } + + if(listPost != null && listPost.length > 0 + && !"".equals(listPost[0]) && chunkListPost(listPost[0]) != null) + { + // listPost[0] is of form "" + 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) + { + try + { + // Create new Article object + Article article = new Article(msg); + boolean posted = false; + + // Check if this mail is already existing the storage + boolean updateReq = + StorageManager.current().isArticleExisting(article.getMessageID()); + + List 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()); + 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; + } + } + + /** + * 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); + + if(rcptAddresses == null || rcptAddresses.size() == 0) + { + Log.get().warning("No ML-address for " + group + " found."); + return; + } + + for(String rcptAddress : rcptAddresses) + { + // Compose message and send it via given SMTP-Host + String smtpHost = Config.inst().get(Config.MLSEND_HOST, "localhost"); + int smtpPort = Config.inst().get(Config.MLSEND_PORT, 25); + String smtpUser = Config.inst().get(Config.MLSEND_USER, "user"); + String smtpPw = Config.inst().get(Config.MLSEND_PASSWORD, "mysecret"); + String smtpFrom = Config.inst().get( + Config.MLSEND_ADDRESS, article.getHeader(Headers.FROM)[0]); + + // TODO: Make Article cloneable() + article.getMessageID(); // Make sure an ID is existing + article.removeHeader(Headers.NEWSGROUPS); + article.removeHeader(Headers.PATH); + article.removeHeader(Headers.LINES); + article.removeHeader(Headers.BYTES); + + article.setHeader("To", rcptAddress); + //article.setHeader("Reply-To", listAddress); + + if (Config.inst().get(Config.MLSEND_RW_SENDER, false)) + { + rewriteSenderAddress(article); // Set the SENDER address + } + + SMTPTransport smtpTransport = new SMTPTransport(smtpHost, smtpPort); + smtpTransport.send(article, smtpFrom, rcptAddress); + smtpTransport.close(); + + Stats.getInstance().mailGatewayed(group); + Log.get().info("MLGateway: Mail " + article.getHeader("Subject")[0] + + " was delivered to " + rcptAddress + "."); + } + } + + /** + * Sets the SENDER header of the given MimeMessage. This might be necessary + * for moderated groups that does not allow the "normal" FROM sender. + * @param msg + * @throws javax.mail.MessagingException + */ + private static void rewriteSenderAddress(Article msg) + throws MessagingException + { + String mlAddress = Config.inst().get(Config.MLSEND_ADDRESS, null); + + if(mlAddress != null) + { + msg.setHeader(Headers.SENDER, mlAddress); + } + else + { + throw new MessagingException("Cannot rewrite SENDER header!"); + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/mlgw/MailPoller.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/mlgw/MailPoller.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,151 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.mlgw; + +import java.util.Properties; +import javax.mail.AuthenticationFailedException; +import javax.mail.Authenticator; +import javax.mail.Flags.Flag; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.NoSuchProviderException; +import javax.mail.PasswordAuthentication; +import javax.mail.Session; +import javax.mail.Store; +import org.sonews.config.Config; +import org.sonews.daemon.AbstractDaemon; +import org.sonews.util.Log; +import org.sonews.util.Stats; + +/** + * Daemon polling for new mails in a POP3 account to be delivered to newsgroups. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class MailPoller extends AbstractDaemon +{ + + static class PasswordAuthenticator extends Authenticator + { + + @Override + public PasswordAuthentication getPasswordAuthentication() + { + final String username = + Config.inst().get(Config.MLPOLL_USER, "user"); + final String password = + Config.inst().get(Config.MLPOLL_PASSWORD, "mysecret"); + + return new PasswordAuthentication(username, password); + } + + } + + @Override + public void run() + { + Log.get().info("Starting Mailinglist Poller..."); + int errors = 0; + while(isRunning()) + { + try + { + // Wait some time between runs. At the beginning has advantages, + // because the wait is not skipped if an exception occurs. + Thread.sleep(60000 * (errors + 1)); // one minute * errors + + final String host = + Config.inst().get(Config.MLPOLL_HOST, "samplehost"); + final String username = + Config.inst().get(Config.MLPOLL_USER, "user"); + final String password = + Config.inst().get(Config.MLPOLL_PASSWORD, "mysecret"); + + Stats.getInstance().mlgwRunStart(); + + // Create empty properties + Properties props = System.getProperties(); + props.put("mail.pop3.host", host); + props.put("mail.mime.address.strict", "false"); + + // Get session + Session session = Session.getInstance(props); + + // Get the store + Store store = session.getStore("pop3"); + store.connect(host, 110, username, password); + + // Get folder + Folder folder = store.getFolder("INBOX"); + folder.open(Folder.READ_WRITE); + + // Get directory + Message[] messages = folder.getMessages(); + + // Dispatch messages and delete it afterwards on the inbox + for(Message message : messages) + { + if(Dispatcher.toGroup(message) + || Config.inst().get(Config.MLPOLL_DELETEUNKNOWN, false)) + { + // Delete the message + message.setFlag(Flag.DELETED, true); + } + } + + // Close connection + folder.close(true); // true to expunge deleted messages + store.close(); + errors = 0; + + Stats.getInstance().mlgwRunEnd(); + } + catch(NoSuchProviderException ex) + { + Log.get().severe(ex.toString()); + shutdown(); + } + catch(AuthenticationFailedException ex) + { + // AuthentificationFailedException may be thrown if credentials are + // bad or if the Mailbox is in use (locked). + ex.printStackTrace(); + errors = errors < 5 ? errors + 1 : errors; + } + catch(InterruptedException ex) + { + System.out.println("sonews: " + this + " returns: " + ex); + return; + } + catch(MessagingException ex) + { + ex.printStackTrace(); + errors = errors < 5 ? errors + 1 : errors; + } + catch(Exception ex) + { + ex.printStackTrace(); + errors = errors < 5 ? errors + 1 : errors; + } + } + Log.get().severe("MailPoller exited."); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/mlgw/SMTPTransport.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/mlgw/SMTPTransport.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,133 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.mlgw; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Socket; +import java.net.UnknownHostException; +import org.sonews.config.Config; +import org.sonews.storage.Article; +import org.sonews.util.io.ArticleInputStream; + +/** + * Connects to a SMTP server and sends a given Article to it. + * @author Christian Lins + * @since sonews/1.0 + */ +class SMTPTransport +{ + + protected BufferedReader in; + protected BufferedOutputStream out; + protected Socket socket; + + public SMTPTransport(String host, int port) + throws IOException, UnknownHostException + { + socket = new Socket(host, port); + this.in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + this.out = new BufferedOutputStream(socket.getOutputStream()); + + // Read helo from server + String line = this.in.readLine(); + if(line == null || !line.startsWith("220 ")) + { + throw new IOException("Invalid helo from server: " + line); + } + + // Send HELO to server + this.out.write( + ("HELO " + Config.inst().get(Config.HOSTNAME, "localhost") + "\r\n").getBytes("UTF-8")); + this.out.flush(); + line = this.in.readLine(); + if(line == null || !line.startsWith("250 ")) + { + throw new IOException("Unexpected reply: " + line); + } + } + + public SMTPTransport(String host) + throws IOException + { + this(host, 25); + } + + public void close() + throws IOException + { + this.out.write("QUIT".getBytes("UTF-8")); + this.out.flush(); + this.in.readLine(); + + this.socket.close(); + } + + public void send(Article article, String mailFrom, String rcptTo) + throws IOException + { + assert(article != null); + assert(mailFrom != null); + assert(rcptTo != null); + + this.out.write(("MAIL FROM: " + mailFrom).getBytes("UTF-8")); + this.out.flush(); + String line = this.in.readLine(); + if(line == null || !line.startsWith("250 ")) + { + throw new IOException("Unexpected reply: " + line); + } + + this.out.write(("RCPT TO: " + rcptTo).getBytes("UTF-8")); + this.out.flush(); + line = this.in.readLine(); + if(line == null || !line.startsWith("250 ")) + { + throw new IOException("Unexpected reply: " + line); + } + + this.out.write("DATA".getBytes("UTF-8")); + this.out.flush(); + line = this.in.readLine(); + if(line == null || !line.startsWith("354 ")) + { + throw new IOException("Unexpected reply: " + line); + } + + ArticleInputStream artStream = new ArticleInputStream(article); + for(int b = artStream.read(); b >= 0; b = artStream.read()) + { + this.out.write(b); + } + + // Flush the binary stream; important because otherwise the output + // will be mixed with the PrintWriter. + this.out.flush(); + this.out.write("\r\n.\r\n".getBytes("UTF-8")); + this.out.flush(); + line = this.in.readLine(); + if(line == null || !line.startsWith("250 ")) + { + throw new IOException("Unexpected reply: " + line); + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/mlgw/package.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/mlgw/package.html Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,1 @@ +Contains classes of the Mailinglist Gateway. \ No newline at end of file diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/plugin/Plugin.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/plugin/Plugin.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,42 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.plugin; + +/** + * A generic Plugin for sonews. Implementing classes do not really add new + * functionality to sonews but can use this interface as convenient procedure + * for installing functionality plugins, e.g. Command-Plugins or Storage-Plugins. + * @author Christian Lins + * @since sonews/1.1 + */ +public interface Plugin +{ + + /** + * Called when the Plugin is loaded by sonews. This method can be used + * by implementing classes to install additional or required plugins. + */ + void load(); + + /** + * Called when the Plugin is unloaded by sonews. + */ + void unload(); + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/Article.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/storage/Article.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,253 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.storage; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.UUID; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import javax.mail.Header; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.InternetHeaders; +import org.sonews.config.Config; + +/** + * Represents a newsgroup article. + * @author Christian Lins + * @author Denis Schwerdel + * @since n3tpd/0.1 + */ +public class Article extends ArticleHead +{ + + /** + * Loads the Article identified by the given ID from the JDBCDatabase. + * @param messageID + * @return null if Article is not found or if an error occurred. + */ + public static Article getByMessageID(final String messageID) + { + try + { + return StorageManager.current().getArticle(messageID); + } + catch(StorageBackendException ex) + { + ex.printStackTrace(); + return null; + } + } + + private byte[] body = new byte[0]; + + /** + * Default constructor. + */ + public Article() + { + } + + /** + * Creates a new Article object using the date from the given + * raw data. + */ + public Article(String headers, byte[] body) + { + try + { + this.body = body; + + // Parse the header + this.headers = new InternetHeaders( + new ByteArrayInputStream(headers.getBytes())); + + this.headerSrc = headers; + } + catch(MessagingException ex) + { + ex.printStackTrace(); + } + } + + /** + * Creates an Article instance using the data from the javax.mail.Message + * object. This constructor is called by the Mailinglist gateway. + * @see javax.mail.Message + * @param msg + * @throws IOException + * @throws MessagingException + */ + public Article(final Message msg) + throws IOException, MessagingException + { + this.headers = new InternetHeaders(); + + for(Enumeration e = msg.getAllHeaders() ; e.hasMoreElements();) + { + final Header header = (Header)e.nextElement(); + this.headers.addHeader(header.getName(), header.getValue()); + } + + // Reads the raw byte body using Message.writeTo(OutputStream out) + this.body = readContent(msg); + + // Validate headers + validateHeaders(); + } + + /** + * Reads from the given Message into a byte array. + * @param in + * @return + * @throws IOException + */ + private byte[] readContent(Message in) + throws IOException, MessagingException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + in.writeTo(out); + return out.toByteArray(); + } + + /** + * Removes the header identified by the given key. + * @param headerKey + */ + public void removeHeader(final String headerKey) + { + this.headers.removeHeader(headerKey); + this.headerSrc = null; + } + + /** + * Generates a message id for this article and sets it into + * the header object. You have to update the JDBCDatabase manually to make this + * change persistent. + * Note: a Message-ID should never be changed and only generated once. + */ + private String generateMessageID() + { + String randomString; + MessageDigest md5; + try + { + md5 = MessageDigest.getInstance("MD5"); + md5.reset(); + md5.update(getBody()); + md5.update(getHeader(Headers.SUBJECT)[0].getBytes()); + md5.update(getHeader(Headers.FROM)[0].getBytes()); + byte[] result = md5.digest(); + StringBuffer hexString = new StringBuffer(); + for (int i = 0; i < result.length; i++) + { + hexString.append(Integer.toHexString(0xFF & result[i])); + } + randomString = hexString.toString(); + } + catch (NoSuchAlgorithmException e) + { + e.printStackTrace(); + randomString = UUID.randomUUID().toString(); + } + String msgID = "<" + randomString + "@" + + Config.inst().get(Config.HOSTNAME, "localhost") + ">"; + + this.headers.setHeader(Headers.MESSAGE_ID, msgID); + + return msgID; + } + + /** + * Returns the body string. + */ + public byte[] getBody() + { + return body; + } + + /** + * @return Numerical IDs of the newsgroups this Article belongs to. + */ + public List 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 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/ArticleHead.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/storage/ArticleHead.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,161 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.storage; + +import java.io.ByteArrayInputStream; +import java.util.Enumeration; +import javax.mail.Header; +import javax.mail.MessagingException; +import javax.mail.internet.InternetHeaders; +import javax.mail.internet.MimeUtility; +import org.sonews.config.Config; + +/** + * An article with no body only headers. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class ArticleHead +{ + + protected InternetHeaders headers = null; + protected String headerSrc = null; + + protected ArticleHead() + { + } + + public ArticleHead(String headers) + { + try + { + // Parse the header + this.headers = new InternetHeaders( + new ByteArrayInputStream(headers.getBytes())); + } + catch(MessagingException ex) + { + ex.printStackTrace(); + } + } + + /** + * Returns the header field with given name. + * @param name Name of the header field(s). + * @param returnNull If set to true, this method will return null instead + * of an empty array if there is no header field found. + * @return Header values or empty string. + */ + public String[] getHeader(String name, boolean returnNull) + { + String[] ret = this.headers.getHeader(name); + if(ret == null && !returnNull) + { + ret = new String[]{""}; + } + return ret; + } + + public String[] getHeader(String name) + { + return getHeader(name, false); + } + + /** + * Sets the header value identified through the header name. + * @param name + * @param value + */ + public void setHeader(String name, String value) + { + this.headers.setHeader(name, value); + this.headerSrc = null; + } + + public Enumeration getAllHeaders() + { + return this.headers.getAllHeaders(); + } + + /** + * @return Header source code of this Article. + */ + public String getHeaderSource() + { + if(this.headerSrc != null) + { + return this.headerSrc; + } + + StringBuffer buf = new StringBuffer(); + + for(Enumeration en = this.headers.getAllHeaders(); en.hasMoreElements();) + { + Header entry = (Header)en.nextElement(); + + String value = entry.getValue().replaceAll("[\r\n]", " "); + buf.append(entry.getName()); + buf.append(": "); + buf.append(MimeUtility.fold(entry.getName().length() + 2, value)); + + if(en.hasMoreElements()) + { + buf.append("\r\n"); + } + } + + this.headerSrc = buf.toString(); + return this.headerSrc; + } + + /** + * Sets the headers of this Article. If headers contain no + * Message-Id a new one is created. + * @param headers + */ + public void setHeaders(InternetHeaders headers) + { + this.headers = headers; + this.headerSrc = null; + validateHeaders(); + } + + /** + * Checks some headers for their validity and generates an + * appropriate Path-header for this host if not yet existing. + * This method is called by some Article constructors and the + * method setHeaders(). + * @return true if something on the headers was changed. + */ + protected void validateHeaders() + { + // Check for valid Path-header + final String path = getHeader(Headers.PATH)[0]; + final String host = Config.inst().get(Config.HOSTNAME, "localhost"); + if(!path.startsWith(host)) + { + StringBuffer pathBuf = new StringBuffer(); + pathBuf.append(host); + pathBuf.append('!'); + pathBuf.append(path); + this.headers.setHeader(Headers.PATH, pathBuf.toString()); + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/Channel.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/storage/Channel.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,111 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.storage; + +import java.util.ArrayList; +import java.util.List; +import org.sonews.util.Pair; + +/** + * A logical communication Channel is the a generic structural element for sets + * of messages; e.g. a Newsgroup for a set of Articles. + * A Channel can either be a real set of messages or an aggregated set of + * several subsets. + * @author Christian Lins + * @since sonews/1.0 + */ +public abstract class Channel +{ + + /** + * If this flag is set the Group is no real newsgroup but a mailing list + * mirror. In that case every posting and receiving mails must go through + * the mailing list gateway. + */ + public static final int MAILINGLIST = 0x1; + + /** + * If this flag is set the Group is marked as readonly and the posting + * is prohibited. This can be useful for groups that are synced only in + * one direction. + */ + public static final int READONLY = 0x2; + + /** + * If this flag is set the Group is marked as deleted and must not occur + * in any output. The deletion is done lazily by a low priority daemon. + */ + public static final int DELETED = 0x80; + + public static List getAll() + { + List all = new ArrayList(); + + /*List agroups = AggregatedGroup.getAll(); + if(agroups != null) + { + all.addAll(agroups); + }*/ + + List groups = Group.getAll(); + if(groups != null) + { + all.addAll(groups); + } + + return all; + } + + public static Channel getByName(String name) + throws StorageBackendException + { + return StorageManager.current().getGroup(name); + } + + public abstract Article getArticle(long idx) + throws StorageBackendException; + + public abstract List> getArticleHeads( + final long first, final long last) + throws StorageBackendException; + + public abstract List getArticleNumbers() + throws StorageBackendException; + + public abstract long getFirstArticleNumber() + throws StorageBackendException; + + public abstract long getIndexOf(Article art) + throws StorageBackendException; + + public abstract long getInternalID(); + + public abstract long getLastArticleNumber() + throws StorageBackendException; + + public abstract String getName(); + + public abstract long getPostingsCount() + throws StorageBackendException; + + public abstract boolean isDeleted(); + + public abstract boolean isWriteable(); + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/Group.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/storage/Group.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,184 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.storage; + +import java.sql.SQLException; +import java.util.List; +import org.sonews.util.Log; +import org.sonews.util.Pair; + +/** + * Represents a logical Group within this newsserver. + * @author Christian Lins + * @since sonews/0.5.0 + */ +// TODO: This class should not be public! +public class Group extends Channel +{ + + private long id = 0; + private int flags = -1; + private String name = null; + + /** + * @return List of all groups this server handles. + */ + public static List getAll() + { + try + { + return StorageManager.current().getGroups(); + } + catch(StorageBackendException ex) + { + Log.get().severe(ex.getMessage()); + return null; + } + } + + /** + * @param name + * @param id + */ + public Group(final String name, final long id, final int flags) + { + this.id = id; + this.flags = flags; + this.name = name; + } + + @Override + public boolean equals(Object obj) + { + if(obj instanceof Group) + { + return ((Group)obj).id == this.id; + } + else + { + return false; + } + } + + public Article getArticle(long idx) + throws StorageBackendException + { + return StorageManager.current().getArticle(idx, this.id); + } + + public List> 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); + } + + public long getFirstArticleNumber() + throws StorageBackendException + { + return StorageManager.current().getFirstArticleNumber(this); + } + + public int getFlags() + { + return this.flags; + } + + public long getIndexOf(Article art) + throws StorageBackendException + { + return StorageManager.current().getArticleIndex(art, this); + } + + /** + * Returns the group id. + */ + public long getInternalID() + { + assert id > 0; + + return id; + } + + public boolean isDeleted() + { + return (this.flags & DELETED) != 0; + } + + public boolean isMailingList() + { + return (this.flags & MAILINGLIST) != 0; + } + + public boolean isWriteable() + { + return true; + } + + public long getLastArticleNumber() + throws StorageBackendException + { + return StorageManager.current().getLastArticleNumber(this); + } + + public String getName() + { + return name; + } + + /** + * Performs this.flags |= flag to set a specified flag and updates the data + * in the JDBCDatabase. + * @param flag + */ + public void setFlag(final int flag) + { + this.flags |= flag; + } + + public void setName(final String name) + { + this.name = name; + } + + /** + * @return Number of posted articles in this group. + * @throws java.sql.SQLException + */ + public long getPostingsCount() + throws StorageBackendException + { + return StorageManager.current().getPostingsCount(this.name); + } + + /** + * Updates flags and name in the backend. + */ + public void update() + throws StorageBackendException + { + StorageManager.current().update(this); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/Headers.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/storage/Headers.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,56 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.storage; + +/** + * Contains header constants. These header keys are no way complete but all + * headers that are relevant for sonews. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public final class Headers +{ + + public static final String BYTES = "bytes"; + public static final String CONTENT_TYPE = "content-type"; + public static final String CONTROL = "control"; + public static final String DATE = "date"; + public static final String FROM = "from"; + public static final String LINES = "lines"; + public static final String LIST_POST = "list-post"; + public static final String MESSAGE_ID = "message-id"; + public static final String NEWSGROUPS = "newsgroups"; + public static final String NNTP_POSTING_DATE = "nntp-posting-date"; + public static final String NNTP_POSTING_HOST = "nntp-posting-host"; + public static final String PATH = "path"; + public static final String REFERENCES = "references"; + public static final String REPLY_TO = "reply-to"; + public static final String SENDER = "sender"; + public static final String SUBJECT = "subject"; + public static final String SUPERSEDES = "subersedes"; + public static final String TO = "to"; + public static final String X_COMPLAINTS_TO = "x-complaints-to"; + public static final String X_LIST_POST = "x-list-post"; + public static final String X_TRACE = "x-trace"; + public static final String XREF = "xref"; + + private Headers() + {} + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/Storage.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/storage/Storage.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,150 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.storage; + +import java.util.List; +import org.sonews.feed.Subscription; +import org.sonews.util.Pair; + +/** + * A generic storage backend interface. + * @author Christian Lins + * @since sonews/1.0 + */ +public interface Storage +{ + + /** + * Stores the given Article in the storage. + * @param art + * @throws StorageBackendException + */ + void addArticle(Article art) + throws StorageBackendException; + + void addEvent(long timestamp, int type, long groupID) + throws StorageBackendException; + + void addGroup(String groupname, int flags) + throws StorageBackendException; + + int countArticles() + throws StorageBackendException; + + int countGroups() + throws StorageBackendException; + + void delete(String messageID) + throws StorageBackendException; + + Article getArticle(String messageID) + throws StorageBackendException; + + Article getArticle(long articleIndex, long groupID) + throws StorageBackendException; + + List> getArticleHeads(Group group, long first, long last) + throws StorageBackendException; + + List> getArticleHeaders(Channel channel, long start, long end, + String header, String pattern) + throws StorageBackendException; + + long getArticleIndex(Article art, Group group) + throws StorageBackendException; + + List getArticleNumbers(long groupID) + throws StorageBackendException; + + String getConfigValue(String key) + throws StorageBackendException; + + int getEventsCount(int eventType, long startTimestamp, long endTimestamp, + Channel channel) + throws StorageBackendException; + + double getEventsPerHour(int key, long gid) + throws StorageBackendException; + + int getFirstArticleNumber(Group group) + throws StorageBackendException; + + Group getGroup(String name) + throws StorageBackendException; + + List 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; + + 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; + + String getOldestArticle() + throws StorageBackendException; + + int getPostingsCount(String groupname) + throws StorageBackendException; + + List getSubscriptions(int type) + throws StorageBackendException; + + boolean isArticleExisting(String messageID) + throws StorageBackendException; + + boolean isGroupExisting(String groupname) + throws StorageBackendException; + + void purgeGroup(Group group) + throws StorageBackendException; + + void setConfigValue(String key, String value) + throws StorageBackendException; + + /** + * Updates headers and channel references of the given article. + * @param article + * @return + * @throws StorageBackendException + */ + boolean update(Article article) + throws StorageBackendException; + + boolean update(Group group) + throws StorageBackendException; + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/StorageBackendException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/storage/StorageBackendException.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,39 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.storage; + +/** + * + * @author Christian Lins + * @since sonews/1.0 + */ +public class StorageBackendException extends Exception +{ + + public StorageBackendException(Throwable cause) + { + super(cause); + } + + public StorageBackendException(String msg) + { + super(msg); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/StorageManager.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/storage/StorageManager.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,89 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.storage; + +/** + * + * @author Christian Lins + * @since sonews/1.0 + */ +public final class StorageManager +{ + + private static StorageProvider provider; + + public static Storage current() + throws StorageBackendException + { + synchronized(StorageManager.class) + { + if(provider == null) + { + return null; + } + else + { + return provider.storage(Thread.currentThread()); + } + } + } + + public static StorageProvider loadProvider(String pluginClassName) + { + try + { + Class clazz = Class.forName(pluginClassName); + Object inst = clazz.newInstance(); + return (StorageProvider)inst; + } + catch(Exception ex) + { + System.err.println(ex); + return null; + } + } + + /** + * Sets the current storage provider. + * @param provider + */ + public static void enableProvider(StorageProvider provider) + { + synchronized(StorageManager.class) + { + if(StorageManager.provider != null) + { + disableProvider(); + } + StorageManager.provider = provider; + } + } + + /** + * Disables the current provider. + */ + public static void disableProvider() + { + synchronized(StorageManager.class) + { + provider = null; + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/StorageProvider.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/storage/StorageProvider.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,40 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.storage; + +/** + * Provides access to storage backend instances. + * @author Christian Lins + * @since sonews/1.0 + */ +public interface StorageProvider +{ + + public boolean isSupported(String uri); + + /** + * This method returns the reference to the associated storage. + * The reference MAY be unique for each thread. In any case it MUST be + * thread-safe to use this method. + * @return The reference to the associated Storage. + */ + public Storage storage(Thread thread) + throws StorageBackendException; + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/impl/JDBCDatabase.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/storage/impl/JDBCDatabase.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,1782 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.storage.impl; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import javax.mail.Header; +import javax.mail.internet.MimeUtility; +import org.sonews.config.Config; +import org.sonews.util.Log; +import org.sonews.feed.Subscription; +import org.sonews.storage.Article; +import org.sonews.storage.ArticleHead; +import org.sonews.storage.Channel; +import org.sonews.storage.Group; +import org.sonews.storage.Storage; +import org.sonews.storage.StorageBackendException; +import org.sonews.util.Pair; + +/** + * JDBCDatabase facade class. + * @author Christian Lins + * @since sonews/0.5.0 + */ +// TODO: Refactor this class to reduce size (e.g. ArticleDatabase GroupDatabase) +public class JDBCDatabase implements Storage +{ + + public static final int MAX_RESTARTS = 2; + + private Connection conn = null; + private PreparedStatement pstmtAddArticle1 = null; + private PreparedStatement pstmtAddArticle2 = null; + private PreparedStatement pstmtAddArticle3 = null; + private PreparedStatement pstmtAddArticle4 = null; + private PreparedStatement pstmtAddGroup0 = null; + private PreparedStatement pstmtAddEvent = null; + private PreparedStatement pstmtCountArticles = null; + private PreparedStatement pstmtCountGroups = null; + private PreparedStatement pstmtDeleteArticle0 = null; + private PreparedStatement pstmtDeleteArticle1 = null; + private PreparedStatement pstmtDeleteArticle2 = null; + private PreparedStatement pstmtDeleteArticle3 = null; + private PreparedStatement pstmtGetArticle0 = null; + private PreparedStatement pstmtGetArticle1 = null; + private PreparedStatement pstmtGetArticleHeaders0 = null; + private PreparedStatement pstmtGetArticleHeaders1 = null; + private PreparedStatement pstmtGetArticleHeads = null; + private PreparedStatement pstmtGetArticleIDs = null; + private PreparedStatement pstmtGetArticleIndex = null; + private PreparedStatement pstmtGetConfigValue = null; + private PreparedStatement pstmtGetEventsCount0 = null; + private PreparedStatement pstmtGetEventsCount1 = null; + private PreparedStatement pstmtGetGroupForList = null; + private PreparedStatement pstmtGetGroup0 = null; + private PreparedStatement pstmtGetGroup1 = null; + private PreparedStatement pstmtGetFirstArticleNumber = null; + private PreparedStatement pstmtGetListForGroup = null; + private PreparedStatement pstmtGetLastArticleNumber = null; + private PreparedStatement pstmtGetMaxArticleID = null; + private PreparedStatement pstmtGetMaxArticleIndex = null; + private PreparedStatement pstmtGetOldestArticle = null; + private PreparedStatement pstmtGetPostingsCount = null; + private PreparedStatement pstmtGetSubscriptions = null; + private PreparedStatement pstmtIsArticleExisting = null; + private PreparedStatement pstmtIsGroupExisting = null; + private PreparedStatement pstmtPurgeGroup0 = null; + private PreparedStatement pstmtPurgeGroup1 = null; + private PreparedStatement pstmtSetConfigValue0 = null; + private PreparedStatement pstmtSetConfigValue1 = null; + private PreparedStatement pstmtUpdateGroup = null; + + /** How many times the database connection was reinitialized */ + private int restarts = 0; + + /** + * Rises the database: reconnect and recreate all prepared statements. + * @throws java.lang.SQLException + */ + protected void arise() + throws SQLException + { + try + { + // Load database driver + Class.forName( + Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DBMSDRIVER, "java.lang.Object")); + + // Establish database connection + this.conn = DriverManager.getConnection( + Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DATABASE, ""), + Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_USER, "root"), + Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_PASSWORD, "")); + + this.conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); + if(this.conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE) + { + Log.get().warning("Database is NOT fully serializable!"); + } + + // Prepare statements for method addArticle() + this.pstmtAddArticle1 = conn.prepareStatement( + "INSERT INTO articles (article_id, body) VALUES(?, ?)"); + this.pstmtAddArticle2 = conn.prepareStatement( + "INSERT INTO headers (article_id, header_key, header_value, header_index) " + + "VALUES (?, ?, ?, ?)"); + this.pstmtAddArticle3 = conn.prepareStatement( + "INSERT INTO postings (group_id, article_id, article_index)" + + "VALUES (?, ?, ?)"); + this.pstmtAddArticle4 = conn.prepareStatement( + "INSERT INTO article_ids (article_id, message_id) VALUES (?, ?)"); + + // Prepare statement for method addStatValue() + this.pstmtAddEvent = conn.prepareStatement( + "INSERT INTO events VALUES (?, ?, ?)"); + + // Prepare statement for method addGroup() + this.pstmtAddGroup0 = conn.prepareStatement( + "INSERT INTO groups (name, flags) VALUES (?, ?)"); + + // Prepare statement for method countArticles() + this.pstmtCountArticles = conn.prepareStatement( + "SELECT Count(article_id) FROM article_ids"); + + // Prepare statement for method countGroups() + this.pstmtCountGroups = conn.prepareStatement( + "SELECT Count(group_id) FROM groups WHERE " + + "flags & " + Channel.DELETED + " = 0"); + + // Prepare statements for method delete(article) + this.pstmtDeleteArticle0 = conn.prepareStatement( + "DELETE FROM articles WHERE article_id = " + + "(SELECT article_id FROM article_ids WHERE message_id = ?)"); + this.pstmtDeleteArticle1 = conn.prepareStatement( + "DELETE FROM headers WHERE article_id = " + + "(SELECT article_id FROM article_ids WHERE message_id = ?)"); + this.pstmtDeleteArticle2 = conn.prepareStatement( + "DELETE FROM postings WHERE article_id = " + + "(SELECT article_id FROM article_ids WHERE message_id = ?)"); + this.pstmtDeleteArticle3 = conn.prepareStatement( + "DELETE FROM article_ids WHERE message_id = ?"); + + // Prepare statements for methods getArticle() + this.pstmtGetArticle0 = conn.prepareStatement( + "SELECT * FROM articles WHERE article_id = " + + "(SELECT article_id FROM article_ids WHERE message_id = ?)"); + this.pstmtGetArticle1 = conn.prepareStatement( + "SELECT * FROM articles WHERE article_id = " + + "(SELECT article_id FROM postings WHERE " + + "article_index = ? AND group_id = ?)"); + + // Prepare statement for method getArticleHeaders() + this.pstmtGetArticleHeaders0 = conn.prepareStatement( + "SELECT header_key, header_value FROM headers WHERE article_id = ? " + + "ORDER BY header_index ASC"); + + // Prepare statement for method getArticleHeaders(regular expr pattern) + this.pstmtGetArticleHeaders1 = conn.prepareStatement( + "SELECT p.article_index, h.header_value FROM headers h " + + "INNER JOIN postings p ON h.article_id = p.article_id " + + "INNER JOIN groups g ON p.group_id = g.group_id " + + "WHERE g.name = ? AND " + + "h.header_key = ? AND " + + "p.article_index >= ? " + + "ORDER BY p.article_index ASC"); + + this.pstmtGetArticleIDs = conn.prepareStatement( + "SELECT article_index FROM postings WHERE group_id = ?"); + + // Prepare statement for method getArticleIndex + this.pstmtGetArticleIndex = conn.prepareStatement( + "SELECT article_index FROM postings WHERE " + + "article_id = (SELECT article_id FROM article_ids " + + "WHERE message_id = ?) " + + " AND group_id = ?"); + + // Prepare statements for method getArticleHeads() + this.pstmtGetArticleHeads = conn.prepareStatement( + "SELECT article_id, article_index FROM postings WHERE " + + "postings.group_id = ? AND article_index >= ? AND " + + "article_index <= ?"); + + // Prepare statements for method getConfigValue() + this.pstmtGetConfigValue = conn.prepareStatement( + "SELECT config_value FROM config WHERE config_key = ?"); + + // Prepare statements for method getEventsCount() + this.pstmtGetEventsCount0 = conn.prepareStatement( + "SELECT Count(*) FROM events WHERE event_key = ? AND " + + "event_time >= ? AND event_time < ?"); + + this.pstmtGetEventsCount1 = conn.prepareStatement( + "SELECT Count(*) FROM events WHERE event_key = ? AND " + + "event_time >= ? AND event_time < ? AND group_id = ?"); + + // Prepare statement for method getGroupForList() + this.pstmtGetGroupForList = conn.prepareStatement( + "SELECT name FROM groups INNER JOIN groups2list " + + "ON groups.group_id = groups2list.group_id " + + "WHERE groups2list.listaddress = ?"); + + // Prepare statement for method getGroup() + this.pstmtGetGroup0 = conn.prepareStatement( + "SELECT group_id, flags FROM groups WHERE Name = ?"); + this.pstmtGetGroup1 = conn.prepareStatement( + "SELECT name FROM groups WHERE group_id = ?"); + + // Prepare statement for method getLastArticleNumber() + this.pstmtGetLastArticleNumber = conn.prepareStatement( + "SELECT Max(article_index) FROM postings WHERE group_id = ?"); + + // Prepare statement for method getListForGroup() + this.pstmtGetListForGroup = conn.prepareStatement( + "SELECT listaddress FROM groups2list INNER JOIN groups " + + "ON groups.group_id = groups2list.group_id WHERE name = ?"); + + // Prepare statement for method getMaxArticleID() + this.pstmtGetMaxArticleID = conn.prepareStatement( + "SELECT Max(article_id) FROM articles"); + + // Prepare statement for method getMaxArticleIndex() + this.pstmtGetMaxArticleIndex = conn.prepareStatement( + "SELECT Max(article_index) FROM postings WHERE group_id = ?"); + + // Prepare statement for method getOldestArticle() + this.pstmtGetOldestArticle = conn.prepareStatement( + "SELECT message_id FROM article_ids WHERE article_id = " + + "(SELECT Min(article_id) FROM article_ids)"); + + // Prepare statement for method getFirstArticleNumber() + this.pstmtGetFirstArticleNumber = conn.prepareStatement( + "SELECT Min(article_index) FROM postings WHERE group_id = ?"); + + // Prepare statement for method getPostingsCount() + this.pstmtGetPostingsCount = conn.prepareStatement( + "SELECT Count(*) FROM postings NATURAL JOIN groups " + + "WHERE groups.name = ?"); + + // Prepare statement for method getSubscriptions() + this.pstmtGetSubscriptions = conn.prepareStatement( + "SELECT host, port, name FROM peers NATURAL JOIN " + + "peer_subscriptions NATURAL JOIN groups WHERE feedtype = ?"); + + // Prepare statement for method isArticleExisting() + this.pstmtIsArticleExisting = conn.prepareStatement( + "SELECT Count(article_id) FROM article_ids WHERE message_id = ?"); + + // Prepare statement for method isGroupExisting() + this.pstmtIsGroupExisting = conn.prepareStatement( + "SELECT * FROM groups WHERE name = ?"); + + // Prepare statement for method setConfigValue() + this.pstmtSetConfigValue0 = conn.prepareStatement( + "DELETE FROM config WHERE config_key = ?"); + this.pstmtSetConfigValue1 = conn.prepareStatement( + "INSERT INTO config VALUES(?, ?)"); + + // Prepare statements for method purgeGroup() + this.pstmtPurgeGroup0 = conn.prepareStatement( + "DELETE FROM peer_subscriptions WHERE group_id = ?"); + this.pstmtPurgeGroup1 = conn.prepareStatement( + "DELETE FROM groups WHERE group_id = ?"); + + // Prepare statement for method update(Group) + this.pstmtUpdateGroup = conn.prepareStatement( + "UPDATE groups SET flags = ?, name = ? WHERE group_id = ?"); + } + catch(ClassNotFoundException ex) + { + throw new Error("JDBC Driver not found!", ex); + } + } + + /** + * Adds an article to the database. + * @param article + * @return + * @throws java.sql.SQLException + */ + @Override + public void addArticle(final Article article) + throws StorageBackendException + { + try + { + this.conn.setAutoCommit(false); + + int newArticleID = getMaxArticleID() + 1; + + // Fill prepared statement with values; + // writes body to article table + pstmtAddArticle1.setInt(1, newArticleID); + pstmtAddArticle1.setBytes(2, article.getBody()); + pstmtAddArticle1.execute(); + + // Add headers + Enumeration headers = article.getAllHeaders(); + for(int n = 0; headers.hasMoreElements(); n++) + { + Header header = (Header)headers.nextElement(); + pstmtAddArticle2.setInt(1, newArticleID); + pstmtAddArticle2.setString(2, header.getName().toLowerCase()); + pstmtAddArticle2.setString(3, + header.getValue().replaceAll("[\r\n]", "")); + pstmtAddArticle2.setInt(4, n); + pstmtAddArticle2.execute(); + } + + // For each newsgroup add a reference + List groups = article.getGroups(); + for(Group group : groups) + { + pstmtAddArticle3.setLong(1, group.getInternalID()); + pstmtAddArticle3.setInt(2, newArticleID); + pstmtAddArticle3.setLong(3, getMaxArticleIndex(group.getInternalID()) + 1); + pstmtAddArticle3.execute(); + } + + // Write message-id to article_ids table + this.pstmtAddArticle4.setInt(1, newArticleID); + this.pstmtAddArticle4.setString(2, article.getMessageID()); + this.pstmtAddArticle4.execute(); + + this.conn.commit(); + this.conn.setAutoCommit(true); + + this.restarts = 0; // Reset error count + } + catch(SQLException ex) + { + try + { + this.conn.rollback(); // Rollback changes + } + catch(SQLException ex2) + { + Log.get().severe("Rollback of addArticle() failed: " + ex2); + } + + try + { + this.conn.setAutoCommit(true); // and release locks + } + catch(SQLException ex2) + { + Log.get().severe("setAutoCommit(true) of addArticle() failed: " + ex2); + } + + restartConnection(ex); + addArticle(article); + } + } + + /** + * Adds a group to the JDBCDatabase. This method is not accessible via NNTP. + * @param name + * @throws java.sql.SQLException + */ + @Override + public void addGroup(String name, int flags) + throws StorageBackendException + { + try + { + this.conn.setAutoCommit(false); + pstmtAddGroup0.setString(1, name); + pstmtAddGroup0.setInt(2, flags); + + pstmtAddGroup0.executeUpdate(); + this.conn.commit(); + this.conn.setAutoCommit(true); + this.restarts = 0; // Reset error count + } + catch(SQLException ex) + { + try + { + this.conn.rollback(); + this.conn.setAutoCommit(true); + } + catch(SQLException ex2) + { + ex2.printStackTrace(); + } + + restartConnection(ex); + addGroup(name, flags); + } + } + + @Override + public void addEvent(long time, int type, long gid) + throws StorageBackendException + { + try + { + this.conn.setAutoCommit(false); + this.pstmtAddEvent.setLong(1, time); + this.pstmtAddEvent.setInt(2, type); + this.pstmtAddEvent.setLong(3, gid); + this.pstmtAddEvent.executeUpdate(); + this.conn.commit(); + this.conn.setAutoCommit(true); + this.restarts = 0; + } + catch(SQLException ex) + { + try + { + this.conn.rollback(); + this.conn.setAutoCommit(true); + } + catch(SQLException ex2) + { + ex2.printStackTrace(); + } + + restartConnection(ex); + addEvent(time, type, gid); + } + } + + @Override + public int countArticles() + throws StorageBackendException + { + ResultSet rs = null; + + try + { + rs = this.pstmtCountArticles.executeQuery(); + if(rs.next()) + { + return rs.getInt(1); + } + else + { + return -1; + } + } + catch(SQLException ex) + { + restartConnection(ex); + return countArticles(); + } + finally + { + if(rs != null) + { + try + { + rs.close(); + } + catch(SQLException ex) + { + ex.printStackTrace(); + } + restarts = 0; + } + } + } + + @Override + public int countGroups() + throws StorageBackendException + { + ResultSet rs = null; + + try + { + rs = this.pstmtCountGroups.executeQuery(); + if(rs.next()) + { + return rs.getInt(1); + } + else + { + return -1; + } + } + catch(SQLException ex) + { + restartConnection(ex); + return countGroups(); + } + finally + { + if(rs != null) + { + try + { + rs.close(); + } + catch(SQLException ex) + { + ex.printStackTrace(); + } + restarts = 0; + } + } + } + + @Override + public void delete(final String messageID) + throws StorageBackendException + { + try + { + this.conn.setAutoCommit(false); + + this.pstmtDeleteArticle0.setString(1, messageID); + int rs = this.pstmtDeleteArticle0.executeUpdate(); + + // We do not trust the ON DELETE CASCADE functionality to delete + // orphaned references... + this.pstmtDeleteArticle1.setString(1, messageID); + rs = this.pstmtDeleteArticle1.executeUpdate(); + + this.pstmtDeleteArticle2.setString(1, messageID); + rs = this.pstmtDeleteArticle2.executeUpdate(); + + this.pstmtDeleteArticle3.setString(1, messageID); + rs = this.pstmtDeleteArticle3.executeUpdate(); + + this.conn.commit(); + this.conn.setAutoCommit(true); + } + catch(SQLException ex) + { + throw new StorageBackendException(ex); + } + } + + @Override + public Article getArticle(String messageID) + throws StorageBackendException + { + ResultSet rs = null; + try + { + pstmtGetArticle0.setString(1, messageID); + rs = pstmtGetArticle0.executeQuery(); + + if(!rs.next()) + { + return null; + } + else + { + byte[] body = rs.getBytes("body"); + String headers = getArticleHeaders(rs.getInt("article_id")); + return new Article(headers, body); + } + } + catch(SQLException ex) + { + restartConnection(ex); + return getArticle(messageID); + } + finally + { + if(rs != null) + { + try + { + rs.close(); + } + catch(SQLException ex) + { + ex.printStackTrace(); + } + restarts = 0; // Reset error count + } + } + } + + /** + * Retrieves an article by its ID. + * @param articleID + * @return + * @throws StorageBackendException + */ + @Override + public Article getArticle(long articleIndex, long gid) + throws StorageBackendException + { + ResultSet rs = null; + + try + { + this.pstmtGetArticle1.setLong(1, articleIndex); + this.pstmtGetArticle1.setLong(2, gid); + + rs = this.pstmtGetArticle1.executeQuery(); + + if(rs.next()) + { + byte[] body = rs.getBytes("body"); + String headers = getArticleHeaders(rs.getInt("article_id")); + return new Article(headers, body); + } + else + { + return null; + } + } + catch(SQLException ex) + { + restartConnection(ex); + return getArticle(articleIndex, gid); + } + finally + { + if(rs != null) + { + try + { + rs.close(); + } + catch(SQLException ex) + { + ex.printStackTrace(); + } + restarts = 0; + } + } + } + + /** + * Searches for fitting header values using the given regular expression. + * @param group + * @param start + * @param end + * @param headerKey + * @param pattern + * @return + * @throws StorageBackendException + */ + @Override + public List> getArticleHeaders(Channel group, long start, + long end, String headerKey, String patStr) + throws StorageBackendException, PatternSyntaxException + { + ResultSet rs = null; + List> heads = new ArrayList>(); + + try + { + this.pstmtGetArticleHeaders1.setString(1, group.getName()); + this.pstmtGetArticleHeaders1.setString(2, headerKey); + this.pstmtGetArticleHeaders1.setLong(3, start); + + rs = this.pstmtGetArticleHeaders1.executeQuery(); + + // Convert the "NNTP" regex to Java regex + patStr = patStr.replace("*", ".*"); + Pattern pattern = Pattern.compile(patStr); + + while(rs.next()) + { + Long articleIndex = rs.getLong(1); + if(end < 0 || articleIndex <= end) // Match start is done via SQL + { + String headerValue = rs.getString(2); + Matcher matcher = pattern.matcher(headerValue); + if(matcher.matches()) + { + heads.add(new Pair(articleIndex, headerValue)); + } + } + } + } + catch(SQLException ex) + { + restartConnection(ex); + return getArticleHeaders(group, start, end, headerKey, patStr); + } + finally + { + if(rs != null) + { + try + { + rs.close(); + } + catch(SQLException ex) + { + ex.printStackTrace(); + } + } + } + + return heads; + } + + private String getArticleHeaders(long articleID) + throws StorageBackendException + { + ResultSet rs = null; + + try + { + this.pstmtGetArticleHeaders0.setLong(1, articleID); + rs = this.pstmtGetArticleHeaders0.executeQuery(); + + StringBuilder buf = new StringBuilder(); + if(rs.next()) + { + for(;;) + { + buf.append(rs.getString(1)); // key + buf.append(": "); + String foldedValue = MimeUtility.fold(0, rs.getString(2)); + buf.append(foldedValue); // value + if(rs.next()) + { + buf.append("\r\n"); + } + else + { + break; + } + } + } + + return buf.toString(); + } + catch(SQLException ex) + { + restartConnection(ex); + return getArticleHeaders(articleID); + } + finally + { + if(rs != null) + { + try + { + rs.close(); + } + catch(SQLException ex) + { + ex.printStackTrace(); + } + } + } + } + + @Override + public long getArticleIndex(Article article, Group group) + throws StorageBackendException + { + ResultSet rs = null; + + try + { + this.pstmtGetArticleIndex.setString(1, article.getMessageID()); + this.pstmtGetArticleIndex.setLong(2, group.getInternalID()); + + rs = this.pstmtGetArticleIndex.executeQuery(); + if(rs.next()) + { + return rs.getLong(1); + } + else + { + return -1; + } + } + catch(SQLException ex) + { + restartConnection(ex); + return getArticleIndex(article, group); + } + finally + { + if(rs != null) + { + try + { + rs.close(); + } + catch(SQLException ex) + { + ex.printStackTrace(); + } + } + } + } + + /** + * Returns a list of Long/Article Pairs. + * @throws java.sql.SQLException + */ + @Override + public List> getArticleHeads(Group group, long first, + long last) + throws StorageBackendException + { + ResultSet rs = null; + + try + { + this.pstmtGetArticleHeads.setLong(1, group.getInternalID()); + this.pstmtGetArticleHeads.setLong(2, first); + this.pstmtGetArticleHeads.setLong(3, last); + rs = pstmtGetArticleHeads.executeQuery(); + + List> articles + = new ArrayList>(); + + 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))); + } + + return articles; + } + catch(SQLException ex) + { + restartConnection(ex); + return getArticleHeads(group, first, last); + } + finally + { + if(rs != null) + { + try + { + rs.close(); + } + catch(SQLException ex) + { + ex.printStackTrace(); + } + } + } + } + + @Override + public List 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 String getConfigValue(String key) + throws StorageBackendException + { + ResultSet rs = null; + try + { + this.pstmtGetConfigValue.setString(1, key); + + rs = this.pstmtGetConfigValue.executeQuery(); + if(rs.next()) + { + return rs.getString(1); // First data on index 1 not 0 + } + else + { + return null; + } + } + catch(SQLException ex) + { + restartConnection(ex); + return getConfigValue(key); + } + finally + { + if(rs != null) + { + try + { + rs.close(); + } + catch(SQLException ex) + { + ex.printStackTrace(); + } + restarts = 0; // Clear the restart count after successful request + } + } + } + + @Override + public int getEventsCount(int type, long start, long end, Channel channel) + throws StorageBackendException + { + ResultSet rs = null; + + try + { + if(channel == null) + { + this.pstmtGetEventsCount0.setInt(1, type); + this.pstmtGetEventsCount0.setLong(2, start); + this.pstmtGetEventsCount0.setLong(3, end); + rs = this.pstmtGetEventsCount0.executeQuery(); + } + else + { + this.pstmtGetEventsCount1.setInt(1, type); + this.pstmtGetEventsCount1.setLong(2, start); + this.pstmtGetEventsCount1.setLong(3, end); + this.pstmtGetEventsCount1.setLong(4, channel.getInternalID()); + rs = this.pstmtGetEventsCount1.executeQuery(); + } + + if(rs.next()) + { + return rs.getInt(1); + } + else + { + return -1; + } + } + catch(SQLException ex) + { + restartConnection(ex); + return getEventsCount(type, start, end, channel); + } + finally + { + if(rs != null) + { + try + { + rs.close(); + } + catch(SQLException ex) + { + ex.printStackTrace(); + } + } + } + } + + /** + * Reads all Groups from the JDBCDatabase. + * @return + * @throws StorageBackendException + */ + @Override + public List getGroups() + throws StorageBackendException + { + ResultSet rs; + List buffer = new ArrayList(); + Statement stmt = null; + + try + { + stmt = conn.createStatement(); + rs = stmt.executeQuery("SELECT * FROM groups ORDER BY name"); + + while(rs.next()) + { + String name = rs.getString("name"); + long id = rs.getLong("group_id"); + int flags = rs.getInt("flags"); + + Group group = new Group(name, id, flags); + buffer.add(group); + } + + return buffer; + } + catch(SQLException ex) + { + restartConnection(ex); + return getGroups(); + } + finally + { + if(stmt != null) + { + try + { + stmt.close(); // Implicitely closes ResultSets + } + catch(SQLException ex) + { + ex.printStackTrace(); + } + } + } + } + + @Override + public List 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 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/impl/JDBCDatabaseProvider.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/storage/impl/JDBCDatabaseProvider.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,69 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.storage.impl; + +import java.sql.SQLException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.sonews.storage.Storage; +import org.sonews.storage.StorageBackendException; +import org.sonews.storage.StorageProvider; + +/** + * + * @author Christian Lins + * @since sonews/1.0 + */ +public class JDBCDatabaseProvider implements StorageProvider +{ + + protected static final Map instances + = new ConcurrentHashMap(); + + @Override + public boolean isSupported(String uri) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Storage storage(Thread thread) + throws StorageBackendException + { + try + { + if(!instances.containsKey(Thread.currentThread())) + { + JDBCDatabase db = new JDBCDatabase(); + db.arise(); + instances.put(Thread.currentThread(), db); + return db; + } + else + { + return instances.get(Thread.currentThread()); + } + } + catch(SQLException ex) + { + throw new StorageBackendException(ex); + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/storage/package.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/storage/package.html Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,2 @@ +Contains classes of the storage backend and the Group and Article +abstraction. \ No newline at end of file diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/DatabaseSetup.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/util/DatabaseSetup.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,127 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.util; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; +import java.util.HashMap; +import java.util.Map; +import org.sonews.config.Config; +import org.sonews.util.io.Resource; + +/** + * Database setup utility class. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public final class DatabaseSetup +{ + + private static final Map 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(); + + System.out.print("Give name of DB user that can create tables: "); + String dbUser = in.readLine(); + + System.out.print("Password: "); + String dbPassword = in.readLine(); + + String url = urlMap.get(dbmsType) + .set("HOSTNAME", dbHostname) + .set("DB", dbName).toString(); + + Connection conn = + DriverManager.getConnection(url, dbUser, dbPassword); + conn.setAutoCommit(false); + + String[] tmplChunks = tmpl.split(";"); + + for(String chunk : tmplChunks) + { + if(chunk.trim().equals("")) + { + continue; + } + + Statement stmt = conn.createStatement(); + stmt.execute(chunk); + } + + conn.commit(); + conn.setAutoCommit(true); + + // Create config file + + System.out.println("Ok"); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/Log.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/util/Log.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,57 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.util; + +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; +import java.util.logging.StreamHandler; +import org.sonews.config.Config; + +/** + * Provides logging and debugging methods. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class Log extends Logger +{ + + private static Log instance = new Log(); + + private Log() + { + super("org.sonews", null); + + StreamHandler handler = new StreamHandler(System.out, new SimpleFormatter()); + Level level = Level.parse(Config.inst().get(Config.LOGLEVEL, "INFO")); + handler.setLevel(level); + addHandler(handler); + setLevel(level); + LogManager.getLogManager().addLogger(this); + } + + public static Logger get() + { + Level level = Level.parse(Config.inst().get(Config.LOGLEVEL, "INFO")); + instance.setLevel(level); + return instance; + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/Pair.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/util/Pair.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,48 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.util; + +/** + * A pair of two objects. + * @author Christian Lins + * @since sonews/0.5.0 + */ +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; + } + + public T2 getB() + { + return b; + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/Purger.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/util/Purger.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,149 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.util; + +import org.sonews.daemon.AbstractDaemon; +import org.sonews.config.Config; +import org.sonews.storage.Article; +import org.sonews.storage.Headers; +import java.util.Date; +import java.util.List; +import org.sonews.storage.Channel; +import org.sonews.storage.Group; +import org.sonews.storage.StorageBackendException; +import org.sonews.storage.StorageManager; + +/** + * The purger is started in configurable intervals to search + * for messages that can be purged. A message must be deleted if its lifetime + * has exceeded, if it was marked as deleted or if the maximum number of + * articles in the database is reached. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class Purger extends AbstractDaemon +{ + + /** + * Loops through all messages and deletes them if their time + * has come. + */ + @Override + public void run() + { + try + { + while(isRunning()) + { + purgeDeleted(); + purgeOutdated(); + + Thread.sleep(120000); // Sleep for two minutes + } + } + catch(StorageBackendException ex) + { + ex.printStackTrace(); + } + catch(InterruptedException ex) + { + Log.get().warning("Purger interrupted: " + ex); + } + } + + private void purgeDeleted() + throws StorageBackendException + { + List 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."); + } + + for(int n = 0; n < ids.size() && n < 10; n++) + { + Article art = StorageManager.current().getArticle(ids.get(n), group.getInternalID()); + StorageManager.current().delete(art.getMessageID()); + Log.get().info("Article " + art.getMessageID() + " purged."); + } + } + } + } + + private void purgeOutdated() + throws InterruptedException, StorageBackendException + { + long articleMaximum = + Config.inst().get("sonews.article.maxnum", Long.MAX_VALUE); + long lifetime = + Config.inst().get("sonews.article.lifetime", -1); + + if(lifetime > 0 || articleMaximum < Stats.getInstance().getNumberOfNews()) + { + Log.get().info("Purging old messages..."); + String mid = StorageManager.current().getOldestArticle(); + if (mid == null) // No articles in the database + { + return; + } + + Article art = StorageManager.current().getArticle(mid); + long artDate = 0; + String dateStr = art.getHeader(Headers.DATE)[0]; + try + { + artDate = Date.parse(dateStr) / 1000 / 60 / 60 / 24; + } + catch (IllegalArgumentException ex) + { + Log.get().warning("Could not parse date string: " + dateStr + " " + ex); + } + + // Should we delete the message because of its age or because the + // article maximum was reached? + if (lifetime < 0 || artDate < (new Date().getTime() + lifetime)) + { + StorageManager.current().delete(mid); + System.out.println("Deleted: " + mid); + } + else + { + Thread.sleep(1000 * 60); // Wait 60 seconds + return; + } + } + else + { + Log.get().info("Lifetime purger is disabled"); + Thread.sleep(1000 * 60 * 30); // Wait 30 minutes + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/Stats.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/util/Stats.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,206 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.util; + +import java.util.Calendar; +import org.sonews.config.Config; +import org.sonews.storage.Channel; +import org.sonews.storage.StorageBackendException; +import org.sonews.storage.StorageManager; + +/** + * Class that capsulates statistical data gathering. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public final class Stats +{ + + public static final byte CONNECTIONS = 1; + public static final byte POSTED_NEWS = 2; + public static final byte GATEWAYED_NEWS = 3; + public static final byte FEEDED_NEWS = 4; + public static final byte MLGW_RUNSTART = 5; + public static final byte MLGW_RUNEND = 6; + + private static Stats instance = new Stats(); + + public static Stats getInstance() + { + return Stats.instance; + } + + private Stats() {} + + private volatile int connectedClients = 0; + + /** + * A generic method that writes event data to the storage backend. + * If event logging is disabled with sonews.eventlog=false this method + * simply does nothing. + * @param type + * @param groupname + */ + private void addEvent(byte type, String groupname) + { + try + { + if (Config.inst().get(Config.EVENTLOG, true)) + { + + Channel group = Channel.getByName(groupname); + if (group != null) + { + StorageManager.current().addEvent( + System.currentTimeMillis(), type, group.getInternalID()); + } + } + else + { + Log.get().info("Group " + groupname + " does not exist."); + } + } + catch (StorageBackendException ex) + { + ex.printStackTrace(); + } + } + + public void clientConnect() + { + this.connectedClients++; + } + + public void clientDisconnect() + { + this.connectedClients--; + } + + public int connectedClients() + { + return this.connectedClients; + } + + public int getNumberOfGroups() + { + try + { + return StorageManager.current().countGroups(); + } + catch(StorageBackendException ex) + { + ex.printStackTrace(); + return -1; + } + } + + public int getNumberOfNews() + { + try + { + return StorageManager.current().countArticles(); + } + catch(StorageBackendException ex) + { + ex.printStackTrace(); + return -1; + } + } + + public int getYesterdaysEvents(final byte eventType, final int hour, + final Channel group) + { + // Determine the timestamp values for yesterday and the given hour + Calendar cal = Calendar.getInstance(); + int year = cal.get(Calendar.YEAR); + int month = cal.get(Calendar.MONTH); + int dayom = cal.get(Calendar.DAY_OF_MONTH) - 1; // Yesterday + + cal.set(year, month, dayom, hour, 0, 0); + long startTimestamp = cal.getTimeInMillis(); + + cal.set(year, month, dayom, hour + 1, 0, 0); + long endTimestamp = cal.getTimeInMillis(); + + try + { + return StorageManager.current() + .getEventsCount(eventType, startTimestamp, endTimestamp, group); + } + catch(StorageBackendException ex) + { + ex.printStackTrace(); + return -1; + } + } + + public void mailPosted(String groupname) + { + addEvent(POSTED_NEWS, groupname); + } + + public void mailGatewayed(String groupname) + { + addEvent(GATEWAYED_NEWS, groupname); + } + + public void mailFeeded(String groupname) + { + addEvent(FEEDED_NEWS, groupname); + } + + public void mlgwRunStart() + { + addEvent(MLGW_RUNSTART, "control"); + } + + public void mlgwRunEnd() + { + addEvent(MLGW_RUNEND, "control"); + } + + private double perHour(int key, long gid) + { + try + { + return StorageManager.current().getEventsPerHour(key, gid); + } + catch(StorageBackendException ex) + { + ex.printStackTrace(); + return -1; + } + } + + public double postedPerHour(long gid) + { + return perHour(POSTED_NEWS, gid); + } + + public double gatewayedPerHour(long gid) + { + return perHour(GATEWAYED_NEWS, gid); + } + + public double feededPerHour(long gid) + { + return perHour(FEEDED_NEWS, gid); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/StringTemplate.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/util/StringTemplate.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,97 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.util; + +import java.util.HashMap; +import java.util.Map; + +/** + * Class that allows simple String template handling. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class StringTemplate +{ + + private String str = null; + private String templateDelimiter = "%"; + private Map templateValues = new HashMap(); + + public StringTemplate(String str, final String templateDelimiter) + { + if(str == null || templateDelimiter == null) + { + throw new IllegalArgumentException("null arguments not allowed"); + } + + this.str = str; + this.templateDelimiter = templateDelimiter; + } + + public StringTemplate(String str) + { + this(str, "%"); + } + + public StringTemplate set(String template, String value) + { + if(template == null || value == null) + { + throw new IllegalArgumentException("null arguments not allowed"); + } + + this.templateValues.put(template, value); + return this; + } + + public StringTemplate set(String template, long value) + { + return set(template, Long.toString(value)); + } + + public StringTemplate set(String template, double value) + { + return set(template, Double.toString(value)); + } + + public StringTemplate set(String template, Object obj) + { + if(template == null || obj == null) + { + throw new IllegalArgumentException("null arguments not allowed"); + } + + return set(template, obj.toString()); + } + + @Override + public String toString() + { + String ret = str; + + for(String key : this.templateValues.keySet()) + { + String value = this.templateValues.get(key); + ret = ret.replace(templateDelimiter + key, value); + } + + return ret; + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/TimeoutMap.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/util/TimeoutMap.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,145 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.util; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Implementation of a Map that will loose its stored values after a + * configurable amount of time. + * This class may be used to cache config values for example. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class TimeoutMap 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); + } + + @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 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/io/ArticleInputStream.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/util/io/ArticleInputStream.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,71 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.util.io; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import org.sonews.storage.Article; + +/** + * Capsulates an Article to provide a raw InputStream. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class ArticleInputStream extends InputStream +{ + + private byte[] buf; + private int pos = 0; + + public ArticleInputStream(final Article art) + throws IOException, UnsupportedEncodingException + { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(art.getHeaderSource().getBytes("UTF-8")); + out.write("\r\n\r\n".getBytes()); + out.write(art.getBody()); // Without CRLF + out.flush(); + this.buf = out.toByteArray(); + } + + /** + * This method reads one byte from the stream. The 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 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/io/ArticleReader.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/util/io/ArticleReader.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,135 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.util.io; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.net.UnknownHostException; +import org.sonews.config.Config; +import org.sonews.util.Log; + +/** + * Reads an news article from a NNTP server. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class ArticleReader +{ + + private BufferedOutputStream out; + private BufferedInputStream in; + private String messageID; + + public ArticleReader(String host, int port, String messageID) + throws IOException, UnknownHostException + { + this.messageID = messageID; + + // Connect to NNTP server + Socket socket = new Socket(host, port); + this.out = new BufferedOutputStream(socket.getOutputStream()); + this.in = new BufferedInputStream(socket.getInputStream()); + String line = readln(this.in); + if(!line.startsWith("200 ")) + { + throw new IOException("Invalid hello from server: " + line); + } + } + + private boolean eofArticle(byte[] buf) + { + if(buf.length < 4) + { + return false; + } + + int l = buf.length - 1; + return buf[l-3] == 10 // '*\n' + && buf[l-2] == '.' // '.' + && buf[l-1] == 13 && buf[l] == 10; // '\r\n' + } + + public byte[] getArticleData() + throws IOException, UnsupportedEncodingException + { + long maxSize = Config.inst().get(Config.ARTICLE_MAXSIZE, 1024) * 1024L; + + try + { + this.out.write(("ARTICLE " + this.messageID + "\r\n").getBytes("UTF-8")); + this.out.flush(); + + String line = readln(this.in); + if(line.startsWith("220 ")) + { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + + while(!eofArticle(buf.toByteArray())) + { + for(int b = in.read(); b != 10; b = in.read()) + { + buf.write(b); + } + + buf.write(10); + if(buf.size() > maxSize) + { + Log.get().warning("Skipping message that is too large: " + buf.size()); + return null; + } + } + + return buf.toByteArray(); + } + else + { + Log.get().warning("ArticleReader: " + line); + return null; + } + } + catch(IOException ex) + { + throw ex; + } + finally + { + this.out.write("QUIT\r\n".getBytes("UTF-8")); + this.out.flush(); + this.out.close(); + } + } + + private String readln(InputStream in) + throws IOException + { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + for(int b = in.read(); b != 10 /* \n */; b = in.read()) + { + buf.write(b); + } + + return new String(buf.toByteArray()); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/io/ArticleWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/util/io/ArticleWriter.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,133 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.util.io; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.net.UnknownHostException; +import org.sonews.storage.Article; + +/** + * Posts an Article to a NNTP server using the POST command. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public class ArticleWriter +{ + + private BufferedOutputStream out; + private BufferedReader inr; + + public ArticleWriter(String host, int port) + throws IOException, UnknownHostException + { + // Connect to NNTP server + Socket socket = new Socket(host, port); + this.out = new BufferedOutputStream(socket.getOutputStream()); + this.inr = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = inr.readLine(); + if(line == null || !line.startsWith("200 ")) + { + throw new IOException("Invalid hello from server: " + line); + } + } + + public void close() + throws IOException, UnsupportedEncodingException + { + this.out.write("QUIT\r\n".getBytes("UTF-8")); + this.out.flush(); + } + + protected void finishPOST() + throws IOException + { + this.out.write("\r\n.\r\n".getBytes()); + this.out.flush(); + String line = inr.readLine(); + if(line == null || !line.startsWith("240 ") || !line.startsWith("441 ")) + { + throw new IOException(line); + } + } + + protected void preparePOST() + throws IOException + { + this.out.write("POST\r\n".getBytes("UTF-8")); + this.out.flush(); + + String line = this.inr.readLine(); + if(line == null || !line.startsWith("340 ")) + { + throw new IOException(line); + } + } + + public void writeArticle(Article article) + throws IOException, UnsupportedEncodingException + { + byte[] buf = new byte[512]; + ArticleInputStream in = new ArticleInputStream(article); + + preparePOST(); + + int len = in.read(buf); + while(len != -1) + { + writeLine(buf, len); + len = in.read(buf); + } + + finishPOST(); + } + + /** + * Writes the raw content of an article to the remote server. This method + * does no charset conversion/handling of any kind so its the preferred + * method for sending an article to remote peers. + * @param rawArticle + * @throws IOException + */ + public void writeArticle(byte[] rawArticle) + throws IOException + { + preparePOST(); + writeLine(rawArticle, rawArticle.length); + finishPOST(); + } + + /** + * Writes the given buffer to the connect remote server. + * @param buffer + * @param len + * @throws IOException + */ + protected void writeLine(byte[] buffer, int len) + throws IOException + { + this.out.write(buffer, 0, len); + this.out.flush(); + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/io/Resource.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/util/io/Resource.java Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,132 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sonews.util.io; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.Charset; + +/** + * Provides method for loading of resources. + * @author Christian Lins + * @since sonews/0.5.0 + */ +public final class Resource +{ + + /** + * Loads a resource and returns it as URL reference. + * The Resource's classloader is used to load the resource, not + * the System's ClassLoader so it may be safe to use this method + * in a sandboxed environment. + * @return + */ + public static URL getAsURL(final String name) + { + if(name == null) + { + return null; + } + + return Resource.class.getClassLoader().getResource(name); + } + + /** + * Loads a resource and returns an InputStream to it. + * @param name + * @return + */ + public static InputStream getAsStream(String name) + { + try + { + URL url = getAsURL(name); + if(url == null) + { + return null; + } + else + { + return url.openStream(); + } + } + catch(IOException e) + { + e.printStackTrace(); + return null; + } + } + + /** + * Loads a plain text resource. + * @param withNewline If false all newlines are removed from the + * return String + */ + public static String getAsString(String name, boolean withNewline) + { + if(name == null) + return null; + + BufferedReader in = null; + try + { + InputStream ins = getAsStream(name); + if(ins == null) + return null; + + in = new BufferedReader( + new InputStreamReader(ins, Charset.forName("UTF-8"))); + StringBuffer buf = new StringBuffer(); + + for(;;) + { + String line = in.readLine(); + if(line == null) + break; + + buf.append(line); + if(withNewline) + buf.append('\n'); + } + + return buf.toString(); + } + catch(Exception e) + { + e.printStackTrace(); + return null; + } + finally + { + try + { + if(in != null) + in.close(); + } + catch(IOException ex) + { + ex.printStackTrace(); + } + } + } + +} diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/io/package.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/util/io/package.html Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,1 @@ +Contains I/O utilitiy classes. \ No newline at end of file diff -r 9f0b95aafaa3 -r ed84c8bdd87b src/org/sonews/util/package.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/sonews/util/package.html Sun Aug 29 17:28:58 2010 +0200 @@ -0,0 +1,1 @@ +Contains various utility classes. \ No newline at end of file