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 * An empty immutable {@code FTPFile} array.
74 */
75 private static final FTPFile[] EMPTY_FTP_FILE_ARRAY = {};
76 private List<String> entries = new LinkedList<>();
77
78 private ListIterator<String> internalIterator = entries.listIterator();
79 private final FTPFileEntryParser parser;
80
81 // Should invalid files (parse failures) be allowed?
82 private final boolean saveUnparseableEntries;
83
84 /**
85 * Constructs a new instance.
86 *
87 * @param parser How to parse file entries.
88 */
89 public FTPListParseEngine(final FTPFileEntryParser parser) {
90 this(parser, null);
91 }
92
93 /**
94 * Intended for use by FTPClient only
95 *
96 * @since 3.4
97 */
98 FTPListParseEngine(final FTPFileEntryParser parser, final FTPClientConfig configuration) {
99 this.parser = parser;
100 this.saveUnparseableEntries = configuration != null && configuration.getUnparseableEntries();
101 }
102
103 /**
104 * 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
105 * before being added to the array.
106 *
107 * @param filter FTPFileFilter, must not be {@code null}.
108 * @return a list of FTPFile objects containing the whole list of files returned by the server as read by this object's parser.
109 * <p>
110 * <strong> NOTE:</strong> This array may contain null members if any of the individual file listings failed to parse. The caller should check each
111 * entry for null before referencing it, or use a filter such as {@link FTPFileFilters#NON_NULL} which does not allow null entries.
112 * @since 3.9.0
113 */
114 public List<FTPFile> getFileList(final FTPFileFilter filter) {
115 return entries.stream().map(e -> {
116 final FTPFile file = parser.parseFTPEntry(e);
117 return file == null && saveUnparseableEntries ? new FTPFile(e) : file;
118 }).filter(filter::accept).collect(Collectors.toList());
119 }
120
121 /**
122 * Gets an array of FTPFile objects containing the whole list of files returned by the server as read by this object's parser.
123 *
124 * @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
125 * be null
126 * @throws IOException - not ever thrown, may be removed in a later release
127 */
128 // TODO remove; not actually thrown
129 public FTPFile[] getFiles() throws IOException {
130 return getFiles(FTPFileFilters.NON_NULL);
131 }
132
133 /**
134 * 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
135 * before being added to the array.
136 *
137 * @param filter FTPFileFilter, must not be {@code null}.
138 * @return an array of FTPFile objects containing the whole list of files returned by the server as read by this object's parser.
139 * <p>
140 * <strong> NOTE:</strong> This array may contain null members if any of the individual file listings failed to parse. The caller should check each
141 * entry for null before referencing it, or use a filter such as {@link FTPFileFilters#NON_NULL} which does not allow null entries.
142 * @throws IOException - not ever thrown, may be removed in a later release
143 * @since 2.2
144 */
145 // TODO remove; not actually thrown
146 public FTPFile[] getFiles(final FTPFileFilter filter) throws IOException {
147 return getFileList(filter).toArray(EMPTY_FTP_FILE_ARRAY);
148 }
149
150 /**
151 * Gets an array of at most {@code quantityRequested} FTPFile objects starting at this object's internal iterator's current position. If fewer than
152 * {@code quantityRequested} such elements are available, the returned array will have a length equal to the number of entries at and after the current
153 * position. If no such entries are found, this array will have a length of 0.
154 *
155 * 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.
156 *
157 * @param quantityRequested the maximum number of entries we want to get.
158 * @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
159 * number of elements which exist in the list at and after its current position.
160 * <p>
161 * <strong> NOTE:</strong> This array may contain null members if any of the individual file listings failed to parse. The caller should check each
162 * entry for null before referencing it.
163 */
164 public FTPFile[] getNext(final int quantityRequested) {
165 final List<FTPFile> tmpResults = new LinkedList<>();
166 int count = quantityRequested;
167 while (count > 0 && internalIterator.hasNext()) {
168 final String entry = internalIterator.next();
169 FTPFile temp = parser.parseFTPEntry(entry);
170 if (temp == null && saveUnparseableEntries) {
171 temp = new FTPFile(entry);
172 }
173 tmpResults.add(temp);
174 count--;
175 }
176 return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY);
177
178 }
179
180 /**
181 * Gets an array of at most {@code quantityRequested} FTPFile objects starting at this object's internal iterator's current position, and working back
182 * toward the beginning.
183 *
184 * 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
185 * the current position. If no such entries are found, this array will have a length of 0.
186 *
187 * 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.
188 *
189 * @param quantityRequested the maximum number of entries we want to get.
190 * @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
191 * 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
192 * reversed).
193 * <p>
194 * <strong> NOTE:</strong> This array may contain null members if any of the individual file listings failed to parse. The caller should check each
195 * entry for null before referencing it.
196 */
197 public FTPFile[] getPrevious(final int quantityRequested) {
198 final List<FTPFile> tmpResults = new LinkedList<>();
199 int count = quantityRequested;
200 while (count > 0 && internalIterator.hasPrevious()) {
201 final String entry = internalIterator.previous();
202 FTPFile temp = parser.parseFTPEntry(entry);
203 if (temp == null && saveUnparseableEntries) {
204 temp = new FTPFile(entry);
205 }
206 tmpResults.add(0, temp);
207 count--;
208 }
209 return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY);
210 }
211
212 /**
213 * convenience method to allow clients to know whether this object's internal iterator's current position is at the end of the list.
214 *
215 * @return true if internal iterator is not at end of list, false otherwise.
216 */
217 public boolean hasNext() {
218 return internalIterator.hasNext();
219 }
220
221 /**
222 * convenience method to allow clients to know whether this object's internal iterator's current position is at the beginning of the list.
223 *
224 * @return true if internal iterator is not at beginning of list, false otherwise.
225 */
226 public boolean hasPrevious() {
227 return internalIterator.hasPrevious();
228 }
229
230 /**
231 * Internal method for reading (and closing) the input into the {@code entries} list. After this method has completed, {@code entries} will contain a
232 * collection of entries (as defined by {@code FTPFileEntryParser.readNextEntry()}), but this may contain various non-entry preliminary lines from the
233 * server output, duplicates, and other data that will not be part of the final listing.
234 *
235 * @param inputStream The socket stream on which the input will be read.
236 * @param charsetName The encoding to use.
237 * @throws IOException thrown on any failure to read the stream
238 */
239 private void read(final InputStream inputStream, final String charsetName) throws IOException {
240 try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charsets.toCharset(charsetName)))) {
241 String line = parser.readNextEntry(reader);
242 while (line != null) {
243 entries.add(line);
244 line = parser.readNextEntry(reader);
245 }
246 }
247 }
248
249 /**
250 * Do not use.
251 *
252 * @param inputStream the stream from which to read
253 * @throws IOException on error
254 * @deprecated use {@link #readServerList(InputStream, String)} instead
255 */
256 @Deprecated
257 public void readServerList(final InputStream inputStream) throws IOException {
258 readServerList(inputStream, null);
259 }
260
261 /**
262 * 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
263 * list of unparsed entries (Strings) each referring to a unique file on the server.
264 *
265 * @param inputStream input stream provided by the server socket.
266 * @param charsetName the encoding to be used for reading the stream
267 * @throws IOException thrown on any failure to read from the sever.
268 */
269 public void readServerList(final InputStream inputStream, final String charsetName) throws IOException {
270 entries = new LinkedList<>();
271 read(inputStream, charsetName);
272 parser.preParse(entries);
273 resetIterator();
274 }
275
276 /**
277 * resets this object's internal iterator to the beginning of the list.
278 */
279 public void resetIterator() {
280 internalIterator = entries.listIterator();
281 }
282
283 }