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
     1 /*
     2  *   SONEWS News Server
     3  *   see AUTHORS for the list of contributors
     4  *
     5  *   This program is free software: you can redistribute it and/or modify
     6  *   it under the terms of the GNU General Public License as published by
     7  *   the Free Software Foundation, either version 3 of the License, or
     8  *   (at your option) any later version.
     9  *
    10  *   This program is distributed in the hope that it will be useful,
    11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  *   GNU General Public License for more details.
    14  *
    15  *   You should have received a copy of the GNU General Public License
    16  *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  */
    18 
    19 package org.sonews.daemon.command;
    20 
    21 import java.io.IOException;
    22 import java.util.List;
    23 import org.sonews.util.Log;
    24 import org.sonews.daemon.NNTPConnection;
    25 import org.sonews.storage.Article;
    26 import org.sonews.storage.ArticleHead;
    27 import org.sonews.storage.Headers;
    28 import org.sonews.storage.StorageBackendException;
    29 import org.sonews.util.Pair;
    30 
    31 /**
    32  * Class handling the OVER/XOVER command.
    33  * 
    34  * Description of the XOVER command:
    35  * <pre>
    36  * XOVER [range]
    37  *
    38  * The XOVER command returns information from the overview
    39  * database for the article(s) specified.
    40  *
    41  * The optional range argument may be any of the following:
    42  *              an article number
    43  *              an article number followed by a dash to indicate
    44  *                 all following
    45  *              an article number followed by a dash followed by
    46  *                 another article number
    47  *
    48  * If no argument is specified, then information from the
    49  * current article is displayed. Successful responses start
    50  * with a 224 response followed by the overview information
    51  * for all matched messages. Once the output is complete, a
    52  * period is sent on a line by itself. If no argument is
    53  * specified, the information for the current article is
    54  * returned.  A news group must have been selected earlier,
    55  * else a 412 error response is returned. If no articles are
    56  * in the range specified, a 420 error response is returned
    57  * by the server. A 502 response will be returned if the
    58  * client only has permission to transfer articles.
    59  *
    60  * Each line of output will be formatted with the article number,
    61  * followed by each of the headers in the overview database or the
    62  * article itself (when the data is not available in the overview
    63  * database) for that article separated by a tab character.  The
    64  * sequence of fields must be in this order: subject, author,
    65  * date, message-id, references, byte count, and line count. Other
    66  * optional fields may follow line count. Other optional fields may
    67  * follow line count. These fields are specified by examining the
    68  * response to the LIST OVERVIEW.FMT command. Where no data exists,
    69  * a null field must be provided (i.e. the output will have two tab
    70  * characters adjacent to each other). Servers should not output
    71  * fields for articles that have been removed since the XOVER database
    72  * was created.
    73  *
    74  * The LIST OVERVIEW.FMT command should be implemented if XOVER
    75  * is implemented. A client can use LIST OVERVIEW.FMT to determine
    76  * what optional fields  and in which order all fields will be
    77  * supplied by the XOVER command. 
    78  *
    79  * Note that any tab and end-of-line characters in any header
    80  * data that is returned will be converted to a space character.
    81  *
    82  * Responses:
    83  *
    84  *   224 Overview information follows
    85  *   412 No news group current selected
    86  *   420 No article(s) selected
    87  *   502 no permission
    88  *
    89  * OVER defines additional responses:
    90  *
    91  *  First form (message-id specified)
    92  *    224    Overview information follows (multi-line)
    93  *    430    No article with that message-id
    94  *
    95  *  Second form (range specified)
    96  *    224    Overview information follows (multi-line)
    97  *    412    No newsgroup selected
    98  *    423    No articles in that range
    99  *
   100  *  Third form (current article number used)
   101  *    224    Overview information follows (multi-line)
   102  *    412    No newsgroup selected
   103  *    420    Current article number is invalid
   104  *
   105  * </pre>
   106  * @author Christian Lins
   107  * @since sonews/0.5.0
   108  */
   109 public class OverCommand implements Command
   110 {
   111 
   112 	public static final int MAX_LINES_PER_DBREQUEST = 200;
   113 
   114 	@Override
   115 	public String[] getSupportedCommandStrings()
   116 	{
   117 		return new String[] {"OVER", "XOVER"};
   118 	}
   119 
   120 	@Override
   121 	public boolean hasFinished()
   122 	{
   123 		return true;
   124 	}
   125 
   126 	@Override
   127 	public String impliedCapability()
   128 	{
   129 		return null;
   130 	}
   131 
   132 	@Override
   133 	public boolean isStateful()
   134 	{
   135 		return false;
   136 	}
   137 
   138 	@Override
   139 	public void processLine(NNTPConnection conn, final String line, byte[] raw)
   140 		throws IOException, StorageBackendException
   141 	{
   142 		if (conn.getCurrentChannel() == null) {
   143 			conn.println("412 no newsgroup selected");
   144 		} else {
   145 			String[] command = line.split(" ");
   146 
   147 			// If no parameter was specified, show information about
   148 			// the currently selected article(s)
   149 			if (command.length == 1) {
   150 				final Article art = conn.getCurrentArticle();
   151 				if (art == null) {
   152 					conn.println("420 no article(s) selected");
   153 					return;
   154 				}
   155 
   156 				conn.println(buildOverview(art, -1));
   157 			} // otherwise print information about the specified range
   158 			else {
   159 				long artStart;
   160 				long artEnd = conn.getCurrentChannel().getLastArticleNumber();
   161 				String[] nums = command[1].split("-");
   162 				if (nums.length >= 1) {
   163 					try {
   164 						artStart = Integer.parseInt(nums[0]);
   165 					} catch (NumberFormatException e) {
   166 						Log.get().info(e.getMessage());
   167 						artStart = Integer.parseInt(command[1]);
   168 					}
   169 				} else {
   170 					artStart = conn.getCurrentChannel().getFirstArticleNumber();
   171 				}
   172 
   173 				if (nums.length >= 2) {
   174 					try {
   175 						artEnd = Integer.parseInt(nums[1]);
   176 					} catch (NumberFormatException e) {
   177 						e.printStackTrace();
   178 					}
   179 				}
   180 
   181 				if (artStart > artEnd) {
   182 					if (command[0].equalsIgnoreCase("OVER")) {
   183 						conn.println("423 no articles in that range");
   184 					} else {
   185 						conn.println("224 (empty) overview information follows:");
   186 						conn.println(".");
   187 					}
   188 				} else {
   189 					for (long n = artStart; n <= artEnd; n += MAX_LINES_PER_DBREQUEST) {
   190 						long nEnd = Math.min(n + MAX_LINES_PER_DBREQUEST - 1, artEnd);
   191 						List<Pair<Long, ArticleHead>> articleHeads = conn.getCurrentChannel().getArticleHeads(n, nEnd);
   192 						if (articleHeads.isEmpty() && n == artStart
   193 							&& command[0].equalsIgnoreCase("OVER")) {
   194 							// This reply is only valid for OVER, not for XOVER command
   195 							conn.println("423 no articles in that range");
   196 							return;
   197 						} else if (n == artStart) {
   198 							// XOVER replies this although there is no data available
   199 							conn.println("224 overview information follows");
   200 						}
   201 
   202 						for (Pair<Long, ArticleHead> article : articleHeads) {
   203 							String overview = buildOverview(article.getB(), article.getA());
   204 							conn.println(overview);
   205 						}
   206 					} // for
   207 					conn.println(".");
   208 				}
   209 			}
   210 		}
   211 	}
   212 
   213 	private String buildOverview(ArticleHead art, long nr)
   214 	{
   215 		StringBuilder overview = new StringBuilder();
   216 		overview.append(nr);
   217 		overview.append('\t');
   218 
   219 		String subject = art.getHeader(Headers.SUBJECT)[0];
   220 		if ("".equals(subject)) {
   221 			subject = "<empty>";
   222 		}
   223 		overview.append(escapeString(subject));
   224 		overview.append('\t');
   225 
   226 		overview.append(escapeString(art.getHeader(Headers.FROM)[0]));
   227 		overview.append('\t');
   228 		overview.append(escapeString(art.getHeader(Headers.DATE)[0]));
   229 		overview.append('\t');
   230 		overview.append(escapeString(art.getHeader(Headers.MESSAGE_ID)[0]));
   231 		overview.append('\t');
   232 		overview.append(escapeString(art.getHeader(Headers.REFERENCES)[0]));
   233 		overview.append('\t');
   234 
   235 		String bytes = art.getHeader(Headers.BYTES)[0];
   236 		if ("".equals(bytes)) {
   237 			bytes = "0";
   238 		}
   239 		overview.append(escapeString(bytes));
   240 		overview.append('\t');
   241 
   242 		String lines = art.getHeader(Headers.LINES)[0];
   243 		if ("".equals(lines)) {
   244 			lines = "0";
   245 		}
   246 		overview.append(escapeString(lines));
   247 		overview.append('\t');
   248 		overview.append(escapeString(art.getHeader(Headers.XREF)[0]));
   249 
   250 		// Remove trailing tabs if some data is empty
   251 		return overview.toString().trim();
   252 	}
   253 
   254 	private String escapeString(String str)
   255 	{
   256 		String nstr = str.replace("\r", "");
   257 		nstr = nstr.replace('\n', ' ');
   258 		nstr = nstr.replace('\t', ' ');
   259 		return nstr.trim();
   260 	}
   261 }