View Javadoc

1   /***
2    *     Ambient - A music player for the Android platform
3    Copyright (C) 2007 Martin Vysny
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 sk.baka.ambient.commons;
20  
21  import java.io.BufferedInputStream;
22  import java.io.Closeable;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.net.InetAddress;
27  import java.net.ServerSocket;
28  import java.net.Socket;
29  import java.net.SocketException;
30  import java.util.concurrent.ExecutorService;
31  import java.util.concurrent.Executors;
32  import java.util.concurrent.ThreadFactory;
33  import java.util.concurrent.atomic.AtomicInteger;
34  import java.util.concurrent.locks.ReadWriteLock;
35  import java.util.concurrent.locks.ReentrantReadWriteLock;
36  
37  import javax.net.ServerSocketFactory;
38  
39  import android.util.Log;
40  
41  /***
42   * A server which listens on given port and executes tasks when the connection
43   * is established. The server is stopped by default.
44   * 
45   * @author Martin Vysny
46   */
47  public abstract class SocketServer implements ThreadFactory, Closeable {
48  	private ExecutorService executor = null;
49  	private ServerSocket serverSocket = null;
50  	private final ReadWriteLock serverStateLock = new ReentrantReadWriteLock();
51  
52  	/***
53  	 * Checks if the server is started.
54  	 * 
55  	 * @return <code>true</code> if the server is started, <code>false</code>
56  	 *         otherwise.
57  	 */
58  	public final boolean isStarted() {
59  		serverStateLock.readLock().lock();
60  		try {
61  			return executor != null;
62  		} finally {
63  			serverStateLock.readLock().unlock();
64  		}
65  	}
66  
67  	/***
68  	 * Starts the server. Does nothing if the server is started.
69  	 * 
70  	 * @param port
71  	 *            the port to listen on.
72  	 * @param bindTo
73  	 *            listen on this interface. See
74  	 *            {@link ServerSocketFactory#createServerSocket(int, int, InetAddress)}
75  	 *            for details.
76  	 * @throws IOException
77  	 *             if server failed to start.
78  	 */
79  	public final void start(final int port, final InetAddress bindTo)
80  			throws IOException {
81  		serverStateLock.writeLock().lock();
82  		try {
83  			if (isStarted()) {
84  				return;
85  			}
86  			executor = Executors.newCachedThreadPool(this);
87  			serverSocket = ServerSocketFactory.getDefault().createServerSocket(
88  					port, 0, bindTo);
89  			executor.execute(new Server());
90  		} finally {
91  			serverStateLock.writeLock().unlock();
92  		}
93  	}
94  
95  	/***
96  	 * Stops the server. Does nothing if the server is not started.
97  	 */
98  	public final void stop() {
99  		serverStateLock.writeLock().lock();
100 		try {
101 			if (!isStarted()) {
102 				return;
103 			}
104 			MiscUtils.closeQuietly(serverSocket);
105 			onStopping();
106 			serverSocket = null;
107 			executor.shutdownNow();
108 			executor = null;
109 		} finally {
110 			serverStateLock.writeLock().unlock();
111 		}
112 	}
113 
114 	/***
115 	 * Invoked when the server is being stopped.
116 	 */
117 	protected void onStopping() {
118 		// by default do nothing
119 	}
120 
121 	public void close() {
122 		stop();
123 	}
124 
125 	private final AtomicInteger threadId = new AtomicInteger();
126 
127 	public final Thread newThread(Runnable r) {
128 		final Thread result = new Thread(r, getClass().getSimpleName() + "-t-"
129 				+ threadId.getAndIncrement());
130 		result.setPriority(Thread.MIN_PRIORITY);
131 		result.setDaemon(true);
132 		return result;
133 	}
134 
135 	/***
136 	 * Handles a request. The communication is already opened and provided.
137 	 * Implementor executes in a new thread. Implementor should periodically
138 	 * check for {@link Thread#isInterrupted()}, terminating ASAP when
139 	 * interrupted.
140 	 * 
141 	 * @param socket
142 	 *            the socket for this request.
143 	 * @param in
144 	 *            opened read pipe from the socket.
145 	 * @param out
146 	 *            opened write pipe to the socket.
147 	 * @throws IOException
148 	 *             if i/o error occurs.
149 	 * @throws ServerHttpException
150 	 *             if HTTP error occurs. This error is written as a HTTP
151 	 *             response. If you decide to throw this error then make sure
152 	 *             that nothing was written yet to <code>out</code>.
153 	 * @throws InterruptedException
154 	 *             if interrupted.
155 	 */
156 	protected abstract void handleRequest(final Socket socket,
157 			final InputStream in, final OutputStream out) throws IOException,
158 			ServerHttpException, InterruptedException;
159 
160 	/***
161 	 * Waits for a connection and starts the transfer.
162 	 * 
163 	 * @author Martin Vysny
164 	 */
165 	private final class Server implements Runnable {
166 		public void run() {
167 			try {
168 				final Socket socket = serverSocket.accept();
169 				// connection accepted. Spawn new listener and start transfer
170 				try {
171 					serverStateLock.readLock().lock();
172 					try {
173 						if (executor == null) {
174 							return;
175 						}
176 						executor.execute(new Server());
177 					} finally {
178 						serverStateLock.readLock().unlock();
179 					}
180 					handleRequest(socket);
181 				} finally {
182 					MiscUtils.closeQuietly(socket);
183 				}
184 			} catch (final SocketException e) {
185 				// this happens when the server is stopped.
186 			} catch (final Exception e) {
187 				if (!Thread.currentThread().isInterrupted()) {
188 					Log.e(SocketServer.class.getSimpleName(),
189 							"Error while handling request", e);
190 				}
191 			}
192 		}
193 	}
194 
195 	private void handleRequest(Socket socket) throws IOException,
196 			InterruptedException {
197 		final InputStream in = new BufferedInputStream(socket.getInputStream());
198 		final OutputStream out = socket.getOutputStream();
199 		try {
200 			handleRequest(socket, in, out);
201 		} catch (ServerHttpException ex) {
202 			Log.e(SocketServer.class.getSimpleName(), "HTTP error", ex);
203 			ex.writeResponse((byte) 0, out);
204 		} finally {
205 			MiscUtils.closeQuietly(out);
206 			MiscUtils.closeQuietly(in);
207 		}
208 	}
209 }