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