001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.net.ftp; 019 020import java.io.BufferedReader; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.InputStreamReader; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.ListIterator; 027import java.util.stream.Collectors; 028 029import org.apache.commons.io.Charsets; 030 031 032/** 033 * This class handles the entire process of parsing a listing of file entries from the server. 034 * <p> 035 * This object defines a two-part parsing mechanism. 036 * </p> 037 * <p> 038 * 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 039 * 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 040 * 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 041 * feed and {@code preParse()} - which handles removal of extraneous matter such as the preliminary lines of a listing, removal of duplicates on versioning 042 * systems, etc. 043 * </p> 044 * <p> 045 * 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 046 * 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 047 * "paged" output of less than the whole list at one time, or by calling the {@code getFiles()} method to return the entire list. 048 * </p> 049 * <p> 050 * Examples: 051 * </p> 052 * <p> 053 * Paged access: 054 * </p> 055 * <pre> 056 * FTPClient f = FTPClient(); 057 * f.connect(server); 058 * f.login(user, password); 059 * FTPListParseEngine engine = f.initiateListParsing(directory); 060 * 061 * while (engine.hasNext()) { 062 * FTPFile[] files = engine.getNext(25); // "page size" you want 063 * // do whatever you want with these files, display them, etc. 064 * // expensive FTPFile objects not created until needed. 065 * } 066 * </pre> 067 * <p> 068 * For unpaged access, simply use FTPClient.listFiles(). That method uses this class transparently. 069 * </p> 070 */ 071public class FTPListParseEngine { 072 /** 073 * An empty immutable {@code FTPFile} array. 074 */ 075 private static final FTPFile[] EMPTY_FTP_FILE_ARRAY = {}; 076 private List<String> entries = new LinkedList<>(); 077 078 private ListIterator<String> internalIterator = entries.listIterator(); 079 private final FTPFileEntryParser parser; 080 081 // Should invalid files (parse failures) be allowed? 082 private final boolean saveUnparseableEntries; 083 084 /** 085 * Constructs a new instance. 086 * 087 * @param parser How to parse file entries. 088 */ 089 public FTPListParseEngine(final FTPFileEntryParser parser) { 090 this(parser, null); 091 } 092 093 /** 094 * Intended for use by FTPClient only 095 * 096 * @since 3.4 097 */ 098 FTPListParseEngine(final FTPFileEntryParser parser, final FTPClientConfig configuration) { 099 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}