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.parser;
019
020import java.io.File;
021import java.text.ParseException;
022import java.util.Locale;
023
024import org.apache.commons.net.ftp.FTPClientConfig;
025import org.apache.commons.net.ftp.FTPFile;
026
027/**
028 * OS400 (IBM i) FTP entry parser.
029 *
030 * <p>
031 * Below are examples of the format of FTP entries on an IBM i.
032 * </p>
033 *
034 * <strong>
035 * Example *FILE/*MEM FTP entries, when the current
036 * working directory is a file of file system QSYS:
037 * </strong>
038 *
039 * <pre>
040 * $ cwd /qsys.lib/rpgunit.lib/rpgunitc1.file
041 *   250-NAMEFMT set to 1.
042 *   250 "/QSYS.LIB/RPGUNIT.LIB/RPGUNITC1.FILE" is current directory.
043 * $ dir
044 *   227 Entering Passive Mode (10,200,36,33,40,249).
045 *   125 List started.
046 *   QPGMR          135168 22.06.13 13:18:19 *FILE
047 *   QPGMR                                   *MEM       MKCMD.MBR
048 *   QPGMR                                   *MEM       RUCALLTST.MBR
049 *   QPGMR                                   *MEM       RUCMDHLP.MBR
050 *   QPGMR                                   *MEM       RUCRTTST.MBR
051 *   250 List completed.
052 * </pre>
053 *
054 * <strong>
055 * Example *FILE entry of an OS/400 save file:
056 * </strong>
057 *
058 * <pre>
059 * $ cwd /qsys.lib/rpgunit.lib
060 *   250 "/QSYS.LIB/RPGUNIT.LIB" is current library.
061 * $ dir rpgunit.file
062 *   227 Entering Passive Mode (10,200,36,33,188,106).
063 *   125 List started.
064 *   QPGMR        16347136 29.06.13 15:45:09 *FILE      RPGUNIT.SAVF
065 *   250 List completed.
066 * </pre>
067 *
068 * <strong>
069 * Example *STMF/*DIR FTP entries, when the
070 * current working directory is in file system "root":
071 * </strong>
072 *
073 * <pre>
074 * $ cwd /home/raddatz
075 *   250 "/home/raddatz" is current directory.
076 * $ dir test*
077 *   227 Entering Passive Mode (10,200,36,33,200,189).
078 *   125 List started.
079 *   RADDATZ           200 21.05.11 12:31:18 *STMF      TEST_RG_02_CRLF.properties
080 *   RADDATZ           187 08.05.11 12:31:40 *STMF      TEST_RG_02_LF.properties
081 *   RADDATZ           187 08.05.11 12:31:52 *STMF      TEST_RG_02_CR.properties
082 *   RADDATZ          8192 04.07.13 09:04:14 *DIR       testDir1/
083 *   RADDATZ          8192 04.07.13 09:04:17 *DIR       testDir2/
084 *   250 List completed.
085 * </pre>
086 *
087 * <strong>
088 * Example 1, using ANT to list specific members of a file:
089 * </strong>
090 *
091 * <pre>
092 *      &lt;echo/&gt;
093 *      &lt;echo&gt;Listing members of a file:&lt;/echo&gt;
094 *
095 *      &lt;ftp action="list"
096 *           server="${ftp.server}"
097 *           userid="${ftp.user}"
098 *           password="${ftp.password}"
099 *           binary="false"
100 *           verbose="true"
101 *           remotedir="/QSYS.LIB/RPGUNIT.LIB/RPGUNITY1.FILE"
102 *           systemTypeKey="OS/400"
103 *           listing="ftp-listing.txt"
104 *           &gt;
105 *          &lt;fileset dir="./i5-downloads-file" casesensitive="false"&gt;
106 *              &lt;include name="run*.mbr" /&gt;
107 *          &lt;/fileset&gt;
108 *      &lt;/ftp&gt;
109 * </pre>
110 *
111 * <strong>Output:</strong>
112 *
113 * <pre>
114 *   [echo] Listing members of a file:
115 *    [ftp] listing files
116 *    [ftp] listing RUN.MBR
117 *    [ftp] listing RUNNER.MBR
118 *    [ftp] listing RUNNERBND.MBR
119 *    [ftp] 3 files listed
120 * </pre>
121 *
122 * <strong>
123 * Example 2, using ANT to list specific members of all files of a library:
124 * </strong>
125 *
126 * <pre>
127 *      &lt;echo/&gt;
128 *      &lt;echo&gt;Listing members of all files of a library:&lt;/echo&gt;
129 *
130 *      &lt;ftp action="list"
131 *           server="${ftp.server}"
132 *           userid="${ftp.user}"
133 *           password="${ftp.password}"
134 *           binary="false"
135 *           verbose="true"
136 *           remotedir="/QSYS.LIB/RPGUNIT.LIB"
137 *           systemTypeKey="OS/400"
138 *           listing="ftp-listing.txt"
139 *           &gt;
140 *          &lt;fileset dir="./i5-downloads-lib" casesensitive="false"&gt;
141 *              &lt;include name="**\run*.mbr" /&gt;
142 *          &lt;/fileset&gt;
143 *      &lt;/ftp&gt;
144 * </pre>
145 *
146 * <strong>Output:</strong>
147 *
148 * <pre>
149 *   [echo] Listing members of all files of a library:
150 *    [ftp] listing files
151 *    [ftp] listing RPGUNIT1.FILE\RUN.MBR
152 *    [ftp] listing RPGUNIT1.FILE\RUNRMT.MBR
153 *    [ftp] listing RPGUNITT1.FILE\RUNT.MBR
154 *    [ftp] listing RPGUNITY1.FILE\RUN.MBR
155 *    [ftp] listing RPGUNITY1.FILE\RUNNER.MBR
156 *    [ftp] listing RPGUNITY1.FILE\RUNNERBND.MBR
157 *    [ftp] 6 files listed
158 * </pre>
159 *
160 * <strong>
161 * Example 3, using ANT to download specific members of a file:
162 * </strong>
163 *
164 * <pre>
165 *      &lt;echo/&gt;
166 *      &lt;echo&gt;Downloading members of a file:&lt;/echo&gt;
167 *
168 *      &lt;ftp action="get"
169 *           server="${ftp.server}"
170 *           userid="${ftp.user}"
171 *           password="${ftp.password}"
172 *           binary="false"
173 *           verbose="true"
174 *           remotedir="/QSYS.LIB/RPGUNIT.LIB/RPGUNITY1.FILE"
175 *           systemTypeKey="OS/400"
176 *           &gt;
177 *          &lt;fileset dir="./i5-downloads-file" casesensitive="false"&gt;
178 *              &lt;include name="run*.mbr" /&gt;
179 *          &lt;/fileset&gt;
180 *      &lt;/ftp&gt;
181 * </pre>
182 *
183 * <strong>Output:</strong>
184 *
185 * <pre>
186 *   [echo] Downloading members of a file:
187 *    [ftp] getting files
188 *    [ftp] transferring RUN.MBR to C:\workspaces\rdp_080\workspace\ANT - FTP\i5-downloads-file\RUN.MBR
189 *    [ftp] transferring RUNNER.MBR to C:\workspaces\rdp_080\workspace\ANT - FTP\i5-downloads-file\RUNNER.MBR
190 *    [ftp] transferring RUNNERBND.MBR to C:\workspaces\rdp_080\workspace\ANT - FTP\i5-downloads-file\RUNNERBND.MBR
191 *    [ftp] 3 files retrieved
192 * </pre>
193 *
194 * <strong>
195 * Example 4, using ANT to download specific members of all files of a library:
196 * </strong>
197 *
198 * <pre>
199 *      &lt;echo/&gt;
200 *      &lt;echo&gt;Downloading members of all files of a library:&lt;/echo&gt;
201 *
202 *      &lt;ftp action="get"
203 *           server="${ftp.server}"
204 *           userid="${ftp.user}"
205 *           password="${ftp.password}"
206 *           binary="false"
207 *           verbose="true"
208 *           remotedir="/QSYS.LIB/RPGUNIT.LIB"
209 *           systemTypeKey="OS/400"
210 *           &gt;
211 *          &lt;fileset dir="./i5-downloads-lib" casesensitive="false"&gt;
212 *              &lt;include name="**\run*.mbr" /&gt;
213 *          &lt;/fileset&gt;
214 *      &lt;/ftp&gt;
215 * </pre>
216 *
217 * <strong>Output:</strong>
218 *
219 * <pre>
220 *   [echo] Downloading members of all files of a library:
221 *    [ftp] getting files
222 *    [ftp] transferring RPGUNIT1.FILE\RUN.MBR to C:\work\rdp_080\space\ANT - FTP\i5-downloads\RPGUNIT1.FILE\RUN.MBR
223 *    [ftp] transferring RPGUNIT1.FILE\RUNRMT.MBR to C:\work\rdp_080\space\ANT - FTP\i5-downloads\RPGUNIT1.FILE\RUNRMT.MBR
224 *    [ftp] transferring RPGUNITT1.FILE\RUNT.MBR to C:\work\rdp_080\space\ANT - FTP\i5-downloads\RPGUNITT1.FILE\RUNT.MBR
225 *    [ftp] transferring RPGUNITY1.FILE\RUN.MBR to C:\work\rdp_080\space\ANT - FTP\i5-downloads\RPGUNITY1.FILE\RUN.MBR
226 *    [ftp] transferring RPGUNITY1.FILE\RUNNER.MBR to C:\work\rdp_080\space\ANT - FTP\i5-downloads\RPGUNITY1.FILE\RUNNER.MBR
227 *    [ftp] transferring RPGUNITY1.FILE\RUNNERBND.MBR to C:\work\rdp_080\space\ANT - FTP\i5-downloads\RPGUNITY1.FILE\RUNNERBND.MBR
228 *    [ftp] 6 files retrieved
229 * </pre>
230 *
231 * <strong>
232 * Example 5, using ANT to download a save file of a library:
233 * </strong>
234 *
235 * <pre>
236 *      &lt;ftp action="get"
237 *           server="${ftp.server}"
238 *           userid="${ftp.user}"
239 *           password="${ftp.password}"
240 *           binary="true"
241 *           verbose="true"
242 *           remotedir="/QSYS.LIB/RPGUNIT.LIB"
243 *           systemTypeKey="OS/400"
244 *           &gt;
245 *        &lt;fileset dir="./i5-downloads-savf" casesensitive="false"&gt;
246 *            &lt;include name="RPGUNIT.SAVF" /&gt;
247 *        &lt;/fileset&gt;
248 *      &lt;/ftp&gt;
249 * </pre>
250 *
251 * <strong>Output:</strong>
252 *
253 * <pre>
254 *   [echo] Downloading save file:
255 *    [ftp] getting files
256 *    [ftp] transferring RPGUNIT.SAVF to C:\workspaces\rdp_080\workspace\net-Test\i5-downloads-lib\RPGUNIT.SAVF
257 *    [ftp] 1 files retrieved
258 * </pre>
259 */
260public class OS400FTPEntryParser extends ConfigurableFTPFileEntryParserImpl {
261    private static final String DEFAULT_DATE_FORMAT = "yy/MM/dd HH:mm:ss"; // 01/11/09 12:30:24
262
263    private static final String REGEX = "(\\S+)\\s+" // user
264            + "(?:(\\d+)\\s+)?" // size, empty for members
265            + "(?:(\\S+)\\s+(\\S+)\\s+)?" // date stuff, empty for members
266            + "(\\*STMF|\\*DIR|\\*FILE|\\*MEM)\\s+" // *STMF/*DIR/*FILE/*MEM
267            + "((\\S+\\s*)+)?"; // file name, missing, when CWD is a *FILE
268
269    /**
270     * Constructs a new instance.
271     *
272     * @throws IllegalArgumentException Thrown if the regular expression is unparseable. Should not be seen under normal conditions. If it is seen, this is a
273     *                                  sign that {@code REGEX} is not a valid regular expression.
274     */
275    public OS400FTPEntryParser() {
276        this(null);
277    }
278
279    /**
280     * Constructs a new instance with something other than the default configuration.
281     *
282     * @param config The {@link FTPClientConfig configuration} object used to configure this parser.
283     * @throws IllegalArgumentException Thrown if the regular expression is unparseable. Should not be seen under normal conditions. If it is seen, this is a
284     *                                  sign that {@code REGEX} is not a valid regular expression.
285     * @since 1.4
286     */
287    public OS400FTPEntryParser(final FTPClientConfig config) {
288        super(REGEX);
289        configure(config);
290    }
291
292    /**
293     * Gets a new default configuration to be used when this class is instantiated without a {@link FTPClientConfig FTPClientConfig} parameter being specified.
294     *
295     * @return the default configuration for this parser.
296     */
297    @Override
298    protected FTPClientConfig getDefaultConfiguration() {
299        return new FTPClientConfig(FTPClientConfig.SYST_OS400, DEFAULT_DATE_FORMAT, null);
300    }
301
302    /**
303     *
304     * @param string String value that is checked for {@code null} or empty.
305     * @return {@code true} for {@code null} or empty values, else {@code false}.
306     */
307    private boolean isNullOrEmpty(final String string) {
308        return string == null || string.isEmpty();
309    }
310
311    @Override
312    public FTPFile parseFTPEntry(final String entry) {
313
314        final FTPFile file = new FTPFile();
315        file.setRawListing(entry);
316        final int type;
317
318        if (matches(entry)) {
319            final String usr = group(1);
320            final String fileSize = group(2);
321            String datestr = "";
322            if (!isNullOrEmpty(group(3)) || !isNullOrEmpty(group(4))) {
323                datestr = group(3) + " " + group(4);
324            }
325            final String typeStr = group(5);
326            String name = group(6);
327
328            boolean mustScanForPathSeparator = true;
329
330            try {
331                file.setTimestamp(super.parseTimestamp(datestr));
332            } catch (final ParseException e) {
333                // intentionally do nothing
334            }
335
336            if (typeStr.equalsIgnoreCase("*STMF")) {
337                type = FTPFile.FILE_TYPE;
338                if (isNullOrEmpty(fileSize) || isNullOrEmpty(name)) {
339                    return null;
340                }
341            } else if (typeStr.equalsIgnoreCase("*DIR")) {
342                type = FTPFile.DIRECTORY_TYPE;
343                if (isNullOrEmpty(fileSize) || isNullOrEmpty(name)) {
344                    return null;
345                }
346            } else if (typeStr.equalsIgnoreCase("*FILE")) {
347                // File, defines the structure of the data (columns of a row)
348                // but the data is stored in one or more members. Typically, a
349                // source file contains multiple members whereas it is
350                // recommended (but not enforced) to use one member per data
351                // file.
352                // Save files are a special type of files which are used
353                // to save objects, e.g. for backups.
354                if (name == null || !name.toUpperCase(Locale.ROOT).endsWith(".SAVF")) {
355                    return null;
356                }
357                mustScanForPathSeparator = false;
358                type = FTPFile.FILE_TYPE;
359            } else if (typeStr.equalsIgnoreCase("*MEM")) {
360                mustScanForPathSeparator = false;
361                type = FTPFile.FILE_TYPE;
362
363                if (isNullOrEmpty(name) || !(isNullOrEmpty(fileSize) && isNullOrEmpty(datestr))) {
364                    return null;
365                }
366
367                // Quick and dirty bug fix to make SelectorUtils work.
368                // Class SelectorUtils uses 'File.separator' to splitt
369                // a given path into pieces. But actually it had to
370                // use the separator of the FTP server, which is a forward
371                // slash in case of an AS/400.
372                name = name.replace('/', File.separatorChar);
373            } else {
374                type = FTPFile.UNKNOWN_TYPE;
375            }
376
377            file.setType(type);
378
379            file.setUser(usr);
380
381            try {
382                file.setSize(Long.parseLong(fileSize));
383            } catch (final NumberFormatException e) {
384                // intentionally do nothing
385            }
386
387            if (name.endsWith("/")) {
388                name = name.substring(0, name.length() - 1);
389            }
390            if (mustScanForPathSeparator) {
391                final int pos = name.lastIndexOf('/');
392                if (pos > -1) {
393                    name = name.substring(pos + 1);
394                }
395            }
396
397            file.setName(name);
398
399            return file;
400        }
401        return null;
402    }
403
404}