org/sonews/daemon/command/OverCommand.java
author cli
Sat May 01 14:27:30 2010 +0200 (2010-05-01)
changeset 28 15d14b110240
parent 15 f2293e8566f5
permissions -rw-r--r--
Make mailinglist gateway more reliable, probably.
     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.command;
    20 
    21 import java.io.IOException;
    22 import java.util.List;
    23 import org.sonews.util.Log;
    24 import org.sonews.daemon.NNTPConnection;
    25 import org.sonews.storage.Article;
    26 import org.sonews.storage.ArticleHead;
    27 import org.sonews.storage.Headers;
    28 import org.sonews.storage.StorageBackendException;
    29 import org.sonews.util.Pair;
    30 
    31 /**
    32  * Class handling the OVER/XOVER command.
    33  * 
    34  * Description of the XOVER command:
    35  * <pre>
    36  * XOVER [range]
    37  *
    38  * The XOVER command returns information from the overview
    39  * database for the article(s) specified.
    40  *
    41  * The optional range argument may be any of the following:
    42  *              an article number
    43  *              an article number followed by a dash to indicate
    44  *                 all following
    45  *              an article number followed by a dash followed by
    46  *                 another article number
    47  *
    48  * If no argument is specified, then information from the
    49  * current article is displayed. Successful responses start
    50  * with a 224 response followed by the overview information
    51  * for all matched messages. Once the output is complete, a
    52  * period is sent on a line by itself. If no argument is
    53  * specified, the information for the current article is
    54  * returned.  A news group must have been selected earlier,
    55  * else a 412 error response is returned. If no articles are
    56  * in the range specified, a 420 error response is returned
    57  * by the server. A 502 response will be returned if the
    58  * client only has permission to transfer articles.
    59  *
    60  * Each line of output will be formatted with the article number,
    61  * followed by each of the headers in the overview database or the
    62  * article itself (when the data is not available in the overview
    63  * database) for that article separated by a tab character.  The
    64  * sequence of fields must be in this order: subject, author,
    65  * date, message-id, references, byte count, and line count. Other
    66  * optional fields may follow line count. Other optional fields may
    67  * follow line count. These fields are specified by examining the
    68  * response to the LIST OVERVIEW.FMT command. Where no data exists,
    69  * a null field must be provided (i.e. the output will have two tab
    70  * characters adjacent to each other). Servers should not output
    71  * fields for articles that have been removed since the XOVER database
    72  * was created.
    73  *
    74  * The LIST OVERVIEW.FMT command should be implemented if XOVER
    75  * is implemented. A client can use LIST OVERVIEW.FMT to determine
    76  * what optional fields  and in which order all fields will be
    77  * supplied by the XOVER command. 
    78  *
    79  * Note that any tab and end-of-line characters in any header
    80  * data that is returned will be converted to a space character.
    81  *
    82  * Responses:
    83  *
    84  *   224 Overview information follows
    85  *   412 No news group current selected
    86  *   420 No article(s) selected
    87  *   502 no permission
    88  *
    89  * OVER defines additional responses:
    90  *
    91  *  First form (message-id specified)
    92  *    224    Overview information follows (multi-line)
    93  *    430    No article with that message-id
    94  *
    95  *  Second form (range specified)
    96  *    224    Overview information follows (multi-line)
    97  *    412    No newsgroup selected
    98  *    423    No articles in that range
    99  *
   100  *  Third form (current article number used)
   101  *    224    Overview information follows (multi-line)
   102  *    412    No newsgroup selected
   103  *    420    Current article number is invalid
   104  *
   105  * </pre>
   106  * @author Christian Lins
   107  * @since sonews/0.5.0
   108  */
   109 public class OverCommand implements Command
   110 {
   111 
   112   public static final int MAX_LINES_PER_DBREQUEST = 200;
   113 
   114   @Override
   115   public String[] getSupportedCommandStrings()
   116   {
   117     return new String[]{"OVER", "XOVER"};
   118   }
   119 
   120   @Override
   121   public boolean hasFinished()
   122   {
   123     return true;
   124   }
   125 
   126   @Override
   127   public String impliedCapability()
   128   {
   129     return null;
   130   }
   131 
   132   @Override
   133   public boolean isStateful()
   134   {
   135     return false;
   136   }
   137 
   138   @Override
   139   public void processLine(NNTPConnection conn, final String line, byte[] raw)
   140     throws IOException, StorageBackendException
   141   {
   142     if(conn.getCurrentChannel() == null)
   143     {
   144       conn.println("412 no newsgroup selected");
   145     }
   146     else
   147     {
   148       String[] command = line.split(" ");
   149 
   150       // If no parameter was specified, show information about
   151       // the currently selected article(s)
   152       if(command.length == 1)
   153       {
   154         final Article art = conn.getCurrentArticle();
   155         if(art == null)
   156         {
   157           conn.println("420 no article(s) selected");
   158           return;
   159         }
   160 
   161         conn.println(buildOverview(art, -1));
   162       }
   163       // otherwise print information about the specified range
   164       else
   165       {
   166         long artStart;
   167         long artEnd   = conn.getCurrentChannel().getLastArticleNumber();
   168         String[] nums = command[1].split("-");
   169         if(nums.length >= 1)
   170         {
   171           try
   172           {
   173             artStart = Integer.parseInt(nums[0]);
   174           }
   175           catch(NumberFormatException e) 
   176           {
   177             Log.get().info(e.getMessage());
   178             artStart = Integer.parseInt(command[1]);
   179           }
   180         }
   181         else
   182         {
   183           artStart = conn.getCurrentChannel().getFirstArticleNumber();
   184         }
   185 
   186         if(nums.length >=2)
   187         {
   188           try
   189           {
   190             artEnd = Integer.parseInt(nums[1]);
   191           }
   192           catch(NumberFormatException e) 
   193           {
   194             e.printStackTrace();
   195           }
   196         }
   197 
   198         if(artStart > artEnd)
   199         {
   200           if(command[0].equalsIgnoreCase("OVER"))
   201           {
   202             conn.println("423 no articles in that range");
   203           }
   204           else
   205           {
   206             conn.println("224 (empty) overview information follows:");
   207             conn.println(".");
   208           }
   209         }
   210         else
   211         {
   212           for(long n = artStart; n <= artEnd; n += MAX_LINES_PER_DBREQUEST)
   213           {
   214             long nEnd = Math.min(n + MAX_LINES_PER_DBREQUEST - 1, artEnd);
   215             List<Pair<Long, ArticleHead>> articleHeads = conn.getCurrentChannel()
   216               .getArticleHeads(n, nEnd);
   217             if(articleHeads.isEmpty() && n == artStart
   218               && command[0].equalsIgnoreCase("OVER"))
   219             {
   220               // This reply is only valid for OVER, not for XOVER command
   221               conn.println("423 no articles in that range");
   222               return;
   223             }
   224             else if(n == artStart)
   225             {
   226               // XOVER replies this although there is no data available
   227               conn.println("224 overview information follows");
   228             }
   229 
   230             for(Pair<Long, ArticleHead> article : articleHeads)
   231             {
   232               String overview = buildOverview(article.getB(), article.getA());
   233               conn.println(overview);
   234             }
   235           } // for
   236           conn.println(".");
   237         }
   238       }
   239     }
   240   }
   241   
   242   private String buildOverview(ArticleHead art, long nr)
   243   {
   244     StringBuilder overview = new StringBuilder();
   245     overview.append(nr);
   246     overview.append('\t');
   247 
   248     String subject = art.getHeader(Headers.SUBJECT)[0];
   249     if("".equals(subject))
   250     {
   251       subject = "<empty>";
   252     }
   253     overview.append(escapeString(subject));
   254     overview.append('\t');
   255 
   256     overview.append(escapeString(art.getHeader(Headers.FROM)[0]));
   257     overview.append('\t');
   258     overview.append(escapeString(art.getHeader(Headers.DATE)[0]));
   259     overview.append('\t');
   260     overview.append(escapeString(art.getHeader(Headers.MESSAGE_ID)[0]));
   261     overview.append('\t');
   262     overview.append(escapeString(art.getHeader(Headers.REFERENCES)[0]));
   263     overview.append('\t');
   264 
   265     String bytes = art.getHeader(Headers.BYTES)[0];
   266     if("".equals(bytes))
   267     {
   268       bytes = "0";
   269     }
   270     overview.append(escapeString(bytes));
   271     overview.append('\t');
   272 
   273     String lines = art.getHeader(Headers.LINES)[0];
   274     if("".equals(lines))
   275     {
   276       lines = "0";
   277     }
   278     overview.append(escapeString(lines));
   279     overview.append('\t');
   280     overview.append(escapeString(art.getHeader(Headers.XREF)[0]));
   281 
   282     // Remove trailing tabs if some data is empty
   283     return overview.toString().trim();
   284   }
   285   
   286   private String escapeString(String str)
   287   {
   288     String nstr = str.replace("\r", "");
   289     nstr = nstr.replace('\n', ' ');
   290     nstr = nstr.replace('\t', ' ');
   291     return nstr.trim();
   292   }
   293   
   294 }