org/sonews/daemon/storage/Article.java
author chris <chris@marvin>
Fri Jun 26 16:48:50 2009 +0200 (2009-06-26)
changeset 1 6fceb66e1ad7
permissions -rw-r--r--
Hooray... sonews/0.5.0 final

HG: Enter commit message. Lines beginning with 'HG:' are removed.
HG: Remove all lines to abort the collapse operation.
     1 /*
     2  *   SONEWS News Server
     3  *   see AUTHORS for the list of contributors
     4  *
     5  *   This program is free software: you can redistribute it and/or modify
     6  *   it under the terms of the GNU General Public License as published by
     7  *   the Free Software Foundation, either version 3 of the License, or
     8  *   (at your option) any later version.
     9  *
    10  *   This program is distributed in the hope that it will be useful,
    11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  *   GNU General Public License for more details.
    14  *
    15  *   You should have received a copy of the GNU General Public License
    16  *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  */
    18 
    19 package org.sonews.daemon.storage;
    20 
    21 import org.sonews.daemon.Config;
    22 import java.io.BufferedReader;
    23 import java.io.ByteArrayInputStream;
    24 import java.io.IOException;
    25 import java.io.InputStream;
    26 import java.io.InputStreamReader;
    27 import java.nio.charset.Charset;
    28 import java.sql.SQLException;
    29 import java.util.UUID;
    30 import java.util.ArrayList;
    31 import java.util.Enumeration;
    32 import java.util.List;
    33 import javax.mail.Header;
    34 import javax.mail.Message;
    35 import javax.mail.MessagingException;
    36 import javax.mail.Multipart;
    37 import javax.mail.internet.InternetHeaders;
    38 import javax.mail.internet.MimeUtility;
    39 import org.sonews.util.Log;
    40 
    41 /**
    42  * Represents a newsgroup article.
    43  * @author Christian Lins
    44  * @author Denis Schwerdel
    45  * @since n3tpd/0.1
    46  */
    47 public class Article extends ArticleHead
    48 {
    49   
    50   /**
    51    * Loads the Article identified by the given ID from the Database.
    52    * @param messageID
    53    * @return null if Article is not found or if an error occurred.
    54    */
    55   public static Article getByMessageID(final String messageID)
    56   {
    57     try
    58     {
    59       return Database.getInstance().getArticle(messageID);
    60     }
    61     catch(SQLException ex)
    62     {
    63       ex.printStackTrace();
    64       return null;
    65     }
    66   }
    67   
    68   public static Article getByArticleNumber(long articleIndex, Group group)
    69     throws SQLException
    70   {
    71     return Database.getInstance().getArticle(articleIndex, group.getID()); 
    72   }
    73   
    74   private String              body       = "";
    75   private String              headerSrc  = null;
    76   
    77   /**
    78    * Default constructor.
    79    */
    80   public Article()
    81   {
    82   }
    83   
    84   /**
    85    * Creates a new Article object using the date from the given
    86    * raw data.
    87    * This construction has only package visibility.
    88    */
    89   Article(String headers, String body)
    90   {
    91     try
    92     {
    93       this.body  = body;
    94 
    95       // Parse the header
    96       this.headers = new InternetHeaders(
    97         new ByteArrayInputStream(headers.getBytes()));
    98       
    99       this.headerSrc = headers;
   100     }
   101     catch(MessagingException ex)
   102     {
   103       ex.printStackTrace();
   104     }
   105   }
   106 
   107   /**
   108    * Creates an Article instance using the data from the javax.mail.Message
   109    * object.
   110    * @see javax.mail.Message
   111    * @param msg
   112    * @throws IOException
   113    * @throws MessagingException
   114    */
   115   public Article(final Message msg)
   116     throws IOException, MessagingException
   117   {
   118     this.headers = new InternetHeaders();
   119 
   120     for(Enumeration e = msg.getAllHeaders() ; e.hasMoreElements();) 
   121     {
   122       final Header header = (Header)e.nextElement();
   123       this.headers.addHeader(header.getName(), header.getValue());
   124     }
   125     
   126     // The "content" of the message can be a String if it's a simple text/plain
   127     // message, a Multipart object or an InputStream if the content is unknown.
   128     final Object content = msg.getContent();
   129     if(content instanceof String)
   130     {
   131       this.body = (String)content;
   132     }
   133     else if(content instanceof Multipart) // probably subclass MimeMultipart
   134     {
   135       // We're are not interested in the different parts of the MultipartMessage,
   136       // so we simply read in all data which *can* be huge.
   137       InputStream in = msg.getInputStream();
   138       this.body = readContent(in);
   139     }
   140     else if(content instanceof InputStream)
   141     {
   142       // The message format is unknown to the Message class, but we can
   143       // simply read in the whole message data.
   144       this.body = readContent((InputStream)content);
   145     }
   146     else
   147     {
   148       // Unknown content is probably a malformed mail we should skip.
   149       // On the other hand we produce an inconsistent mail mirror, but no
   150       // mail system must transport invalid content.
   151       Log.msg("Skipping message due to unknown content. Throwing exception...", true);
   152       throw new MessagingException("Unknown content: " + content);
   153     }
   154     
   155     // Validate headers
   156     validateHeaders();
   157   }
   158 
   159   /**
   160    * Reads lines from the given InputString into a String object.
   161    * TODO: Move this generalized method to org.sonews.util.io.Resource.
   162    * @param in
   163    * @return
   164    * @throws IOException
   165    */
   166   private String readContent(InputStream in)
   167     throws IOException
   168   {
   169     StringBuilder buf = new StringBuilder();
   170     
   171     BufferedReader rin = new BufferedReader(new InputStreamReader(in));
   172     String line =  rin.readLine();
   173     while(line != null)
   174     {
   175       buf.append('\n');
   176       buf.append(line);
   177       line = rin.readLine();
   178     }
   179     
   180     return buf.toString();
   181   }
   182 
   183   /**
   184    * Removes the header identified by the given key.
   185    * @param headerKey
   186    */
   187   public void removeHeader(final String headerKey)
   188   {
   189     this.headers.removeHeader(headerKey);
   190     this.headerSrc = null;
   191   }
   192 
   193   /**
   194    * Generates a message id for this article and sets it into
   195    * the header object. You have to update the Database manually to make this
   196    * change persistent.
   197    * Note: a Message-ID should never be changed and only generated once.
   198    */
   199   private String generateMessageID()
   200   {
   201     String msgID = "<" + UUID.randomUUID() + "@"
   202         + Config.getInstance().get(Config.HOSTNAME, "localhost") + ">";
   203     
   204     this.headers.setHeader(Headers.MESSAGE_ID, msgID);
   205     
   206     return msgID;
   207   }
   208 
   209   /**
   210    * Returns the body string.
   211    */
   212   public String getBody()
   213   {
   214     return body;
   215   }
   216 
   217   /**
   218    * @return Charset of the body text
   219    */
   220   public Charset getBodyCharset()
   221   {
   222     // We espect something like 
   223     // Content-Type: text/plain; charset=ISO-8859-15
   224     String contentType = getHeader(Headers.CONTENT_TYPE)[0];
   225     int idxCharsetStart = contentType.indexOf("charset=") + "charset=".length();
   226     int idxCharsetEnd   = contentType.indexOf(";", idxCharsetStart);
   227     
   228     String charsetName = "UTF-8";
   229     if(idxCharsetStart >= 0 && idxCharsetStart < contentType.length())
   230     {
   231       if(idxCharsetEnd < 0)
   232       {
   233         charsetName = contentType.substring(idxCharsetStart);
   234       }
   235       else
   236       {
   237         charsetName = contentType.substring(idxCharsetStart, idxCharsetEnd);
   238       }
   239     }
   240     
   241     // Sometimes there are '"' around the name
   242     if(charsetName.length() > 2 &&
   243       charsetName.charAt(0) == '"' && charsetName.endsWith("\""))
   244     {
   245       charsetName = charsetName.substring(1, charsetName.length() - 2);
   246     }
   247     
   248     // Create charset
   249     Charset charset = Charset.forName("UTF-8"); // This MUST be supported by JVM
   250     try
   251     {
   252       charset = Charset.forName(charsetName);
   253     }
   254     catch(Exception ex)
   255     {
   256       Log.msg(ex.getMessage(), false);
   257       Log.msg("Article.getBodyCharset(): Unknown charset: " + charsetName, false);
   258     }
   259     return charset;
   260   }
   261   
   262   /**
   263    * @return Numerical IDs of the newsgroups this Article belongs to.
   264    */
   265   List<Group> getGroups()
   266   {
   267     String[]         groupnames = getHeader(Headers.NEWSGROUPS)[0].split(",");
   268     ArrayList<Group> groups     = new ArrayList<Group>();
   269 
   270     try
   271     {
   272       for(String newsgroup : groupnames)
   273       {
   274         newsgroup = newsgroup.trim();
   275         Group group = Database.getInstance().getGroup(newsgroup);
   276         if(group != null &&         // If the server does not provide the group, ignore it
   277           !groups.contains(group))  // Yes, there may be duplicates
   278         {
   279           groups.add(group);
   280         }
   281       }
   282     }
   283     catch (SQLException ex)
   284     {
   285       ex.printStackTrace();
   286       return null;
   287     }
   288     return groups;
   289   }
   290 
   291   public void setBody(String body)
   292   {
   293     this.body = body;
   294   }
   295   
   296   /**
   297    * 
   298    * @param groupname Name(s) of newsgroups
   299    */
   300   public void setGroup(String groupname)
   301   {
   302     this.headers.setHeader(Headers.NEWSGROUPS, groupname);
   303   }
   304 
   305   public String getMessageID()
   306   {
   307     String[] msgID = getHeader(Headers.MESSAGE_ID);
   308     return msgID[0];
   309   }
   310 
   311   public Enumeration getAllHeaders()
   312   {
   313     return this.headers.getAllHeaders();
   314   }
   315   
   316   /**
   317    * @return Header source code of this Article.
   318    */
   319   public String getHeaderSource()
   320   {
   321     if(this.headerSrc != null)
   322     {
   323       return this.headerSrc;
   324     }
   325 
   326     StringBuffer buf = new StringBuffer();
   327     
   328     for(Enumeration en = this.headers.getAllHeaders(); en.hasMoreElements();)
   329     {
   330       Header entry = (Header)en.nextElement();
   331 
   332       buf.append(entry.getName());
   333       buf.append(": ");
   334       buf.append(
   335         MimeUtility.fold(entry.getName().length() + 2, entry.getValue()));
   336 
   337       if(en.hasMoreElements())
   338       {
   339         buf.append("\r\n");
   340       }
   341     }
   342     
   343     this.headerSrc = buf.toString();
   344     return this.headerSrc;
   345   }
   346   
   347   public long getIndexInGroup(Group group)
   348     throws SQLException
   349   {
   350     return Database.getInstance().getArticleIndex(this, group);
   351   }
   352   
   353   /**
   354    * Sets the headers of this Article. If headers contain no
   355    * Message-Id a new one is created.
   356    * @param headers
   357    */
   358   public void setHeaders(InternetHeaders headers)
   359   {
   360     this.headers = headers;
   361     validateHeaders();
   362   }
   363   
   364   /**
   365    * @return String containing the Message-ID.
   366    */
   367   @Override
   368   public String toString()
   369   {
   370     return getMessageID();
   371   }
   372   
   373   /**
   374    * Checks some headers for their validity and generates an
   375    * appropriate Path-header for this host if not yet existing.
   376    * This method is called by some Article constructors and the
   377    * method setHeaders().
   378    * @return true if something on the headers was changed.
   379    */
   380   private void validateHeaders()
   381   {
   382     // Check for valid Path-header
   383     final String path = getHeader(Headers.PATH)[0];
   384     final String host = Config.getInstance().get(Config.HOSTNAME, "localhost");
   385     if(!path.startsWith(host))
   386     {
   387       StringBuffer pathBuf = new StringBuffer();
   388       pathBuf.append(host);
   389       pathBuf.append('!');
   390       pathBuf.append(path);
   391       this.headers.setHeader(Headers.PATH, pathBuf.toString());
   392     }
   393     
   394     // Generate a messageID if no one is existing
   395     if(getMessageID().equals(""))
   396     {
   397       generateMessageID();
   398     }
   399   }
   400 
   401 }