This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-net.git


The following commit(s) were added to refs/heads/master by this push:
     new f92bfee1 Use for-each loop
f92bfee1 is described below

commit f92bfee1115c9b9cb6831327335bbd3734c15ce8
Author: Gary Gregory <garydgreg...@gmail.com>
AuthorDate: Sat Jul 9 09:04:24 2022 -0400

    Use for-each loop
---
 .../apache/commons/net/ftp/FTPListParseEngine.java |  641 ++++---
 .../java/org/apache/commons/net/nntp/Threader.java |  933 +++++-----
 .../org/apache/commons/net/tftp/TFTPServer.java    | 1899 ++++++++++----------
 3 files changed, 1730 insertions(+), 1743 deletions(-)

diff --git a/src/main/java/org/apache/commons/net/ftp/FTPListParseEngine.java 
b/src/main/java/org/apache/commons/net/ftp/FTPListParseEngine.java
index 44abe3cb..bb1189d5 100644
--- a/src/main/java/org/apache/commons/net/ftp/FTPListParseEngine.java
+++ b/src/main/java/org/apache/commons/net/ftp/FTPListParseEngine.java
@@ -1,322 +1,319 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.commons.net.ftp;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.ListIterator;
-
-import org.apache.commons.net.util.Charsets;
-
-
-/**
- * This class handles the entire process of parsing a listing of
- * file entries from the server.
- * <p>
- * This object defines a two-part parsing mechanism.
- * <p>
- * The first part is comprised of reading the raw input into an internal
- * list of strings.  Every item in this list corresponds to an actual
- * file.  All extraneous matter emitted by the server will have been
- * removed by the end of this phase.  This is accomplished in conjunction
- * with the FTPFileEntryParser associated with this engine, by calling
- * its methods <code>readNextEntry()</code> - which handles the issue of
- * what delimits one entry from another, usually but not always a line
- * feed and <code>preParse()</code> - which handles removal of
- * extraneous matter such as the preliminary lines of a listing, removal
- * of duplicates on versioning systems, etc.
- * <p>
- * The second part is composed of the actual parsing, again in conjunction
- * with the particular parser used by this engine.  This is controlled
- * by an iterator over the internal list of strings.  This may be done
- * either in block mode, by calling the <code>getNext()</code> and
- * <code>getPrevious()</code> methods to provide "paged" output of less
- * than the whole list at one time, or by calling the
- * <code>getFiles()</code> method to return the entire list.
- * <p>
- * Examples:
- * <p>
- * Paged access:
- * <pre>
- *    FTPClient f=FTPClient();
- *    f.connect(server);
- *    f.login(username, password);
- *    FTPListParseEngine engine = f.initiateListParsing(directory);
- *
- *    while (engine.hasNext()) {
- *       FTPFile[] files = engine.getNext(25);  // "page size" you want
- *       //do whatever you want with these files, display them, etc.
- *       //expensive FTPFile objects not created until needed.
- *    }
- * </pre>
- * <p>
- * For unpaged access, simply use FTPClient.listFiles().  That method
- * uses this class transparently.
- */
-public class FTPListParseEngine {
-    /**
-     * An empty immutable {@code FTPFile} array.
-     */
-    private static final FTPFile[] EMPTY_FTP_FILE_ARRAY = new FTPFile[0];
-    private List<String> entries = new LinkedList<>();
-
-    private ListIterator<String> internalIterator = entries.listIterator();
-    private final FTPFileEntryParser parser;
-
-    // Should invalid files (parse failures) be allowed?
-    private final boolean saveUnparseableEntries;
-
-    public FTPListParseEngine(final FTPFileEntryParser parser) {
-        this(parser, null);
-    }
-
-    /**
-     * Intended for use by FTPClient only
-     * @since 3.4
-     */
-    FTPListParseEngine(final FTPFileEntryParser parser, final FTPClientConfig 
configuration) {
-        this.parser = parser;
-        if (configuration != null) {
-            this.saveUnparseableEntries = 
configuration.getUnparseableEntries();
-        } else {
-            this.saveUnparseableEntries = false;
-        }
-    }
-
-    /**
-     * Returns an array of FTPFile objects containing the whole list of
-     * files returned by the server as read by this object's parser.
-     *
-     * @return an array of FTPFile objects containing the whole list of
-     *         files returned by the server as read by this object's parser.
-     * None of the entries will be null
-     * @throws IOException - not ever thrown, may be removed in a later release
-     */
-    public FTPFile[] getFiles()
-    throws IOException // TODO remove; not actually thrown
-    {
-        return getFiles(FTPFileFilters.NON_NULL);
-    }
-
-    /**
-     * Returns an array of FTPFile objects containing the whole list of
-     * files returned by the server as read by this object's parser.
-     * The files are filtered before being added to the array.
-     *
-     * @param filter FTPFileFilter, must not be <code>null</code>.
-     *
-     * @return an array of FTPFile objects containing the whole list of
-     *         files returned by the server as read by this object's parser.
-     * <p><b>
-     * NOTE:</b> This array may contain null members if any of the
-     * individual file listings failed to parse.  The caller should
-     * check each entry for null before referencing it, or use the
-     * a filter such as {@link FTPFileFilters#NON_NULL} which does not
-     * allow null entries.
-     * @since 2.2
-     * @throws IOException - not ever thrown, may be removed in a later release
-     */
-    public FTPFile[] getFiles(final FTPFileFilter filter)
-    throws IOException // TODO remove; not actually thrown
-    {
-        final List<FTPFile> tmpResults = new ArrayList<>();
-        final Iterator<String> iter = this.entries.iterator();
-        while (iter.hasNext()) {
-            final String entry = iter.next();
-            FTPFile temp = this.parser.parseFTPEntry(entry);
-            if (temp == null && saveUnparseableEntries) {
-                temp = new FTPFile(entry);
-            }
-            if (filter.accept(temp)) {
-                tmpResults.add(temp);
-            }
-        }
-        return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY);
-
-    }
-
-    /**
-     * Returns an array of at most <code>quantityRequested</code> FTPFile
-     * objects starting at this object's internal iterator's current position.
-     * If fewer than <code>quantityRequested</code> such
-     * elements are available, the returned array will have a length equal
-     * to the number of entries at and after after the current position.
-     * If no such entries are found, this array will have a length of 0.
-     *
-     * After this method is called this object's internal iterator is advanced
-     * by a number of positions equal to the size of the array returned.
-     *
-     * @param quantityRequested
-     * the maximum number of entries we want to get.
-     *
-     * @return an array of at most <code>quantityRequested</code> FTPFile
-     * objects starting at the current position of this iterator within its
-     * list and at least the number of elements which  exist in the list at
-     * and after its current position.
-     * <p><b>
-     * NOTE:</b> This array may contain null members if any of the
-     * individual file listings failed to parse.  The caller should
-     * check each entry for null before referencing it.
-     */
-    public FTPFile[] getNext(final int quantityRequested) {
-        final List<FTPFile> tmpResults = new LinkedList<>();
-        int count = quantityRequested;
-        while (count > 0 && this.internalIterator.hasNext()) {
-            final String entry = this.internalIterator.next();
-            FTPFile temp = this.parser.parseFTPEntry(entry);
-            if (temp == null && saveUnparseableEntries) {
-                temp = new FTPFile(entry);
-            }
-            tmpResults.add(temp);
-            count--;
-        }
-        return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY);
-
-    }
-
-    /**
-     * Returns an array of at most <code>quantityRequested</code> FTPFile
-     * objects starting at this object's internal iterator's current position,
-     * and working back toward the beginning.
-     *
-     * If fewer than <code>quantityRequested</code> such
-     * elements are available, the returned array will have a length equal
-     * to the number of entries at and after after the current position.
-     * If no such entries are found, this array will have a length of 0.
-     *
-     * After this method is called this object's internal iterator is moved
-     * back by a number of positions equal to the size of the array returned.
-     *
-     * @param quantityRequested
-     * the maximum number of entries we want to get.
-     *
-     * @return an array of at most <code>quantityRequested</code> FTPFile
-     * objects starting at the current position of this iterator within its
-     * list and at least the number of elements which  exist in the list at
-     * and after its current position.  This array will be in the same order
-     * as the underlying list (not reversed).
-     * <p><b>
-     * NOTE:</b> This array may contain null members if any of the
-     * individual file listings failed to parse.  The caller should
-     * check each entry for null before referencing it.
-     */
-    public FTPFile[] getPrevious(final int quantityRequested) {
-        final List<FTPFile> tmpResults = new LinkedList<>();
-        int count = quantityRequested;
-        while (count > 0 && this.internalIterator.hasPrevious()) {
-            final String entry = this.internalIterator.previous();
-            FTPFile temp = this.parser.parseFTPEntry(entry);
-            if (temp == null && saveUnparseableEntries) {
-                temp = new FTPFile(entry);
-            }
-            tmpResults.add(0,temp);
-            count--;
-        }
-        return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY);
-    }
-
-    /**
-     * convenience method to allow clients to know whether this object's
-     * internal iterator's current position is at the end of the list.
-     *
-     * @return true if internal iterator is not at end of list, false
-     * otherwise.
-     */
-    public boolean hasNext() {
-        return internalIterator.hasNext();
-    }
-
-    /**
-     * convenience method to allow clients to know whether this object's
-     * internal iterator's current position is at the beginning of the list.
-     *
-     * @return true if internal iterator is not at beginning of list, false
-     * otherwise.
-     */
-    public boolean hasPrevious() {
-        return internalIterator.hasPrevious();
-    }
-
-    /**
-     * Internal method for reading (and closing) the input into the 
<code>entries</code> list. After this method has
-     * completed, <code>entries</code> will contain a collection of entries 
(as defined by
-     * <code>FTPFileEntryParser.readNextEntry()</code>), but this may contain 
various non-entry preliminary lines from
-     * the server output, duplicates, and other data that will not be part of 
the final listing.
-     *
-     * @param inputStream The socket stream on which the input will be read.
-     * @param charsetName The encoding to use.
-     *
-     * @throws IOException thrown on any failure to read the stream
-     */
-    private void read(final InputStream inputStream, final String charsetName) 
throws IOException {
-        try (final BufferedReader reader = new BufferedReader(
-            new InputStreamReader(inputStream, 
Charsets.toCharset(charsetName)))) {
-
-            String line = this.parser.readNextEntry(reader);
-
-            while (line != null) {
-                this.entries.add(line);
-                line = this.parser.readNextEntry(reader);
-            }
-        }
-    }
-
-    /**
-     * Do not use.
-     * @param inputStream the stream from which to read
-     * @throws IOException on error
-     * @deprecated use {@link #readServerList(InputStream, String)} instead
-    */
-    @Deprecated
-    public void readServerList(final InputStream inputStream) throws 
IOException {
-        readServerList(inputStream, null);
-    }
-
-    /**
-     * Reads (and closes) the initial reading and preparsing of the list 
returned by the server. After this method has
-     * completed, this object will contain a list of unparsed entries 
(Strings) each referring to a unique file on the
-     * server.
-     *
-     * @param inputStream input stream provided by the server socket.
-     * @param charsetName the encoding to be used for reading the stream
-     *
-     * @throws IOException thrown on any failure to read from the sever.
-     */
-    public void readServerList(final InputStream inputStream, final String 
charsetName) throws IOException {
-        this.entries = new LinkedList<>();
-        read(inputStream, charsetName);
-        this.parser.preParse(this.entries);
-        resetIterator();
-    }
-
-    // DEPRECATED METHODS - for API compatibility only - DO NOT USE
-
-    /**
-     * resets this object's internal iterator to the beginning of the list.
-     */
-    public void resetIterator() {
-        this.internalIterator = this.entries.listIterator();
-    }
-
-}
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.net.ftp;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+
+import org.apache.commons.net.util.Charsets;
+
+
+/**
+ * This class handles the entire process of parsing a listing of
+ * file entries from the server.
+ * <p>
+ * This object defines a two-part parsing mechanism.
+ * <p>
+ * The first part is comprised of reading the raw input into an internal
+ * list of strings.  Every item in this list corresponds to an actual
+ * file.  All extraneous matter emitted by the server will have been
+ * removed by the end of this phase.  This is accomplished in conjunction
+ * with the FTPFileEntryParser associated with this engine, by calling
+ * its methods <code>readNextEntry()</code> - which handles the issue of
+ * what delimits one entry from another, usually but not always a line
+ * feed and <code>preParse()</code> - which handles removal of
+ * extraneous matter such as the preliminary lines of a listing, removal
+ * of duplicates on versioning systems, etc.
+ * <p>
+ * The second part is composed of the actual parsing, again in conjunction
+ * with the particular parser used by this engine.  This is controlled
+ * by an iterator over the internal list of strings.  This may be done
+ * either in block mode, by calling the <code>getNext()</code> and
+ * <code>getPrevious()</code> methods to provide "paged" output of less
+ * than the whole list at one time, or by calling the
+ * <code>getFiles()</code> method to return the entire list.
+ * <p>
+ * Examples:
+ * <p>
+ * Paged access:
+ * <pre>
+ *    FTPClient f=FTPClient();
+ *    f.connect(server);
+ *    f.login(username, password);
+ *    FTPListParseEngine engine = f.initiateListParsing(directory);
+ *
+ *    while (engine.hasNext()) {
+ *       FTPFile[] files = engine.getNext(25);  // "page size" you want
+ *       //do whatever you want with these files, display them, etc.
+ *       //expensive FTPFile objects not created until needed.
+ *    }
+ * </pre>
+ * <p>
+ * For unpaged access, simply use FTPClient.listFiles().  That method
+ * uses this class transparently.
+ */
+public class FTPListParseEngine {
+    /**
+     * An empty immutable {@code FTPFile} array.
+     */
+    private static final FTPFile[] EMPTY_FTP_FILE_ARRAY = new FTPFile[0];
+    private List<String> entries = new LinkedList<>();
+
+    private ListIterator<String> internalIterator = entries.listIterator();
+    private final FTPFileEntryParser parser;
+
+    // Should invalid files (parse failures) be allowed?
+    private final boolean saveUnparseableEntries;
+
+    public FTPListParseEngine(final FTPFileEntryParser parser) {
+        this(parser, null);
+    }
+
+    /**
+     * Intended for use by FTPClient only
+     * @since 3.4
+     */
+    FTPListParseEngine(final FTPFileEntryParser parser, final FTPClientConfig 
configuration) {
+        this.parser = parser;
+        if (configuration != null) {
+            this.saveUnparseableEntries = 
configuration.getUnparseableEntries();
+        } else {
+            this.saveUnparseableEntries = false;
+        }
+    }
+
+    /**
+     * Returns an array of FTPFile objects containing the whole list of
+     * files returned by the server as read by this object's parser.
+     *
+     * @return an array of FTPFile objects containing the whole list of
+     *         files returned by the server as read by this object's parser.
+     * None of the entries will be null
+     * @throws IOException - not ever thrown, may be removed in a later release
+     */
+    public FTPFile[] getFiles()
+    throws IOException // TODO remove; not actually thrown
+    {
+        return getFiles(FTPFileFilters.NON_NULL);
+    }
+
+    /**
+     * Returns an array of FTPFile objects containing the whole list of
+     * files returned by the server as read by this object's parser.
+     * The files are filtered before being added to the array.
+     *
+     * @param filter FTPFileFilter, must not be <code>null</code>.
+     *
+     * @return an array of FTPFile objects containing the whole list of
+     *         files returned by the server as read by this object's parser.
+     * <p><b>
+     * NOTE:</b> This array may contain null members if any of the
+     * individual file listings failed to parse.  The caller should
+     * check each entry for null before referencing it, or use the
+     * a filter such as {@link FTPFileFilters#NON_NULL} which does not
+     * allow null entries.
+     * @since 2.2
+     * @throws IOException - not ever thrown, may be removed in a later release
+     */
+    public FTPFile[] getFiles(final FTPFileFilter filter)
+    throws IOException // TODO remove; not actually thrown
+    {
+        final List<FTPFile> tmpResults = new ArrayList<>();
+        for (final String entry : entries) {
+            FTPFile temp = this.parser.parseFTPEntry(entry);
+            if (temp == null && saveUnparseableEntries) {
+                temp = new FTPFile(entry);
+            }
+            if (filter.accept(temp)) {
+                tmpResults.add(temp);
+            }
+        }
+        return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY);
+
+    }
+
+    /**
+     * Returns an array of at most <code>quantityRequested</code> FTPFile
+     * objects starting at this object's internal iterator's current position.
+     * If fewer than <code>quantityRequested</code> such
+     * elements are available, the returned array will have a length equal
+     * to the number of entries at and after after the current position.
+     * If no such entries are found, this array will have a length of 0.
+     *
+     * After this method is called this object's internal iterator is advanced
+     * by a number of positions equal to the size of the array returned.
+     *
+     * @param quantityRequested
+     * the maximum number of entries we want to get.
+     *
+     * @return an array of at most <code>quantityRequested</code> FTPFile
+     * objects starting at the current position of this iterator within its
+     * list and at least the number of elements which  exist in the list at
+     * and after its current position.
+     * <p><b>
+     * NOTE:</b> This array may contain null members if any of the
+     * individual file listings failed to parse.  The caller should
+     * check each entry for null before referencing it.
+     */
+    public FTPFile[] getNext(final int quantityRequested) {
+        final List<FTPFile> tmpResults = new LinkedList<>();
+        int count = quantityRequested;
+        while (count > 0 && this.internalIterator.hasNext()) {
+            final String entry = this.internalIterator.next();
+            FTPFile temp = this.parser.parseFTPEntry(entry);
+            if (temp == null && saveUnparseableEntries) {
+                temp = new FTPFile(entry);
+            }
+            tmpResults.add(temp);
+            count--;
+        }
+        return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY);
+
+    }
+
+    /**
+     * Returns an array of at most <code>quantityRequested</code> FTPFile
+     * objects starting at this object's internal iterator's current position,
+     * and working back toward the beginning.
+     *
+     * If fewer than <code>quantityRequested</code> such
+     * elements are available, the returned array will have a length equal
+     * to the number of entries at and after after the current position.
+     * If no such entries are found, this array will have a length of 0.
+     *
+     * After this method is called this object's internal iterator is moved
+     * back by a number of positions equal to the size of the array returned.
+     *
+     * @param quantityRequested
+     * the maximum number of entries we want to get.
+     *
+     * @return an array of at most <code>quantityRequested</code> FTPFile
+     * objects starting at the current position of this iterator within its
+     * list and at least the number of elements which  exist in the list at
+     * and after its current position.  This array will be in the same order
+     * as the underlying list (not reversed).
+     * <p><b>
+     * NOTE:</b> This array may contain null members if any of the
+     * individual file listings failed to parse.  The caller should
+     * check each entry for null before referencing it.
+     */
+    public FTPFile[] getPrevious(final int quantityRequested) {
+        final List<FTPFile> tmpResults = new LinkedList<>();
+        int count = quantityRequested;
+        while (count > 0 && this.internalIterator.hasPrevious()) {
+            final String entry = this.internalIterator.previous();
+            FTPFile temp = this.parser.parseFTPEntry(entry);
+            if (temp == null && saveUnparseableEntries) {
+                temp = new FTPFile(entry);
+            }
+            tmpResults.add(0,temp);
+            count--;
+        }
+        return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY);
+    }
+
+    /**
+     * convenience method to allow clients to know whether this object's
+     * internal iterator's current position is at the end of the list.
+     *
+     * @return true if internal iterator is not at end of list, false
+     * otherwise.
+     */
+    public boolean hasNext() {
+        return internalIterator.hasNext();
+    }
+
+    /**
+     * convenience method to allow clients to know whether this object's
+     * internal iterator's current position is at the beginning of the list.
+     *
+     * @return true if internal iterator is not at beginning of list, false
+     * otherwise.
+     */
+    public boolean hasPrevious() {
+        return internalIterator.hasPrevious();
+    }
+
+    /**
+     * Internal method for reading (and closing) the input into the 
<code>entries</code> list. After this method has
+     * completed, <code>entries</code> will contain a collection of entries 
(as defined by
+     * <code>FTPFileEntryParser.readNextEntry()</code>), but this may contain 
various non-entry preliminary lines from
+     * the server output, duplicates, and other data that will not be part of 
the final listing.
+     *
+     * @param inputStream The socket stream on which the input will be read.
+     * @param charsetName The encoding to use.
+     *
+     * @throws IOException thrown on any failure to read the stream
+     */
+    private void read(final InputStream inputStream, final String charsetName) 
throws IOException {
+        try (final BufferedReader reader = new BufferedReader(
+            new InputStreamReader(inputStream, 
Charsets.toCharset(charsetName)))) {
+
+            String line = this.parser.readNextEntry(reader);
+
+            while (line != null) {
+                this.entries.add(line);
+                line = this.parser.readNextEntry(reader);
+            }
+        }
+    }
+
+    /**
+     * Do not use.
+     * @param inputStream the stream from which to read
+     * @throws IOException on error
+     * @deprecated use {@link #readServerList(InputStream, String)} instead
+    */
+    @Deprecated
+    public void readServerList(final InputStream inputStream) throws 
IOException {
+        readServerList(inputStream, null);
+    }
+
+    /**
+     * Reads (and closes) the initial reading and preparsing of the list 
returned by the server. After this method has
+     * completed, this object will contain a list of unparsed entries 
(Strings) each referring to a unique file on the
+     * server.
+     *
+     * @param inputStream input stream provided by the server socket.
+     * @param charsetName the encoding to be used for reading the stream
+     *
+     * @throws IOException thrown on any failure to read from the sever.
+     */
+    public void readServerList(final InputStream inputStream, final String 
charsetName) throws IOException {
+        this.entries = new LinkedList<>();
+        read(inputStream, charsetName);
+        this.parser.preParse(this.entries);
+        resetIterator();
+    }
+
+    // DEPRECATED METHODS - for API compatibility only - DO NOT USE
+
+    /**
+     * resets this object's internal iterator to the beginning of the list.
+     */
+    public void resetIterator() {
+        this.internalIterator = this.entries.listIterator();
+    }
+
+}
diff --git a/src/main/java/org/apache/commons/net/nntp/Threader.java 
b/src/main/java/org/apache/commons/net/nntp/Threader.java
index bbf00370..481fcde0 100644
--- a/src/main/java/org/apache/commons/net/nntp/Threader.java
+++ b/src/main/java/org/apache/commons/net/nntp/Threader.java
@@ -1,469 +1,464 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package org.apache.commons.net.nntp;
-
-/**
- * This is an implementation of a message threading algorithm, as originally 
devised by Zamie Zawinski.
- * See <a 
href="http://www.jwz.org/doc/threading.html";>http://www.jwz.org/doc/threading.html</a>
 for details.
- * For his Java implementation, see
- * <a 
href="http://lxr.mozilla.org/mozilla/source/grendel/sources/grendel/view/Threader.java";>
- * 
http://lxr.mozilla.org/mozilla/source/grendel/sources/grendel/view/Threader.java</a>
- */
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
-public class Threader {
-
-    /**
-     *
-     * @param threadable
-     * @param idTable
-     */
-    private void buildContainer(final Threadable threadable, final 
HashMap<String,ThreadContainer> idTable) {
-        String id = threadable.messageThreadId();
-        ThreadContainer container = idTable.get(id);
-        int bogusIdCount = 0;
-
-        // A ThreadContainer exists for this id already. This should be a 
forward reference, but may
-        // be a duplicate id, in which case we will need to generate a bogus 
placeholder id
-        if (container != null) {
-            if (container.threadable != null) { // oops! duplicate ids...
-                bogusIdCount++ ; // Avoid dead local store warning
-                id = "<Bogus-id:" + (bogusIdCount) + ">";
-                container = null;
-            } else {
-                // The container just contained a forward reference to this 
message, so let's
-                // fill in the threadable field of the container with this 
message
-                container.threadable = threadable;
-            }
-        }
-
-        // No container exists for that message Id. Create one and insert it 
into the hash table.
-        if (container == null) {
-            container = new ThreadContainer();
-            container.threadable = threadable;
-            idTable.put(id, container);
-        }
-
-        // Iterate through all of the references and create ThreadContainers 
for any references that
-        // don't have them.
-        ThreadContainer parentRef = null;
-        {
-            final String[] references = threadable.messageThreadReferences();
-            for (final String refString : references)
-            {
-                ThreadContainer ref = idTable.get(refString);
-
-                // if this id doesnt have a container, create one
-                if (ref == null) {
-                    ref = new ThreadContainer();
-                    idTable.put(refString, ref);
-                }
-
-                // Link references together in the order they appear in the 
References: header,
-                // IF they dont have a have a parent already &&
-                // IF it will not cause a circular reference
-                if ((parentRef != null)
-                    && (ref.parent == null)
-                    && (parentRef != ref)
-                    && !(ref.findChild(parentRef))) {
-                    // Link ref into the parent's child list
-                    ref.parent = parentRef;
-                    ref.next = parentRef.child;
-                    parentRef.child = ref;
-                }
-                parentRef = ref;
-            }
-        }
-
-        // parentRef is now set to the container of the last element in the 
references field. make that
-        // be the parent of this container, unless doing so causes a circular 
reference
-        if (parentRef != null
-            && (parentRef == container || container.findChild(parentRef)))
-        {
-            parentRef = null;
-        }
-
-        // if it has a parent already, its because we saw this message in a 
References: field, and presumed
-        // a parent based on the other entries in that field. Now that we have 
the actual message, we can
-        // throw away the old parent and use this new one
-        if (container.parent != null) {
-            ThreadContainer rest, prev;
-
-            for (prev = null, rest = container.parent.child;
-                rest != null;
-                prev = rest, rest = rest.next) {
-                if (rest == container) {
-                    break;
-                }
-            }
-
-            if (rest == null) {
-                throw new RuntimeException(
-                    "Didnt find "
-                        + container
-                        + " in parent"
-                        + container.parent);
-            }
-
-            // Unlink this container from the parent's child list
-            if (prev == null) {
-                container.parent.child = container.next;
-            } else {
-                prev.next = container.next;
-            }
-
-            container.next = null;
-            container.parent = null;
-        }
-
-        // If we have a parent, link container into the parents child list
-        if (parentRef != null) {
-            container.parent = parentRef;
-            container.next = parentRef.child;
-            parentRef.child = container;
-        }
-    }
-
-    /**
-     * Find the root set of all existing ThreadContainers
-     * @param idTable
-     * @return root the ThreadContainer representing the root node
-     */
-    private ThreadContainer findRootSet(final HashMap<String, ThreadContainer> 
idTable) {
-        final ThreadContainer root = new ThreadContainer();
-        final Iterator<Map.Entry<String, ThreadContainer>> iter = 
idTable.entrySet().iterator();
-
-        while (iter.hasNext()) {
-            final Map.Entry<String, ThreadContainer> entry = iter.next();
-            final ThreadContainer c = entry.getValue();
-            if (c.parent == null) {
-                if (c.next != null) {
-                    throw new RuntimeException(
-                            "c.next is " + c.next.toString());
-                }
-                c.next = root.child;
-                root.child = c;
-            }
-        }
-        return root;
-    }
-
-    /**
-     *  If any two members of the root set have the same subject, merge them.
-     *  This is to attempt to accomodate messages without References: headers.
-     * @param root
-     */
-    private void gatherSubjects(final ThreadContainer root) {
-
-        int count = 0;
-
-        for (ThreadContainer c = root.child; c != null; c = c.next) {
-            count++;
-        }
-
-        // TODO verify this will avoid rehashing
-        HashMap<String, ThreadContainer> subjectTable = new HashMap<>((int) 
(count * 1.2), (float) 0.9);
-        count = 0;
-
-        for (ThreadContainer c = root.child; c != null; c = c.next) {
-            Threadable threadable = c.threadable;
-
-            // No threadable? If so, it is a dummy node in the root set.
-            // Only root set members may be dummies, and they alway have at 
least 2 kids
-            // Take the first kid as representative of the subject
-            if (threadable == null) {
-                threadable = c.child.threadable;
-            }
-
-            final String subj = threadable.simplifiedSubject();
-
-            if (subj == null || subj.isEmpty()) {
-                continue;
-            }
-
-            final ThreadContainer old = subjectTable.get(subj);
-
-            // Add this container to the table iff:
-            // - There exists no container with this subject
-            // - or this is a dummy container and the old one is not - the 
dummy one is
-            // more interesting as a root, so put it in the table instead
-            // - The container in the table has a "Re:" version of this 
subject, and
-            // this container has a non-"Re:" version of this subject. The 
non-"Re:" version
-            // is the more interesting of the two.
-            if (old == null
-                || (c.threadable == null && old.threadable != null)
-                || (old.threadable != null
-                    && old.threadable.subjectIsReply()
-                    && c.threadable != null
-                    && !c.threadable.subjectIsReply())) {
-                subjectTable.put(subj, c);
-                count++;
-            }
-        }
-
-        // If the table is empty, we're done
-        if (count == 0) {
-            return;
-        }
-
-        // subjectTable is now populated with one entry for each subject which 
occurs in the
-        // root set. Iterate over the root set, and gather together the 
difference.
-        ThreadContainer prev, c, rest;
-        for (prev = null, c = root.child, rest = c.next;
-            c != null;
-            prev = c, c = rest, rest = (rest == null ? null : rest.next)) {
-            Threadable threadable = c.threadable;
-
-            // is it a dummy node?
-            if (threadable == null) {
-                threadable = c.child.threadable;
-            }
-
-            final String subj = threadable.simplifiedSubject();
-
-            // Dont thread together all subjectless messages
-            if (subj == null || subj.isEmpty()) {
-                continue;
-            }
-
-            final ThreadContainer old = subjectTable.get(subj);
-
-            if (old == c) { // That's us
-                continue;
-            }
-
-            // We have now found another container in the root set with the 
same subject
-            // Remove the "second" message from the root set
-            if (prev == null) {
-                root.child = c.next;
-            } else {
-                prev.next = c.next;
-            }
-            c.next = null;
-
-            if (old.threadable == null && c.threadable == null) {
-                // both dummies - merge them
-                ThreadContainer tail;
-                for (tail = old.child;
-                    tail != null && tail.next != null;
-                    tail = tail.next) {
-                    // do nothing
-                }
-
-                if (tail != null) { // protect against possible NPE
-                    tail.next = c.child;
-                }
-
-                for (tail = c.child; tail != null; tail = tail.next) {
-                    tail.parent = old;
-                }
-
-                c.child = null;
-            } else if (
-                old.threadable == null
-                    || (c.threadable != null
-                        && c.threadable.subjectIsReply()
-                        && !old.threadable.subjectIsReply())) {
-                // Else if old is empty, or c has "Re:" and old does not  ==> 
make this message a child of old
-                c.parent = old;
-                c.next = old.child;
-                old.child = c;
-            } else {
-                // else make the old and new messages be children of a new 
dummy container.
-                // We create a new container object for old.msg and empty the 
old container
-                final ThreadContainer newc = new ThreadContainer();
-                newc.threadable = old.threadable;
-                newc.child = old.child;
-
-                for (ThreadContainer tail = newc.child;
-                    tail != null;
-                    tail = tail.next)
-                {
-                    tail.parent = newc;
-                }
-
-                old.threadable = null;
-                old.child = null;
-
-                c.parent = old;
-                newc.parent = old;
-
-                // Old is now a dummy- give it 2 kids , c and newc
-                old.child = c;
-                c.next = newc;
-            }
-            // We've done a merge, so keep the same prev
-            c = prev;
-        }
-
-        subjectTable.clear();
-        subjectTable = null;
-
-    }
-
-    /**
-     * Delete any empty or dummy ThreadContainers
-     * @param parent
-     */
-    private void pruneEmptyContainers(final ThreadContainer parent) {
-        ThreadContainer container, prev, next;
-        for (prev = null, container = parent.child, next = container.next;
-            container != null;
-            prev = container,
-                container = next,
-                next = (container == null ? null : container.next)) {
-
-            // Is it empty and without any children? If so,delete it
-            if (container.threadable == null && container.child == null) {
-                if (prev == null) {
-                    parent.child = container.next;
-                } else {
-                    prev.next = container.next;
-                }
-
-                // Set container to prev so that prev keeps its same value the 
next time through the loop
-                container = prev;
-            }
-
-            // Else if empty, with kids, and (not at root or only one kid)
-            else if (
-                container.threadable == null
-                    && container.child != null
-                    && (container.parent != null
-                        || container.child.next == null)) {
-                // We have an invalid/expired message with kids. Promote the 
kids to this level.
-                ThreadContainer tail;
-                final ThreadContainer kids = container.child;
-
-                // Remove this container and replace with 'kids'.
-                if (prev == null) {
-                    parent.child = kids;
-                } else {
-                    prev.next = kids;
-                }
-
-                // Make each child's parent be this level's parent -> i.e. 
promote the children.
-                // Make the last child's next point to this container's next
-                // i.e. splice kids into the list in place of container
-                for (tail = kids; tail.next != null; tail = tail.next) {
-                    tail.parent = container.parent;
-                }
-
-                tail.parent = container.parent;
-                tail.next = container.next;
-
-                // next currently points to the item after the inserted items 
in the chain - reset that so we process the newly
-                // promoted items next time round
-                next = kids;
-
-                // Set container to prev so that prev keeps its same value the 
next time through the loop
-                container = prev;
-            } else if (container.child != null) {
-                // A real message , with kids
-                // Iterate over the children
-                pruneEmptyContainers(container);
-            }
-        }
-    }
-
-    /**
-     * The client passes in a list of Iterable objects, and
-     * the Threader constructs a connected 'graph' of messages
-     * @param messages iterable of messages to thread, must not be empty
-     * @return null if messages == null or root.child == null or messages list 
is empty
-     * @since 3.0
-     */
-    public Threadable thread(final Iterable<? extends Threadable> messages) {
-        if (messages == null) {
-            return null;
-        }
-
-        HashMap<String,ThreadContainer> idTable = new HashMap<>();
-
-        // walk through each Threadable element
-        for (final Threadable t : messages) {
-            if (!t.isDummy()) {
-                buildContainer(t, idTable);
-            }
-        }
-
-        if (idTable.isEmpty()) {
-            return null;
-        }
-
-        final ThreadContainer root = findRootSet(idTable);
-        idTable.clear();
-        idTable = null;
-
-        pruneEmptyContainers(root);
-
-        root.reverseChildren();
-        gatherSubjects(root);
-
-        if (root.next != null) {
-            throw new RuntimeException("root node has a next:" + root);
-        }
-
-        for (ThreadContainer r = root.child; r != null; r = r.next) {
-            if (r.threadable == null) {
-                r.threadable = r.child.threadable.makeDummy();
-            }
-        }
-
-        final Threadable result = (root.child == null ? null : 
root.child.threadable);
-        root.flush();
-
-        return result;
-    }
-
-    /**
-     * The client passes in a list of Threadable objects, and
-     * the Threader constructs a connected 'graph' of messages
-     * @param messages list of messages to thread, must not be empty
-     * @return null if messages == null or root.child == null or messages list 
is empty
-     * @since 2.2
-     */
-    public Threadable thread(final List<? extends Threadable> messages) {
-        return thread((Iterable<? extends Threadable>)messages);
-    }
-
-
-    // DEPRECATED METHODS - for API compatibility only - DO NOT USE
-
-    /**
-     * The client passes in an array of Threadable objects, and
-     * the Threader constructs a connected 'graph' of messages
-     * @param messages array of messages to thread, must not be empty
-     * @return null if messages == null or root.child == null or messages 
array is empty
-     * @deprecated (2.2) prefer {@link #thread(List)}
-     */
-    @Deprecated
-    public Threadable thread(final Threadable[] messages) {
-        if (messages == null) {
-            return null;
-        }
-        return thread(Arrays.asList(messages));
-    }
-
-}
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.commons.net.nntp;
+
+/**
+ * This is an implementation of a message threading algorithm, as originally 
devised by Zamie Zawinski.
+ * See <a 
href="http://www.jwz.org/doc/threading.html";>http://www.jwz.org/doc/threading.html</a>
 for details.
+ * For his Java implementation, see
+ * <a 
href="http://lxr.mozilla.org/mozilla/source/grendel/sources/grendel/view/Threader.java";>
+ * 
http://lxr.mozilla.org/mozilla/source/grendel/sources/grendel/view/Threader.java</a>
+ */
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Threader {
+
+    /**
+     *
+     * @param threadable
+     * @param idTable
+     */
+    private void buildContainer(final Threadable threadable, final 
HashMap<String,ThreadContainer> idTable) {
+        String id = threadable.messageThreadId();
+        ThreadContainer container = idTable.get(id);
+        int bogusIdCount = 0;
+
+        // A ThreadContainer exists for this id already. This should be a 
forward reference, but may
+        // be a duplicate id, in which case we will need to generate a bogus 
placeholder id
+        if (container != null) {
+            if (container.threadable != null) { // oops! duplicate ids...
+                bogusIdCount++ ; // Avoid dead local store warning
+                id = "<Bogus-id:" + (bogusIdCount) + ">";
+                container = null;
+            } else {
+                // The container just contained a forward reference to this 
message, so let's
+                // fill in the threadable field of the container with this 
message
+                container.threadable = threadable;
+            }
+        }
+
+        // No container exists for that message Id. Create one and insert it 
into the hash table.
+        if (container == null) {
+            container = new ThreadContainer();
+            container.threadable = threadable;
+            idTable.put(id, container);
+        }
+
+        // Iterate through all of the references and create ThreadContainers 
for any references that
+        // don't have them.
+        ThreadContainer parentRef = null;
+        {
+            final String[] references = threadable.messageThreadReferences();
+            for (final String refString : references)
+            {
+                ThreadContainer ref = idTable.get(refString);
+
+                // if this id doesnt have a container, create one
+                if (ref == null) {
+                    ref = new ThreadContainer();
+                    idTable.put(refString, ref);
+                }
+
+                // Link references together in the order they appear in the 
References: header,
+                // IF they dont have a have a parent already &&
+                // IF it will not cause a circular reference
+                if ((parentRef != null)
+                    && (ref.parent == null)
+                    && (parentRef != ref)
+                    && !(ref.findChild(parentRef))) {
+                    // Link ref into the parent's child list
+                    ref.parent = parentRef;
+                    ref.next = parentRef.child;
+                    parentRef.child = ref;
+                }
+                parentRef = ref;
+            }
+        }
+
+        // parentRef is now set to the container of the last element in the 
references field. make that
+        // be the parent of this container, unless doing so causes a circular 
reference
+        if (parentRef != null
+            && (parentRef == container || container.findChild(parentRef)))
+        {
+            parentRef = null;
+        }
+
+        // if it has a parent already, its because we saw this message in a 
References: field, and presumed
+        // a parent based on the other entries in that field. Now that we have 
the actual message, we can
+        // throw away the old parent and use this new one
+        if (container.parent != null) {
+            ThreadContainer rest, prev;
+
+            for (prev = null, rest = container.parent.child;
+                rest != null;
+                prev = rest, rest = rest.next) {
+                if (rest == container) {
+                    break;
+                }
+            }
+
+            if (rest == null) {
+                throw new RuntimeException(
+                    "Didnt find "
+                        + container
+                        + " in parent"
+                        + container.parent);
+            }
+
+            // Unlink this container from the parent's child list
+            if (prev == null) {
+                container.parent.child = container.next;
+            } else {
+                prev.next = container.next;
+            }
+
+            container.next = null;
+            container.parent = null;
+        }
+
+        // If we have a parent, link container into the parents child list
+        if (parentRef != null) {
+            container.parent = parentRef;
+            container.next = parentRef.child;
+            parentRef.child = container;
+        }
+    }
+
+    /**
+     * Find the root set of all existing ThreadContainers
+     * @param idTable
+     * @return root the ThreadContainer representing the root node
+     */
+    private ThreadContainer findRootSet(final HashMap<String, ThreadContainer> 
idTable) {
+        final ThreadContainer root = new ThreadContainer();
+        for (final Map.Entry<String, ThreadContainer> entry : 
idTable.entrySet()) {
+            final ThreadContainer c = entry.getValue();
+            if (c.parent == null) {
+                if (c.next != null) {
+                    throw new RuntimeException("c.next is " + 
c.next.toString());
+                }
+                c.next = root.child;
+                root.child = c;
+            }
+        }
+        return root;
+    }
+
+    /**
+     *  If any two members of the root set have the same subject, merge them.
+     *  This is to attempt to accomodate messages without References: headers.
+     * @param root
+     */
+    private void gatherSubjects(final ThreadContainer root) {
+
+        int count = 0;
+
+        for (ThreadContainer c = root.child; c != null; c = c.next) {
+            count++;
+        }
+
+        // TODO verify this will avoid rehashing
+        HashMap<String, ThreadContainer> subjectTable = new HashMap<>((int) 
(count * 1.2), (float) 0.9);
+        count = 0;
+
+        for (ThreadContainer c = root.child; c != null; c = c.next) {
+            Threadable threadable = c.threadable;
+
+            // No threadable? If so, it is a dummy node in the root set.
+            // Only root set members may be dummies, and they alway have at 
least 2 kids
+            // Take the first kid as representative of the subject
+            if (threadable == null) {
+                threadable = c.child.threadable;
+            }
+
+            final String subj = threadable.simplifiedSubject();
+
+            if (subj == null || subj.isEmpty()) {
+                continue;
+            }
+
+            final ThreadContainer old = subjectTable.get(subj);
+
+            // Add this container to the table iff:
+            // - There exists no container with this subject
+            // - or this is a dummy container and the old one is not - the 
dummy one is
+            // more interesting as a root, so put it in the table instead
+            // - The container in the table has a "Re:" version of this 
subject, and
+            // this container has a non-"Re:" version of this subject. The 
non-"Re:" version
+            // is the more interesting of the two.
+            if (old == null
+                || (c.threadable == null && old.threadable != null)
+                || (old.threadable != null
+                    && old.threadable.subjectIsReply()
+                    && c.threadable != null
+                    && !c.threadable.subjectIsReply())) {
+                subjectTable.put(subj, c);
+                count++;
+            }
+        }
+
+        // If the table is empty, we're done
+        if (count == 0) {
+            return;
+        }
+
+        // subjectTable is now populated with one entry for each subject which 
occurs in the
+        // root set. Iterate over the root set, and gather together the 
difference.
+        ThreadContainer prev, c, rest;
+        for (prev = null, c = root.child, rest = c.next;
+            c != null;
+            prev = c, c = rest, rest = (rest == null ? null : rest.next)) {
+            Threadable threadable = c.threadable;
+
+            // is it a dummy node?
+            if (threadable == null) {
+                threadable = c.child.threadable;
+            }
+
+            final String subj = threadable.simplifiedSubject();
+
+            // Dont thread together all subjectless messages
+            if (subj == null || subj.isEmpty()) {
+                continue;
+            }
+
+            final ThreadContainer old = subjectTable.get(subj);
+
+            if (old == c) { // That's us
+                continue;
+            }
+
+            // We have now found another container in the root set with the 
same subject
+            // Remove the "second" message from the root set
+            if (prev == null) {
+                root.child = c.next;
+            } else {
+                prev.next = c.next;
+            }
+            c.next = null;
+
+            if (old.threadable == null && c.threadable == null) {
+                // both dummies - merge them
+                ThreadContainer tail;
+                for (tail = old.child;
+                    tail != null && tail.next != null;
+                    tail = tail.next) {
+                    // do nothing
+                }
+
+                if (tail != null) { // protect against possible NPE
+                    tail.next = c.child;
+                }
+
+                for (tail = c.child; tail != null; tail = tail.next) {
+                    tail.parent = old;
+                }
+
+                c.child = null;
+            } else if (
+                old.threadable == null
+                    || (c.threadable != null
+                        && c.threadable.subjectIsReply()
+                        && !old.threadable.subjectIsReply())) {
+                // Else if old is empty, or c has "Re:" and old does not  ==> 
make this message a child of old
+                c.parent = old;
+                c.next = old.child;
+                old.child = c;
+            } else {
+                // else make the old and new messages be children of a new 
dummy container.
+                // We create a new container object for old.msg and empty the 
old container
+                final ThreadContainer newc = new ThreadContainer();
+                newc.threadable = old.threadable;
+                newc.child = old.child;
+
+                for (ThreadContainer tail = newc.child;
+                    tail != null;
+                    tail = tail.next)
+                {
+                    tail.parent = newc;
+                }
+
+                old.threadable = null;
+                old.child = null;
+
+                c.parent = old;
+                newc.parent = old;
+
+                // Old is now a dummy- give it 2 kids , c and newc
+                old.child = c;
+                c.next = newc;
+            }
+            // We've done a merge, so keep the same prev
+            c = prev;
+        }
+
+        subjectTable.clear();
+        subjectTable = null;
+
+    }
+
+    /**
+     * Delete any empty or dummy ThreadContainers
+     * @param parent
+     */
+    private void pruneEmptyContainers(final ThreadContainer parent) {
+        ThreadContainer container, prev, next;
+        for (prev = null, container = parent.child, next = container.next;
+            container != null;
+            prev = container,
+                container = next,
+                next = (container == null ? null : container.next)) {
+
+            // Is it empty and without any children? If so,delete it
+            if (container.threadable == null && container.child == null) {
+                if (prev == null) {
+                    parent.child = container.next;
+                } else {
+                    prev.next = container.next;
+                }
+
+                // Set container to prev so that prev keeps its same value the 
next time through the loop
+                container = prev;
+            }
+
+            // Else if empty, with kids, and (not at root or only one kid)
+            else if (
+                container.threadable == null
+                    && container.child != null
+                    && (container.parent != null
+                        || container.child.next == null)) {
+                // We have an invalid/expired message with kids. Promote the 
kids to this level.
+                ThreadContainer tail;
+                final ThreadContainer kids = container.child;
+
+                // Remove this container and replace with 'kids'.
+                if (prev == null) {
+                    parent.child = kids;
+                } else {
+                    prev.next = kids;
+                }
+
+                // Make each child's parent be this level's parent -> i.e. 
promote the children.
+                // Make the last child's next point to this container's next
+                // i.e. splice kids into the list in place of container
+                for (tail = kids; tail.next != null; tail = tail.next) {
+                    tail.parent = container.parent;
+                }
+
+                tail.parent = container.parent;
+                tail.next = container.next;
+
+                // next currently points to the item after the inserted items 
in the chain - reset that so we process the newly
+                // promoted items next time round
+                next = kids;
+
+                // Set container to prev so that prev keeps its same value the 
next time through the loop
+                container = prev;
+            } else if (container.child != null) {
+                // A real message , with kids
+                // Iterate over the children
+                pruneEmptyContainers(container);
+            }
+        }
+    }
+
+    /**
+     * The client passes in a list of Iterable objects, and
+     * the Threader constructs a connected 'graph' of messages
+     * @param messages iterable of messages to thread, must not be empty
+     * @return null if messages == null or root.child == null or messages list 
is empty
+     * @since 3.0
+     */
+    public Threadable thread(final Iterable<? extends Threadable> messages) {
+        if (messages == null) {
+            return null;
+        }
+
+        HashMap<String,ThreadContainer> idTable = new HashMap<>();
+
+        // walk through each Threadable element
+        for (final Threadable t : messages) {
+            if (!t.isDummy()) {
+                buildContainer(t, idTable);
+            }
+        }
+
+        if (idTable.isEmpty()) {
+            return null;
+        }
+
+        final ThreadContainer root = findRootSet(idTable);
+        idTable.clear();
+        idTable = null;
+
+        pruneEmptyContainers(root);
+
+        root.reverseChildren();
+        gatherSubjects(root);
+
+        if (root.next != null) {
+            throw new RuntimeException("root node has a next:" + root);
+        }
+
+        for (ThreadContainer r = root.child; r != null; r = r.next) {
+            if (r.threadable == null) {
+                r.threadable = r.child.threadable.makeDummy();
+            }
+        }
+
+        final Threadable result = (root.child == null ? null : 
root.child.threadable);
+        root.flush();
+
+        return result;
+    }
+
+    /**
+     * The client passes in a list of Threadable objects, and
+     * the Threader constructs a connected 'graph' of messages
+     * @param messages list of messages to thread, must not be empty
+     * @return null if messages == null or root.child == null or messages list 
is empty
+     * @since 2.2
+     */
+    public Threadable thread(final List<? extends Threadable> messages) {
+        return thread((Iterable<? extends Threadable>)messages);
+    }
+
+
+    // DEPRECATED METHODS - for API compatibility only - DO NOT USE
+
+    /**
+     * The client passes in an array of Threadable objects, and
+     * the Threader constructs a connected 'graph' of messages
+     * @param messages array of messages to thread, must not be empty
+     * @return null if messages == null or root.child == null or messages 
array is empty
+     * @deprecated (2.2) prefer {@link #thread(List)}
+     */
+    @Deprecated
+    public Threadable thread(final Threadable[] messages) {
+        if (messages == null) {
+            return null;
+        }
+        return thread(Arrays.asList(messages));
+    }
+
+}
diff --git a/src/test/java/org/apache/commons/net/tftp/TFTPServer.java 
b/src/test/java/org/apache/commons/net/tftp/TFTPServer.java
index 21f6fa84..cddedf42 100644
--- a/src/test/java/org/apache/commons/net/tftp/TFTPServer.java
+++ b/src/test/java/org/apache/commons/net/tftp/TFTPServer.java
@@ -1,952 +1,947 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.commons.net.tftp;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PrintStream;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.SocketTimeoutException;
-import java.util.Enumeration;
-import java.util.HashSet;
-import java.util.Iterator;
-
-import org.apache.commons.net.io.FromNetASCIIOutputStream;
-import org.apache.commons.net.io.ToNetASCIIInputStream;
-
-/**
- * A fully multi-threaded tftp server. Can handle multiple clients at the same 
time. Implements RFC
- * 1350 and wrapping block numbers for large file support.
- *
- * To launch, just create an instance of the class. An IOException will be 
thrown if the server
- * fails to start for reasons such as port in use, port denied, etc.
- *
- * To stop, use the shutdown method.
- *
- * To check to see if the server is still running (or if it stopped because of 
an error), call the
- * isRunning() method.
- *
- * By default, events are not logged to stdout/stderr. This can be changed 
with the
- * setLog and setLogError methods.
- *
- * <p>
- * Example usage is below:
- *
- * <code>
- * public static void main(String[] args) throws Exception
- *  {
- *      if (args.length != 1)
- *      {
- *          System.out
- *                  .println("You must provide 1 argument - the base path for 
the server to serve from.");
- *          System.exit(1);
- *      }
- *
- *      TFTPServer ts = new TFTPServer(new File(args[0]), new File(args[0]), 
GET_AND_PUT);
- *      ts.setSocketTimeout(2000);
- *
- *      System.out.println("TFTP Server running.  Press enter to stop.");
- *      new InputStreamReader(System.in).read();
- *
- *      ts.shutdown();
- *      System.out.println("Server shut down.");
- *      System.exit(0);
- *  }
- *
- * </code>
- *
- * @since 2.0
- */
-
-public class TFTPServer implements Runnable
-{
-    public enum ServerMode { GET_ONLY, PUT_ONLY, GET_AND_PUT}
-    /*
-     * An instance of an ongoing transfer.
-     */
-    private class TFTPTransfer implements Runnable
-    {
-        private final TFTPPacket tftpPacket_;
-
-        private boolean shutdownTransfer;
-
-        TFTP transferTftp_;
-
-        public TFTPTransfer(final TFTPPacket tftpPacket)
-        {
-            tftpPacket_ = tftpPacket;
-        }
-
-        /*
-         * Utility method to make sure that paths provided by tftp clients do 
not get outside of the
-         * serverRoot directory.
-         */
-        private File buildSafeFile(final File serverDirectory, final String 
fileName, final boolean createSubDirs)
-                throws IOException
-        {
-            File temp = new File(serverDirectory, fileName);
-            temp = temp.getCanonicalFile();
-
-            if (!isSubdirectoryOf(serverDirectory, temp))
-            {
-                throw new IOException("Cannot access files outside of tftp 
server root.");
-            }
-
-            // ensure directory exists (if requested)
-            if (createSubDirs)
-            {
-                createDirectory(temp.getParentFile());
-            }
-
-            return temp;
-        }
-
-        /*
-         * recursively create subdirectories
-         */
-        private void createDirectory(final File file) throws IOException
-        {
-            final File parent = file.getParentFile();
-            if (parent == null)
-            {
-                throw new IOException("Unexpected error creating requested 
directory");
-            }
-            if (!parent.exists())
-            {
-                // recurse...
-                createDirectory(parent);
-            }
-
-            if (!parent.isDirectory()) {
-                throw new IOException(
-                        "Invalid directory path - file in the way of requested 
folder");
-            }
-            if (file.isDirectory())
-            {
-                return;
-            }
-            final boolean result = file.mkdir();
-            if (!result)
-            {
-                throw new IOException("Couldn't create requested directory");
-            }
-        }
-
-        /*
-         * Handle a tftp read request.
-         */
-        private void handleRead(final TFTPReadRequestPacket trrp) throws 
IOException, TFTPPacketException
-        {
-            InputStream is = null;
-            try
-            {
-                if (mode_ == ServerMode.PUT_ONLY)
-                {
-                    transferTftp_.bufferedSend(new 
TFTPErrorPacket(trrp.getAddress(), trrp
-                            .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION,
-                            "Read not allowed by server."));
-                    return;
-                }
-
-                try
-                {
-                    is = new BufferedInputStream(new 
FileInputStream(buildSafeFile(
-                            serverReadDirectory_, trrp.getFilename(), false)));
-                }
-                catch (final FileNotFoundException e)
-                {
-                    transferTftp_.bufferedSend(new 
TFTPErrorPacket(trrp.getAddress(), trrp
-                            .getPort(), TFTPErrorPacket.FILE_NOT_FOUND, 
e.getMessage()));
-                    return;
-                }
-                catch (final Exception e)
-                {
-                    transferTftp_.bufferedSend(new 
TFTPErrorPacket(trrp.getAddress(), trrp
-                            .getPort(), TFTPErrorPacket.UNDEFINED, 
e.getMessage()));
-                    return;
-                }
-
-                if (trrp.getMode() == TFTP.NETASCII_MODE)
-                {
-                    is = new ToNetASCIIInputStream(is);
-                }
-
-                final byte[] temp = new byte[TFTPDataPacket.MAX_DATA_LENGTH];
-
-                TFTPPacket answer;
-
-                int block = 1;
-                boolean sendNext = true;
-
-                int readLength = TFTPDataPacket.MAX_DATA_LENGTH;
-
-                TFTPDataPacket lastSentData = null;
-
-                // We are reading a file, so when we read less than the
-                // requested bytes, we know that we are at the end of the file.
-                while (readLength == TFTPDataPacket.MAX_DATA_LENGTH && 
!shutdownTransfer)
-                {
-                    if (sendNext)
-                    {
-                        readLength = is.read(temp);
-                        if (readLength == -1)
-                        {
-                            readLength = 0;
-                        }
-
-                        lastSentData = new TFTPDataPacket(trrp.getAddress(), 
trrp.getPort(), block,
-                                temp, 0, readLength);
-                        sendData(transferTftp_, lastSentData); // send the data
-                    }
-
-                    answer = null;
-
-                    int timeoutCount = 0;
-
-                    while (!shutdownTransfer
-                            && (answer == null || 
!answer.getAddress().equals(trrp.getAddress()) || answer
-                                    .getPort() != trrp.getPort()))
-                    {
-                        // listen for an answer.
-                        if (answer != null)
-                        {
-                            // The answer that we got didn't come from the
-                            // expected source, fire back an error, and 
continue
-                            // listening.
-                            log_.println("TFTP Server ignoring message from 
unexpected source.");
-                            transferTftp_.bufferedSend(new 
TFTPErrorPacket(answer.getAddress(),
-                                    answer.getPort(), 
TFTPErrorPacket.UNKNOWN_TID,
-                                    "Unexpected Host or Port"));
-                        }
-                        try
-                        {
-                            answer = transferTftp_.bufferedReceive();
-                        }
-                        catch (final SocketTimeoutException e)
-                        {
-                            if (timeoutCount >= maxTimeoutRetries_)
-                            {
-                                throw e;
-                            }
-                            // didn't get an ack for this data. need to resend
-                            // it.
-                            timeoutCount++;
-                            transferTftp_.bufferedSend(lastSentData);
-                            continue;
-                        }
-                    }
-
-                    if (answer == null || !(answer instanceof TFTPAckPacket))
-                    {
-                        if (!shutdownTransfer)
-                        {
-                            logError_
-                                    .println("Unexpected response from tftp 
client during transfer ("
-                                            + answer + ").  Transfer 
aborted.");
-                        }
-                        break;
-                    }
-                    // once we get here, we know we have an answer packet
-                    // from the correct host.
-                    final TFTPAckPacket ack = (TFTPAckPacket) answer;
-                    if (ack.getBlockNumber() != block)
-                    {
-                        /*
-                         * The origional tftp spec would have called on us to 
resend the
-                         * previous data here, however, that causes the SAS 
Syndrome.
-                         * http://www.faqs.org/rfcs/rfc1123.html section 
4.2.3.1 The modified
-                         * spec says that we ignore a duplicate ack. If the 
packet was really
-                         * lost, we will time out on receive, and resend the 
previous data at
-                         * that point.
-                         */
-                        sendNext = false;
-                    }
-                    else
-                    {
-                        // send the next block
-                        block++;
-                        if (block > 65535)
-                        {
-                            // wrap the block number
-                            block = 0;
-                        }
-                        sendNext = true;
-                    }
-                }
-            }
-            finally
-            {
-                try
-                {
-                    if (is != null)
-                    {
-                        is.close();
-                    }
-                }
-                catch (final IOException e)
-                {
-                    // noop
-                }
-            }
-        }
-
-        /*
-         * handle a tftp write request.
-         */
-        private void handleWrite(final TFTPWriteRequestPacket twrp) throws 
IOException,
-                TFTPPacketException
-        {
-            OutputStream bos = null;
-            try
-            {
-                if (mode_ == ServerMode.GET_ONLY)
-                {
-                    transferTftp_.bufferedSend(new 
TFTPErrorPacket(twrp.getAddress(), twrp
-                            .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION,
-                            "Write not allowed by server."));
-                    return;
-                }
-
-                int lastBlock = 0;
-                final String fileName = twrp.getFilename();
-
-                try
-                {
-                    final File temp = buildSafeFile(serverWriteDirectory_, 
fileName, true);
-                    if (temp.exists())
-                    {
-                        transferTftp_.bufferedSend(new 
TFTPErrorPacket(twrp.getAddress(), twrp
-                                .getPort(), TFTPErrorPacket.FILE_EXISTS, "File 
already exists"));
-                        return;
-                    }
-                    bos = new BufferedOutputStream(new FileOutputStream(temp));
-
-                    if (twrp.getMode() == TFTP.NETASCII_MODE)
-                    {
-                        bos = new FromNetASCIIOutputStream(bos);
-                    }
-                }
-                catch (final Exception e)
-                {
-                    transferTftp_.bufferedSend(new 
TFTPErrorPacket(twrp.getAddress(), twrp
-                            .getPort(), TFTPErrorPacket.UNDEFINED, 
e.getMessage()));
-                    return;
-                }
-
-                TFTPAckPacket lastSentAck = new 
TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0);
-                sendData(transferTftp_, lastSentAck); // send the data
-
-                while (true)
-                {
-                    // get the response - ensure it is from the right place.
-                    TFTPPacket dataPacket = null;
-
-                    int timeoutCount = 0;
-
-                    while (!shutdownTransfer
-                            && (dataPacket == null
-                                    || 
!dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket
-                                    .getPort() != twrp.getPort()))
-                    {
-                        // listen for an answer.
-                        if (dataPacket != null)
-                        {
-                            // The data that we got didn't come from the
-                            // expected source, fire back an error, and 
continue
-                            // listening.
-                            log_.println("TFTP Server ignoring message from 
unexpected source.");
-                            transferTftp_.bufferedSend(new 
TFTPErrorPacket(dataPacket.getAddress(),
-                                    dataPacket.getPort(), 
TFTPErrorPacket.UNKNOWN_TID,
-                                    "Unexpected Host or Port"));
-                        }
-
-                        try
-                        {
-                            dataPacket = transferTftp_.bufferedReceive();
-                        }
-                        catch (final SocketTimeoutException e)
-                        {
-                            if (timeoutCount >= maxTimeoutRetries_)
-                            {
-                                throw e;
-                            }
-                            // It didn't get our ack. Resend it.
-                            transferTftp_.bufferedSend(lastSentAck);
-                            timeoutCount++;
-                            continue;
-                        }
-                    }
-
-                    if (dataPacket instanceof TFTPWriteRequestPacket)
-                    {
-                        // it must have missed our initial ack. Send another.
-                        lastSentAck = new TFTPAckPacket(twrp.getAddress(), 
twrp.getPort(), 0);
-                        transferTftp_.bufferedSend(lastSentAck);
-                    }
-                    else if (dataPacket == null || !(dataPacket instanceof 
TFTPDataPacket))
-                    {
-                        if (!shutdownTransfer)
-                        {
-                            logError_
-                                    .println("Unexpected response from tftp 
client during transfer ("
-                                            + dataPacket + ").  Transfer 
aborted.");
-                        }
-                        break;
-                    }
-                    else
-                    {
-                        final int block = ((TFTPDataPacket) 
dataPacket).getBlockNumber();
-                        final byte[] data = ((TFTPDataPacket) 
dataPacket).getData();
-                        final int dataLength = ((TFTPDataPacket) 
dataPacket).getDataLength();
-                        final int dataOffset = ((TFTPDataPacket) 
dataPacket).getDataOffset();
-
-                        if (block > lastBlock || lastBlock == 65535 && block 
== 0)
-                        {
-                            // it might resend a data block if it missed our 
ack
-                            // - don't rewrite the block.
-                            bos.write(data, dataOffset, dataLength);
-                            lastBlock = block;
-                        }
-
-                        lastSentAck = new TFTPAckPacket(twrp.getAddress(), 
twrp.getPort(), block);
-                        sendData(transferTftp_, lastSentAck); // send the data
-                        if (dataLength < TFTPDataPacket.MAX_DATA_LENGTH)
-                        {
-                            // end of stream signal - The tranfer is complete.
-                            bos.close();
-
-                            // But my ack may be lost - so listen to see if I
-                            // need to resend the ack.
-                            for (int i = 0; i < maxTimeoutRetries_; i++)
-                            {
-                                try
-                                {
-                                    dataPacket = 
transferTftp_.bufferedReceive();
-                                }
-                                catch (final SocketTimeoutException e)
-                                {
-                                    // this is the expected route - the client
-                                    // shouldn't be sending any more packets.
-                                    break;
-                                }
-
-                                if (dataPacket != null
-                                        && 
(!dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket
-                                                .getPort() != twrp.getPort()))
-                                {
-                                    // make sure it was from the right 
client...
-                                    transferTftp_
-                                            .bufferedSend(new 
TFTPErrorPacket(dataPacket
-                                                    .getAddress(), 
dataPacket.getPort(),
-                                                    
TFTPErrorPacket.UNKNOWN_TID,
-                                                    "Unexpected Host or 
Port"));
-                                }
-                                else
-                                {
-                                    // This means they sent us the last
-                                    // datapacket again, must have missed our
-                                    // ack. resend it.
-                                    transferTftp_.bufferedSend(lastSentAck);
-                                }
-                            }
-
-                            // all done.
-                            break;
-                        }
-                    }
-                }
-            }
-            finally
-            {
-                if (bos != null)
-                {
-                    bos.close();
-                }
-            }
-        }
-
-        /*
-         * recursively check to see if one directory is a parent of another.
-         */
-        private boolean isSubdirectoryOf(final File parent, final File child)
-        {
-            final File childsParent = child.getParentFile();
-            if (childsParent == null)
-            {
-                return false;
-            }
-            if (childsParent.equals(parent))
-            {
-                return true;
-            }
-            return isSubdirectoryOf(parent, childsParent);
-        }
-
-        @Override
-        public void run()
-        {
-            try
-            {
-                transferTftp_ = newTFTP();
-
-                transferTftp_.beginBufferedOps();
-                transferTftp_.setDefaultTimeout(socketTimeout_);
-
-                transferTftp_.open();
-
-                if (tftpPacket_ instanceof TFTPReadRequestPacket)
-                {
-                    handleRead((TFTPReadRequestPacket) tftpPacket_);
-                }
-                else if (tftpPacket_ instanceof TFTPWriteRequestPacket)
-                {
-                    handleWrite((TFTPWriteRequestPacket) tftpPacket_);
-                }
-                else
-                {
-                    log_.println("Unsupported TFTP request (" + tftpPacket_ + 
") - ignored.");
-                }
-            }
-            catch (final Exception e)
-            {
-                if (!shutdownTransfer)
-                {
-                    logError_
-                            .println("Unexpected Error in during TFTP file 
transfer.  Transfer aborted. "
-                                    + e);
-                }
-            }
-            finally
-            {
-                try
-                {
-                    if (transferTftp_ != null && transferTftp_.isOpen())
-                    {
-                        transferTftp_.endBufferedOps();
-                        transferTftp_.close();
-                    }
-                }
-                catch (final Exception e)
-                {
-                    // noop
-                }
-                synchronized(transfers_)
-                {
-                    transfers_.remove(this);
-                }
-            }
-        }
-
-        public void shutdown()
-        {
-            shutdownTransfer = true;
-            try
-            {
-                transferTftp_.close();
-            }
-            catch (final RuntimeException e)
-            {
-                // noop
-            }
-        }
-    }
-
-    private static final int DEFAULT_TFTP_PORT = 69;
-    /* /dev/null output stream (default) */
-    private static final PrintStream nullStream = new PrintStream(
-            new OutputStream() {
-                @Override
-                public void write(final byte[] b) throws IOException {}
-                @Override
-                public void write(final int b){}
-                }
-            );
-    private final HashSet<TFTPTransfer> transfers_ = new HashSet<>();
-    private volatile boolean shutdownServer;
-    private TFTP serverTftp_;
-    private File serverReadDirectory_;
-    private File serverWriteDirectory_;
-    private final int port_;
-    private final InetAddress laddr_;
-
-    private Exception serverException;
-
-    private final ServerMode mode_;
-    // don't have access to a logger api, so we will log to these streams, 
which
-    // by default are set to a no-op logger
-    private PrintStream log_;
-
-    private PrintStream logError_;
-    private int maxTimeoutRetries_ = 3;
-    private int socketTimeout_;
-
-
-    private Thread serverThread;
-
-    /**
-     * Start a TFTP Server on the specified port. Gets and Puts occur in the 
specified directory.
-     *
-     * The server will start in another thread, allowing this constructor to 
return immediately.
-     *
-     * If a get or a put comes in with a relative path that tries to get 
outside of the
-     * serverDirectory, then the get or put will be denied.
-     *
-     * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and 
GET_AND_PUT allows both.
-     * Modes are defined as int constants in this class.
-     *
-     * @param serverReadDirectory directory for GET requests
-     * @param serverWriteDirectory directory for PUT requests
-     * @param port The local port to bind to.
-     * @param localaddr The local address to bind to.
-     * @param mode A value as specified above.
-     * @param log Stream to write log message to. If not provided, uses 
System.out
-     * @param errorLog Stream to write error messages to. If not provided, 
uses System.err.
-     * @throws IOException if the server directory is invalid or does not 
exist.
-     */
-    public TFTPServer(final File serverReadDirectory, final File 
serverWriteDirectory, final int port,
-        final InetAddress localaddr, final ServerMode mode, final PrintStream 
log, final PrintStream errorLog)
-        throws IOException
-    {
-        port_ = port;
-        mode_ = mode;
-        laddr_ = localaddr;
-        log_ = log == null ? nullStream: log;
-        logError_ = errorLog == null ? nullStream : errorLog;
-        launch(serverReadDirectory, serverWriteDirectory);
-    }
-
-    /**
-     * Start a TFTP Server on the specified port. Gets and Puts occur in the 
specified directory.
-     *
-     * The server will start in another thread, allowing this constructor to 
return immediately.
-     *
-     * If a get or a put comes in with a relative path that tries to get 
outside of the
-     * serverDirectory, then the get or put will be denied.
-     *
-     * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and 
GET_AND_PUT allows both.
-     * Modes are defined as int constants in this class.
-     *
-     * @param serverReadDirectory directory for GET requests
-     * @param serverWriteDirectory directory for PUT requests
-     * @param port the port to use
-     * @param localiface The local network interface to bind to.
-     *  The interface's first address wil be used.
-     * @param mode A value as specified above.
-     * @param log Stream to write log message to. If not provided, uses 
System.out
-     * @param errorLog Stream to write error messages to. If not provided, 
uses System.err.
-     * @throws IOException if the server directory is invalid or does not 
exist.
-     */
-    public TFTPServer(final File serverReadDirectory, final File 
serverWriteDirectory, final int port,
-        final NetworkInterface localiface, final ServerMode mode, final 
PrintStream log, final PrintStream errorLog)
-        throws IOException
-    {
-        mode_ = mode;
-        port_= port;
-        InetAddress iaddr = null;
-        if (localiface != null)
-        {
-            final Enumeration<InetAddress> ifaddrs = 
localiface.getInetAddresses();
-            if ((ifaddrs != null) && ifaddrs.hasMoreElements()) {
-                iaddr = ifaddrs.nextElement();
-            }
-        }
-        log_ = log == null ? nullStream: log;
-        logError_ = errorLog == null ? nullStream : errorLog;
-        laddr_ = iaddr;
-        launch(serverReadDirectory, serverWriteDirectory);
-    }
-
-    /**
-     * Start a TFTP Server on the specified port. Gets and Puts occur in the 
specified directory.
-     *
-     * The server will start in another thread, allowing this constructor to 
return immediately.
-     *
-     * If a get or a put comes in with a relative path that tries to get 
outside of the
-     * serverDirectory, then the get or put will be denied.
-     *
-     * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and 
GET_AND_PUT allows both.
-     * Modes are defined as int constants in this class.
-     *
-     * @param serverReadDirectory directory for GET requests
-     * @param serverWriteDirectory directory for PUT requests
-     * @param port the port to use
-     * @param mode A value as specified above.
-     * @param log Stream to write log message to. If not provided, uses 
System.out
-     * @param errorLog Stream to write error messages to. If not provided, 
uses System.err.
-     * @throws IOException if the server directory is invalid or does not 
exist.
-     */
-    public TFTPServer(final File serverReadDirectory, final File 
serverWriteDirectory, final int port, final ServerMode mode,
-            final PrintStream log, final PrintStream errorLog) throws 
IOException
-    {
-        port_ = port;
-        mode_ = mode;
-        log_ = log == null ? nullStream: log;
-        logError_ = errorLog == null ? nullStream : errorLog;
-        laddr_ = null;
-        launch(serverReadDirectory, serverWriteDirectory);
-    }
-
-    /**
-     * Start a TFTP Server on the default port (69). Gets and Puts occur in 
the specified
-     * directories.
-     *
-     * The server will start in another thread, allowing this constructor to 
return immediately.
-     *
-     * If a get or a put comes in with a relative path that tries to get 
outside of the
-     * serverDirectory, then the get or put will be denied.
-     *
-     * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and 
GET_AND_PUT allows both.
-     * Modes are defined as int constants in this class.
-     *
-     * @param serverReadDirectory directory for GET requests
-     * @param serverWriteDirectory directory for PUT requests
-     * @param mode A value as specified above.
-     * @throws IOException if the server directory is invalid or does not 
exist.
-     */
-    public TFTPServer(final File serverReadDirectory, final File 
serverWriteDirectory, final ServerMode mode)
-            throws IOException
-    {
-        this(serverReadDirectory, serverWriteDirectory, DEFAULT_TFTP_PORT, 
mode, null, null);
-    }
-
-    @Override
-    protected void finalize() throws Throwable
-    {
-        shutdown();
-    }
-
-    /**
-     * Get the current value for maxTimeoutRetries
-     * @return the max allowed number of retries
-     */
-    public int getMaxTimeoutRetries()
-    {
-        return maxTimeoutRetries_;
-    }
-
-    /**
-     * The current socket timeout used during transfers in milliseconds.
-     * @return the timeout value
-     */
-    public int getSocketTimeout()
-    {
-        return socketTimeout_;
-    }
-
-    /**
-     * check if the server thread is still running.
-     *
-     * @return true if running, false if stopped.
-     * @throws Exception throws the exception that stopped the server if the 
server is stopped from
-     *             an exception.
-     */
-    public boolean isRunning() throws Exception
-    {
-        if (shutdownServer && serverException != null)
-        {
-            throw serverException;
-        }
-        return !shutdownServer;
-    }
-
-    /*
-     * start the server, throw an error if it can't start.
-     */
-    private void launch(final File serverReadDirectory, final File 
serverWriteDirectory) throws IOException
-    {
-        log_.println("Starting TFTP Server on port " + port_ + ".  Read 
directory: "
-                + serverReadDirectory + " Write directory: " + 
serverWriteDirectory
-                + " Server Mode is " + mode_);
-
-        serverReadDirectory_ = serverReadDirectory.getCanonicalFile();
-        if (!serverReadDirectory_.exists() || 
!serverReadDirectory.isDirectory())
-        {
-            throw new IOException("The server read directory " + 
serverReadDirectory_
-                    + " does not exist");
-        }
-
-        serverWriteDirectory_ = serverWriteDirectory.getCanonicalFile();
-        if (!serverWriteDirectory_.exists() || 
!serverWriteDirectory.isDirectory())
-        {
-            throw new IOException("The server write directory " + 
serverWriteDirectory_
-                    + " does not exist");
-        }
-
-        serverTftp_ = new TFTP();
-
-        // This is the value used in response to each client.
-        socketTimeout_ = serverTftp_.getDefaultTimeout();
-
-        // we want the server thread to listen forever.
-        serverTftp_.setDefaultTimeout(0);
-
-        if (laddr_ != null) {
-            serverTftp_.open(port_, laddr_);
-        } else {
-            serverTftp_.open(port_);
-        }
-
-        serverThread = new Thread(this);
-        serverThread.setDaemon(true);
-        serverThread.start();
-    }
-
-    /*
-     * Allow test code to customise the TFTP instance
-     */
-    TFTP newTFTP() {
-        return new TFTP();
-    }
-
-    @Override
-    public void run()
-    {
-        try
-        {
-            while (!shutdownServer)
-            {
-                final TFTPPacket tftpPacket;
-
-                tftpPacket = serverTftp_.receive();
-
-                final TFTPTransfer tt = new TFTPTransfer(tftpPacket);
-                synchronized(transfers_)
-                {
-                    transfers_.add(tt);
-                }
-
-                final Thread thread = new Thread(tt);
-                thread.setDaemon(true);
-                thread.start();
-            }
-        }
-        catch (final Exception e)
-        {
-            if (!shutdownServer)
-            {
-                serverException = e;
-                logError_.println("Unexpected Error in TFTP Server - Server 
shut down! + " + e);
-            }
-        }
-        finally
-        {
-            shutdownServer = true; // set this to true, so the launching 
thread can check to see if it started.
-            if (serverTftp_ != null && serverTftp_.isOpen())
-            {
-                serverTftp_.close();
-            }
-        }
-    }
-
-    /*
-     * Also allow customisation of sending data/ack so can generate errors if 
needed
-     */
-    void sendData(final TFTP tftp, final TFTPPacket data) throws IOException {
-        tftp.bufferedSend(data);
-    }
-
-    /**
-     * Set the stream object to log debug / informational messages. By 
default, this is a no-op
-     *
-     * @param log the stream to use for logging
-     */
-    public void setLog(final PrintStream log)
-    {
-        this.log_ = log;
-    }
-
-    /**
-     * Set the stream object to log error messsages. By default, this is a 
no-op
-     *
-     * @param logError the stream to use for logging errors
-     */
-    public void setLogError(final PrintStream logError)
-    {
-        this.logError_ = logError;
-    }
-
-    /**
-     * Set the max number of retries in response to a timeout. Default 3. Min 
0.
-     *
-     * @param retries number of retries, must be &gt; 0
-     */
-    public void setMaxTimeoutRetries(final int retries)
-    {
-        if (retries < 0)
-        {
-            throw new RuntimeException("Invalid Value");
-        }
-        maxTimeoutRetries_ = retries;
-    }
-
-    /**
-     * Set the socket timeout in milliseconds used in transfers. Defaults to 
the value here:
-     * 
https://commons.apache.org/net/apidocs/org/apache/commons/net/tftp/TFTP.html#DEFAULT_TIMEOUT
-     * (5000 at the time I write this) Min value of 10.
-     * @param timeout the timeout; must be larger than 10
-     */
-    public void setSocketTimeout(final int timeout)
-    {
-        if (timeout < 10)
-        {
-            throw new RuntimeException("Invalid Value");
-        }
-        socketTimeout_ = timeout;
-    }
-
-    /**
-     * Stop the tftp server (and any currently running transfers) and release 
all opened network
-     * resources.
-     */
-    public void shutdown()
-    {
-        shutdownServer = true;
-
-        synchronized(transfers_)
-        {
-            final Iterator<TFTPTransfer> it = transfers_.iterator();
-            while (it.hasNext())
-            {
-                it.next().shutdown();
-            }
-        }
-
-        try
-        {
-            serverTftp_.close();
-        }
-        catch (final RuntimeException e)
-        {
-            // noop
-        }
-
-        try {
-            serverThread.join();
-        } catch (final InterruptedException e) {
-            // we've done the best we could, return
-        }
-    }
-}
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.net.tftp;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketTimeoutException;
+import java.util.Enumeration;
+import java.util.HashSet;
+
+import org.apache.commons.net.io.FromNetASCIIOutputStream;
+import org.apache.commons.net.io.ToNetASCIIInputStream;
+
+/**
+ * A fully multi-threaded tftp server. Can handle multiple clients at the same 
time. Implements RFC
+ * 1350 and wrapping block numbers for large file support.
+ *
+ * To launch, just create an instance of the class. An IOException will be 
thrown if the server
+ * fails to start for reasons such as port in use, port denied, etc.
+ *
+ * To stop, use the shutdown method.
+ *
+ * To check to see if the server is still running (or if it stopped because of 
an error), call the
+ * isRunning() method.
+ *
+ * By default, events are not logged to stdout/stderr. This can be changed 
with the
+ * setLog and setLogError methods.
+ *
+ * <p>
+ * Example usage is below:
+ *
+ * <code>
+ * public static void main(String[] args) throws Exception
+ *  {
+ *      if (args.length != 1)
+ *      {
+ *          System.out
+ *                  .println("You must provide 1 argument - the base path for 
the server to serve from.");
+ *          System.exit(1);
+ *      }
+ *
+ *      TFTPServer ts = new TFTPServer(new File(args[0]), new File(args[0]), 
GET_AND_PUT);
+ *      ts.setSocketTimeout(2000);
+ *
+ *      System.out.println("TFTP Server running.  Press enter to stop.");
+ *      new InputStreamReader(System.in).read();
+ *
+ *      ts.shutdown();
+ *      System.out.println("Server shut down.");
+ *      System.exit(0);
+ *  }
+ *
+ * </code>
+ *
+ * @since 2.0
+ */
+
+public class TFTPServer implements Runnable
+{
+    public enum ServerMode { GET_ONLY, PUT_ONLY, GET_AND_PUT}
+    /*
+     * An instance of an ongoing transfer.
+     */
+    private class TFTPTransfer implements Runnable
+    {
+        private final TFTPPacket tftpPacket_;
+
+        private boolean shutdownTransfer;
+
+        TFTP transferTftp_;
+
+        public TFTPTransfer(final TFTPPacket tftpPacket)
+        {
+            tftpPacket_ = tftpPacket;
+        }
+
+        /*
+         * Utility method to make sure that paths provided by tftp clients do 
not get outside of the
+         * serverRoot directory.
+         */
+        private File buildSafeFile(final File serverDirectory, final String 
fileName, final boolean createSubDirs)
+                throws IOException
+        {
+            File temp = new File(serverDirectory, fileName);
+            temp = temp.getCanonicalFile();
+
+            if (!isSubdirectoryOf(serverDirectory, temp))
+            {
+                throw new IOException("Cannot access files outside of tftp 
server root.");
+            }
+
+            // ensure directory exists (if requested)
+            if (createSubDirs)
+            {
+                createDirectory(temp.getParentFile());
+            }
+
+            return temp;
+        }
+
+        /*
+         * recursively create subdirectories
+         */
+        private void createDirectory(final File file) throws IOException
+        {
+            final File parent = file.getParentFile();
+            if (parent == null)
+            {
+                throw new IOException("Unexpected error creating requested 
directory");
+            }
+            if (!parent.exists())
+            {
+                // recurse...
+                createDirectory(parent);
+            }
+
+            if (!parent.isDirectory()) {
+                throw new IOException(
+                        "Invalid directory path - file in the way of requested 
folder");
+            }
+            if (file.isDirectory())
+            {
+                return;
+            }
+            final boolean result = file.mkdir();
+            if (!result)
+            {
+                throw new IOException("Couldn't create requested directory");
+            }
+        }
+
+        /*
+         * Handle a tftp read request.
+         */
+        private void handleRead(final TFTPReadRequestPacket trrp) throws 
IOException, TFTPPacketException
+        {
+            InputStream is = null;
+            try
+            {
+                if (mode_ == ServerMode.PUT_ONLY)
+                {
+                    transferTftp_.bufferedSend(new 
TFTPErrorPacket(trrp.getAddress(), trrp
+                            .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION,
+                            "Read not allowed by server."));
+                    return;
+                }
+
+                try
+                {
+                    is = new BufferedInputStream(new 
FileInputStream(buildSafeFile(
+                            serverReadDirectory_, trrp.getFilename(), false)));
+                }
+                catch (final FileNotFoundException e)
+                {
+                    transferTftp_.bufferedSend(new 
TFTPErrorPacket(trrp.getAddress(), trrp
+                            .getPort(), TFTPErrorPacket.FILE_NOT_FOUND, 
e.getMessage()));
+                    return;
+                }
+                catch (final Exception e)
+                {
+                    transferTftp_.bufferedSend(new 
TFTPErrorPacket(trrp.getAddress(), trrp
+                            .getPort(), TFTPErrorPacket.UNDEFINED, 
e.getMessage()));
+                    return;
+                }
+
+                if (trrp.getMode() == TFTP.NETASCII_MODE)
+                {
+                    is = new ToNetASCIIInputStream(is);
+                }
+
+                final byte[] temp = new byte[TFTPDataPacket.MAX_DATA_LENGTH];
+
+                TFTPPacket answer;
+
+                int block = 1;
+                boolean sendNext = true;
+
+                int readLength = TFTPDataPacket.MAX_DATA_LENGTH;
+
+                TFTPDataPacket lastSentData = null;
+
+                // We are reading a file, so when we read less than the
+                // requested bytes, we know that we are at the end of the file.
+                while (readLength == TFTPDataPacket.MAX_DATA_LENGTH && 
!shutdownTransfer)
+                {
+                    if (sendNext)
+                    {
+                        readLength = is.read(temp);
+                        if (readLength == -1)
+                        {
+                            readLength = 0;
+                        }
+
+                        lastSentData = new TFTPDataPacket(trrp.getAddress(), 
trrp.getPort(), block,
+                                temp, 0, readLength);
+                        sendData(transferTftp_, lastSentData); // send the data
+                    }
+
+                    answer = null;
+
+                    int timeoutCount = 0;
+
+                    while (!shutdownTransfer
+                            && (answer == null || 
!answer.getAddress().equals(trrp.getAddress()) || answer
+                                    .getPort() != trrp.getPort()))
+                    {
+                        // listen for an answer.
+                        if (answer != null)
+                        {
+                            // The answer that we got didn't come from the
+                            // expected source, fire back an error, and 
continue
+                            // listening.
+                            log_.println("TFTP Server ignoring message from 
unexpected source.");
+                            transferTftp_.bufferedSend(new 
TFTPErrorPacket(answer.getAddress(),
+                                    answer.getPort(), 
TFTPErrorPacket.UNKNOWN_TID,
+                                    "Unexpected Host or Port"));
+                        }
+                        try
+                        {
+                            answer = transferTftp_.bufferedReceive();
+                        }
+                        catch (final SocketTimeoutException e)
+                        {
+                            if (timeoutCount >= maxTimeoutRetries_)
+                            {
+                                throw e;
+                            }
+                            // didn't get an ack for this data. need to resend
+                            // it.
+                            timeoutCount++;
+                            transferTftp_.bufferedSend(lastSentData);
+                            continue;
+                        }
+                    }
+
+                    if (answer == null || !(answer instanceof TFTPAckPacket))
+                    {
+                        if (!shutdownTransfer)
+                        {
+                            logError_
+                                    .println("Unexpected response from tftp 
client during transfer ("
+                                            + answer + ").  Transfer 
aborted.");
+                        }
+                        break;
+                    }
+                    // once we get here, we know we have an answer packet
+                    // from the correct host.
+                    final TFTPAckPacket ack = (TFTPAckPacket) answer;
+                    if (ack.getBlockNumber() != block)
+                    {
+                        /*
+                         * The origional tftp spec would have called on us to 
resend the
+                         * previous data here, however, that causes the SAS 
Syndrome.
+                         * http://www.faqs.org/rfcs/rfc1123.html section 
4.2.3.1 The modified
+                         * spec says that we ignore a duplicate ack. If the 
packet was really
+                         * lost, we will time out on receive, and resend the 
previous data at
+                         * that point.
+                         */
+                        sendNext = false;
+                    }
+                    else
+                    {
+                        // send the next block
+                        block++;
+                        if (block > 65535)
+                        {
+                            // wrap the block number
+                            block = 0;
+                        }
+                        sendNext = true;
+                    }
+                }
+            }
+            finally
+            {
+                try
+                {
+                    if (is != null)
+                    {
+                        is.close();
+                    }
+                }
+                catch (final IOException e)
+                {
+                    // noop
+                }
+            }
+        }
+
+        /*
+         * handle a tftp write request.
+         */
+        private void handleWrite(final TFTPWriteRequestPacket twrp) throws 
IOException,
+                TFTPPacketException
+        {
+            OutputStream bos = null;
+            try
+            {
+                if (mode_ == ServerMode.GET_ONLY)
+                {
+                    transferTftp_.bufferedSend(new 
TFTPErrorPacket(twrp.getAddress(), twrp
+                            .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION,
+                            "Write not allowed by server."));
+                    return;
+                }
+
+                int lastBlock = 0;
+                final String fileName = twrp.getFilename();
+
+                try
+                {
+                    final File temp = buildSafeFile(serverWriteDirectory_, 
fileName, true);
+                    if (temp.exists())
+                    {
+                        transferTftp_.bufferedSend(new 
TFTPErrorPacket(twrp.getAddress(), twrp
+                                .getPort(), TFTPErrorPacket.FILE_EXISTS, "File 
already exists"));
+                        return;
+                    }
+                    bos = new BufferedOutputStream(new FileOutputStream(temp));
+
+                    if (twrp.getMode() == TFTP.NETASCII_MODE)
+                    {
+                        bos = new FromNetASCIIOutputStream(bos);
+                    }
+                }
+                catch (final Exception e)
+                {
+                    transferTftp_.bufferedSend(new 
TFTPErrorPacket(twrp.getAddress(), twrp
+                            .getPort(), TFTPErrorPacket.UNDEFINED, 
e.getMessage()));
+                    return;
+                }
+
+                TFTPAckPacket lastSentAck = new 
TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0);
+                sendData(transferTftp_, lastSentAck); // send the data
+
+                while (true)
+                {
+                    // get the response - ensure it is from the right place.
+                    TFTPPacket dataPacket = null;
+
+                    int timeoutCount = 0;
+
+                    while (!shutdownTransfer
+                            && (dataPacket == null
+                                    || 
!dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket
+                                    .getPort() != twrp.getPort()))
+                    {
+                        // listen for an answer.
+                        if (dataPacket != null)
+                        {
+                            // The data that we got didn't come from the
+                            // expected source, fire back an error, and 
continue
+                            // listening.
+                            log_.println("TFTP Server ignoring message from 
unexpected source.");
+                            transferTftp_.bufferedSend(new 
TFTPErrorPacket(dataPacket.getAddress(),
+                                    dataPacket.getPort(), 
TFTPErrorPacket.UNKNOWN_TID,
+                                    "Unexpected Host or Port"));
+                        }
+
+                        try
+                        {
+                            dataPacket = transferTftp_.bufferedReceive();
+                        }
+                        catch (final SocketTimeoutException e)
+                        {
+                            if (timeoutCount >= maxTimeoutRetries_)
+                            {
+                                throw e;
+                            }
+                            // It didn't get our ack. Resend it.
+                            transferTftp_.bufferedSend(lastSentAck);
+                            timeoutCount++;
+                            continue;
+                        }
+                    }
+
+                    if (dataPacket instanceof TFTPWriteRequestPacket)
+                    {
+                        // it must have missed our initial ack. Send another.
+                        lastSentAck = new TFTPAckPacket(twrp.getAddress(), 
twrp.getPort(), 0);
+                        transferTftp_.bufferedSend(lastSentAck);
+                    }
+                    else if (dataPacket == null || !(dataPacket instanceof 
TFTPDataPacket))
+                    {
+                        if (!shutdownTransfer)
+                        {
+                            logError_
+                                    .println("Unexpected response from tftp 
client during transfer ("
+                                            + dataPacket + ").  Transfer 
aborted.");
+                        }
+                        break;
+                    }
+                    else
+                    {
+                        final int block = ((TFTPDataPacket) 
dataPacket).getBlockNumber();
+                        final byte[] data = ((TFTPDataPacket) 
dataPacket).getData();
+                        final int dataLength = ((TFTPDataPacket) 
dataPacket).getDataLength();
+                        final int dataOffset = ((TFTPDataPacket) 
dataPacket).getDataOffset();
+
+                        if (block > lastBlock || lastBlock == 65535 && block 
== 0)
+                        {
+                            // it might resend a data block if it missed our 
ack
+                            // - don't rewrite the block.
+                            bos.write(data, dataOffset, dataLength);
+                            lastBlock = block;
+                        }
+
+                        lastSentAck = new TFTPAckPacket(twrp.getAddress(), 
twrp.getPort(), block);
+                        sendData(transferTftp_, lastSentAck); // send the data
+                        if (dataLength < TFTPDataPacket.MAX_DATA_LENGTH)
+                        {
+                            // end of stream signal - The tranfer is complete.
+                            bos.close();
+
+                            // But my ack may be lost - so listen to see if I
+                            // need to resend the ack.
+                            for (int i = 0; i < maxTimeoutRetries_; i++)
+                            {
+                                try
+                                {
+                                    dataPacket = 
transferTftp_.bufferedReceive();
+                                }
+                                catch (final SocketTimeoutException e)
+                                {
+                                    // this is the expected route - the client
+                                    // shouldn't be sending any more packets.
+                                    break;
+                                }
+
+                                if (dataPacket != null
+                                        && 
(!dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket
+                                                .getPort() != twrp.getPort()))
+                                {
+                                    // make sure it was from the right 
client...
+                                    transferTftp_
+                                            .bufferedSend(new 
TFTPErrorPacket(dataPacket
+                                                    .getAddress(), 
dataPacket.getPort(),
+                                                    
TFTPErrorPacket.UNKNOWN_TID,
+                                                    "Unexpected Host or 
Port"));
+                                }
+                                else
+                                {
+                                    // This means they sent us the last
+                                    // datapacket again, must have missed our
+                                    // ack. resend it.
+                                    transferTftp_.bufferedSend(lastSentAck);
+                                }
+                            }
+
+                            // all done.
+                            break;
+                        }
+                    }
+                }
+            }
+            finally
+            {
+                if (bos != null)
+                {
+                    bos.close();
+                }
+            }
+        }
+
+        /*
+         * recursively check to see if one directory is a parent of another.
+         */
+        private boolean isSubdirectoryOf(final File parent, final File child)
+        {
+            final File childsParent = child.getParentFile();
+            if (childsParent == null)
+            {
+                return false;
+            }
+            if (childsParent.equals(parent))
+            {
+                return true;
+            }
+            return isSubdirectoryOf(parent, childsParent);
+        }
+
+        @Override
+        public void run()
+        {
+            try
+            {
+                transferTftp_ = newTFTP();
+
+                transferTftp_.beginBufferedOps();
+                transferTftp_.setDefaultTimeout(socketTimeout_);
+
+                transferTftp_.open();
+
+                if (tftpPacket_ instanceof TFTPReadRequestPacket)
+                {
+                    handleRead((TFTPReadRequestPacket) tftpPacket_);
+                }
+                else if (tftpPacket_ instanceof TFTPWriteRequestPacket)
+                {
+                    handleWrite((TFTPWriteRequestPacket) tftpPacket_);
+                }
+                else
+                {
+                    log_.println("Unsupported TFTP request (" + tftpPacket_ + 
") - ignored.");
+                }
+            }
+            catch (final Exception e)
+            {
+                if (!shutdownTransfer)
+                {
+                    logError_
+                            .println("Unexpected Error in during TFTP file 
transfer.  Transfer aborted. "
+                                    + e);
+                }
+            }
+            finally
+            {
+                try
+                {
+                    if (transferTftp_ != null && transferTftp_.isOpen())
+                    {
+                        transferTftp_.endBufferedOps();
+                        transferTftp_.close();
+                    }
+                }
+                catch (final Exception e)
+                {
+                    // noop
+                }
+                synchronized(transfers_)
+                {
+                    transfers_.remove(this);
+                }
+            }
+        }
+
+        public void shutdown()
+        {
+            shutdownTransfer = true;
+            try
+            {
+                transferTftp_.close();
+            }
+            catch (final RuntimeException e)
+            {
+                // noop
+            }
+        }
+    }
+
+    private static final int DEFAULT_TFTP_PORT = 69;
+    /* /dev/null output stream (default) */
+    private static final PrintStream nullStream = new PrintStream(
+            new OutputStream() {
+                @Override
+                public void write(final byte[] b) throws IOException {}
+                @Override
+                public void write(final int b){}
+                }
+            );
+    private final HashSet<TFTPTransfer> transfers_ = new HashSet<>();
+    private volatile boolean shutdownServer;
+    private TFTP serverTftp_;
+    private File serverReadDirectory_;
+    private File serverWriteDirectory_;
+    private final int port_;
+    private final InetAddress laddr_;
+
+    private Exception serverException;
+
+    private final ServerMode mode_;
+    // don't have access to a logger api, so we will log to these streams, 
which
+    // by default are set to a no-op logger
+    private PrintStream log_;
+
+    private PrintStream logError_;
+    private int maxTimeoutRetries_ = 3;
+    private int socketTimeout_;
+
+
+    private Thread serverThread;
+
+    /**
+     * Start a TFTP Server on the specified port. Gets and Puts occur in the 
specified directory.
+     *
+     * The server will start in another thread, allowing this constructor to 
return immediately.
+     *
+     * If a get or a put comes in with a relative path that tries to get 
outside of the
+     * serverDirectory, then the get or put will be denied.
+     *
+     * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and 
GET_AND_PUT allows both.
+     * Modes are defined as int constants in this class.
+     *
+     * @param serverReadDirectory directory for GET requests
+     * @param serverWriteDirectory directory for PUT requests
+     * @param port The local port to bind to.
+     * @param localaddr The local address to bind to.
+     * @param mode A value as specified above.
+     * @param log Stream to write log message to. If not provided, uses 
System.out
+     * @param errorLog Stream to write error messages to. If not provided, 
uses System.err.
+     * @throws IOException if the server directory is invalid or does not 
exist.
+     */
+    public TFTPServer(final File serverReadDirectory, final File 
serverWriteDirectory, final int port,
+        final InetAddress localaddr, final ServerMode mode, final PrintStream 
log, final PrintStream errorLog)
+        throws IOException
+    {
+        port_ = port;
+        mode_ = mode;
+        laddr_ = localaddr;
+        log_ = log == null ? nullStream: log;
+        logError_ = errorLog == null ? nullStream : errorLog;
+        launch(serverReadDirectory, serverWriteDirectory);
+    }
+
+    /**
+     * Start a TFTP Server on the specified port. Gets and Puts occur in the 
specified directory.
+     *
+     * The server will start in another thread, allowing this constructor to 
return immediately.
+     *
+     * If a get or a put comes in with a relative path that tries to get 
outside of the
+     * serverDirectory, then the get or put will be denied.
+     *
+     * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and 
GET_AND_PUT allows both.
+     * Modes are defined as int constants in this class.
+     *
+     * @param serverReadDirectory directory for GET requests
+     * @param serverWriteDirectory directory for PUT requests
+     * @param port the port to use
+     * @param localiface The local network interface to bind to.
+     *  The interface's first address wil be used.
+     * @param mode A value as specified above.
+     * @param log Stream to write log message to. If not provided, uses 
System.out
+     * @param errorLog Stream to write error messages to. If not provided, 
uses System.err.
+     * @throws IOException if the server directory is invalid or does not 
exist.
+     */
+    public TFTPServer(final File serverReadDirectory, final File 
serverWriteDirectory, final int port,
+        final NetworkInterface localiface, final ServerMode mode, final 
PrintStream log, final PrintStream errorLog)
+        throws IOException
+    {
+        mode_ = mode;
+        port_= port;
+        InetAddress iaddr = null;
+        if (localiface != null)
+        {
+            final Enumeration<InetAddress> ifaddrs = 
localiface.getInetAddresses();
+            if ((ifaddrs != null) && ifaddrs.hasMoreElements()) {
+                iaddr = ifaddrs.nextElement();
+            }
+        }
+        log_ = log == null ? nullStream: log;
+        logError_ = errorLog == null ? nullStream : errorLog;
+        laddr_ = iaddr;
+        launch(serverReadDirectory, serverWriteDirectory);
+    }
+
+    /**
+     * Start a TFTP Server on the specified port. Gets and Puts occur in the 
specified directory.
+     *
+     * The server will start in another thread, allowing this constructor to 
return immediately.
+     *
+     * If a get or a put comes in with a relative path that tries to get 
outside of the
+     * serverDirectory, then the get or put will be denied.
+     *
+     * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and 
GET_AND_PUT allows both.
+     * Modes are defined as int constants in this class.
+     *
+     * @param serverReadDirectory directory for GET requests
+     * @param serverWriteDirectory directory for PUT requests
+     * @param port the port to use
+     * @param mode A value as specified above.
+     * @param log Stream to write log message to. If not provided, uses 
System.out
+     * @param errorLog Stream to write error messages to. If not provided, 
uses System.err.
+     * @throws IOException if the server directory is invalid or does not 
exist.
+     */
+    public TFTPServer(final File serverReadDirectory, final File 
serverWriteDirectory, final int port, final ServerMode mode,
+            final PrintStream log, final PrintStream errorLog) throws 
IOException
+    {
+        port_ = port;
+        mode_ = mode;
+        log_ = log == null ? nullStream: log;
+        logError_ = errorLog == null ? nullStream : errorLog;
+        laddr_ = null;
+        launch(serverReadDirectory, serverWriteDirectory);
+    }
+
+    /**
+     * Start a TFTP Server on the default port (69). Gets and Puts occur in 
the specified
+     * directories.
+     *
+     * The server will start in another thread, allowing this constructor to 
return immediately.
+     *
+     * If a get or a put comes in with a relative path that tries to get 
outside of the
+     * serverDirectory, then the get or put will be denied.
+     *
+     * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and 
GET_AND_PUT allows both.
+     * Modes are defined as int constants in this class.
+     *
+     * @param serverReadDirectory directory for GET requests
+     * @param serverWriteDirectory directory for PUT requests
+     * @param mode A value as specified above.
+     * @throws IOException if the server directory is invalid or does not 
exist.
+     */
+    public TFTPServer(final File serverReadDirectory, final File 
serverWriteDirectory, final ServerMode mode)
+            throws IOException
+    {
+        this(serverReadDirectory, serverWriteDirectory, DEFAULT_TFTP_PORT, 
mode, null, null);
+    }
+
+    @Override
+    protected void finalize() throws Throwable
+    {
+        shutdown();
+    }
+
+    /**
+     * Get the current value for maxTimeoutRetries
+     * @return the max allowed number of retries
+     */
+    public int getMaxTimeoutRetries()
+    {
+        return maxTimeoutRetries_;
+    }
+
+    /**
+     * The current socket timeout used during transfers in milliseconds.
+     * @return the timeout value
+     */
+    public int getSocketTimeout()
+    {
+        return socketTimeout_;
+    }
+
+    /**
+     * check if the server thread is still running.
+     *
+     * @return true if running, false if stopped.
+     * @throws Exception throws the exception that stopped the server if the 
server is stopped from
+     *             an exception.
+     */
+    public boolean isRunning() throws Exception
+    {
+        if (shutdownServer && serverException != null)
+        {
+            throw serverException;
+        }
+        return !shutdownServer;
+    }
+
+    /*
+     * start the server, throw an error if it can't start.
+     */
+    private void launch(final File serverReadDirectory, final File 
serverWriteDirectory) throws IOException
+    {
+        log_.println("Starting TFTP Server on port " + port_ + ".  Read 
directory: "
+                + serverReadDirectory + " Write directory: " + 
serverWriteDirectory
+                + " Server Mode is " + mode_);
+
+        serverReadDirectory_ = serverReadDirectory.getCanonicalFile();
+        if (!serverReadDirectory_.exists() || 
!serverReadDirectory.isDirectory())
+        {
+            throw new IOException("The server read directory " + 
serverReadDirectory_
+                    + " does not exist");
+        }
+
+        serverWriteDirectory_ = serverWriteDirectory.getCanonicalFile();
+        if (!serverWriteDirectory_.exists() || 
!serverWriteDirectory.isDirectory())
+        {
+            throw new IOException("The server write directory " + 
serverWriteDirectory_
+                    + " does not exist");
+        }
+
+        serverTftp_ = new TFTP();
+
+        // This is the value used in response to each client.
+        socketTimeout_ = serverTftp_.getDefaultTimeout();
+
+        // we want the server thread to listen forever.
+        serverTftp_.setDefaultTimeout(0);
+
+        if (laddr_ != null) {
+            serverTftp_.open(port_, laddr_);
+        } else {
+            serverTftp_.open(port_);
+        }
+
+        serverThread = new Thread(this);
+        serverThread.setDaemon(true);
+        serverThread.start();
+    }
+
+    /*
+     * Allow test code to customise the TFTP instance
+     */
+    TFTP newTFTP() {
+        return new TFTP();
+    }
+
+    @Override
+    public void run()
+    {
+        try
+        {
+            while (!shutdownServer)
+            {
+                final TFTPPacket tftpPacket;
+
+                tftpPacket = serverTftp_.receive();
+
+                final TFTPTransfer tt = new TFTPTransfer(tftpPacket);
+                synchronized(transfers_)
+                {
+                    transfers_.add(tt);
+                }
+
+                final Thread thread = new Thread(tt);
+                thread.setDaemon(true);
+                thread.start();
+            }
+        }
+        catch (final Exception e)
+        {
+            if (!shutdownServer)
+            {
+                serverException = e;
+                logError_.println("Unexpected Error in TFTP Server - Server 
shut down! + " + e);
+            }
+        }
+        finally
+        {
+            shutdownServer = true; // set this to true, so the launching 
thread can check to see if it started.
+            if (serverTftp_ != null && serverTftp_.isOpen())
+            {
+                serverTftp_.close();
+            }
+        }
+    }
+
+    /*
+     * Also allow customisation of sending data/ack so can generate errors if 
needed
+     */
+    void sendData(final TFTP tftp, final TFTPPacket data) throws IOException {
+        tftp.bufferedSend(data);
+    }
+
+    /**
+     * Set the stream object to log debug / informational messages. By 
default, this is a no-op
+     *
+     * @param log the stream to use for logging
+     */
+    public void setLog(final PrintStream log)
+    {
+        this.log_ = log;
+    }
+
+    /**
+     * Set the stream object to log error messsages. By default, this is a 
no-op
+     *
+     * @param logError the stream to use for logging errors
+     */
+    public void setLogError(final PrintStream logError)
+    {
+        this.logError_ = logError;
+    }
+
+    /**
+     * Set the max number of retries in response to a timeout. Default 3. Min 
0.
+     *
+     * @param retries number of retries, must be &gt; 0
+     */
+    public void setMaxTimeoutRetries(final int retries)
+    {
+        if (retries < 0)
+        {
+            throw new RuntimeException("Invalid Value");
+        }
+        maxTimeoutRetries_ = retries;
+    }
+
+    /**
+     * Set the socket timeout in milliseconds used in transfers. Defaults to 
the value here:
+     * 
https://commons.apache.org/net/apidocs/org/apache/commons/net/tftp/TFTP.html#DEFAULT_TIMEOUT
+     * (5000 at the time I write this) Min value of 10.
+     * @param timeout the timeout; must be larger than 10
+     */
+    public void setSocketTimeout(final int timeout)
+    {
+        if (timeout < 10)
+        {
+            throw new RuntimeException("Invalid Value");
+        }
+        socketTimeout_ = timeout;
+    }
+
+    /**
+     * Stop the tftp server (and any currently running transfers) and release 
all opened network
+     * resources.
+     */
+    public void shutdown()
+    {
+        shutdownServer = true;
+
+        synchronized(transfers_)
+        {
+            transfers_.forEach(TFTPTransfer::shutdown);
+        }
+
+        try
+        {
+            serverTftp_.close();
+        }
+        catch (final RuntimeException e)
+        {
+            // noop
+        }
+
+        try {
+            serverThread.join();
+        } catch (final InterruptedException e) {
+            // we've done the best we could, return
+        }
+    }
+}

Reply via email to