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