001: // Client.java
002: // $Id: Client.java,v 1.2 2001/10/05 08:07:52 ylafon Exp $
003: // (c) COPYRIGHT MIT, INRIA and Keio, 2001.
004: // Please first read the full copyright statement in file COPYRIGHT.html
005:
006: package org.w3c.www.protocol.http.cache.push;
007:
008: import java.io.IOException;
009: import java.io.InputStream;
010: import java.io.OutputStream;
011: import java.io.DataInputStream;
012: import java.io.DataOutputStream;
013: import java.io.ByteArrayInputStream;
014: import java.io.ByteArrayOutputStream;
015:
016: import java.net.Socket;
017: import java.net.InetAddress;
018: import java.net.ConnectException;
019: import java.net.UnknownHostException;
020:
021: /**
022: * PushCache Client - send messages to push cache server
023: *
024: * @author Paul Henshaw, The Fantastic Corporation, Paul.Henshaw@fantastic.com
025: * @version $Revision: 1.2 $
026: * $Id: Client.java,v 1.2 2001/10/05 08:07:52 ylafon Exp $
027: */
028: public class Client {
029: /**
030: * Return/exit values
031: */
032: public static final int OK = 0, NO = 1, FAILED = -1;
033:
034: /**
035: * How many times should we try to reconnect? 10
036: */
037: public static final int MAX_TRIES = 10;
038:
039: /**
040: * Default hostname - localhost
041: */
042: public static final String DEFAULT_SERVER = "localhost";
043:
044: /**
045: * Version Control Revision
046: */
047: public static final String VC_REVISION = "$Revision: 1.2 $";
048:
049: /**
050: * Version Control Id
051: */
052: public static final String VC_ID = "$Id: Client.java,v 1.2 2001/10/05 08:07:52 ylafon Exp $";
053:
054: private int _verbose = 0;
055: private int _port = -1;
056: private String _host_name = "";
057: private byte[] _buffer = null;
058: private byte[] _text_buffer = null;
059: private Socket _socket = null;
060: private InputStream _in = null;
061: private OutputStream _out = null;
062: private DataInputStream _data_stream = null;
063: private ByteArrayOutputStream _baos = null;
064: private DataOutputStream _dos = null;
065: private int _reply_com = -1;
066: private String _reply_command;
067:
068: /**
069: * Construct a Client for the default hostname and port number
070: */
071: public Client() {
072: _host_name = DEFAULT_SERVER;
073: _port = PushCacheFilter.DEFAULT_PORT_NUM;
074: }
075:
076: /**
077: * Construct a Client for the specified hostname and port number
078: */
079: public Client(String hostname, int port) {
080: _host_name = hostname;
081: _port = port;
082: }
083:
084: /**
085: * Construct client with command line arguments and execute
086: * command. Called by main, for use as stand alone program -
087: * calls System.exit at the end of execution
088: */
089: public Client(String[] argv) throws IOException {
090: handleArgs(argv);
091: }
092:
093: /**
094: * Display version information
095: */
096: protected void version() {
097: System.err.println("Jigsaw Push Cache Java Client 1.0 "
098: + "by Paul Henshaw, The Fantastic Corporation\n"
099: + VC_REVISION + "\n" + VC_ID + "\n");
100: }
101:
102: /**
103: * Display usage/help message
104: */
105: protected void usage() {
106: System.err
107: .println("Usage: -c <command> [-u <url>] [ -p <path>] "
108: + "[ -s <server>] [ -P <port> ] "
109: + "[ -v ] [ -h ] [ -V ]\n"
110: + "\t-s\t\t connect to specified server, default is "
111: + DEFAULT_SERVER
112: + ".\n"
113: + "\t-P\t\t connect to specified port, default is "
114: + PushCacheFilter.DEFAULT_PORT_NUM
115: + ".\n"
116: + "\t-v\t\t verbose operation, repeat for more verbosity.\n"
117: + "\t-h\t\t show this help message.\n"
118: + "\t-V\t\t show version information.\n\n"
119: + "Commands recognised:\n"
120: + "\tADD\t\t add <path> to cache as if downloaded from <url>.\n"
121: + "\tDEL\t\t delete <url> from cache.\n"
122: + "\tPRS\t\t check if <url> is present in cache.\n"
123: + "\tCLN\t\t clean cache.\n"
124: + "\tNOP\t\t do nothing.\n\n"
125: + "Notes:\n"
126: + "\tURLs should be fully qualified including trailing slashes\n"
127: + "\tfor directories, e.g.:\n"
128: + "\t\thttp://www.fantastic.com/\n\trather than:"
129: + "\n\t\thttp://www.fantastic.com\n\n"
130: + "\tBy default the operation of the PRS command is silent, \n"
131: + "\tuse the -v flag to display a text message indicating the\n"
132: + "\tpresence of a URL in the cache.\n\n"
133: + "\tWhen ADDing a file only the <path> is sent to the server\n"
134: + "\tNOT the contents of the file, this means that files can\n"
135: + "\tbe added to remote caches only if the client and server\n"
136: + "\tshare a file system via NFS or similar.\n\n"
137: + "Exit Value:\n"
138: + "\tThis program exits with one of the following values:\n"
139: + "\t0\t OK, command successful (PRS: URL present)\n"
140: + "\t1\t (PRS only) command successful, URL not present\n"
141: + "\t255\t command unsuccessful\n");
142: }
143:
144: /**
145: * Interpret command line arguments
146: */
147: protected void handleArgs(String[] argv) throws IOException {
148: String url = null, path = null, com = null;
149: char c;
150: boolean got_com = false;
151: boolean error = false;
152:
153: for (int i = 0; i < argv.length; i++) {
154: if (argv[i].charAt(0) != '-' || argv[i].length() != 2) {
155: error = true;
156: break;
157: }
158: c = argv[i].charAt(1);
159: switch (c) {
160: case 'c':
161: if (i == argv.length - 1) {
162: System.err.println("Missing argument for -" + c);
163: error = true;
164: break;
165: }
166: com = argv[++i].toUpperCase() + "\0";
167: got_com = true;
168: break;
169:
170: case 'u':
171: if (i == argv.length - 1) {
172: System.err.println("Missing argument for -" + c);
173: error = true;
174: break;
175: }
176: url = argv[++i];
177: break;
178:
179: case 'p':
180: if (i == argv.length - 1) {
181: System.err.println("Missing argument for -" + c);
182: error = true;
183: break;
184: }
185: path = argv[++i];
186: break;
187:
188: case 's':
189: if (i == argv.length - 1) {
190: System.err.println("Missing argument for -" + c);
191: error = true;
192: break;
193: }
194: _host_name = argv[++i];
195: break;
196:
197: case 'P':
198: if (i == argv.length - 1) {
199: System.err.println("Missing argument for -" + c);
200: error = true;
201: break;
202: }
203: try {
204: _port = Integer.parseInt(argv[++i]);
205: } catch (Exception e) {
206: System.err.println("Invalid port number \""
207: + argv[i] + "\"");
208: _port = -1;
209: error = true;
210: }
211: break;
212:
213: case 'v':
214: ++_verbose;
215: break;
216:
217: case 'h':
218: usage();
219: System.exit(OK);
220:
221: case 'V':
222: version();
223: System.exit(OK);
224:
225: default:
226: error = true;
227: }
228: }
229:
230: if (!got_com) {
231: System.err.println("No command specified");
232: error = true;
233: }
234:
235: if (error) {
236: usage();
237: System.exit(FAILED);
238: }
239:
240: if (_port == -1) {
241: _port = PushCacheFilter.DEFAULT_PORT_NUM;
242: }
243:
244: if (_host_name.length() == 0) {
245: _host_name = DEFAULT_SERVER;
246: }
247:
248: int ev = 0;
249: try {
250: switch (PushCacheProtocol.instance().parseCommand(com)) {
251: case PushCacheProtocol.ADD:
252: add(path, url);
253: break;
254: case PushCacheProtocol.DEL:
255: del(url);
256: break;
257: case PushCacheProtocol.PRS:
258: if (!isPresent(url)) {
259: ev = 1;
260: }
261: break;
262: default:
263: simpleCommand(com);
264: }
265: } catch (IllegalArgumentException e) {
266: System.err.println(e.getMessage());
267: usage();
268: ev = FAILED;
269: }
270: sendBye();
271: System.exit(ev);
272: }
273:
274: /**
275: * Connect to server - try up to MAX_TRIES times, then give up
276: */
277: protected void connect() throws UnknownHostException, IOException {
278: int tries = 0;
279: while (_socket == null) {
280: try {
281: _socket = new Socket(_host_name, _port);
282: _in = _socket.getInputStream();
283: _out = _socket.getOutputStream();
284: } catch (ConnectException e) {
285: System.err.println("Failed to connect to " + _host_name
286: + " on port " + _port + ": " + e.getMessage());
287: if (++tries > MAX_TRIES) {
288: throw new ConnectException("Failed to connect to "
289: + _host_name + " on port " + _port
290: + " after " + MAX_TRIES + " attempts:\n"
291: + e);
292: }
293:
294: System.err.println("Retrying....");
295: try {
296: Thread.sleep(2000);
297: } catch (InterruptedException ex) {
298: // IGNORE
299: }
300: }
301: }
302: }
303:
304: /**
305: * Write header information into packet buffer
306: */
307: protected void writeHeader() throws IOException {
308: _baos = new ByteArrayOutputStream();
309: _dos = new DataOutputStream(_baos);
310: _dos.write(PushCacheProtocol.instance().header(), 0,
311: PushCacheProtocol.instance().header().length);
312: }
313:
314: /**
315: * Read remaining payload length bytes into buffer
316: */
317: protected void readPayload() throws IOException {
318: _data_stream.skipBytes(PushCacheProtocol.COMMAND_LEN);
319: int rlen = _data_stream.readInt();
320:
321: if (rlen == 0) {
322: return;
323: }
324: _text_buffer = new byte[rlen];
325:
326: int sofar = 0;
327: int toread = rlen;
328:
329: sofar = 0;
330: toread = rlen;
331: int rv = -1;
332: while (toread > 0) {
333: rv = _in.read(_text_buffer, sofar, toread);
334: if (rv < 0) {
335: throw new IOException("read returned " + rv);
336: }
337: sofar += rv;
338: toread -= rv;
339: }
340: _data_stream = new DataInputStream(new ByteArrayInputStream(
341: _text_buffer));
342: }
343:
344: /**
345: * Await reply packet
346: */
347: protected void readReply() throws IOException {
348: // Read packet
349: _buffer = new byte[PushCacheProtocol.PACKET_LEN];
350: int rv = _in.read(_buffer);
351: if (rv < 0) {
352: throw new IOException("read returned " + rv);
353: }
354: if (rv < PushCacheProtocol.PACKET_LEN) {
355: throw new IOException(
356: "read returned less than PACKET_LEN bytes " + rv);
357: }
358:
359: // Check protocol tag
360: if (!PushCacheProtocol.instance().isValidProtocolTag(_buffer)) {
361: throw new IOException("Bad protocol tag");
362: }
363:
364: // Use a DataInputStream to read bytes and shorts
365: _data_stream = new DataInputStream(new ByteArrayInputStream(
366: _buffer));
367: _data_stream.skipBytes(PushCacheProtocol.TAG_LEN);
368:
369: // Check protol version
370: short maj = _data_stream.readShort();
371: short min = _data_stream.readShort();
372: if (maj != PushCacheProtocol.MAJ_PROTO_VERSION
373: || min > PushCacheProtocol.MIN_PROTO_VERSION) {
374: throw new IOException("Bad protocol version");
375: }
376:
377: //
378: // Check command
379: // Note that check includes NULL characters they are
380: // part of the command.
381: //
382: _reply_command = new String(_buffer,
383: PushCacheProtocol.HEADER_LEN,
384: PushCacheProtocol.COMMAND_LEN);
385: _reply_com = PushCacheProtocol.instance().parseCommand(
386: _reply_command);
387: readPayload();
388: }
389:
390: /**
391: * Handle an ERR message from server - cleanup, throw IOException
392: */
393: protected void serverError() throws IOException {
394: cleanup();
395: throw new IOException("Recieved error message from server:\n"
396: + new String(_text_buffer));
397: }
398:
399: /**
400: * Handle an unexpected message from server - send BYE, throw IOException
401: */
402: protected void unexpectedReply() throws IOException {
403: sendBye();
404: throw new IOException("Unexpected reply from server "
405: + _reply_command);
406: }
407:
408: /**
409: * Cleanup, shutdown and close socket and streams
410: */
411: protected void cleanup() {
412: try {
413: if (_in != null) {
414: _in.close();
415: _in = null;
416: }
417: if (_out != null) {
418: _out.close();
419: _out = null;
420: }
421: if (_socket != null) {
422: // _socket.shutdownInput();
423: // _socket.shutdownOutput();
424: _socket.close();
425: _socket = null;
426: }
427: } catch (java.io.IOException e) {
428: // IGNORE
429: }
430: }
431:
432: /**
433: * Send a BYE packet, and cleanup
434: */
435: public void sendBye() throws IOException {
436: if (_out != null) {
437: writeHeader();
438: _dos.writeBytes("BYE\0");
439: _dos.writeInt(0);
440: _baos.writeTo(_out);
441: cleanup();
442: }
443: }
444:
445: /**
446: * Add file into cache as url
447: */
448: public void add(String path, String url) throws IOException,
449: IllegalArgumentException {
450: if (url == null || url.length() == 0 || path == null
451: || path.length() == 0) {
452: throw new IllegalArgumentException(
453: "Zero length path or url");
454: }
455:
456: connect();
457: writeHeader();
458: _dos.writeBytes("ADD\0");
459: // ulen+plen+2*'\0'+ 2*int
460: _dos.writeInt(url.length() + path.length() + 10);
461: _dos.writeInt(path.length() + 1);
462: _dos.writeInt(url.length() + 1);
463: _dos.writeBytes(path + "\0");
464: _dos.writeBytes(url + "\0");
465: _baos.writeTo(_out);
466:
467: readReply();
468: switch (_reply_com) {
469: case PushCacheProtocol.OK:
470: break;
471: case PushCacheProtocol.ERR:
472: serverError();
473: break;
474: default:
475: unexpectedReply();
476: }
477: }
478:
479: /**
480: * Throws IllegalArgumentException iff com is not a null
481: * terminated string of the correct length
482: */
483: protected void checkCommand(String com)
484: throws IllegalArgumentException {
485: if (com.length() != PushCacheProtocol.COMMAND_LEN) {
486: throw new IllegalArgumentException("Command \"" + com
487: + "\" is wrong length");
488: }
489: if (com.charAt(3) != '\0') {
490: throw new IllegalArgumentException("Command \"" + com
491: + "\" is not null terminated");
492: }
493: }
494:
495: /**
496: * Send a packet with specified command and a url
497: */
498: public void urlCommand(String com, String url) throws IOException,
499: IllegalArgumentException {
500: if (url == null || url.length() == 0) {
501: throw new IllegalArgumentException("Zero length url");
502: }
503: checkCommand(com);
504: connect();
505: writeHeader();
506: _dos.writeBytes(com);
507: _dos.writeInt(url.length() + 1);
508: _dos.writeBytes(url + "\0");
509: _baos.writeTo(_out);
510: }
511:
512: /**
513: * Remove url from cache
514: */
515: public void del(String url) throws IOException {
516: urlCommand("DEL\0", url);
517: readReply();
518: switch (_reply_com) {
519: case PushCacheProtocol.OK:
520: break;
521: case PushCacheProtocol.ERR:
522: serverError();
523: break;
524: default:
525: unexpectedReply();
526: }
527: }
528:
529: /**
530: * True iff url is present in cache
531: */
532: public boolean isPresent(String url) throws IOException {
533: urlCommand("PRS\0", url);
534: readReply();
535: switch (_reply_com) {
536: case PushCacheProtocol.OK:
537: if (_verbose > 0) {
538: System.err.println(url + " is present");
539: }
540: return (true);
541:
542: case PushCacheProtocol.NO:
543: if (_verbose > 0) {
544: System.err.println(url + " is not present");
545: }
546: return (false);
547:
548: case PushCacheProtocol.ERR:
549: serverError();
550: break;
551: default:
552: unexpectedReply();
553: }
554: return (false);
555: }
556:
557: /**
558: * Send a simple packet with specified command
559: */
560: public void simpleCommand(String com) throws IOException {
561: checkCommand(com);
562: connect();
563: writeHeader();
564: _dos.writeBytes(com);
565: _dos.writeInt(0);
566: _baos.writeTo(_out);
567:
568: readReply();
569: switch (_reply_com) {
570: case PushCacheProtocol.OK:
571: break;
572: case PushCacheProtocol.ERR:
573: serverError();
574: break;
575: default:
576: unexpectedReply();
577: }
578: }
579:
580: /**
581: * Send a NOP packet
582: */
583: public void nop() throws IOException {
584: simpleCommand("NOP\0");
585: }
586:
587: /**
588: * Clean cache - remove all entries
589: */
590: public void clean() throws IOException {
591: simpleCommand("CLN\0");
592: }
593:
594: /**
595: * For use as stand alone program - constructs Client with argv
596: */
597: public static void main(String[] argv) {
598: try {
599: Client c = new Client(argv);
600: } catch (Exception e) {
601: e.printStackTrace();
602: }
603: }
604: }
|