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 . 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: *
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:  * 
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: cli@37: public static final int MAX_LINES_PER_DBREQUEST = 200; chris@3: cli@37: @Override cli@37: public String[] getSupportedCommandStrings() cli@37: { cli@37: return new String[] {"OVER", "XOVER"}; cli@37: } chris@1: cli@37: @Override cli@37: public boolean hasFinished() cli@37: { cli@37: return true; cli@37: } chris@1: cli@37: @Override cli@37: public String impliedCapability() cli@37: { cli@37: return null; cli@37: } cli@20: cli@37: @Override cli@37: public boolean isStateful() cli@37: { cli@37: return false; cli@37: } chris@3: cli@37: @Override cli@37: public void processLine(NNTPConnection conn, final String line, byte[] raw) cli@37: throws IOException, StorageBackendException cli@37: { cli@37: if (conn.getCurrentChannel() == null) { cli@37: conn.println("412 no newsgroup selected"); cli@37: } else { cli@37: String[] command = line.split(" "); chris@1: cli@37: // If no parameter was specified, show information about cli@37: // the currently selected article(s) cli@37: if (command.length == 1) { cli@37: final Article art = conn.getCurrentArticle(); cli@37: if (art == null) { cli@37: conn.println("420 no article(s) selected"); cli@37: return; cli@37: } chris@1: cli@37: conn.println(buildOverview(art, -1)); cli@37: } // otherwise print information about the specified range cli@37: else { cli@37: long artStart; cli@37: long artEnd = conn.getCurrentChannel().getLastArticleNumber(); cli@37: String[] nums = command[1].split("-"); cli@37: if (nums.length >= 1) { cli@37: try { cli@37: artStart = Integer.parseInt(nums[0]); cli@37: } catch (NumberFormatException e) { cli@37: Log.get().info(e.getMessage()); cli@37: artStart = Integer.parseInt(command[1]); cli@37: } cli@37: } else { cli@37: artStart = conn.getCurrentChannel().getFirstArticleNumber(); cli@37: } chris@1: cli@37: if (nums.length >= 2) { cli@37: try { cli@37: artEnd = Integer.parseInt(nums[1]); cli@37: } catch (NumberFormatException e) { cli@37: e.printStackTrace(); cli@37: } cli@37: } chris@1: cli@37: if (artStart > artEnd) { cli@37: if (command[0].equalsIgnoreCase("OVER")) { cli@37: conn.println("423 no articles in that range"); cli@37: } else { cli@37: conn.println("224 (empty) overview information follows:"); cli@37: conn.println("."); cli@37: } cli@37: } else { cli@37: for (long n = artStart; n <= artEnd; n += MAX_LINES_PER_DBREQUEST) { cli@37: long nEnd = Math.min(n + MAX_LINES_PER_DBREQUEST - 1, artEnd); cli@37: List> articleHeads = conn.getCurrentChannel().getArticleHeads(n, nEnd); cli@37: if (articleHeads.isEmpty() && n == artStart cli@37: && command[0].equalsIgnoreCase("OVER")) { cli@37: // This reply is only valid for OVER, not for XOVER command cli@37: conn.println("423 no articles in that range"); cli@37: return; cli@37: } else if (n == artStart) { cli@37: // XOVER replies this although there is no data available cli@37: conn.println("224 overview information follows"); cli@37: } chris@1: cli@37: for (Pair article : articleHeads) { cli@37: String overview = buildOverview(article.getB(), article.getA()); cli@37: conn.println(overview); cli@37: } cli@37: } // for cli@37: conn.println("."); cli@37: } cli@37: } cli@37: } cli@37: } chris@1: cli@37: private String buildOverview(ArticleHead art, long nr) cli@37: { cli@37: StringBuilder overview = new StringBuilder(); cli@37: overview.append(nr); cli@37: overview.append('\t'); chris@1: cli@37: String subject = art.getHeader(Headers.SUBJECT)[0]; cli@37: if ("".equals(subject)) { cli@37: subject = ""; cli@37: } cli@37: overview.append(escapeString(subject)); cli@37: overview.append('\t'); chris@1: cli@37: overview.append(escapeString(art.getHeader(Headers.FROM)[0])); cli@37: overview.append('\t'); cli@37: overview.append(escapeString(art.getHeader(Headers.DATE)[0])); cli@37: overview.append('\t'); cli@37: overview.append(escapeString(art.getHeader(Headers.MESSAGE_ID)[0])); cli@37: overview.append('\t'); cli@37: overview.append(escapeString(art.getHeader(Headers.REFERENCES)[0])); cli@37: overview.append('\t'); chris@1: cli@37: String bytes = art.getHeader(Headers.BYTES)[0]; cli@37: if ("".equals(bytes)) { cli@37: bytes = "0"; cli@37: } cli@37: overview.append(escapeString(bytes)); cli@37: overview.append('\t'); chris@1: cli@37: String lines = art.getHeader(Headers.LINES)[0]; cli@37: if ("".equals(lines)) { cli@37: lines = "0"; cli@37: } cli@37: overview.append(escapeString(lines)); cli@37: overview.append('\t'); cli@37: overview.append(escapeString(art.getHeader(Headers.XREF)[0])); cli@37: cli@37: // Remove trailing tabs if some data is empty cli@37: return overview.toString().trim(); cli@37: } cli@37: cli@37: private String escapeString(String str) cli@37: { cli@37: String nstr = str.replace("\r", ""); cli@37: nstr = nstr.replace('\n', ' '); cli@37: nstr = nstr.replace('\t', ' '); cli@37: return nstr.trim(); cli@37: } chris@1: }