001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    
018    package org.apache.commons.net.tftp;
019    
020    import java.io.BufferedInputStream;
021    import java.io.BufferedOutputStream;
022    import java.io.File;
023    import java.io.FileInputStream;
024    import java.io.FileNotFoundException;
025    import java.io.FileOutputStream;
026    import java.io.IOException;
027    import java.io.InputStream;
028    import java.io.OutputStream;
029    import java.io.PrintStream;
030    import java.net.SocketTimeoutException;
031    import java.util.HashSet;
032    import java.util.Iterator;
033    
034    import org.apache.commons.net.io.FromNetASCIIOutputStream;
035    import org.apache.commons.net.io.ToNetASCIIInputStream;
036    
037    /**
038     * A fully multi-threaded tftp server. Can handle multiple clients at the same time. Implements RFC
039     * 1350 and wrapping block numbers for large file support.
040     * 
041     * To launch, just create an instance of the class. An IOException will be thrown if the server
042     * fails to start for reasons such as port in use, port denied, etc.
043     * 
044     * To stop, use the shutdown method.
045     * 
046     * To check to see if the server is still running (or if it stopped because of an error), call the
047     * isRunning() method.
048     * 
049     * By default, events are not logged to stdout/stderr. This can be changed with the
050     * setLog and setLogError methods.
051     * 
052     * <p>
053     * Example usage is below:
054     * 
055     * <code>
056     * public static void main(String[] args) throws Exception
057     *  {
058     *      if (args.length != 1)
059     *      {
060     *          System.out
061     *                  .println("You must provide 1 argument - the base path for the server to serve from.");
062     *          System.exit(1);
063     *      }
064     *
065     *      TFTPServer ts = new TFTPServer(new File(args[0]), new File(args[0]), GET_AND_PUT);
066     *      ts.setSocketTimeout(2000);
067     *
068     *      System.out.println("TFTP Server running.  Press enter to stop.");
069     *      new InputStreamReader(System.in).read();
070     *
071     *      ts.shutdown();
072     *      System.out.println("Server shut down.");
073     *      System.exit(0);
074     *  }
075     *
076     * </code>
077     * 
078     * 
079     * @author <A HREF="mailto:daniel.armbrust.list@gmail.com">Dan Armbrust</A>
080     * @since 2.0
081     */
082    
083    public class TFTPServer implements Runnable
084    {
085        private static final int DEFAULT_TFTP_PORT = 69;
086        public static enum ServerMode { GET_ONLY, PUT_ONLY, GET_AND_PUT; }
087    
088        private HashSet<TFTPTransfer> transfers_ = new HashSet<TFTPTransfer>();
089        private volatile boolean shutdownServer = false;
090        private TFTP serverTftp_;
091        private File serverReadDirectory_;
092        private File serverWriteDirectory_;
093        private int port_;
094        private Exception serverException = null;
095        private ServerMode mode_;
096    
097        /* /dev/null output stream (default) */ 
098        private static final PrintStream nullStream = new PrintStream(
099                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    }