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