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