View Javadoc
1   /******************************************************************************
2    * ClientConnection.java - Manage a connection to the FIBS server
3    * $Id$
4    * 
5    * BuckoFIBS - Backgammon by BuckoSoft
6    * Copyright(c) 2009,2010 - Dick Balaska - BuckoSoft, Corp.
7    * 
8    * $Log$
9    * Revision 1.10  2013/10/01 07:13:23  dick
10   * Send our ping message once a minute.
11   *
12   * Revision 1.9  2013/09/25 03:23:00  dick
13   * isConnected() returns if we are up.
14   *
15   * Revision 1.8  2013/09/19 07:34:44  dick
16   * Add a 25 minute keep-alive ping timer.
17   *
18   * Revision 1.7  2013/09/08 06:16:43  dick
19   * Don't print stack traces after failing to connect.  The message is enough.
20   *
21   * Revision 1.6  2011/06/15 00:41:51  dick
22   * resetFIBSCookieMonster() becomes just reset().
23   *
24   * Revision 1.5  2011/05/22 05:48:15  dick
25   * Point to the "new" cvs location.
26   *
27   * Revision 1.4  2011/05/21 05:07:46  dick
28   * readMessage goes in a try block.
29   *
30   * Revision 1.3  2010/03/03 13:12:21  inim
31   * Replaced (c) sign in comment mangled by CVS default encoding back to UTF-8
32   *
33   * Revision 1.2  2010/03/03 12:19:49  inim
34   * Moved source to UTF8 encoding from CP1252 encoding. To this end all source files' (c) message was updated to "Copyright© 2009,2010 - Dick Balaska - BuckoSoft, Corp.". This replaces the (c) sign to UTF8, and adds the new year 2010.
35   *
36   * Revision 1.1  2010/02/04 05:57:53  inim
37   * Mavenized project folder layout
38   *
39   * Revision 1.20  2009/02/27 05:49:50  dick
40   * Check for bare cr and bare lf at the beginning of a line.
41   *
42   * Revision 1.19  2009/02/25 08:12:14  dick
43   * Check for a leading line feed, not a cr.
44   *
45   * Revision 1.18  2009/02/24 05:36:59  dick
46   * Handle connectionAborted (on write).
47   *
48   * Revision 1.17  2009/02/17 14:45:44  dick
49   * Wrap debug with DEBUG.
50   *
51   * Revision 1.16  2009/02/14 12:29:19  dick
52   * Check for runon messages with just a cr between them.
53   *
54   * Revision 1.15  2009/02/02 08:38:12  dick
55   * login with the app signature instead of hardcoded BuckoFIBS.
56   *
57   * Revision 1.14  2009/01/29 08:27:42  dick
58   * Fix the pushback handling.
59   *
60   * Revision 1.13  2009/01/28 19:42:28  dick
61   * Prettier cvs link in the javadoc.
62   *
63   * Revision 1.12  2009/01/28 08:30:24  dick
64   * Handle the Malformed Messages (Bad_board) as documented as occuring in http://www.fibs.com/fcm/ .
65   * However, we do begin and end regexp on ALL messages.
66   * ClientConnection now supports a pushback buffer so that CookieMonster can deal with the second message later.
67   *
68   * Revision 1.11  2009/01/27 19:17:33  dick
69   * Display the cookie number with the string in stdout.
70   *
71   * Revision 1.10  2009/01/26 17:35:15  dick
72   * Push the CookieMonster down to the ClientConnection.  ClientAdapter now emits the cookie with the string.
73   * There are many messages besides the known "BadBoard" messages that are a result of runon messages (missing crlf).
74   * CookieMonster is going to need to do a full regexp scan and see if there is anything on the right that needs to be pushed back as another message.
75   *
76   * Revision 1.9  2009/01/12 21:48:53  dick
77   * Add setFibsAttributes.
78   *
79   * Revision 1.8  2009/01/12 07:47:07  dick
80   * Update to use the interfaces as ClientConnection is used across multiple applications.
81   *
82   * Revision 1.7  2009/01/09 07:37:48  dick
83   * Turn off debug.
84   *
85   * Revision 1.6  2009/01/08 18:33:49  dick
86   * Only shutdown the socket if it exists.
87   *
88   * Revision 1.5  2009/01/07 04:29:16  dick
89   * Javadoc.
90   *
91   * Revision 1.4  2009/01/07 02:36:56  dick
92   * Better handling when destroying ourselves.
93   *
94   * Revision 1.3  2009/01/05 07:17:20  dick
95   * Use the selected profileId when determining values for the connection.
96   *
97   * Revision 1.2  2008/12/17 04:39:19  dick
98   * Close the socket before deleting it.
99   *
100  * Revision 1.1  2008/12/13 06:57:11  dick
101  * Move ClientConnection, CookieMonster, and FIBSMessages to their own network package.
102  *
103  * Revision 1.8  2008/12/13 06:19:45  dick
104  * getDisplayXmit() becomes isDisplayXmit().
105  *
106  * Revision 1.7  2008/12/12 14:44:41  dick
107  * Handle partial lines received.
108  *
109  * Revision 1.6  2008/12/11 20:25:01  dick
110  * Echo the Xmit messages to the screen if pref enabled.
111  *
112  * Revision 1.5  2008/12/10 17:58:12  dick
113  * Fix the list lock.
114  * Output network messages in color.
115  *
116  * Revision 1.4  2008/12/09 19:31:44  dick
117  * close the sockets on shutdown.
118  *
119  * Revision 1.3  2008/12/09 01:48:24  dick
120  * Buffer receiving doesn't monitor for logins.  CookieMonster does that now.
121  *
122  * Revision 1.2  2008/03/31 07:07:41  dick
123  * Login using clips message handling.
124  *
125  * Revision 1.1  2008/03/30 05:40:33  dick
126  * Early BuckoFIBS networking.  We connect.
127  *
128  */
129 
130 /* 
131  * This program is free software: you can redistribute it and/or modify
132  * it under the terms of the GNU General Public License as published by
133  * the Free Software Foundation, either version 3 of the License, or
134  * (at your option) any later version.
135  *
136  * This program is distributed in the hope that it will be useful,
137  * but WITHOUT ANY WARRANTY; without even the implied warranty of
138  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
139  * GNU General Public License for more details.
140  *
141  * You should have received a copy of the GNU General Public License
142  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
143  *
144  * The Original Code is BuckoFIBS, <http://www.buckosoft.com/BuckoFIBS/>.
145  * The Initial Developer of the Original Code is Dick Balaska and BuckoSoft, Corp.
146  * 
147  */
148 package com.buckosoft.fibs.net;
149 
150 import java.io.IOException;
151 import java.io.InputStream;
152 import java.io.OutputStream;
153 import java.net.Socket;
154 import java.net.SocketException;
155 import java.net.UnknownHostException;
156 import java.util.LinkedList;
157 import java.util.Timer;
158 import java.util.TimerTask;
159 
160 
161 /** Manage a connection to the FIBS server.
162  * This object runs in it's own Thread.
163  * The only synchronized object is the outbound message queue.
164  * @author Dick Balaska
165  * @since 2008/03/29
166  * @version $Revision$ <br> $Date$
167  * @see <a href="http://cvs.buckosoft.com/Projects/BuckoFIBS/BuckoFIBS/src/main/java/com/buckosoft/fibs/net/ClientConnection.java">cvs ClientConnection.java</a>
168  */
169 public class ClientConnection extends Thread {
170 	private	final static boolean DEBUG = false;
171 	
172 	final static String	eol = "\r\n";
173 	
174 	private	Socket	sock = null;
175 	private	InputStream is = null;
176 	private	OutputStream os = null;
177 
178 	private	ClientAdapter		clientAdapter = null;
179 	private	CookieMonster		cookieMonster;
180 	private	FIBSAttributes		fibsAttributes;
181 	private	boolean				shuttingDown = false;
182 	private	LinkedList<String>	outMessages = new LinkedList<String>();
183 	private	Boolean				listLock = new Boolean(false);
184 	private	String				leftover = null;
185 	private	String				pushbackString = null;
186 	private	final int			resetCountdown = 60*1;		// must send something every 25 minutes or one minute.
187 	private	int					timerCountdown = resetCountdown;
188 	private	Timer				timer;
189 	private	PingTimerTask		pingTimerTask;
190 
191 	public ClientConnection() {
192 		cookieMonster = new CookieMonster();
193 		cookieMonster.setClientConnection(this);
194 		timer = new Timer();
195 		pingTimerTask = new PingTimerTask();
196 		timer.scheduleAtFixedRate(pingTimerTask, 1000, 1000);
197 	}
198 
199 	private class PingTimerTask extends TimerTask {
200 
201 		/* (non-Javadoc)
202 		 * @see java.util.TimerTask#run()
203 		 */
204 		@Override
205 		public void run() {
206 			if (--timerCountdown <= 0) {
207 				timerCountdown = resetCountdown;
208 				sendMessage("\r\n");
209 			}	
210 		}	
211 	}
212 
213 	/** Are we connected to the fibs server?
214 	 * @return true if we are.
215 	 */
216 	public boolean isConnected() {
217 		return(sock != null);
218 	}
219 
220 	/** Set the ClientAdapter that we will be interfacing with
221 	 * @param clientAdapter The object that we communicate with using the ClientAdapter interface
222 	 */
223 	public void setClientAdapter(ClientAdapter clientAdapter) {
224 		this.clientAdapter = clientAdapter;
225 	}
226 
227 	/** Set the FIBSAttributes that we fetch our server attributes from
228 	 * @param fibsAttributes The FIBSAtrributes that we need.
229 	 */
230 	public void setFibsAttributes(FIBSAttributes fibsAttributes) {
231 		this.fibsAttributes = fibsAttributes;
232 	}
233 
234 	/** Reset the state of the cookie monster
235 	 */
236 	public void resetFIBSCookieMonster() {
237 		this.cookieMonster.reset();
238 	}
239 
240 	public void pushBack(String s) {
241 		pushbackString = s;
242 	}
243 
244 	/** Queue a message up for transmission to the server
245 	 * @param s The message to send (must include eol).
246 	 */
247 	public	void sendMessage(String s) {
248 		if (s == null)
249 			throw new RuntimeException("Can't send a null message");
250 		if (DEBUG)
251 			System.out.println("User sent '" + s + "'");
252 		accessOutMessages(s);
253 		timerCountdown = resetCountdown;
254 	}
255 
256 	/** Kill the connection to the server.
257 	 */
258 	public void shutDown() {
259 		shuttingDown = true;
260 		try {
261 			if (timer != null) {
262 				timer.cancel();
263 				timer = null;
264 			}
265 			if (pingTimerTask != null) {
266 				pingTimerTask.cancel();
267 				pingTimerTask = null;
268 			}
269 		} catch (Exception e1) {
270 			e1.printStackTrace();
271 		}
272 		try {
273 			if (os != null)
274 				os.close();
275 			os = null;
276 			if (is != null)
277 				is.close();
278 			is = null;
279 			if (sock != null) {
280 				sock.shutdownInput();
281 				sock.shutdownOutput();
282 				sock.close();
283 			}
284 			sock = null;
285 		} catch (SocketException sex) {	
286 		} catch (IOException e) {
287 			e.printStackTrace();
288 		}
289 	}
290 
291 	/** Either append a message to the end, or remove a message from the head
292 	 * @param s a network message or null
293 	 */
294 	private void accessOutMessages(String s) {
295 		synchronized (listLock) {
296 			if (s == null)
297 				outMessages.remove();
298 			else
299 				outMessages.add(s);
300 		}
301 	}
302 
303 	/* (non-Javadoc)
304 	 * @see java.lang.Thread#start()
305 	 */
306 	@Override
307 	public synchronized void run() {
308 		sock = null;
309 		String server = this.fibsAttributes.getServerName();
310 		int port = this.fibsAttributes.getServerPort();
311 		this.clientAdapter.writeSystemMessage(ClientAdapter.MessageRoute.SYSTEM, "Connecting to Server:" + eol);
312 		this.clientAdapter.writeSystemMessage(ClientAdapter.MessageRoute.SYSTEM, server + ":" + port + eol);
313 		try {
314 			sock = new Socket(server, port);
315 		} catch (UnknownHostException e) {
316 			this.clientAdapter.writeSystemMessage(ClientAdapter.MessageRoute.SYSTEM, "Can't connect: Unknown host");
317 			e.printStackTrace();
318 			return;
319 		} catch (IOException e) {
320 			this.clientAdapter.writeSystemMessage(ClientAdapter.MessageRoute.SYSTEM, "Can't connect: " + e.getLocalizedMessage());
321 			//e.printStackTrace();
322 			return;
323 		} catch (Exception e) {
324 			this.clientAdapter.writeSystemMessage(ClientAdapter.MessageRoute.SYSTEM, "Can't connect: " + e.getLocalizedMessage());
325 			//e.printStackTrace();
326 			return;
327 		}
328 
329 		try {
330 			is = sock.getInputStream();
331 			os = sock.getOutputStream();
332 		
333 			while (!shuttingDown) {
334 				if (pushbackString != null) {
335 					String s = pushbackString;
336 					pushbackString = null;
337 					handleMessage(s);
338 					continue;
339 				}
340 				if (is.available() > 0) {
341 					try {
342 						readMessage();
343 					} catch (Exception e) {
344 						System.err.println(e.getMessage());
345 						e.printStackTrace();
346 					}
347 					continue;
348 				}
349 				if (!outMessages.isEmpty()) {
350 					writeMessage();
351 					continue;
352 				}
353 				Thread.sleep(100);
354 			}
355 		} catch (IOException e) {
356 			e.printStackTrace();
357 		} catch (InterruptedException e) {
358 			e.printStackTrace();
359 		}
360 		if (sock != null) {
361 			try {
362 				sock.shutdownInput();
363 				sock.shutdownOutput();
364 				sock.close();
365 				sock = null;
366 			} catch (SocketException e) {
367 			} catch (IOException e) {
368 				e.printStackTrace();
369 			}
370 		}
371 	}
372 
373 	private	void readMessage() {
374 		byte[] b;
375 		try {
376 			int r = is.available();
377 			b = new byte[r];
378 			is.read(b);
379 		} catch (IOException e) {
380 			e.printStackTrace();
381 			shuttingDown = true;
382 			return;
383 		}
384 		String s = new String(b);
385 		if (DEBUG)
386 			System.out.println(s);
387 		if (DEBUG)
388 			this.clientAdapter.writeSystemMessageln(ClientAdapter.MessageRoute.DEBUG, s);
389 		boolean hasLeftOver = false;
390 		if (!s.endsWith("\r\n")) {
391 			if (DEBUG)
392 				System.out.println("Doesn't end with cr/lf" + eol);
393 			hasLeftOver = true;
394 		}
395 		String[] ss = s.split("\r\n");
396 		if (ss.length == 0)
397 			return;
398 		if (DEBUG)
399 			System.out.println("Got " + ss.length + " lines" + eol);
400 		if (leftover != null) {
401 			ss[0] = leftover + ss[0];
402 			leftover = null;
403 		}
404 		for (int i=0; i<ss.length; i++) {
405 			if (i == ss.length-1 && hasLeftOver && !ss[i].startsWith("login:"))
406 				leftover = ss[i];
407 			else
408 				handleMessage(ss[i]);
409 			while (pushbackString != null) {
410 				String t = pushbackString;
411 				pushbackString = null;
412 				handleMessage(t);
413 			}
414 		}
415 	}
416 
417 	/** Send the login message to FIBS.
418 	 */
419 	public void sendLogin() {
420 		StringBuffer sb = new StringBuffer();
421 		sb.append("login ");
422 		sb.append(this.fibsAttributes.getAppSignature());
423 		sb.append(" 1008 ");
424 		sb.append(this.fibsAttributes.getUserName());
425 		sb.append(" ");
426 		sb.append(this.fibsAttributes.getUserPassword());
427 		sb.append(eol);
428 		this.sendMessage(sb.toString());
429 	}
430  
431 	private	void writeMessage() {
432 		String s = outMessages.getFirst();
433 		if (this.fibsAttributes.isDisplayXmit()) {
434 			if (s.startsWith("login"))
435 				this.clientAdapter.writeSystemMessageln(ClientAdapter.MessageRoute.NETWORKOUT, "login...");
436 			else
437 				this.clientAdapter.writeSystemMessage(ClientAdapter.MessageRoute.NETWORKOUT, s);
438 		}
439 		if (this.fibsAttributes.isStdoutNetworkMessages())
440 			System.out.println("writeNetMessage: '" + s + "'");
441 		try {
442 			if (os == null) {
443 				shuttingDown = true;
444 				return;
445 			}
446 			os.write(s.getBytes());
447 		} catch (Exception e) {
448 			this.clientAdapter.writeSystemMessageln(ClientAdapter.MessageRoute.ERROR, "Disconnected from FIBS");
449 			this.clientAdapter.writeSystemMessageln(ClientAdapter.MessageRoute.ERROR, e.getLocalizedMessage());
450 			this.clientAdapter.connectionAborted();
451 			e.printStackTrace();
452 			shuttingDown = true;
453 		}
454 		this.accessOutMessages(null);
455 	}
456 	
457 	private	void handleMessage(String s) {
458 		if (s.length() > 1 && s.charAt(0) == 13)	// I have seen two '5's runon with just a cr between them,
459 			s = s.substring(1);						// not a crlf.  Check for that here.
460 		if (s.length() > 1 && s.charAt(0) == 10)	// I have seen two '5's runon with just a lf between them,
461 			s = s.substring(1);						// not a crlf.  Check for that here.
462 		int cookie = this.cookieMonster.fIBSCookie(s);
463 		if (this.fibsAttributes.isStdoutNetworkMessages())
464 			System.out.println("Handle: " + cookie + "'" + s + "'");
465 		if (this.pushbackString != null) {
466 			s = s.substring(0, s.length()-this.pushbackString.length());
467 			if (DEBUG)
468 				System.out.println("Truncating to '" + s + "'");
469 		}
470 		this.clientAdapter.dispatch(cookie, s);
471 	}
472 }