Refactoring.
3 * see AUTHORS for the list of contributors
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.
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.
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/>.
19 package org.sonews.daemon;
21 import java.io.IOException;
22 import java.net.InetSocketAddress;
23 import java.net.SocketException;
24 import java.nio.ByteBuffer;
25 import java.nio.CharBuffer;
26 import java.nio.channels.ClosedChannelException;
27 import java.nio.channels.SelectionKey;
28 import java.nio.channels.SocketChannel;
29 import java.nio.charset.Charset;
30 import java.util.Arrays;
31 import java.util.Timer;
32 import java.util.TimerTask;
33 import org.sonews.daemon.command.Command;
34 import org.sonews.storage.Article;
35 import org.sonews.storage.Channel;
36 import org.sonews.storage.StorageBackendException;
37 import org.sonews.util.Log;
38 import org.sonews.util.Stats;
41 * For every SocketChannel (so TCP/IP connection) there is an instance of
43 * @author Christian Lins
46 public final class NNTPConnection
49 public static final String NEWLINE = "\r\n"; // RFC defines this as newline
50 public static final String MESSAGE_ID_PATTERN = "<[^>]+>";
51 private static final Timer cancelTimer = new Timer(true); // Thread-safe? True for run as daemon
52 /** SocketChannel is generally thread-safe */
53 private SocketChannel channel = null;
54 private Charset charset = Charset.forName("UTF-8");
55 private Command command = null;
56 private Article currentArticle = null;
57 private Channel currentGroup = null;
58 private volatile long lastActivity = System.currentTimeMillis();
59 private ChannelLineBuffers lineBuffers = new ChannelLineBuffers();
60 private int readLock = 0;
61 private final Object readLockGate = new Object();
62 private SelectionKey writeSelKey = null;
64 public NNTPConnection(final SocketChannel channel)
67 if (channel == null) {
68 throw new IllegalArgumentException("channel is null");
71 this.channel = channel;
72 Stats.getInstance().clientConnect();
76 * Tries to get the read lock for this NNTPConnection. This method is Thread-
77 * safe and returns true of the read lock was successfully set. If the lock
78 * is still hold by another Thread the method returns false.
82 // As synchronizing simple types may cause deadlocks,
83 // we use a gate object.
84 synchronized (readLockGate) {
88 readLock = Thread.currentThread().hashCode();
95 * Releases the read lock in a Thread-safe way.
96 * @throws IllegalMonitorStateException if a Thread not holding the lock
97 * tries to release it.
101 synchronized (readLockGate) {
102 if (readLock == Thread.currentThread().hashCode()) {
105 throw new IllegalMonitorStateException();
111 * @return Current input buffer of this NNTPConnection instance.
113 public ByteBuffer getInputBuffer()
115 return this.lineBuffers.getInputBuffer();
119 * @return Output buffer of this NNTPConnection which has at least one byte
122 public ByteBuffer getOutputBuffer()
124 return this.lineBuffers.getOutputBuffer();
128 * @return ChannelLineBuffers instance associated with this NNTPConnection.
130 public ChannelLineBuffers getBuffers()
132 return this.lineBuffers;
136 * @return true if this connection comes from a local remote address.
138 public boolean isLocalConnection()
140 return ((InetSocketAddress) this.channel.socket().getRemoteSocketAddress()).getHostName().equalsIgnoreCase("localhost");
143 void setWriteSelectionKey(SelectionKey selKey)
145 this.writeSelKey = selKey;
148 public void shutdownInput()
151 // Closes the input line of the channel's socket, so no new data
152 // will be received and a timeout can be triggered.
153 this.channel.socket().shutdownInput();
154 } catch (IOException ex) {
155 Log.get().warning("Exception in NNTPConnection.shutdownInput(): " + ex);
159 public void shutdownOutput()
161 cancelTimer.schedule(new TimerTask()
168 // Closes the output line of the channel's socket.
169 channel.socket().shutdownOutput();
171 } catch (SocketException ex) {
172 // Socket was already disconnected
173 Log.get().info("NNTPConnection.shutdownOutput(): " + ex);
174 } catch (Exception ex) {
175 Log.get().warning("NNTPConnection.shutdownOutput(): " + ex);
181 public SocketChannel getSocketChannel()
186 public Article getCurrentArticle()
188 return this.currentArticle;
191 public Charset getCurrentCharset()
197 * @return The currently selected communication channel (not SocketChannel)
199 public Channel getCurrentChannel()
201 return this.currentGroup;
204 public void setCurrentArticle(final Article article)
206 this.currentArticle = article;
209 public void setCurrentGroup(final Channel group)
211 this.currentGroup = group;
214 public long getLastActivity()
216 return this.lastActivity;
220 * Due to the readLockGate there is no need to synchronize this method.
222 * @throws IllegalArgumentException if raw is null.
223 * @throws IllegalStateException if calling thread does not own the readLock.
225 void lineReceived(byte[] raw)
228 throw new IllegalArgumentException("raw is null");
231 if (readLock == 0 || readLock != Thread.currentThread().hashCode()) {
232 throw new IllegalStateException("readLock not properly set");
235 this.lastActivity = System.currentTimeMillis();
237 String line = new String(raw, this.charset);
239 // There might be a trailing \r, but trim() is a bad idea
240 // as it removes also leading spaces from long header lines.
241 if (line.endsWith("\r")) {
242 line = line.substring(0, line.length() - 1);
243 raw = Arrays.copyOf(raw, raw.length - 1);
246 Log.get().fine("<< " + line);
248 if (command == null) {
249 command = parseCommandLine(line);
250 assert command != null;
254 // The command object will process the line we just received
256 command.processLine(this, line, raw);
257 } catch (StorageBackendException ex) {
258 Log.get().info("Retry command processing after StorageBackendException");
260 // Try it a second time, so that the backend has time to recover
261 command.processLine(this, line, raw);
263 } catch (ClosedChannelException ex0) {
265 Log.get().info("Connection to " + channel.socket().getRemoteSocketAddress()
266 + " closed: " + ex0);
267 } catch (Exception ex0a) {
268 ex0a.printStackTrace();
270 } catch (Exception ex1) // This will catch a second StorageBackendException
274 ex1.printStackTrace();
275 println("500 Internal server error");
276 } catch (Exception ex2) {
277 ex2.printStackTrace();
281 if (command == null || command.hasFinished()) {
283 charset = Charset.forName("UTF-8"); // Reset to default
288 * This method determines the fitting command processing class.
292 private Command parseCommandLine(String line)
294 String cmdStr = line.split(" ")[0];
295 return CommandSelector.getInstance().get(cmdStr);
299 * Puts the given line into the output buffer, adds a newline character
300 * and returns. The method returns immediately and does not block until
301 * the line was sent. If line is longer than 510 octets it is split up in
302 * several lines. Each line is terminated by \r\n (NNTPConnection.NEWLINE).
305 public void println(final CharSequence line, final Charset charset)
308 writeToChannel(CharBuffer.wrap(line), charset, line);
309 writeToChannel(CharBuffer.wrap(NEWLINE), charset, null);
313 * Writes the given raw lines to the output buffers and finishes with
314 * a newline character (\r\n).
317 public void println(final byte[] rawLines)
320 this.lineBuffers.addOutputBuffer(ByteBuffer.wrap(rawLines));
321 writeToChannel(CharBuffer.wrap(NEWLINE), charset, null);
325 * Encodes the given CharBuffer using the given Charset to a bunch of
326 * ByteBuffers (each 512 bytes large) and enqueues them for writing at the
327 * connected SocketChannel.
328 * @throws java.io.IOException
330 private void writeToChannel(CharBuffer characters, final Charset charset,
331 CharSequence debugLine)
334 if (!charset.canEncode()) {
335 Log.get().severe("FATAL: Charset " + charset + " cannot encode!");
339 // Write characters to output buffers
340 LineEncoder lenc = new LineEncoder(characters, charset);
341 lenc.encode(lineBuffers);
343 enableWriteEvents(debugLine);
346 private void enableWriteEvents(CharSequence debugLine)
348 // Enable OP_WRITE events so that the buffers are processed
350 this.writeSelKey.interestOps(SelectionKey.OP_WRITE);
351 ChannelWriter.getInstance().getSelector().wakeup();
352 } catch (Exception ex) // CancelledKeyException and ChannelCloseException
354 Log.get().warning("NNTPConnection.writeToChannel(): " + ex);
358 // Update last activity timestamp
359 this.lastActivity = System.currentTimeMillis();
360 if (debugLine != null) {
361 Log.get().fine(">> " + debugLine);
365 public void println(final CharSequence line)
368 println(line, charset);
371 public void print(final String line)
374 writeToChannel(CharBuffer.wrap(line), charset, line);
377 public void setCurrentCharset(final Charset charset)
379 this.charset = charset;
382 void setLastActivity(long timestamp)
384 this.lastActivity = timestamp;