diff -r 000000000000 -r 1090e2141798 org/sonews/daemon/storage/Article.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/org/sonews/daemon/storage/Article.java Wed Jul 01 10:48:22 2009 +0200 @@ -0,0 +1,401 @@ +/* + * SONEWS News Server + * see AUTHORS for the list of contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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.storage; + +import org.sonews.daemon.Config; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.sql.SQLException; +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.Multipart; +import javax.mail.internet.InternetHeaders; +import javax.mail.internet.MimeUtility; +import org.sonews.util.Log; + +/** + * 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 Database. + * @param messageID + * @return null if Article is not found or if an error occurred. + */ + public static Article getByMessageID(final String messageID) + { + try + { + return Database.getInstance().getArticle(messageID); + } + catch(SQLException ex) + { + ex.printStackTrace(); + return null; + } + } + + public static Article getByArticleNumber(long articleIndex, Group group) + throws SQLException + { + return Database.getInstance().getArticle(articleIndex, group.getID()); + } + + private String body = ""; + private String headerSrc = null; + + /** + * Default constructor. + */ + public Article() + { + } + + /** + * Creates a new Article object using the date from the given + * raw data. + * This construction has only package visibility. + */ + Article(String headers, String 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. + * @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()); + } + + // The "content" of the message can be a String if it's a simple text/plain + // message, a Multipart object or an InputStream if the content is unknown. + final Object content = msg.getContent(); + if(content instanceof String) + { + this.body = (String)content; + } + else if(content instanceof Multipart) // probably subclass MimeMultipart + { + // We're are not interested in the different parts of the MultipartMessage, + // so we simply read in all data which *can* be huge. + InputStream in = msg.getInputStream(); + this.body = readContent(in); + } + else if(content instanceof InputStream) + { + // The message format is unknown to the Message class, but we can + // simply read in the whole message data. + this.body = readContent((InputStream)content); + } + else + { + // Unknown content is probably a malformed mail we should skip. + // On the other hand we produce an inconsistent mail mirror, but no + // mail system must transport invalid content. + Log.msg("Skipping message due to unknown content. Throwing exception...", true); + throw new MessagingException("Unknown content: " + content); + } + + // Validate headers + validateHeaders(); + } + + /** + * Reads lines from the given InputString into a String object. + * TODO: Move this generalized method to org.sonews.util.io.Resource. + * @param in + * @return + * @throws IOException + */ + private String readContent(InputStream in) + throws IOException + { + StringBuilder buf = new StringBuilder(); + + BufferedReader rin = new BufferedReader(new InputStreamReader(in)); + String line = rin.readLine(); + while(line != null) + { + buf.append('\n'); + buf.append(line); + line = rin.readLine(); + } + + return buf.toString(); + } + + /** + * 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 Database manually to make this + * change persistent. + * Note: a Message-ID should never be changed and only generated once. + */ + private String generateMessageID() + { + String msgID = "<" + UUID.randomUUID() + "@" + + Config.getInstance().get(Config.HOSTNAME, "localhost") + ">"; + + this.headers.setHeader(Headers.MESSAGE_ID, msgID); + + return msgID; + } + + /** + * Returns the body string. + */ + public String getBody() + { + return body; + } + + /** + * @return Charset of the body text + */ + public Charset getBodyCharset() + { + // We espect something like + // Content-Type: text/plain; charset=ISO-8859-15 + String contentType = getHeader(Headers.CONTENT_TYPE)[0]; + int idxCharsetStart = contentType.indexOf("charset=") + "charset=".length(); + int idxCharsetEnd = contentType.indexOf(";", idxCharsetStart); + + String charsetName = "UTF-8"; + if(idxCharsetStart >= 0 && idxCharsetStart < contentType.length()) + { + if(idxCharsetEnd < 0) + { + charsetName = contentType.substring(idxCharsetStart); + } + else + { + charsetName = contentType.substring(idxCharsetStart, idxCharsetEnd); + } + } + + // Sometimes there are '"' around the name + if(charsetName.length() > 2 && + charsetName.charAt(0) == '"' && charsetName.endsWith("\"")) + { + charsetName = charsetName.substring(1, charsetName.length() - 2); + } + + // Create charset + Charset charset = Charset.forName("UTF-8"); // This MUST be supported by JVM + try + { + charset = Charset.forName(charsetName); + } + catch(Exception ex) + { + Log.msg(ex.getMessage(), false); + Log.msg("Article.getBodyCharset(): Unknown charset: " + charsetName, false); + } + return charset; + } + + /** + * @return Numerical IDs of the newsgroups this Article belongs to. + */ + List getGroups() + { + String[] groupnames = getHeader(Headers.NEWSGROUPS)[0].split(","); + ArrayList groups = new ArrayList(); + + try + { + for(String newsgroup : groupnames) + { + newsgroup = newsgroup.trim(); + Group group = Database.getInstance().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 (SQLException ex) + { + ex.printStackTrace(); + return null; + } + return groups; + } + + public void setBody(String body) + { + this.body = body; + } + + /** + * + * @param groupname Name(s) of newsgroups + */ + public void setGroup(String groupname) + { + this.headers.setHeader(Headers.NEWSGROUPS, groupname); + } + + public String getMessageID() + { + String[] msgID = getHeader(Headers.MESSAGE_ID); + return msgID[0]; + } + + 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(); + + buf.append(entry.getName()); + buf.append(": "); + buf.append( + MimeUtility.fold(entry.getName().length() + 2, entry.getValue())); + + if(en.hasMoreElements()) + { + buf.append("\r\n"); + } + } + + this.headerSrc = buf.toString(); + return this.headerSrc; + } + + public long getIndexInGroup(Group group) + throws SQLException + { + return Database.getInstance().getArticleIndex(this, group); + } + + /** + * 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; + validateHeaders(); + } + + /** + * @return String containing the Message-ID. + */ + @Override + public String toString() + { + return getMessageID(); + } + + /** + * 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. + */ + private void validateHeaders() + { + // Check for valid Path-header + final String path = getHeader(Headers.PATH)[0]; + final String host = Config.getInstance().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()); + } + + // Generate a messageID if no one is existing + if(getMessageID().equals("")) + { + generateMessageID(); + } + } + +}