1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.net.ftp;
19
20 import java.io.BufferedReader;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.InputStreamReader;
24 import java.util.LinkedList;
25 import java.util.List;
26 import java.util.ListIterator;
27 import java.util.stream.Collectors;
28
29 import org.apache.commons.io.Charsets;
30
31
32 /**
33 * This class handles the entire process of parsing a listing of file entries from the server.
34 * <p>
35 * This object defines a two-part parsing mechanism.
36 * </p>
37 * <p>
38 * The first part consists of reading the raw input into an internal list of strings. Every item in this list corresponds to an actual file. All extraneous
39 * 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
40 * this engine, by calling its methods {@code readNextEntry()} - which handles the issue of what delimits one entry from another, usually but not always a line
41 * feed and {@code preParse()} - which handles removal of extraneous matter such as the preliminary lines of a listing, removal of duplicates on versioning
42 * systems, etc.
43 * </p>
44 * <p>
45 * 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
46 * over the internal list of strings. This may be done either in block mode, by calling the {@code getNext()} and {@code getPrevious()} methods to provide
47 * "paged" output of less than the whole list at one time, or by calling the {@code getFiles()} method to return the entire list.
48 * </p>
49 * <p>
50 * Examples:
51 * </p>
52 * <p>
53 * Paged access:
54 * </p>
55 * <pre>
56 * FTPClient f = FTPClient();
57 * f.connect(server);
58 * f.login(user, password);
59 * FTPListParseEngine engine = f.initiateListParsing(directory);
60 *
61 * while (engine.hasNext()) {
62 * FTPFile[] files = engine.getNext(25); // "page size" you want
63 * // do whatever you want with these files, display them, etc.
64 * // expensive FTPFile objects not created until needed.
65 * }
66 * </pre>
67 * <p>
68 * For unpaged access, simply use FTPClient.listFiles(). That method uses this class transparently.
69 * </p>
70 */
71 public class FTPListParseEngine {
72
73 /**
74 * An empty immutable {@code FTPFile} array.
75 */
76 private static final FTPFile[] EMPTY_FTP_FILE_ARRAY = {};
77 private List<String> entries = new LinkedList<>();
78
79 private ListIterator<String> internalIterator = entries.listIterator();
80 private final FTPFileEntryParser parser;
81
82 // Should invalid files (parse failures) be allowed?
83 private final boolean saveUnparseableEntries;
84
85 /**
86 * Constructs a new instance.
87 *
88 * @param parser How to parse file entries.
89 */
90 public FTPListParseEngine(final FTPFileEntryParser parser) {
91 this(parser, null);
92 }
93
94 /**
95 * Intended for use by FTPClient only
96 *
97 * @since 3.4
98 */
99 FTPListParseEngine(final FTPFileEntryParser parser, final FTPClientConfig configuration) {
100 this.parser = parser;
101 this.saveUnparseableEntries = configuration != null && configuration.getUnparseableEntries();
102 }
103
104 /**
105 * Gets a list of FTPFile objects containing the whole list of files returned by the server as read by this object's parser. The files are filtered
106 * before being added to the array.
107 *
108 * @param filter FTPFileFilter, must not be {@code null}.
109 * @return a list of FTPFile objects containing the whole list of files returned by the server as read by this object's parser.
110 * <p>
111 * <strong> NOTE:</strong> This array may contain null members if any of the individual file listings failed to parse. The caller should check each
112 * entry for null before referencing it, or use a filter such as {@link FTPFileFilters#NON_NULL} which does not allow null entries.
113 * @since 3.9.0
114 */
115 public List<FTPFile> getFileList(final FTPFileFilter filter) {
116 return entries.stream().map(e -> {
117 final FTPFile file = parser.parseFTPEntry(e);
118 return file == null && saveUnparseableEntries ? new FTPFile(e) : file;
119 }).filter(filter::accept).collect(Collectors.toList());
120 }
121
122 /**
123 * Gets an array of FTPFile objects containing the whole list of files returned by the server as read by this object's parser.
124 *
125 * @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
126 * be null
127 * @throws IOException - not ever thrown, may be removed in a later release
128 */
129 // TODO remove; not actually thrown
130 public FTPFile[] getFiles() throws IOException {
131 return getFiles(FTPFileFilters.NON_NULL);
132 }
133
134 /**
135 * Gets 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
136 * before being added to the array.
137 *
138 * @param filter FTPFileFilter, must not be {@code null}.
139 * @return an array of FTPFile objects containing the whole list of files returned by the server as read by this object's parser.
140 * <p>
141 * <strong> NOTE:</strong> This array may contain null members if any of the individual file listings failed to parse. The caller should check each
142 * entry for null before referencing it, or use a filter such as {@link FTPFileFilters#NON_NULL} which does not allow null entries.
143 * @throws IOException - not ever thrown, may be removed in a later release
144 * @since 2.2
145 */
146 // TODO remove; not actually thrown
147 public FTPFile[] getFiles(final FTPFileFilter filter) throws IOException {
148 return getFileList(filter).toArray(EMPTY_FTP_FILE_ARRAY);
149 }
150
151 /**
152 * Gets an array of at most {@code quantityRequested} FTPFile objects starting at this object's internal iterator's current position. If fewer than
153 * {@code quantityRequested} such elements are available, the returned array will have a length equal to the number of entries at and after the current
154 * position. If no such entries are found, this array will have a length of 0.
155 *
156 * 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.
157 *
158 * @param quantityRequested the maximum number of entries we want to get.
159 * @return an array of at most {@code quantityRequested} FTPFile objects starting at the current position of this iterator within its list and at least the
160 * number of elements which exist in the list at and after its current position.
161 * <p>
162 * <strong> NOTE:</strong> This array may contain null members if any of the individual file listings failed to parse. The caller should check each
163 * entry for null before referencing it.
164 */
165 public FTPFile[] getNext(final int quantityRequested) {
166 final List<FTPFile> tmpResults = new LinkedList<>();
167 int count = quantityRequested;
168 while (count > 0 && internalIterator.hasNext()) {
169 final String entry = internalIterator.next();
170 FTPFile temp = parser.parseFTPEntry(entry);
171 if (temp == null && saveUnparseableEntries) {
172 temp = new FTPFile(entry);
173 }
174 tmpResults.add(temp);
175 count--;
176 }
177 return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY);
178
179 }
180
181 /**
182 * Gets an array of at most {@code quantityRequested} FTPFile objects starting at this object's internal iterator's current position, and working back
183 * toward the beginning.
184 *
185 * If fewer than {@code quantityRequested} such elements are available, the returned array will have a length equal to the number of entries at and after
186 * the current position. If no such entries are found, this array will have a length of 0.
187 *
188 * 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.
189 *
190 * @param quantityRequested the maximum number of entries we want to get.
191 * @return an array of at most {@code quantityRequested} FTPFile objects starting at the current position of this iterator within its list and at least the
192 * 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
193 * reversed).
194 * <p>
195 * <strong> NOTE:</strong> This array may contain null members if any of the individual file listings failed to parse. The caller should check each
196 * entry for null before referencing it.
197 */
198 public FTPFile[] getPrevious(final int quantityRequested) {
199 final List<FTPFile> tmpResults = new LinkedList<>();
200 int count = quantityRequested;
201 while (count > 0 && internalIterator.hasPrevious()) {
202 final String entry = internalIterator.previous();
203 FTPFile temp = parser.parseFTPEntry(entry);
204 if (temp == null && saveUnparseableEntries) {
205 temp = new FTPFile(entry);
206 }
207 tmpResults.add(0, temp);
208 count--;
209 }
210 return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY);
211 }
212
213 /**
214 * convenience method to allow clients to know whether this object's internal iterator's current position is at the end of the list.
215 *
216 * @return true if internal iterator is not at end of list, false otherwise.
217 */
218 public boolean hasNext() {
219 return internalIterator.hasNext();
220 }
221
222 /**
223 * convenience method to allow clients to know whether this object's internal iterator's current position is at the beginning of the list.
224 *
225 * @return true if internal iterator is not at beginning of list, false otherwise.
226 */
227 public boolean hasPrevious() {
228 return internalIterator.hasPrevious();
229 }
230
231 /**
232 * Internal method for reading (and closing) the input into the {@code entries} list. After this method has completed, {@code entries} will contain a
233 * collection of entries (as defined by {@code FTPFileEntryParser.readNextEntry()}), but this may contain various non-entry preliminary lines from the
234 * server output, duplicates, and other data that will not be part of the final listing.
235 *
236 * @param inputStream The socket stream on which the input will be read.
237 * @param charsetName The encoding to use.
238 * @throws IOException thrown on any failure to read the stream
239 */
240 private void read(final InputStream inputStream, final String charsetName) throws IOException {
241 try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charsets.toCharset(charsetName)))) {
242 String line = parser.readNextEntry(reader);
243 while (line != null) {
244 entries.add(line);
245 line = parser.readNextEntry(reader);
246 }
247 }
248 }
249
250 /**
251 * Do not use.
252 *
253 * @param inputStream the stream from which to read
254 * @throws IOException on error
255 * @deprecated Use {@link #readServerList(InputStream, String)} instead
256 */
257 @Deprecated
258 public void readServerList(final InputStream inputStream) throws IOException {
259 readServerList(inputStream, null);
260 }
261
262 /**
263 * 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
264 * list of unparsed entries (Strings) each referring to a unique file on the server.
265 *
266 * @param inputStream input stream provided by the server socket.
267 * @param charsetName the encoding to be used for reading the stream
268 * @throws IOException thrown on any failure to read from the sever.
269 */
270 public void readServerList(final InputStream inputStream, final String charsetName) throws IOException {
271 entries = new LinkedList<>();
272 read(inputStream, charsetName);
273 parser.preParse(entries);
274 resetIterator();
275 }
276
277 /**
278 * resets this object's internal iterator to the beginning of the list.
279 */
280 public void resetIterator() {
281 internalIterator = entries.listIterator();
282 }
283
284 }