org/sonews/daemon/command/OverCommand.java
author cli
Thu Aug 20 14:31:19 2009 +0200 (2009-08-20)
changeset 12 bb6990c0dd1a
parent 1 6fceb66e1ad7
child 15 f2293e8566f5
permissions -rw-r--r--
Merging fixes from sonews/1.0.3
     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 boolean isStateful()
   128   {
   129     return false;
   130   }
   131 
   132   @Override
   133   public void processLine(NNTPConnection conn, final String line, byte[] raw)
   134     throws IOException, StorageBackendException
   135   {
   136     if(conn.getCurrentChannel() == null)
   137     {
   138       conn.println("412 no newsgroup selected");
   139     }
   140     else
   141     {
   142       String[] command = line.split(" ");
   143 
   144       // If no parameter was specified, show information about
   145       // the currently selected article(s)
   146       if(command.length == 1)
   147       {
   148         final Article art = conn.getCurrentArticle();
   149         if(art == null)
   150         {
   151           conn.println("420 no article(s) selected");
   152           return;
   153         }
   154 
   155         conn.println(buildOverview(art, -1));
   156       }
   157       // otherwise print information about the specified range
   158       else
   159       {
   160         long artStart;
   161         long artEnd   = conn.getCurrentChannel().getLastArticleNumber();
   162         String[] nums = command[1].split("-");
   163         if(nums.length >= 1)
   164         {
   165           try
   166           {
   167             artStart = Integer.parseInt(nums[0]);
   168           }
   169           catch(NumberFormatException e) 
   170           {
   171             Log.msg(e.getMessage(), true);
   172             artStart = Integer.parseInt(command[1]);
   173           }
   174         }
   175         else
   176         {
   177           artStart = conn.getCurrentChannel().getFirstArticleNumber();
   178         }
   179 
   180         if(nums.length >=2)
   181         {
   182           try
   183           {
   184             artEnd = Integer.parseInt(nums[1]);
   185           }
   186           catch(NumberFormatException e) 
   187           {
   188             e.printStackTrace();
   189           }
   190         }
   191 
   192         if(artStart > artEnd)
   193         {
   194           if(command[0].equalsIgnoreCase("OVER"))
   195           {
   196             conn.println("423 no articles in that range");
   197           }
   198           else
   199           {
   200             conn.println("224 (empty) overview information follows:");
   201             conn.println(".");
   202           }
   203         }
   204         else
   205         {
   206           for(long n = artStart; n <= artEnd; n += MAX_LINES_PER_DBREQUEST)
   207           {
   208             long nEnd = Math.min(n + MAX_LINES_PER_DBREQUEST - 1, artEnd);
   209             List<Pair<Long, ArticleHead>> articleHeads = conn.getCurrentChannel()
   210               .getArticleHeads(n, nEnd);
   211             if(articleHeads.isEmpty() && n == artStart
   212               && command[0].equalsIgnoreCase("OVER"))
   213             {
   214               // This reply is only valid for OVER, not for XOVER command
   215               conn.println("423 no articles in that range");
   216               return;
   217             }
   218             else if(n == artStart)
   219             {
   220               // XOVER replies this although there is no data available
   221               conn.println("224 overview information follows");
   222             }
   223 
   224             for(Pair<Long, ArticleHead> article : articleHeads)
   225             {
   226               String overview = buildOverview(article.getB(), article.getA());
   227               conn.println(overview);
   228             }
   229           } // for
   230           conn.println(".");
   231         }
   232       }
   233     }
   234   }
   235   
   236   private String buildOverview(ArticleHead art, long nr)
   237   {
   238     StringBuilder overview = new StringBuilder();
   239     overview.append(nr);
   240     overview.append('\t');
   241 
   242     String subject = art.getHeader(Headers.SUBJECT)[0];
   243     if("".equals(subject))
   244     {
   245       subject = "<empty>";
   246     }
   247     overview.append(escapeString(subject));
   248     overview.append('\t');
   249 
   250     overview.append(escapeString(art.getHeader(Headers.FROM)[0]));
   251     overview.append('\t');
   252     overview.append(escapeString(art.getHeader(Headers.DATE)[0]));
   253     overview.append('\t');
   254     overview.append(escapeString(art.getHeader(Headers.MESSAGE_ID)[0]));
   255     overview.append('\t');
   256     overview.append(escapeString(art.getHeader(Headers.REFERENCES)[0]));
   257     overview.append('\t');
   258 
   259     String bytes = art.getHeader(Headers.BYTES)[0];
   260     if("".equals(bytes))
   261     {
   262       bytes = "0";
   263     }
   264     overview.append(escapeString(bytes));
   265     overview.append('\t');
   266 
   267     String lines = art.getHeader(Headers.LINES)[0];
   268     if("".equals(lines))
   269     {
   270       lines = "0";
   271     }
   272     overview.append(escapeString(lines));
   273     overview.append('\t');
   274     overview.append(escapeString(art.getHeader(Headers.XREF)[0]));
   275 
   276     // Remove trailing tabs if some data is empty
   277     return overview.toString().trim();
   278   }
   279   
   280   private String escapeString(String str)
   281   {
   282     String nstr = str.replace("\r", "");
   283     nstr = nstr.replace('\n', ' ');
   284     nstr = nstr.replace('\t', ' ');
   285     return nstr.trim();
   286   }
   287   
   288 }