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 <[email protected]>
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 > 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 > 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
+ }
+ }
+}