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