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 /** 074 * An empty immutable {@code FTPFile} array. 075 */ 076 private static final FTPFile[] EMPTY_FTP_FILE_ARRAY = {}; 077 private List<String> entries = new LinkedList<>(); 078 079 private ListIterator<String> internalIterator = entries.listIterator(); 080 private final FTPFileEntryParser parser; 081 082 // Should invalid files (parse failures) be allowed? 083 private final boolean saveUnparseableEntries; 084 085 /** 086 * Constructs a new instance. 087 * 088 * @param parser How to parse file entries. 089 */ 090 public FTPListParseEngine(final FTPFileEntryParser parser) { 091 this(parser, null); 092 } 093 094 /** 095 * Intended for use by FTPClient only 096 * 097 * @since 3.4 098 */ 099 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}