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
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
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
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 }