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