View Javadoc

1   /*
2    * Trip Tracker, a real-time position tracking system for the Internet.
3    * Copyright (C) 2006  Team Trip Tracker
4    *
5    * This program is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU General Public License as published by the
7    * Free Software Foundation; either version 2 of the License, or (at your
8    * option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful, but
11   * WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13   * General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License along
16   * with this program; if not, write to the Free Software Foundation, Inc.,
17   * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
18   */
19  
20  package triptracker.client.net;
21  
22  import static triptracker.core.ConnectionState.*;
23  import static triptracker.core.Protocol.*;
24  
25  import java.io.BufferedReader;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.InputStreamReader;
29  import java.io.OutputStream;
30  import java.io.PrintStream;
31  import java.net.InetSocketAddress;
32  import java.net.Socket;
33  import java.util.Set;
34  import java.util.concurrent.CopyOnWriteArraySet;
35  
36  import triptracker.core.ConnectionState;
37  import triptracker.core.Protocol;
38  import triptracker.core.Route;
39  
40  /***
41   * Connects to the server through a socket.
42   */
43  public abstract class SocketConnection<E extends SocketListener> {
44  	private Socket socket = null;
45  	protected InputStream inStream = null;
46  	protected OutputStream outStream = null; 
47  	protected BufferedReader in = null;
48  	protected PrintStream out = null;
49  	
50  	private String username;
51  	private String host = HOST;
52  	private int port = PORTNR;
53  	
54  	protected boolean loggedIn = false;
55  	
56  	/*** Listener set for Model View Controller (MVC) separation. */
57  	protected Set<E> listeners = new CopyOnWriteArraySet<E>();
58  	
59  	/***
60  	 * Connects to the hostname and port number set by {@link #setHost(String)}
61  	 * and {@link #setPort(int)}.
62  	 * 
63  	 * @throws IOException on connection failure
64  	 * @see #connect(String, int)
65  	 */
66  	public void connect() throws IOException {
67  		connect(host, port);
68  	}
69  	
70  	/***
71  	 * Connect to server on specified port.
72  	 * 
73  	 * @param host server hostname
74  	 * @param port destination port
75  	 * @throws IOException on connection failure
76  	 * @see #connect(String)
77  	 */
78  	public void connect(String host, int port) throws IOException {
79  		// Disconnect existing connection.
80  		if (!isClosed()) {
81  			disconnect();
82  		}
83  		
84  		// Connect to server.
85  		socket = new Socket();
86  		
87  		try {
88  			socket.connect(new InetSocketAddress(host, port));
89  		} catch (IOException e) {
90  			socket = null;
91  			throw e;
92  		}
93  
94  		// Setup streams.
95  		inStream = socket.getInputStream();
96  		outStream = socket.getOutputStream();
97  		in = new BufferedReader(new InputStreamReader(inStream));
98  		out = new PrintStream(outStream, true);
99  
100 		connectionUpdate(CONNECTED);
101 
102 		// Start socket data reader thread.
103 		Thread t = new DataReaderThread();
104 		// Demonize so that it doesn't prevent the program from closing.
105 		t.setDaemon(true); 
106 		t.start();
107 
108 	}
109 	
110 	/***
111 	 * Logon a user.
112 	 * 
113 	 * @param type type of user
114 	 * @param user username
115 	 * @param pass password
116 	 * @throws IOException on connection failure
117 	 */
118 	protected void logon(int type, String user, String pass)
119 			throws IOException {
120 		// Tries to authenticate with the server
121 		if (!isConnected()) {
122 			connect();
123 		}
124 		
125 		this.username = user;
126 		
127 		// Sends message to server, loggin in.
128 		sendMessage(type, user, pass);
129 	}
130 	
131 	/***
132 	 * Sets route to track
133 	 */
134 	public void setRoute(Route route){
135 		sendMessage(SET_ROUTE, route.getRouteId());
136 	}
137 	
138 	/***
139 	 * Request a list of routes for a given user. When the requested list
140 	 * is received from the server.
141 	 */
142 	public void getRoutes(String username){
143 		sendMessage(VIEW_ROUTES, username);
144 		//TODO make it work with zip
145 	}
146 	
147 	/***
148 	 * Sends a message to the server
149 	 * 
150 	 * @param message
151 	 */
152 	protected void sendMessage(Object... message) {
153 		if (!isConnected()){
154 			// TODO It might be enough to check isClosed() because it might
155 			// work to send a message while the socket is still connecting.
156 			// Then it might send it right after successfully connecting.
157 			// This needs to be tested.
158 			System.out.println("socket not connected");
159 			// No use announcing disconnected state when already disconnected.
160 			// This must be detected somewhere else.
161 //			connectionUpdate(DISCONNECTED);
162 			return;
163 		}
164 
165 		try {
166 			send(outStream, message);
167 		} catch (IOException e) {
168 			socketErrorEvent(e);
169 		}
170 		
171 		// TODO for testing purposes only
172 //		System.out.println("%% sent to server: " + Arrays.toString(message));
173 	}
174 
175 	/*** 
176 	 * Checks if the client is connected to the server.
177 	 * 
178 	 * @return true if connected, false if disconnected
179 	 */
180 	public boolean isConnected() {
181 		if (socket == null) {
182 			return false;
183 		} else {
184 			return socket.isConnected();
185 		}
186 	}
187 
188 	/*** 
189 	 * Checks if the client connection is closed.
190 	 * 
191 	 * @return true if connected, false if disconnected
192 	 */
193 	public boolean isClosed() {
194 		if (socket == null) {
195 			return true;
196 		} else {
197 			return socket.isClosed();
198 		}
199 	}
200 
201 	/***
202 	 * Checks if the client is logged in.
203 	 * 
204 	 * @return true if logged in, false if not logged in
205 	 */
206 	public boolean isLoggedIn(){
207 		return loggedIn;
208 	}
209 	
210 	/***
211 	 * Checks for messages recieved from the server, and pass them to the
212 	 * message handler for further processing. Subclasses can override this
213 	 * method to provide their own message loop if message processing needs to
214 	 * be more fine grained than simple line based delimiting.
215 	 */
216 	protected void messageLoop() throws IOException {
217 		String message;
218 		
219 		while (true) {
220 			message = in.readLine();
221 			if (message == null) {
222 				break;
223 			// Delegate message handling.
224 			} else if (!messageHandler(message)) {
225 				break;
226 			}
227 		}
228 	}
229 	
230 	/***
231 	 * Message handler for data received on socket. Subclasses must implement
232 	 * this method to provide handing of recieved messages. 
233 	 * 
234 	 * @param message received message from server
235 	 * @return true if more messages can be processed, false to end the message
236 	 * 		loop, stop message handling and close the socket connection
237 	 */
238 	protected abstract boolean messageHandler(String message);
239 
240 	/***
241 	 * Disconnect from the socket.
242 	 * 
243 	 * @throws IOException on socket error
244 	 */
245 	public void disconnect() throws IOException {
246 		if (loggedIn) {
247 			sendMessage(QUIT);
248 			loggedIn = false;
249 		}
250 		
251 		if (isClosed()) {
252 			return;
253 		}
254 
255 		// TODO Server closes socket on QUIT, but should have timeout on
256 		// closing to ensure disconnection.
257 		socket.close();
258 		socket = null;
259 		
260 		 // XXX Hoping for close() to kill the data reader thread.
261 		connectionUpdate(DISCONNECTED);
262 	}
263 	
264 	/***
265 	 * Register a listener for server events.
266 	 * 
267 	 * @param listener event receiver to register
268 	 */
269 	public void addListener(E listener) {
270 		listeners.add(listener);
271 	}
272 	
273 	/***
274 	 * Remove listener from listener queue.
275 	 * 
276 	 * @param listener event receiver to remove
277 	 */
278 	public void removeListener(E listener) {
279 		listeners.remove(listener);
280 	}
281 	
282 	/***
283 	 * Reports a broken listener, removes it from the listener queue and logs
284 	 * the error.
285 	 * 
286 	 * @param listener broken listener
287 	 * @param exception exception thrown by listener
288 	 */
289 	protected void brokenListener(SocketListener listener,
290 			RuntimeException exception) {
291 		System.err.println("Unexpected exception in listener");
292 		exception.printStackTrace(); // XXX Temp error logging. Add log to file?
293 		
294 		// Remove listener from queue.
295 		listeners.remove(listener);
296 	}
297 	
298 	/***
299 	 * Publish a socket error event to all listeners. This generally means that
300 	 * the client socket connection has been closed. 
301 	 * 
302 	 * @param exception socket exception thrown
303 	 */
304 	protected void socketErrorEvent(IOException exception) {
305 		for (SocketListener listener : listeners) {
306 			try {
307 				listener.socketError(exception);
308 			} catch (RuntimeException e) {
309 				brokenListener(listener, e);
310 			}
311 		}
312 	}
313 
314 	/***
315 	 * Publish an invalid message event to all listeners. This signals an
316 	 * unsupported or malformed message recieved from the server. 
317 	 * 
318 	 * @param message full message
319 	 */
320 	protected void invalidMessage(String message) {
321 //		System.out.println("invalidMessage: " + message); // XXX Temporary
322 
323 		for (SocketListener listener : listeners) {
324 			try {
325 				listener.invalidMessage(message);
326 			} catch (RuntimeException e) {
327 				brokenListener(listener, e);
328 			}
329 		}
330 	}
331 	
332 	/***
333 	 * Publish a connection state change event to all listeners. This notifies
334 	 * the clients that the connection state of the underlying socket has
335 	 * changed.
336 	 * 
337 	 * @param state new connection state
338 	 */
339 	protected void connectionUpdate(ConnectionState state) {
340 //		System.out.println("connectionUpdate: " + state); // XXX Temporary
341 
342 		// Update logged in status.
343 		loggedIn = (state == AUTH_SUCCESS);
344 		
345 		for (SocketListener listener : listeners) {
346 			try {
347 				listener.connectionUpdate(state);
348 			} catch (RuntimeException e) {
349 				brokenListener(listener, e);
350 			}
351 		}
352 	}
353 	
354 	/***
355 	 * Inner class to receive incomming messages.
356 	 */
357 	private class DataReaderThread extends Thread {
358 		/***
359 		 * Start the message loop that recieves messages from the socket.
360 		 */
361 		@Override
362 		public void run() {
363 			try {
364 				// Start message loop
365 				messageLoop();
366 			}  catch (IOException e) {
367 				socketErrorEvent(e);
368 				//e.printStackTrace();
369 			}  catch (RuntimeException e) {
370 				e.printStackTrace();
371 			} finally {
372 				// Gracefully exit.
373 				try {
374 					in.close();
375 					out.close();
376 					socket.close();
377 				} catch (IOException e) {
378 					//socketErrorEvent(e);
379 				} finally {
380 					connectionUpdate(DISCONNECTED);
381 				}
382 			}
383 		}
384 	}
385 
386 	/***
387 	 * Returns the current hostname. By default the hostname is set to
388 	 * {@link Protocol#HOST}.
389 	 * 
390 	 * @return hostname
391 	 */
392 	public String getHost() {
393 		return host;
394 	}
395 
396 	/***
397 	 * Set hostname to connect to. By default the hostname is set to
398 	 * {@link Protocol#HOST}.
399 	 * 
400 	 * @param host host to set
401 	 */
402 	public void setHost(String host) {
403 		this.host = host;
404 	}
405 
406 	/***
407 	 * Returns the current port. By default the port number is set to
408 	 * {@link Protocol#PORTNR}.
409 	 * 
410 	 * @return port number
411 	 */
412 	public int getPort() {
413 		return port;
414 	}
415 
416 	/***
417 	 * Set port to connect to. By default the port number is set to
418 	 * {@link Protocol#PORTNR}.
419 	 * 
420 	 * @param port The port to set.
421 	 */
422 	public void setPort(int port) {
423 		this.port = port;
424 	}
425 	
426 	/***
427 	 * Returns the name of the active user. This is set during login when
428 	 * connecting to the server.
429 	 * 
430 	 * @return active user name
431 	 */
432 	public String getUsername() {
433 		return username;
434 	}
435 }