src/org/sonews/daemon/command/OverCommand.java
author Christian Lins <christian@lins.me>
Wed Sep 14 23:25:00 2011 +0200 (2011-09-14)
changeset 62 be4e87479855
parent 35 ed84c8bdd87b
permissions -rwxr-xr-x
Reformatting XDaemonCommand
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
cli@37
   112
	public static final int MAX_LINES_PER_DBREQUEST = 200;
chris@3
   113
cli@37
   114
	@Override
cli@37
   115
	public String[] getSupportedCommandStrings()
cli@37
   116
	{
cli@37
   117
		return new String[] {"OVER", "XOVER"};
cli@37
   118
	}
chris@1
   119
cli@37
   120
	@Override
cli@37
   121
	public boolean hasFinished()
cli@37
   122
	{
cli@37
   123
		return true;
cli@37
   124
	}
chris@1
   125
cli@37
   126
	@Override
cli@37
   127
	public String impliedCapability()
cli@37
   128
	{
cli@37
   129
		return null;
cli@37
   130
	}
cli@20
   131
cli@37
   132
	@Override
cli@37
   133
	public boolean isStateful()
cli@37
   134
	{
cli@37
   135
		return false;
cli@37
   136
	}
chris@3
   137
cli@37
   138
	@Override
cli@37
   139
	public void processLine(NNTPConnection conn, final String line, byte[] raw)
cli@37
   140
		throws IOException, StorageBackendException
cli@37
   141
	{
cli@37
   142
		if (conn.getCurrentChannel() == null) {
cli@37
   143
			conn.println("412 no newsgroup selected");
cli@37
   144
		} else {
cli@37
   145
			String[] command = line.split(" ");
chris@1
   146
cli@37
   147
			// If no parameter was specified, show information about
cli@37
   148
			// the currently selected article(s)
cli@37
   149
			if (command.length == 1) {
cli@37
   150
				final Article art = conn.getCurrentArticle();
cli@37
   151
				if (art == null) {
cli@37
   152
					conn.println("420 no article(s) selected");
cli@37
   153
					return;
cli@37
   154
				}
chris@1
   155
cli@37
   156
				conn.println(buildOverview(art, -1));
cli@37
   157
			} // otherwise print information about the specified range
cli@37
   158
			else {
cli@37
   159
				long artStart;
cli@37
   160
				long artEnd = conn.getCurrentChannel().getLastArticleNumber();
cli@37
   161
				String[] nums = command[1].split("-");
cli@37
   162
				if (nums.length >= 1) {
cli@37
   163
					try {
cli@37
   164
						artStart = Integer.parseInt(nums[0]);
cli@37
   165
					} catch (NumberFormatException e) {
cli@37
   166
						Log.get().info(e.getMessage());
cli@37
   167
						artStart = Integer.parseInt(command[1]);
cli@37
   168
					}
cli@37
   169
				} else {
cli@37
   170
					artStart = conn.getCurrentChannel().getFirstArticleNumber();
cli@37
   171
				}
chris@1
   172
cli@37
   173
				if (nums.length >= 2) {
cli@37
   174
					try {
cli@37
   175
						artEnd = Integer.parseInt(nums[1]);
cli@37
   176
					} catch (NumberFormatException e) {
cli@37
   177
						e.printStackTrace();
cli@37
   178
					}
cli@37
   179
				}
chris@1
   180
cli@37
   181
				if (artStart > artEnd) {
cli@37
   182
					if (command[0].equalsIgnoreCase("OVER")) {
cli@37
   183
						conn.println("423 no articles in that range");
cli@37
   184
					} else {
cli@37
   185
						conn.println("224 (empty) overview information follows:");
cli@37
   186
						conn.println(".");
cli@37
   187
					}
cli@37
   188
				} else {
cli@37
   189
					for (long n = artStart; n <= artEnd; n += MAX_LINES_PER_DBREQUEST) {
cli@37
   190
						long nEnd = Math.min(n + MAX_LINES_PER_DBREQUEST - 1, artEnd);
cli@37
   191
						List<Pair<Long, ArticleHead>> articleHeads = conn.getCurrentChannel().getArticleHeads(n, nEnd);
cli@37
   192
						if (articleHeads.isEmpty() && n == artStart
cli@37
   193
							&& command[0].equalsIgnoreCase("OVER")) {
cli@37
   194
							// This reply is only valid for OVER, not for XOVER command
cli@37
   195
							conn.println("423 no articles in that range");
cli@37
   196
							return;
cli@37
   197
						} else if (n == artStart) {
cli@37
   198
							// XOVER replies this although there is no data available
cli@37
   199
							conn.println("224 overview information follows");
cli@37
   200
						}
chris@1
   201
cli@37
   202
						for (Pair<Long, ArticleHead> article : articleHeads) {
cli@37
   203
							String overview = buildOverview(article.getB(), article.getA());
cli@37
   204
							conn.println(overview);
cli@37
   205
						}
cli@37
   206
					} // for
cli@37
   207
					conn.println(".");
cli@37
   208
				}
cli@37
   209
			}
cli@37
   210
		}
cli@37
   211
	}
chris@1
   212
cli@37
   213
	private String buildOverview(ArticleHead art, long nr)
cli@37
   214
	{
cli@37
   215
		StringBuilder overview = new StringBuilder();
cli@37
   216
		overview.append(nr);
cli@37
   217
		overview.append('\t');
chris@1
   218
cli@37
   219
		String subject = art.getHeader(Headers.SUBJECT)[0];
cli@37
   220
		if ("".equals(subject)) {
cli@37
   221
			subject = "<empty>";
cli@37
   222
		}
cli@37
   223
		overview.append(escapeString(subject));
cli@37
   224
		overview.append('\t');
chris@1
   225
cli@37
   226
		overview.append(escapeString(art.getHeader(Headers.FROM)[0]));
cli@37
   227
		overview.append('\t');
cli@37
   228
		overview.append(escapeString(art.getHeader(Headers.DATE)[0]));
cli@37
   229
		overview.append('\t');
cli@37
   230
		overview.append(escapeString(art.getHeader(Headers.MESSAGE_ID)[0]));
cli@37
   231
		overview.append('\t');
cli@37
   232
		overview.append(escapeString(art.getHeader(Headers.REFERENCES)[0]));
cli@37
   233
		overview.append('\t');
chris@1
   234
cli@37
   235
		String bytes = art.getHeader(Headers.BYTES)[0];
cli@37
   236
		if ("".equals(bytes)) {
cli@37
   237
			bytes = "0";
cli@37
   238
		}
cli@37
   239
		overview.append(escapeString(bytes));
cli@37
   240
		overview.append('\t');
chris@1
   241
cli@37
   242
		String lines = art.getHeader(Headers.LINES)[0];
cli@37
   243
		if ("".equals(lines)) {
cli@37
   244
			lines = "0";
cli@37
   245
		}
cli@37
   246
		overview.append(escapeString(lines));
cli@37
   247
		overview.append('\t');
cli@37
   248
		overview.append(escapeString(art.getHeader(Headers.XREF)[0]));
cli@37
   249
cli@37
   250
		// Remove trailing tabs if some data is empty
cli@37
   251
		return overview.toString().trim();
cli@37
   252
	}
cli@37
   253
cli@37
   254
	private String escapeString(String str)
cli@37
   255
	{
cli@37
   256
		String nstr = str.replace("\r", "");
cli@37
   257
		nstr = nstr.replace('\n', ' ');
cli@37
   258
		nstr = nstr.replace('\t', ' ');
cli@37
   259
		return nstr.trim();
cli@37
   260
	}
chris@1
   261
}