View Javadoc

1   /********************************************************************************
2    * $Id: HylaFAXClient.java 125 2008-07-10 20:32:42Z sjardine $
3    * 
4    * Copyright 1999, 2000 Joe Phillips <jaiger@net-foundry.com>
5    * Copyright 2001 Innovation Software Group, LLC - http://www.innovationsw.com
6    * Copyright 2006 John Yeary <jyeary@javanetwork.net>
7    * Copyright 2007, 2008 Steven Jardine, MJN Services, Inc. <sjardine@users.sourceforge.net>
8    * 
9    * All rights reserved. This program and the accompanying materials are made
10   * available under the terms of the GNU Lesser Public License v2.1 which 
11   * accompanies this distribution, and is available at
12   * 	http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
13   *
14   * For more information on the HylaFAX Fax Server please see
15   * 	HylaFAX  - http://www.hylafax.org or 
16   * 	Hylafax+ - http://hylafax.sourceforge.net
17   * 
18   * Contributors:
19   * 	Stefan Unterhofer - Initial API and implementation
20   * 	Steven Jardine - Misc fixes, Code formatting, rework of license header,
21   * 			javadoc 
22   ******************************************************************************/
23  package gnu.hylafax;
24  
25  import gnu.inet.ftp.ActiveGetter;
26  import gnu.inet.ftp.ActivePutter;
27  import gnu.inet.ftp.ConnectionListener;
28  import gnu.inet.ftp.Getter;
29  import gnu.inet.ftp.PassiveConnection;
30  import gnu.inet.ftp.PassiveGetter;
31  import gnu.inet.ftp.PassivePutter;
32  import gnu.inet.ftp.Putter;
33  import gnu.inet.ftp.ServerResponseException;
34  import gnu.inet.ftp.TransferListener;
35  
36  import java.io.BufferedReader;
37  import java.io.ByteArrayInputStream;
38  import java.io.ByteArrayOutputStream;
39  import java.io.FileNotFoundException;
40  import java.io.IOException;
41  import java.io.InputStream;
42  import java.io.InputStreamReader;
43  import java.io.OutputStream;
44  import java.util.ArrayList;
45  import java.util.Enumeration;
46  import java.util.List;
47  import java.util.Vector;
48  
49  /***
50   * This class implements convenience methods that wrapper the ClientProtocol
51   * methods for common functionality.
52   * 
53   * Most developers will want to use this class rather than the lower-level
54   * ClientProtocol methods directly.
55   * 
56   * @version $Revision: 125 $
57   * @author Joe Phillips <jaiger@net-foundry.com>
58   * @author John Yeary <jyeary@javanetwork.net>
59   * @author Steven Jardine <steve@mjnservices.com>
60   */
61  public class HylaFAXClient extends HylaFAXClientProtocol implements Client {
62  
63      private static final int GET = 0;
64  
65      private static final int LIST = 1;
66  
67      private static final int NAMELIST = 2;
68  
69      /***
70       * This is a cached PassiveConnection instance. After some hair-pulling I
71       * came to realize that in order to avoid an annoying bug, I needed to cache
72       * the last PassiveConnection instance in case an error was encountered in
73       * the last passive transfer. In my test cases the error condition that
74       * triggered the wierd bug could be duplicated by attempting to
75       * getList(some-non-exising-file). This caused an exception during the LIST
76       * transfer. The next PASV command would return the same IP/Port values.
77       * Opening a second socket to the same IP/Port would yield an exception or
78       * other errors. Caching the last PassiveConnection (and by extension the
79       * Socket) I could reuse the last PassiveConnection values avoiding the
80       * bug/error-condition. This is not very pretty but it works. Unfortunately,
81       * I'm worried that it may not be very portable since it relies on the
82       * behavior of the HylaFAX server. I'll have to see if this behavior is in
83       * the FTP RFC (RFC0959.)
84       * <P>
85       * Whenever a successful passive transfer occurrs, this variable should be
86       * set to null, thereby invalidating the cached value.
87       */
88      protected PassiveConnection connection = null;
89  
90      private Vector connectionListeners;
91  
92      private char mode; // the current mode setting
93  
94      // indicate whether passive transfers should be used
95      private boolean passive;
96  
97      private Vector transferListeners;
98  
99      /***
100      * default constructor. initialize class state.
101      */
102     public HylaFAXClient() {
103 	passive = false; // disable passive transfers by default
104 	mode = MODE_STREAM; // default mode is stream mode
105 	connectionListeners = new Vector();
106 	transferListeners = new Vector();
107     }
108 
109     /***
110      * Register a connection listener with the event source.
111      * 
112      * @param listener
113      *            the listener to register with the event source
114      */
115     public void addConnectionListener(ConnectionListener listener) {
116 	connectionListeners.addElement(listener);
117     }
118 
119     /***
120      * Register a set of connection listeners with the event source.
121      * 
122      * @param listeners
123      *            the listeners to register with the event source
124      */
125     public void addConnectionListeners(Vector listeners) {
126 	Enumeration enumeration = listeners.elements();
127 
128 	while (enumeration.hasMoreElements()) {
129 	    ConnectionListener listener = (ConnectionListener) enumeration
130 		    .nextElement();
131 	    connectionListeners.addElement(listener);
132 	}
133     }
134 
135     /***
136      * Register a transfer listener with the event source.
137      * 
138      * @param listener
139      *            the listener to register with the event source
140      */
141     public void addTransferListener(TransferListener listener) {
142 	transferListeners.addElement(listener);
143     }
144 
145     /***
146      * Register a set of transfer listeners with the event source.
147      * 
148      * @param listeners
149      *            the listeners to register with the event source
150      */
151     public void addTransferListeners(Vector listeners) {
152 	Enumeration enumeration = listeners.elements();
153 
154 	while (enumeration.hasMoreElements()) {
155 	    TransferListener listener = (TransferListener) enumeration
156 		    .nextElement();
157 	    transferListeners.addElement(listener);
158 	}
159     }
160 
161     /***
162      * Create a new job in the server
163      * 
164      * @return a new Job instance on the server
165      * @exception ServerResponseException
166      * @exception IOException
167      *                an IO error occurred while communicating with the server
168      */
169     public Job createJob() throws ServerResponseException, IOException {
170 	return new gnu.hylafax.job.Job(this);
171     }
172 
173     /***
174      * Delete the given done or suspended job.
175      * 
176      * @param job
177      *            the (done or suspended) job to delete
178      * @exception ServerResponseException
179      * @exception IOException
180      *                an IO error occurred while communicating with the server
181      */
182     public void delete(Job job) throws ServerResponseException, IOException {
183 	jdele(job.getId());
184     }
185 
186     /***
187      * GET the named file, FTP style.
188      * 
189      * @param path
190      *            the name of the file to GET. This can be a full or partial
191      *            path.
192      * @param out
193      *            the OutputStream to write the file data to
194      * @exception IOException
195      *                an IO error occurred
196      * @exception ServerResponseException
197      *                the server reported an error
198      * @exception FileNotFoundException
199      *                the given path does not exist
200      */
201     public synchronized void get(String path, OutputStream out)
202 	    throws IOException, FileNotFoundException, ServerResponseException {
203 	get(path, out, GET);
204     }
205 
206     private synchronized void get(String path, OutputStream out, int type)
207 	    throws IOException, FileNotFoundException, ServerResponseException {
208 
209 	Getter getter;
210 	if (passive == true) {
211 	    // do a passive transfer
212 	    if (connection == null) {
213 		connection = new PassiveConnection(pasv());
214 	    }
215 	    getter = new PassiveGetter(out, connection);
216 	} else {
217 	    getter = new ActiveGetter(out);
218 	    // do a non-passive (active) transfer
219 	    port(getInetAddress(), ((ActiveGetter) getter).getPort());
220 	}
221 
222 	// start transfer
223 	getter.addConnectionListeners(connectionListeners);
224 	getter.addTransferListeners(transferListeners);
225 	getter.start();
226 
227 	// start transmission
228 	try {
229 	    switch (type) {
230 	    case GET:
231 		retr(path);
232 		break;
233 	    case LIST:
234 		list(path);
235 		break;
236 	    case NAMELIST:
237 		nlst(path);
238 		break;
239 	    }
240 	} catch (FileNotFoundException fnfe) {
241 	    getter.cancel();
242 	    throw fnfe;
243 	} catch (IOException ioe) {
244 	    getter.cancel();
245 	    throw ioe;
246 	} catch (ServerResponseException sree) {
247 	    getter.cancel();
248 	    throw sree;
249 	} finally {
250 	    // wait for thread to end
251 	    try {
252 		getter.join();
253 	    } catch (InterruptedException ie) {
254 		// not really an error
255 	    }
256 	}
257 	connection = null;
258     }
259 
260     /***
261      * Get a Job instance for the given job id
262      * 
263      * @param id
264      *            the id of the job to get
265      * @exception ServerResponseException
266      * @exception IOException
267      *                an IO error occurred while communicating with the server
268      */
269     public Job getJob(long id) throws ServerResponseException, IOException {
270 	return new gnu.hylafax.job.Job(this, id);
271     }
272 
273     /***
274      * Get a long-style listing of files in the current directory.
275      * 
276      * <b>NOTE:</b> this calls the list() method internally with the "." path.
277      * 
278      * @exception IOException
279      *                an IO error occurred
280      * @exception FileNotFoundException
281      *                the "." path doesn't exist
282      * @exception ServerResponseException
283      *                the server reported an error
284      * @return a Vector of Strings containing the list information
285      */
286     public synchronized Vector getList() throws IOException,
287 	    FileNotFoundException, ServerResponseException {
288 	return getList(null, false);
289     }
290 
291     /***
292      * Get a long-style listing of files in the given directory.
293      * 
294      * <b>NOTE:</b> this calls the list() method internally.
295      * 
296      * @param path
297      *            the path that we're interested in finding the contents of
298      * @exception IOException
299      *                an IO error occurred
300      * @exception FileNotFoundException
301      *                the given path doesn't exist
302      * @exception ServerResponseException
303      *                the server reported an error
304      * @return a Vector of Strings containing the list information
305      */
306     public synchronized Vector getList(String path) throws IOException,
307 	    FileNotFoundException, ServerResponseException {
308 	return getList(path, false);
309     }
310 
311     private synchronized Vector getList(String path, boolean namelist)
312 	    throws IOException, FileNotFoundException, ServerResponseException {
313 
314 	ByteArrayOutputStream buffer = new ByteArrayOutputStream();
315 	get(path, buffer, namelist ? NAMELIST : LIST);
316 
317 	Vector result = new Vector();
318 	BufferedReader data = new BufferedReader(new InputStreamReader(
319 		new ByteArrayInputStream(buffer.toByteArray())));
320 	try {
321 	    String line = null;
322 	    String next = null;
323 	    while ((next = data.readLine()) != null) {
324 		next = next.trim();
325 		if (next.endsWith("//")) {
326 		    next = next.substring(0, next.lastIndexOf("//"));
327 		    line = (line == null ? next : line + " " + next).trim();
328 		    continue;
329 		}
330 		line = (line == null ? next : line + " " + next).trim();
331 		result.add(line);
332 		line = null;
333 	    }
334 	} finally {
335 	    data.close();
336 	}
337 
338 	return result;
339 
340     }
341 
342     /***
343      * get name list of files in the current directory. Similar to getList() but
344      * returns filenames only where getList() returns other, system dependant
345      * information.
346      * 
347      * @exception IOException
348      *                an IO error occurred
349      * @exception ServerResponseException
350      *                the server reported an error
351      * @exception FileNotFoundException
352      *                the requested path does not exist
353      * @return Vector of Strings containing filenames
354      */
355     public synchronized Vector getNameList() throws IOException,
356 	    ServerResponseException, FileNotFoundException {
357 	return getNameList(null);
358     }
359 
360     /***
361      * Get name list of files in the given directory. Similar to getList() but
362      * returns filenames only where getList() returns other, system dependant
363      * information.
364      * 
365      * @param path
366      *            the path of the directory that we want the name list of
367      * @exception IOException
368      *                an IO error occurred
369      * @exception ServerResponseException
370      *                the server reported an error
371      * @exception FileNotFoundException
372      *                the requested path does not exist
373      * @return Vector of Strings containing filenames
374      */
375     public synchronized Vector getNameList(String path) throws IOException,
376 	    ServerResponseException, FileNotFoundException {
377 	return getList(path, true);
378     }
379 
380     /***
381      * Check whether passive transfers have been enabled
382      * 
383      * @return true if passive transfers are enabled, false otherwise
384      */
385     public synchronized boolean getPassive() {
386 	return passive;
387     }
388 
389     /***
390      * Interrupt the given job.
391      * 
392      * @param job
393      *            the job to interrupt
394      * @exception ServerResponseException
395      * @exception IOException
396      *                an IO error occurred while communicating with the server
397      */
398     public void interrupt(Job job) throws ServerResponseException, IOException {
399 	jintr(job.getId());
400     }
401 
402     /***
403      * Kill the given job
404      * 
405      * @param job
406      *            the job to kill
407      * @exception ServerResponseException
408      * @exception IOException
409      *                an IO error occurred while communicating with the server
410      */
411     public void kill(Job job) throws ServerResponseException, IOException {
412 	jkill(job.getId());
413     }
414 
415     /***
416      * Set the transfer mode. Valid mode values are MODE_* listed in the
417      * ClientProtocol class.
418      * 
419      * @param mode
420      *            the new mode setting
421      * @exception IOException
422      *                an io error occurred talking to the server
423      * @exception ServerResponseException
424      *                the server replied with an error code
425      */
426     public synchronized void mode(char newMode) throws IOException,
427 	    ServerResponseException {
428 	super.mode(newMode);
429 	this.mode = newMode; // cache the mode for later use
430     }
431 
432     /***
433      * put a file with a unique name.
434      * 
435      * <b>NOTE:</b> this calls stou() internally.
436      * 
437      * @exception IOException
438      *                a socket IO error occurred
439      * @exception ServerResponseException
440      *                the server responded with an error code
441      * @return the name of the file created
442      */
443     public synchronized String put(InputStream data) throws IOException,
444 	    ServerResponseException {
445 	return put(data, null, false);
446     }
447 
448     /***
449      * Store a file.
450      * 
451      * <b>NOTE:</b> this calls stor() internally.
452      * 
453      * @param pathname
454      *            name of file to store on server (where to put the file on the
455      *            server)
456      * @exception IOException
457      *                a socket IO error occurred
458      * @exception ServerResponseException
459      *                the server responded with an error
460      */
461     public synchronized void put(InputStream in, String pathname)
462 	    throws IOException, ServerResponseException {
463 	put(in, pathname, false);
464     }
465 
466     /***
467      * Puts a file on the server. If temporary is set the file is a temporary
468      * file and will be deleted when the connection is closed.
469      * 
470      * <b>NOTE:</b> this calls stot(), stou() or stor() internally.
471      * 
472      * @param data
473      * @param pathname
474      *            the pathname of the file. Should be set to null if file is a
475      *            temporary file or no specific pathname is desired.
476      * @param temporary
477      *            is the file a temporary file?
478      * @return the filename of the file. Will be NULL when pathname is not null.
479      * @throws IOException
480      *             io error occurred talking to the server
481      * @throws ServerResponseException
482      *             server replied with error code
483      */
484     private synchronized String put(InputStream data, String pathname,
485 	    boolean temporary) throws IOException, ServerResponseException {
486 	String filename;
487 	Putter put;
488 
489 	if (passive == true) {
490 	    // do a passive transfer
491 	    if (connection == null) {
492 		connection = new PassiveConnection(pasv());
493 	    }
494 	    put = new PassivePutter(data, connection);
495 	} else {
496 	    // do a non-passive (active) transfer
497 	    put = new ActivePutter(data);
498 	    port(getInetAddress(), ((ActivePutter) put).getPort());
499 	}
500 
501 	put.setMode(mode);
502 	put.addConnectionListeners(connectionListeners);
503 	put.addTransferListeners(transferListeners);
504 	put.start();
505 
506 	// start transmission
507 	try {
508 	    if (pathname != null) {
509 		stor(data, pathname);
510 		return null;
511 	    }
512 	    filename = temporary ? stot(data) : stou(data);
513 	} catch (IOException ioe) {
514 	    put.cancel();
515 	    throw ioe;
516 	} catch (ServerResponseException sree) {
517 	    put.cancel();
518 	    throw sree;
519 	} finally {
520 	    // wait for thread to end
521 	    try {
522 		put.join();
523 	    } catch (InterruptedException ie) {
524 		// not really an error
525 	    }
526 	}
527 	connection = null;
528 	return filename;
529     }
530 
531     /***
532      * Put a temp file, the data is stored in a uniquely named file on the
533      * server. The remote temp file is deleted when the connection is closed.
534      * 
535      * <b>NOTE:</b> this calls stot() internally.
536      * 
537      * @exception IOException
538      *                io error occurred talking to the server
539      * @exception ServerResponseException
540      *                server replied with error code
541      * @return the filename of the temp file
542      */
543     public synchronized String putTemporary(InputStream data)
544 	    throws IOException, ServerResponseException {
545 	return put(data, null, true);
546     }
547 
548     /***
549      * De-register a connection listener with the event source.
550      * 
551      * @param listener
552      *            the listener to de-register with the event source
553      */
554     public void removeConnectionListener(ConnectionListener listener) {
555 	connectionListeners.removeElement(listener);
556     }
557 
558     /***
559      * De-register a transfer listener with the event source.
560      * 
561      * @param listener
562      *            the listener to de-register with the event source
563      */
564     public void removeTransferListener(TransferListener listener) {
565 	transferListeners.removeElement(listener);
566     }
567 
568     /***
569      * Retry a given job with a default killtime of "now + 3 hours".
570      * 
571      * @param id
572      *            the job id to retry.
573      * @return the job id associated with the new job.
574      * @throws ServerResponseException
575      * @throws IOException
576      */
577     public long retry(long id) throws ServerResponseException, IOException {
578 	return retry(id, "000259");
579     }
580 
581     /***
582      * Retry a given job.
583      * 
584      * @param id
585      *            the job id to retry.
586      * @param killTime
587      *            the new killTime for the job.
588      * @return the job id associated with the new job.
589      * @throws ServerResponseException
590      * @throws IOException
591      */
592     public long retry(long id, String killTime) throws ServerResponseException,
593 	    IOException {
594 	job(id);
595 	// parse the document names.
596 	List documents = new ArrayList();
597 	String[] docs = jparm("document").split("\n");
598 	for (int count = 0; count < docs.length; count++) {
599 	    String document = docs[count];
600 	    if (document.equals("End of documents."))
601 		break;
602 	    documents.add(document.split(" ")[1]);
603 	}
604 	jnew();
605 	for (int index = 0; index < documents.size(); index++) {
606 	    jparm("document", documents.get(index));
607 	}
608 	jparm("lasttime", killTime);
609 	return jsubm();
610     }
611 
612     /***
613      * enable or disable passive transfers
614      * 
615      * @param passive
616      *            indicates whether passive transfers should be used
617      */
618     public synchronized void setPassive(boolean passive) {
619 	this.passive = passive;
620     }
621 
622     /***
623      * Submit the given job to the scheduler.
624      * 
625      * @param job
626      *            the Job to submit
627      * @exception ServerResponseException
628      * @exception IOException
629      *                an IO error occurred while communicating with the server
630      */
631     public void submit(Job job) throws ServerResponseException, IOException {
632 	jsubm(job.getId());
633     }
634 
635     /***
636      * Suspend the given job from the scheduler.
637      * 
638      * @param job
639      *            the Job to suspend
640      * @exception ServerResponseException
641      * @exception IOException
642      *                an IO error occurred while communicating with the server
643      */
644     public void suspend(Job job) throws ServerResponseException, IOException {
645 	jsusp(job.getId());
646     }
647 
648     /***
649      * wait for the given job to complete
650      * 
651      * @param job
652      *            the job to wait for
653      * @exception ServerResponseException
654      * @exception IOException
655      *                an IO error occurred while communicating with the server
656      */
657     public void wait(Job job) throws ServerResponseException, IOException {
658 	jwait(job.getId());
659     }
660 
661 }