View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.net.tftp;
19  
20  import java.io.BufferedInputStream;
21  import java.io.BufferedOutputStream;
22  import java.io.File;
23  import java.io.FileInputStream;
24  import java.io.FileNotFoundException;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.OutputStream;
29  import java.io.PrintStream;
30  import java.net.SocketTimeoutException;
31  import java.util.HashSet;
32  import java.util.Iterator;
33  
34  import org.apache.commons.net.io.FromNetASCIIOutputStream;
35  import org.apache.commons.net.io.ToNetASCIIInputStream;
36  
37  /**
38   * A fully multi-threaded tftp server. Can handle multiple clients at the same time. Implements RFC
39   * 1350 and wrapping block numbers for large file support.
40   * 
41   * To launch, just create an instance of the class. An IOException will be thrown if the server
42   * fails to start for reasons such as port in use, port denied, etc.
43   * 
44   * To stop, use the shutdown method.
45   * 
46   * To check to see if the server is still running (or if it stopped because of an error), call the
47   * isRunning() method.
48   * 
49   * By default, events are not logged to stdout/stderr. This can be changed with the
50   * setLog and setLogError methods.
51   * 
52   * <p>
53   * Example usage is below:
54   * 
55   * <code>
56   * public static void main(String[] args) throws Exception
57   *  {
58   *      if (args.length != 1)
59   *      {
60   *          System.out
61   *                  .println("You must provide 1 argument - the base path for the server to serve from.");
62   *          System.exit(1);
63   *      }
64   *
65   *      TFTPServer ts = new TFTPServer(new File(args[0]), new File(args[0]), GET_AND_PUT);
66   *      ts.setSocketTimeout(2000);
67   *
68   *      System.out.println("TFTP Server running.  Press enter to stop.");
69   *      new InputStreamReader(System.in).read();
70   *
71   *      ts.shutdown();
72   *      System.out.println("Server shut down.");
73   *      System.exit(0);
74   *  }
75   *
76   * </code>
77   * 
78   * 
79   * @author <A HREF="mailto:daniel.armbrust.list@gmail.com">Dan Armbrust</A>
80   * @since 2.0
81   */
82  
83  public class TFTPServer implements Runnable
84  {
85      private static final int DEFAULT_TFTP_PORT = 69;
86      public static enum ServerMode { GET_ONLY, PUT_ONLY, GET_AND_PUT; }
87  
88      private HashSet<TFTPTransfer> transfers_ = new HashSet<TFTPTransfer>();
89      private volatile boolean shutdownServer = false;
90      private TFTP serverTftp_;
91      private File serverReadDirectory_;
92      private File serverWriteDirectory_;
93      private int port_;
94      private Exception serverException = null;
95      private ServerMode mode_;
96  
97      /* /dev/null output stream (default) */ 
98      private static final PrintStream nullStream = new PrintStream(
99              new OutputStream() { 
100                 @Override
101                 public void write(int b){}
102                 @Override
103                 public void write(byte[] b) throws IOException {}
104                 }
105             );
106 
107     // don't have access to a logger api, so we will log to these streams, which
108     // by default are set to a no-op logger
109     private PrintStream log_;
110     private PrintStream logError_;
111 
112     private int maxTimeoutRetries_ = 3;
113     private int socketTimeout_;
114     private Thread serverThread;
115     
116     
117     /**
118      * Start a TFTP Server on the default port (69). Gets and Puts occur in the specified
119      * directories.
120      * 
121      * The server will start in another thread, allowing this constructor to return immediately.
122      * 
123      * If a get or a put comes in with a relative path that tries to get outside of the
124      * serverDirectory, then the get or put will be denied.
125      * 
126      * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both.
127      * Modes are defined as int constants in this class.
128      * 
129      * @param serverReadDirectory directory for GET requests
130      * @param serverWriteDirectory directory for PUT requests
131      * @param mode A value as specified above.
132      * @throws IOException if the server directory is invalid or does not exist.
133      */
134     public TFTPServer(File serverReadDirectory, File serverWriteDirectory, ServerMode mode)
135             throws IOException
136     {
137         this(serverReadDirectory, serverWriteDirectory, DEFAULT_TFTP_PORT, mode, null, null);
138     }
139 
140     /**
141      * Start a TFTP Server on the specified port. Gets and Puts occur in the specified directory.
142      * 
143      * The server will start in another thread, allowing this constructor to return immediately.
144      * 
145      * If a get or a put comes in with a relative path that tries to get outside of the
146      * serverDirectory, then the get or put will be denied.
147      * 
148      * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both.
149      * Modes are defined as int constants in this class.
150      * 
151      * @param serverReadDirectory directory for GET requests
152      * @param serverWriteDirectory directory for PUT requests
153      * @param mode A value as specified above.
154      * @param log Stream to write log message to. If not provided, uses System.out
155      * @param errorLog Stream to write error messages to. If not provided, uses System.err.
156      * @throws IOException if the server directory is invalid or does not exist.
157      */
158     public TFTPServer(File serverReadDirectory, File serverWriteDirectory, int port, ServerMode mode,
159             PrintStream log, PrintStream errorLog) throws IOException
160     {
161         port_ = port;
162         mode_ = mode;
163         log_ = (log == null ? nullStream: log);
164         logError_ = (errorLog == null ? nullStream : errorLog);
165         launch(serverReadDirectory, serverWriteDirectory);
166     }
167 
168     /**
169      * Set the max number of retries in response to a timeout. Default 3. Min 0.
170      * 
171      * @param retries
172      */
173     public void setMaxTimeoutRetries(int retries)
174     {
175         if (retries < 0)
176         {
177             throw new RuntimeException("Invalid Value");
178         }
179         maxTimeoutRetries_ = retries;
180     }
181 
182     /**
183      * Get the current value for maxTimeoutRetries
184      */
185     public int getMaxTimeoutRetries()
186     {
187         return maxTimeoutRetries_;
188     }
189 
190     /**
191      * Set the socket timeout in milliseconds used in transfers. Defaults to the value here:
192      * http://commons.apache.org/net/apidocs/org/apache/commons/net/tftp/TFTP.html#DEFAULT_TIMEOUT
193      * (5000 at the time I write this) Min value of 10.
194      */
195     public void setSocketTimeout(int timeout)
196     {
197         if (timeout < 10)
198         {
199             throw new RuntimeException("Invalid Value");
200         }
201         socketTimeout_ = timeout;
202     }
203 
204     /**
205      * The current socket timeout used during transfers in milliseconds.
206      */
207     public int getSocketTimeout()
208     {
209         return socketTimeout_;
210     }
211 
212     /*
213      * start the server, throw an error if it can't start.
214      */
215     private void launch(File serverReadDirectory, File serverWriteDirectory) throws IOException
216     {
217         log_.println("Starting TFTP Server on port " + port_ + ".  Read directory: "
218                 + serverReadDirectory + " Write directory: " + serverWriteDirectory
219                 + " Server Mode is " + mode_);
220 
221         serverReadDirectory_ = serverReadDirectory.getCanonicalFile();
222         if (!serverReadDirectory_.exists() || !serverReadDirectory.isDirectory())
223         {
224             throw new IOException("The server read directory " + serverReadDirectory_
225                     + " does not exist");
226         }
227 
228         serverWriteDirectory_ = serverWriteDirectory.getCanonicalFile();
229         if (!serverWriteDirectory_.exists() || !serverWriteDirectory.isDirectory())
230         {
231             throw new IOException("The server write directory " + serverWriteDirectory_
232                     + " does not exist");
233         }
234 
235         serverTftp_ = new TFTP();
236 
237         // This is the value used in response to each client.
238         socketTimeout_ = serverTftp_.getDefaultTimeout();
239 
240         // we want the server thread to listen forever.
241         serverTftp_.setDefaultTimeout(0);
242 
243         serverTftp_.open(port_);
244 
245         serverThread = new Thread(this);
246         serverThread.setDaemon(true);
247         serverThread.start();
248     }
249 
250     @Override
251     protected void finalize() throws Throwable
252     {
253         shutdown();
254     }
255 
256     /**
257      * check if the server thread is still running.
258      * 
259      * @return true if running, false if stopped.
260      * @throws Exception throws the exception that stopped the server if the server is stopped from
261      *             an exception.
262      */
263     public boolean isRunning() throws Exception
264     {
265         if (shutdownServer && serverException != null)
266         {
267             throw serverException;
268         }
269         return !shutdownServer;
270     }
271 
272     public void run()
273     {
274         try
275         {
276             while (!shutdownServer)
277             {
278                 TFTPPacket tftpPacket;
279 
280                 tftpPacket = serverTftp_.receive();
281 
282                 TFTPTransfer tt = new TFTPTransfer(tftpPacket);
283                 synchronized(transfers_)
284                 {
285                     transfers_.add(tt);
286                 }
287 
288                 Thread thread = new Thread(tt);
289                 thread.setDaemon(true);
290                 thread.start();
291             }
292         }
293         catch (Exception e)
294         {
295             if (!shutdownServer)
296             {
297                 serverException = e;
298                 logError_.println("Unexpected Error in TFTP Server - Server shut down! + " + e);
299             }
300         }
301         finally
302         {
303             shutdownServer = true; // set this to true, so the launching thread can check to see if it started.
304             if (serverTftp_ != null && serverTftp_.isOpen())
305             {
306                 serverTftp_.close();
307             }
308         }
309     }
310 
311     /**
312      * Stop the tftp server (and any currently running transfers) and release all opened network
313      * resources.
314      */
315     public void shutdown()
316     {
317         shutdownServer = true;
318 
319         synchronized(transfers_)
320         {
321             Iterator<TFTPTransfer> it = transfers_.iterator();
322             while (it.hasNext())
323             {
324                 it.next().shutdown();
325             }
326         }
327 
328         try
329         {
330             serverTftp_.close();
331         }
332         catch (RuntimeException e)
333         {
334             // noop
335         }
336         
337         try {
338             serverThread.join();
339         } catch (InterruptedException e) {
340             // we've done the best we could, return
341         }
342     }
343 
344     /*
345      * An instance of an ongoing transfer.
346      */
347     private class TFTPTransfer implements Runnable
348     {
349         private TFTPPacket tftpPacket_;
350 
351         private boolean shutdownTransfer = false;
352 
353         TFTP transferTftp_ = null;
354 
355         public TFTPTransfer(TFTPPacket tftpPacket)
356         {
357             tftpPacket_ = tftpPacket;
358         }
359 
360         public void shutdown()
361         {
362             shutdownTransfer = true;
363             try
364             {
365                 transferTftp_.close();
366             }
367             catch (RuntimeException e)
368             {
369                 // noop
370             }
371         }
372 
373         public void run()
374         {
375             try
376             {
377                 transferTftp_ = new TFTP();
378 
379                 transferTftp_.beginBufferedOps();
380                 transferTftp_.setDefaultTimeout(socketTimeout_);
381 
382                 transferTftp_.open();
383 
384                 if (tftpPacket_ instanceof TFTPReadRequestPacket)
385                 {
386                     handleRead(((TFTPReadRequestPacket) tftpPacket_));
387                 }
388                 else if (tftpPacket_ instanceof TFTPWriteRequestPacket)
389                 {
390                     handleWrite((TFTPWriteRequestPacket) tftpPacket_);
391                 }
392                 else
393                 {
394                     log_.println("Unsupported TFTP request (" + tftpPacket_ + ") - ignored.");
395                 }
396             }
397             catch (Exception e)
398             {
399                 if (!shutdownTransfer)
400                 {
401                     logError_
402                             .println("Unexpected Error in during TFTP file transfer.  Transfer aborted. "
403                                     + e);
404                 }
405             }
406             finally
407             {
408                 try
409                 {
410                     if (transferTftp_ != null && transferTftp_.isOpen())
411                     {
412                         transferTftp_.endBufferedOps();
413                         transferTftp_.close();
414                     }
415                 }
416                 catch (Exception e)
417                 {
418                     // noop
419                 }
420                 synchronized(transfers_)
421                 {
422                     transfers_.remove(this);
423                 }
424             }
425         }
426 
427         /*
428          * Handle a tftp read request.
429          */
430         private void handleRead(TFTPReadRequestPacket trrp) throws IOException, TFTPPacketException
431         {
432             InputStream is = null;
433             try
434             {
435                 if (mode_ == ServerMode.PUT_ONLY)
436                 {
437                     transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp
438                             .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION,
439                             "Read not allowed by server."));
440                     return;
441                 }
442 
443                 try
444                 {
445                     is = new BufferedInputStream(new FileInputStream(buildSafeFile(
446                             serverReadDirectory_, trrp.getFilename(), false)));
447                 }
448                 catch (FileNotFoundException e)
449                 {
450                     transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp
451                             .getPort(), TFTPErrorPacket.FILE_NOT_FOUND, e.getMessage()));
452                     return;
453                 }
454                 catch (Exception e)
455                 {
456                     transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp
457                             .getPort(), TFTPErrorPacket.UNDEFINED, e.getMessage()));
458                     return;
459                 }
460 
461                 if (trrp.getMode() == TFTP.NETASCII_MODE)
462                 {
463                     is = new ToNetASCIIInputStream(is);
464                 }
465 
466                 byte[] temp = new byte[TFTPDataPacket.MAX_DATA_LENGTH];
467 
468                 TFTPPacket answer;
469 
470                 int block = 1;
471                 boolean sendNext = true;
472 
473                 int readLength = TFTPDataPacket.MAX_DATA_LENGTH;
474 
475                 TFTPDataPacket lastSentData = null;
476 
477                 // We are reading a file, so when we read less than the
478                 // requested bytes, we know that we are at the end of the file.
479                 while (readLength == TFTPDataPacket.MAX_DATA_LENGTH && !shutdownTransfer)
480                 {
481                     if (sendNext)
482                     {
483                         readLength = is.read(temp);
484                         if (readLength == -1)
485                         {
486                             readLength = 0;
487                         }
488 
489                         lastSentData = new TFTPDataPacket(trrp.getAddress(), trrp.getPort(), block,
490                                 temp, 0, readLength);
491                         transferTftp_.bufferedSend(lastSentData);
492                     }
493 
494                     answer = null;
495 
496                     int timeoutCount = 0;
497 
498                     while (!shutdownTransfer
499                             && (answer == null || !answer.getAddress().equals(trrp.getAddress()) || answer
500                                     .getPort() != trrp.getPort()))
501                     {
502                         // listen for an answer.
503                         if (answer != null)
504                         {
505                             // The answer that we got didn't come from the
506                             // expected source, fire back an error, and continue
507                             // listening.
508                             log_.println("TFTP Server ignoring message from unexpected source.");
509                             transferTftp_.bufferedSend(new TFTPErrorPacket(answer.getAddress(),
510                                     answer.getPort(), TFTPErrorPacket.UNKNOWN_TID,
511                                     "Unexpected Host or Port"));
512                         }
513                         try
514                         {
515                             answer = transferTftp_.bufferedReceive();
516                         }
517                         catch (SocketTimeoutException e)
518                         {
519                             if (timeoutCount >= maxTimeoutRetries_)
520                             {
521                                 throw e;
522                             }
523                             // didn't get an ack for this data. need to resend
524                             // it.
525                             timeoutCount++;
526                             transferTftp_.bufferedSend(lastSentData);
527                             continue;
528                         }
529                     }
530 
531                     if (answer == null || !(answer instanceof TFTPAckPacket))
532                     {
533                         if (!shutdownTransfer)
534                         {
535                             logError_
536                                     .println("Unexpected response from tftp client during transfer ("
537                                             + answer + ").  Transfer aborted.");
538                         }
539                         break;
540                     }
541                     else
542                     {
543                         // once we get here, we know we have an answer packet
544                         // from the correct host.
545                         TFTPAckPacket ack = (TFTPAckPacket) answer;
546                         if (ack.getBlockNumber() != block)
547                         {
548                             /*
549                              * The origional tftp spec would have called on us to resend the
550                              * previous data here, however, that causes the SAS Syndrome.
551                              * http://www.faqs.org/rfcs/rfc1123.html section 4.2.3.1 The modified
552                              * spec says that we ignore a duplicate ack. If the packet was really
553                              * lost, we will time out on receive, and resend the previous data at
554                              * that point.
555                              */
556                             sendNext = false;
557                         }
558                         else
559                         {
560                             // send the next block
561                             block++;
562                             if (block > 65535)
563                             {
564                                 // wrap the block number
565                                 block = 0;
566                             }
567                             sendNext = true;
568                         }
569                     }
570                 }
571             }
572             finally
573             {
574                 try
575                 {
576                     if (is != null)
577                     {
578                         is.close();
579                     }
580                 }
581                 catch (IOException e)
582                 {
583                     // noop
584                 }
585             }
586         }
587 
588         /*
589          * handle a tftp write request.
590          */
591         private void handleWrite(TFTPWriteRequestPacket twrp) throws IOException,
592                 TFTPPacketException
593         {
594             OutputStream bos = null;
595             try
596             {
597                 if (mode_ == ServerMode.GET_ONLY)
598                 {
599                     transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp
600                             .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION,
601                             "Write not allowed by server."));
602                     return;
603                 }
604 
605                 int lastBlock = 0;
606                 String fileName = twrp.getFilename();
607 
608                 try
609                 {
610                     File temp = buildSafeFile(serverWriteDirectory_, fileName, true);
611                     if (temp.exists())
612                     {
613                         transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp
614                                 .getPort(), TFTPErrorPacket.FILE_EXISTS, "File already exists"));
615                         return;
616                     }
617                     bos = new BufferedOutputStream(new FileOutputStream(temp));
618                     
619                     if (twrp.getMode() == TFTP.NETASCII_MODE)
620                     {
621                         bos = new FromNetASCIIOutputStream(bos);
622                     }
623                 }
624                 catch (Exception e)
625                 {
626                     transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp
627                             .getPort(), TFTPErrorPacket.UNDEFINED, e.getMessage()));
628                     return;
629                 }
630 
631                 TFTPAckPacket lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0);
632                 transferTftp_.bufferedSend(lastSentAck);
633 
634                 while (true)
635                 {
636                     // get the response - ensure it is from the right place.
637                     TFTPPacket dataPacket = null;
638 
639                     int timeoutCount = 0;
640 
641                     while (!shutdownTransfer
642                             && (dataPacket == null
643                                     || !dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket
644                                     .getPort() != twrp.getPort()))
645                     {
646                         // listen for an answer.
647                         if (dataPacket != null)
648                         {
649                             // The data that we got didn't come from the
650                             // expected source, fire back an error, and continue
651                             // listening.
652                             log_.println("TFTP Server ignoring message from unexpected source.");
653                             transferTftp_.bufferedSend(new TFTPErrorPacket(dataPacket.getAddress(),
654                                     dataPacket.getPort(), TFTPErrorPacket.UNKNOWN_TID,
655                                     "Unexpected Host or Port"));
656                         }
657 
658                         try
659                         {
660                             dataPacket = transferTftp_.bufferedReceive();
661                         }
662                         catch (SocketTimeoutException e)
663                         {
664                             if (timeoutCount >= maxTimeoutRetries_)
665                             {
666                                 throw e;
667                             }
668                             // It didn't get our ack. Resend it.
669                             transferTftp_.bufferedSend(lastSentAck);
670                             timeoutCount++;
671                             continue;
672                         }
673                     }
674 
675                     if (dataPacket != null && dataPacket instanceof TFTPWriteRequestPacket)
676                     {
677                         // it must have missed our initial ack. Send another.
678                         lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0);
679                         transferTftp_.bufferedSend(lastSentAck);
680                     }
681                     else if (dataPacket == null || !(dataPacket instanceof TFTPDataPacket))
682                     {
683                         if (!shutdownTransfer)
684                         {
685                             logError_
686                                     .println("Unexpected response from tftp client during transfer ("
687                                             + dataPacket + ").  Transfer aborted.");
688                         }
689                         break;
690                     }
691                     else
692                     {
693                         int block = ((TFTPDataPacket) dataPacket).getBlockNumber();
694                         byte[] data = ((TFTPDataPacket) dataPacket).getData();
695                         int dataLength = ((TFTPDataPacket) dataPacket).getDataLength();
696                         int dataOffset = ((TFTPDataPacket) dataPacket).getDataOffset();
697 
698                         if (block > lastBlock || (lastBlock == 65535 && block == 0))
699                         {
700                             // it might resend a data block if it missed our ack
701                             // - don't rewrite the block.
702                             bos.write(data, dataOffset, dataLength);
703                             lastBlock = block;
704                         }
705 
706                         lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), block);
707                         transferTftp_.bufferedSend(lastSentAck);
708                         if (dataLength < TFTPDataPacket.MAX_DATA_LENGTH)
709                         {
710                             // end of stream signal - The tranfer is complete.
711                             bos.close();
712 
713                             // But my ack may be lost - so listen to see if I
714                             // need to resend the ack.
715                             for (int i = 0; i < maxTimeoutRetries_; i++)
716                             {
717                                 try
718                                 {
719                                     dataPacket = transferTftp_.bufferedReceive();
720                                 }
721                                 catch (SocketTimeoutException e)
722                                 {
723                                     // this is the expected route - the client
724                                     // shouldn't be sending any more packets.
725                                     break;
726                                 }
727 
728                                 if (dataPacket != null
729                                         && (!dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket
730                                                 .getPort() != twrp.getPort()))
731                                 {
732                                     // make sure it was from the right client...
733                                     transferTftp_
734                                             .bufferedSend(new TFTPErrorPacket(dataPacket
735                                                     .getAddress(), dataPacket.getPort(),
736                                                     TFTPErrorPacket.UNKNOWN_TID,
737                                                     "Unexpected Host or Port"));
738                                 }
739                                 else
740                                 {
741                                     // This means they sent us the last
742                                     // datapacket again, must have missed our
743                                     // ack. resend it.
744                                     transferTftp_.bufferedSend(lastSentAck);
745                                 }
746                             }
747 
748                             // all done.
749                             break;
750                         }
751                     }
752                 }
753             }
754             finally
755             {
756                 if (bos != null)
757                 {
758                     bos.close();
759                 }
760             }
761         }
762 
763         /*
764          * Utility method to make sure that paths provided by tftp clients do not get outside of the
765          * serverRoot directory.
766          */
767         private File buildSafeFile(File serverDirectory, String fileName, boolean createSubDirs)
768                 throws IOException
769         {
770             File temp = new File(serverDirectory, fileName);
771             temp = temp.getCanonicalFile();
772 
773             if (!isSubdirectoryOf(serverDirectory, temp))
774             {
775                 throw new IOException("Cannot access files outside of tftp server root.");
776             }
777 
778             // ensure directory exists (if requested)
779             if (createSubDirs)
780             {
781                 createDirectory(temp.getParentFile());
782             }
783 
784             return temp;
785         }
786 
787         /*
788          * recursively create subdirectories
789          */
790         private void createDirectory(File file) throws IOException
791         {
792             File parent = file.getParentFile();
793             if (parent == null)
794             {
795                 throw new IOException("Unexpected error creating requested directory");
796             }
797             if (!parent.exists())
798             {
799                 // recurse...
800                 createDirectory(parent);
801             }
802 
803             if (parent.isDirectory())
804             {
805                 if (file.isDirectory())
806                 {
807                     return;
808                 }
809                 boolean result = file.mkdir();
810                 if (!result)
811                 {
812                     throw new IOException("Couldn't create requested directory");
813                 }
814             }
815             else
816             {
817                 throw new IOException(
818                         "Invalid directory path - file in the way of requested folder");
819             }
820         }
821 
822         /*
823          * recursively check to see if one directory is a parent of another.
824          */
825         private boolean isSubdirectoryOf(File parent, File child)
826         {
827             File childsParent = child.getParentFile();
828             if (childsParent == null)
829             {
830                 return false;
831             }
832             if (childsParent.equals(parent))
833             {
834                 return true;
835             }
836             else
837             {
838                 return isSubdirectoryOf(parent, childsParent);
839             }
840         }
841     }
842 
843     /**
844      * Set the stream object to log debug / informational messages. By default, this is a no-op
845      * 
846      * @param log
847      */
848     public void setLog(PrintStream log)
849     {
850         this.log_ = log;
851     }
852 
853     /**
854      * Set the stream object to log error messsages. By default, this is a no-op
855      * 
856      * @param logError
857      */
858     public void setLogError(PrintStream logError)
859     {
860         this.logError_ = logError;
861     }
862 }