chris@1: /*
chris@1:  *   SONEWS News Server
chris@1:  *   see AUTHORS for the list of contributors
chris@1:  *
chris@1:  *   This program is free software: you can redistribute it and/or modify
chris@1:  *   it under the terms of the GNU General Public License as published by
chris@1:  *   the Free Software Foundation, either version 3 of the License, or
chris@1:  *   (at your option) any later version.
chris@1:  *
chris@1:  *   This program is distributed in the hope that it will be useful,
chris@1:  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
chris@1:  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
chris@1:  *   GNU General Public License for more details.
chris@1:  *
chris@1:  *   You should have received a copy of the GNU General Public License
chris@1:  *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
chris@1:  */
chris@1: 
chris@1: package org.sonews.daemon.command;
chris@1: 
chris@1: import java.io.IOException;
chris@1: import java.util.List;
chris@1: import org.sonews.util.Log;
chris@1: import org.sonews.daemon.NNTPConnection;
chris@3: import org.sonews.storage.Article;
chris@3: import org.sonews.storage.ArticleHead;
chris@3: import org.sonews.storage.Headers;
chris@3: import org.sonews.storage.StorageBackendException;
chris@1: import org.sonews.util.Pair;
chris@1: 
chris@1: /**
chris@1:  * Class handling the OVER/XOVER command.
chris@1:  * 
chris@1:  * Description of the XOVER command:
chris@1:  * <pre>
chris@1:  * XOVER [range]
chris@1:  *
chris@1:  * The XOVER command returns information from the overview
chris@1:  * database for the article(s) specified.
chris@1:  *
chris@1:  * The optional range argument may be any of the following:
chris@1:  *              an article number
chris@1:  *              an article number followed by a dash to indicate
chris@1:  *                 all following
chris@1:  *              an article number followed by a dash followed by
chris@1:  *                 another article number
chris@1:  *
chris@1:  * If no argument is specified, then information from the
chris@1:  * current article is displayed. Successful responses start
chris@1:  * with a 224 response followed by the overview information
chris@1:  * for all matched messages. Once the output is complete, a
chris@1:  * period is sent on a line by itself. If no argument is
chris@1:  * specified, the information for the current article is
chris@1:  * returned.  A news group must have been selected earlier,
chris@1:  * else a 412 error response is returned. If no articles are
chris@1:  * in the range specified, a 420 error response is returned
chris@1:  * by the server. A 502 response will be returned if the
chris@1:  * client only has permission to transfer articles.
chris@1:  *
chris@1:  * Each line of output will be formatted with the article number,
chris@1:  * followed by each of the headers in the overview database or the
chris@1:  * article itself (when the data is not available in the overview
chris@1:  * database) for that article separated by a tab character.  The
chris@1:  * sequence of fields must be in this order: subject, author,
chris@1:  * date, message-id, references, byte count, and line count. Other
chris@1:  * optional fields may follow line count. Other optional fields may
chris@1:  * follow line count. These fields are specified by examining the
chris@1:  * response to the LIST OVERVIEW.FMT command. Where no data exists,
chris@1:  * a null field must be provided (i.e. the output will have two tab
chris@1:  * characters adjacent to each other). Servers should not output
chris@1:  * fields for articles that have been removed since the XOVER database
chris@1:  * was created.
chris@1:  *
chris@1:  * The LIST OVERVIEW.FMT command should be implemented if XOVER
chris@1:  * is implemented. A client can use LIST OVERVIEW.FMT to determine
chris@1:  * what optional fields  and in which order all fields will be
chris@1:  * supplied by the XOVER command. 
chris@1:  *
chris@1:  * Note that any tab and end-of-line characters in any header
chris@1:  * data that is returned will be converted to a space character.
chris@1:  *
chris@1:  * Responses:
chris@1:  *
chris@1:  *   224 Overview information follows
chris@1:  *   412 No news group current selected
chris@1:  *   420 No article(s) selected
chris@1:  *   502 no permission
chris@1:  *
chris@1:  * OVER defines additional responses:
chris@1:  *
chris@1:  *  First form (message-id specified)
chris@1:  *    224    Overview information follows (multi-line)
chris@1:  *    430    No article with that message-id
chris@1:  *
chris@1:  *  Second form (range specified)
chris@1:  *    224    Overview information follows (multi-line)
chris@1:  *    412    No newsgroup selected
chris@1:  *    423    No articles in that range
chris@1:  *
chris@1:  *  Third form (current article number used)
chris@1:  *    224    Overview information follows (multi-line)
chris@1:  *    412    No newsgroup selected
chris@1:  *    420    Current article number is invalid
chris@1:  *
chris@1:  * </pre>
chris@1:  * @author Christian Lins
chris@1:  * @since sonews/0.5.0
chris@1:  */
chris@3: public class OverCommand implements Command
chris@1: {
chris@1: 
chris@3:   public static final int MAX_LINES_PER_DBREQUEST = 200;
chris@3: 
chris@3:   @Override
chris@3:   public String[] getSupportedCommandStrings()
chris@1:   {
chris@3:     return new String[]{"OVER", "XOVER"};
chris@1:   }
chris@1: 
chris@1:   @Override
chris@1:   public boolean hasFinished()
chris@1:   {
chris@1:     return true;
chris@1:   }
chris@1: 
chris@1:   @Override
cli@20:   public String impliedCapability()
cli@20:   {
cli@20:     return null;
cli@20:   }
cli@20: 
cli@20:   @Override
chris@3:   public boolean isStateful()
chris@1:   {
chris@3:     return false;
chris@3:   }
chris@3: 
chris@3:   @Override
chris@3:   public void processLine(NNTPConnection conn, final String line, byte[] raw)
chris@3:     throws IOException, StorageBackendException
chris@3:   {
chris@3:     if(conn.getCurrentChannel() == null)
chris@1:     {
chris@3:       conn.println("412 no newsgroup selected");
chris@1:     }
chris@1:     else
chris@1:     {
chris@1:       String[] command = line.split(" ");
chris@1: 
chris@1:       // If no parameter was specified, show information about
chris@1:       // the currently selected article(s)
chris@1:       if(command.length == 1)
chris@1:       {
chris@3:         final Article art = conn.getCurrentArticle();
chris@1:         if(art == null)
chris@1:         {
chris@3:           conn.println("420 no article(s) selected");
chris@1:           return;
chris@1:         }
chris@1: 
chris@3:         conn.println(buildOverview(art, -1));
chris@1:       }
chris@1:       // otherwise print information about the specified range
chris@1:       else
chris@1:       {
chris@3:         long artStart;
chris@3:         long artEnd   = conn.getCurrentChannel().getLastArticleNumber();
chris@1:         String[] nums = command[1].split("-");
chris@1:         if(nums.length >= 1)
chris@1:         {
chris@1:           try
chris@1:           {
chris@1:             artStart = Integer.parseInt(nums[0]);
chris@1:           }
chris@1:           catch(NumberFormatException e) 
chris@1:           {
cli@15:             Log.get().info(e.getMessage());
chris@1:             artStart = Integer.parseInt(command[1]);
chris@1:           }
chris@1:         }
chris@1:         else
chris@1:         {
chris@3:           artStart = conn.getCurrentChannel().getFirstArticleNumber();
chris@1:         }
chris@1: 
chris@1:         if(nums.length >=2)
chris@1:         {
chris@1:           try
chris@1:           {
chris@1:             artEnd = Integer.parseInt(nums[1]);
chris@1:           }
chris@1:           catch(NumberFormatException e) 
chris@1:           {
chris@1:             e.printStackTrace();
chris@1:           }
chris@1:         }
chris@1: 
chris@1:         if(artStart > artEnd)
chris@1:         {
chris@1:           if(command[0].equalsIgnoreCase("OVER"))
chris@1:           {
chris@3:             conn.println("423 no articles in that range");
chris@1:           }
chris@1:           else
chris@1:           {
chris@3:             conn.println("224 (empty) overview information follows:");
chris@3:             conn.println(".");
chris@1:           }
chris@1:         }
chris@1:         else
chris@1:         {
chris@3:           for(long n = artStart; n <= artEnd; n += MAX_LINES_PER_DBREQUEST)
chris@1:           {
chris@3:             long nEnd = Math.min(n + MAX_LINES_PER_DBREQUEST - 1, artEnd);
chris@3:             List<Pair<Long, ArticleHead>> articleHeads = conn.getCurrentChannel()
chris@1:               .getArticleHeads(n, nEnd);
chris@1:             if(articleHeads.isEmpty() && n == artStart
chris@1:               && command[0].equalsIgnoreCase("OVER"))
chris@1:             {
chris@1:               // This reply is only valid for OVER, not for XOVER command
chris@3:               conn.println("423 no articles in that range");
chris@1:               return;
chris@1:             }
chris@1:             else if(n == artStart)
chris@1:             {
chris@1:               // XOVER replies this although there is no data available
chris@3:               conn.println("224 overview information follows");
chris@1:             }
chris@1: 
chris@1:             for(Pair<Long, ArticleHead> article : articleHeads)
chris@1:             {
chris@1:               String overview = buildOverview(article.getB(), article.getA());
chris@3:               conn.println(overview);
chris@1:             }
chris@1:           } // for
chris@3:           conn.println(".");
chris@1:         }
chris@1:       }
chris@1:     }
chris@1:   }
chris@1:   
chris@1:   private String buildOverview(ArticleHead art, long nr)
chris@1:   {
chris@1:     StringBuilder overview = new StringBuilder();
chris@1:     overview.append(nr);
chris@1:     overview.append('\t');
chris@1: 
chris@1:     String subject = art.getHeader(Headers.SUBJECT)[0];
chris@1:     if("".equals(subject))
chris@1:     {
chris@1:       subject = "<empty>";
chris@1:     }
chris@1:     overview.append(escapeString(subject));
chris@1:     overview.append('\t');
chris@1: 
chris@1:     overview.append(escapeString(art.getHeader(Headers.FROM)[0]));
chris@1:     overview.append('\t');
chris@1:     overview.append(escapeString(art.getHeader(Headers.DATE)[0]));
chris@1:     overview.append('\t');
chris@1:     overview.append(escapeString(art.getHeader(Headers.MESSAGE_ID)[0]));
chris@1:     overview.append('\t');
chris@1:     overview.append(escapeString(art.getHeader(Headers.REFERENCES)[0]));
chris@1:     overview.append('\t');
chris@1: 
chris@1:     String bytes = art.getHeader(Headers.BYTES)[0];
chris@1:     if("".equals(bytes))
chris@1:     {
chris@1:       bytes = "0";
chris@1:     }
chris@1:     overview.append(escapeString(bytes));
chris@1:     overview.append('\t');
chris@1: 
chris@1:     String lines = art.getHeader(Headers.LINES)[0];
chris@1:     if("".equals(lines))
chris@1:     {
chris@1:       lines = "0";
chris@1:     }
chris@1:     overview.append(escapeString(lines));
chris@1:     overview.append('\t');
chris@1:     overview.append(escapeString(art.getHeader(Headers.XREF)[0]));
chris@1: 
chris@1:     // Remove trailing tabs if some data is empty
chris@1:     return overview.toString().trim();
chris@1:   }
chris@1:   
chris@1:   private String escapeString(String str)
chris@1:   {
chris@1:     String nstr = str.replace("\r", "");
chris@1:     nstr = nstr.replace('\n', ' ');
chris@1:     nstr = nstr.replace('\t', ' ');
chris@1:     return nstr.trim();
chris@1:   }
chris@1:   
chris@1: }