View Javadoc
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.parser;
19  
20  import java.text.ParseException;
21  import java.util.List;
22  
23  import org.apache.commons.net.ftp.Configurable;
24  import org.apache.commons.net.ftp.FTPClientConfig;
25  import org.apache.commons.net.ftp.FTPFile;
26  import org.apache.commons.net.ftp.FTPFileEntryParser;
27  
28  /**
29   * Implements {@link FTPFileEntryParser} and {@link Configurable} for IBM zOS/MVS Systems.
30   *
31   * @see FTPFileEntryParser Usage instructions.
32   */
33  public class MVSFTPEntryParser extends ConfigurableFTPFileEntryParserImpl {
34  
35      static final int UNKNOWN_LIST_TYPE = -1;
36      static final int FILE_LIST_TYPE = 0;
37      static final int MEMBER_LIST_TYPE = 1;
38      static final int UNIX_LIST_TYPE = 2;
39      static final int JES_LEVEL_1_LIST_TYPE = 3;
40      static final int JES_LEVEL_2_LIST_TYPE = 4;
41  
42      /**
43       * Dates are ignored for file lists, but are used for member lists where possible
44       */
45      static final String DEFAULT_DATE_FORMAT = "yyyy/MM/dd HH:mm"; // 2001/09/18
46                                                                    // 13:52
47  
48      /**
49       * Matches these entries:
50       *
51       * <pre>
52       *  Volume Unit    Referred Ext Used Recfm Lrecl BlkSz Dsorg Dsname
53       *  B10142 3390   2006/03/20  2   31  F       80    80  PS   MDI.OKL.WORK
54       * </pre>
55       *
56       * @see <a href= "https://www.ibm.com/support/knowledgecenter/zosbasics/com.ibm.zos.zconcepts/zconcepts_159.htm">Data set record formats</a>
57       */
58      static final String FILE_LIST_REGEX = "\\S+\\s+" + // volume
59                                                         // ignored
60              "\\S+\\s+" + // unit - ignored
61              "\\S+\\s+" + // access date - ignored
62              "\\S+\\s+" + // extents -ignored
63              // If the values are too large, the fields may be merged (NET-639)
64              "(?:\\S+\\s+)?" + // used - ignored
65              "\\S+\\s+" + // recfm - ignored
66              "\\S+\\s+" + // logical record length -ignored
67              "\\S+\\s+" + // block size - ignored
68              "(PS|PO|PO-E)\\s+" + // Dataset organization. Many exist
69              // but only support: PS, PO, PO-E
70              "(\\S+)\\s*"; // Dataset Name (file name)
71  
72      /**
73       * Matches these entries:
74       *
75       * <pre>
76       *   Name      VV.MM   Created       Changed      Size  Init   Mod   Id
77       *   TBSHELF   01.03 2002/09/12 2002/10/11 09:37    11    11     0 KIL001
78       * </pre>
79       */
80      static final String MEMBER_LIST_REGEX = "(\\S+)\\s+" + // name
81              "\\S+\\s+" + // version, modification (ignored)
82              "\\S+\\s+" + // create date (ignored)
83              "(\\S+)\\s+" + // modification date
84              "(\\S+)\\s+" + // modification time
85              "\\S+\\s+" + // size in lines (ignored)
86              "\\S+\\s+" + // size in lines at creation(ignored)
87              "\\S+\\s+" + // lines modified (ignored)
88              "\\S+\\s*"; // id of user who modified (ignored)
89  
90      /**
91       * Matches these entries, note: no header:
92       *
93       * <pre>
94       *   IBMUSER1  JOB01906  OUTPUT    3 Spool Files
95       *   012345678901234567890123456789012345678901234
96       *             1         2         3         4
97       * </pre>
98       */
99      static final String JES_LEVEL_1_LIST_REGEX = "(\\S+)\\s+" + // job name ignored
100             "(\\S+)\\s+" + // job number
101             "(\\S+)\\s+" + // job status (OUTPUT,INPUT,ACTIVE)
102             "(\\S+)\\s+" + // number of spool files
103             "(\\S+)\\s+" + // Text "Spool" ignored
104             "(\\S+)\\s*" // Text "Files" ignored
105     ;
106 
107     /**
108      * JES INTERFACE LEVEL 2 parser Matches these entries:
109      *
110      * <pre>
111      * JOBNAME  JOBID    OWNER    STATUS CLASS
112      * IBMUSER1 JOB01906 IBMUSER  OUTPUT A        RC=0000 3 spool files
113      * IBMUSER  TSU01830 IBMUSER  OUTPUT TSU      ABEND=522 3 spool files
114      * </pre>
115      *
116      * Sample output from FTP session:
117      *
118      * <pre>
119      * ftp> quote site filetype=jes
120      * 200 SITE command was accepted
121      * ftp> ls
122      * 200 Port request OK.
123      * 125 List started OK for JESJOBNAME=IBMUSER*, JESSTATUS=ALL and JESOWNER=IBMUSER
124      * JOBNAME  JOBID    OWNER    STATUS CLASS
125      * IBMUSER1 JOB01906 IBMUSER  OUTPUT A        RC=0000 3 spool files
126      * IBMUSER  TSU01830 IBMUSER  OUTPUT TSU      ABEND=522 3 spool files
127      * 250 List completed successfully.
128      * ftp> ls job01906
129      * 200 Port request OK.
130      * 125 List started OK for JESJOBNAME=IBMUSER*, JESSTATUS=ALL and JESOWNER=IBMUSER
131      * JOBNAME  JOBID    OWNER    STATUS CLASS
132      * IBMUSER1 JOB01906 IBMUSER  OUTPUT A        RC=0000
133      * --------
134      * ID  STEPNAME PROCSTEP C DDNAME   BYTE-COUNT
135      * 001 JES2              A JESMSGLG       858
136      * 002 JES2              A JESJCL         128
137      * 003 JES2              A JESYSMSG       443
138      * 3 spool files
139      * 250 List completed successfully.
140      * </pre>
141      */
142 
143     static final String JES_LEVEL_2_LIST_REGEX = "(\\S+)\\s+" + // job name ignored
144             "(\\S+)\\s+" + // job number
145             "(\\S+)\\s+" + // owner ignored
146             "(\\S+)\\s+" + // job status (OUTPUT,INPUT,ACTIVE) ignored
147             "(\\S+)\\s+" + // job class ignored
148             "(\\S+).*" // rest ignored
149     ;
150 
151     private int isType = UNKNOWN_LIST_TYPE;
152 
153     /**
154      * Fallback parser for Unix-style listings
155      */
156     private UnixFTPEntryParser unixFTPEntryParser;
157 
158     /*
159      * --------------------------------------------------------------------- Very brief and incomplete description of the zOS/MVS-file system. (Note: "zOS" is
160      * the operating system on the mainframe, and is the new name for MVS)
161      *
162      * The file system on the mainframe does not have hierarchical structure as for example the Unix file system. For a more comprehensive description,
163      * please refer to the IBM manuals
164      *
165      * @LINK: https://publibfp.boulder.ibm.com/cgi-bin/bookmgr/BOOKS/dgt2d440/CONTENTS
166      *
167      *
168      * Dataset names =============
169      *
170      * A dataset name consist of a number of qualifiers separated by '.', each qualifier can be at most 8 characters, and the total length of a dataset can be
171      * max 44 characters including the dots.
172      *
173      *
174      * Dataset organization ====================
175      *
176      * A dataset represents a piece of storage allocated on one or more disks. The structure of the storage is described with the field dataset organization
177      * (DSORG). There are a number of dataset organizations, but only two are usable for FTP transfer.
178      *
179      * DSORG: PS: sequential, or flat file PO: partitioned dataset PO-E: extended partitioned dataset
180      *
181      * The PS file is just a flat file, as you would find it on the Unix file system.
182      *
183      * The PO and PO-E files, can be compared to a single level directory structure. A PO file consist of a number of dataset members, or files if you will. It
184      * is possible to CD into the file, and to retrieve the individual members.
185      *
186      *
187      * Dataset record format =====================
188      *
189      * The physical layout of the dataset is described on the dataset itself. There are a number of record formats (RECFM), but just a few is relevant for the
190      * FTP transfer.
191      *
192      * Any one beginning with either F or V can safely be used by FTP transfer. All others should only be used with great care. F means a fixed number of
193      * records per allocated storage, and V means a variable number of records.
194      *
195      *
196      * Other notes ===========
197      *
198      * The file system supports automatically backup and retrieval of datasets. If a file is backed up, the ftp LIST command will return: ARCIVE Not Direct
199      * Access Device KJ.IOP998.ERROR.PL.UNITTEST
200      *
201      *
202      * Implementation notes ====================
203      *
204      * Only datasets that have dsorg PS, PO or PO-E and have recfm beginning with F or V or U, is fully parsed.
205      *
206      * The following fields in FTPFile is used: FTPFile.Rawlisting: Always set. FTPFile.Type: DIRECTORY_TYPE or FILE_TYPE or UNKNOWN FTPFile.Name: name
207      * FTPFile.Timestamp: change time or null
208      *
209      *
210      *
211      * Additional information ======================
212      *
213      * The MVS ftp server supports a number of features via the FTP interface. The features are controlled with the FTP command quote site
214      * filetype=<SEQ|JES|DB2> SEQ is the default and used for normal file transfer JES is used to interact with the Job Entry Subsystem (JES) similar to a job
215      * scheduler DB2 is used to interact with a DB2 subsystem
216      *
217      * This parser supports SEQ and JES.
218      */
219 
220     /**
221      * The sole constructor for a MVSFTPEntryParser object.
222      */
223     public MVSFTPEntryParser() {
224         super(""); // note the regex is set in preParse.
225         super.configure(null); // configure parser with default configurations
226     }
227 
228     @Override
229     protected FTPClientConfig getDefaultConfiguration() {
230         return new FTPClientConfig(FTPClientConfig.SYST_MVS, DEFAULT_DATE_FORMAT, null);
231     }
232 
233     /**
234      * Parses entries representing a dataset list.
235      * <pre>
236      * Format of ZOS/MVS file list: 1 2 3 4 5 6 7 8 9 10
237      * Volume Unit Referred Ext Used Recfm Lrecl BlkSz Dsorg Dsname
238      * B10142 3390 2006/03/20 2 31 F 80 80 PS MDI.OKL.WORK
239      * ARCIVE Not Direct Access Device KJ.IOP998.ERROR.PL.UNITTEST
240      * B1N231 3390 2006/03/20 1 15 VB 256 27998 PO PLU
241      * B1N231 3390 2006/03/20 1 15 VB 256 27998 PO-E PLB
242      * Migrated                                                HLQ.DATASET.NAME
243      * </pre>
244      * <pre>
245      * ----------------------------------- Group within Regex [1] Volume [2] Unit [3] Referred [4] Ext: number of extents [5] Used [6] Recfm: Record format [7]
246      * Lrecl: Logical record length [8] BlkSz: Block size [9] Dsorg: Dataset organization. Many exists but only support: PS, PO, PO-E [10] Dsname: Dataset name
247      * </pre>
248      *
249      * @param entry zosDirectoryEntry
250      * @return null: entry was not parsed.
251      */
252     private FTPFile parseFileList(final String entry) {
253         if (matches(entry)) {
254             final FTPFile file = new FTPFile();
255             file.setRawListing(entry);
256             final String name = group(2);
257             final String dsorg = group(1);
258             file.setName(name);
259 
260             // DSORG
261             if ("PS".equals(dsorg)) {
262                 file.setType(FTPFile.FILE_TYPE);
263             } else if ("PO".equals(dsorg) || "PO-E".equals(dsorg)) {
264                 // regex already ruled out anything other than PO or PO-E
265                 file.setType(FTPFile.DIRECTORY_TYPE);
266             } else {
267                 return null;
268             }
269 
270             return file;
271         }
272 
273         final boolean migrated = entry.startsWith("Migrated");
274         if (migrated || entry.startsWith("ARCIVE")) {
275             // Type of file is unknown for migrated datasets
276             final FTPFile file = new FTPFile();
277             file.setRawListing(entry);
278             file.setType(FTPFile.UNKNOWN_TYPE);
279             file.setName(entry.split("\\s+")[migrated ? 1 : 5]);
280             return file;
281         }
282 
283         return null;
284     }
285 
286     /**
287      * Parses a line of a z/OS - MVS FTP server file listing and converts it into a usable format in the form of an {@code FTPFile} instance. If the
288      * file listing line doesn't describe a file, then {@code null} is returned. Otherwise, a {@code FTPFile} instance representing the file is
289      * returned.
290      *
291      * @param entry A line of text from the file listing
292      * @return An FTPFile instance corresponding to the supplied entry
293      */
294     @Override
295     public FTPFile parseFTPEntry(final String entry) {
296         switch (isType) {
297         case FILE_LIST_TYPE:
298             return parseFileList(entry);
299         case MEMBER_LIST_TYPE:
300             return parseMemberList(entry);
301         case UNIX_LIST_TYPE:
302             return unixFTPEntryParser.parseFTPEntry(entry);
303         case JES_LEVEL_1_LIST_TYPE:
304             return parseJeslevel1List(entry);
305         case JES_LEVEL_2_LIST_TYPE:
306             return parseJeslevel2List(entry);
307         default:
308             break;
309         }
310 
311         return null;
312     }
313 
314     /**
315      * Matches these entries, note: no header:
316      *
317      * <pre>
318      * [1]      [2]      [3]   [4] [5]
319      * IBMUSER1 JOB01906 OUTPUT 3 Spool Files
320      * 012345678901234567890123456789012345678901234
321      *           1         2         3         4
322      * -------------------------------------------
323      * Group in regex
324      * [1] Job name
325      * [2] Job number
326      * [3] Job status (INPUT,ACTIVE,OUTPUT)
327      * [4] Number of sysout files
328      * [5] The string "Spool Files"
329      * </pre>
330      *
331      * @param entry zosDirectoryEntry
332      * @return null: entry was not parsed.
333      */
334     private FTPFile parseJeslevel1List(final String entry) {
335         return parseJeslevelList(entry, 3);
336     }
337 
338     /**
339      * Matches these entries:
340      *
341      * <pre>
342      * [1]      [2]      [3]     [4]    [5]
343      * JOBNAME  JOBID    OWNER   STATUS CLASS
344      * IBMUSER1 JOB01906 IBMUSER OUTPUT A       RC=0000 3 spool files
345      * IBMUSER  TSU01830 IBMUSER OUTPUT TSU     ABEND=522 3 spool files
346      * 012345678901234567890123456789012345678901234
347      *           1         2         3         4
348      * -------------------------------------------
349      * Group in regex
350      * [1] Job name
351      * [2] Job number
352      * [3] Owner
353      * [4] Job status (INPUT,ACTIVE,OUTPUT)
354      * [5] Job Class
355      * [6] The rest
356      * </pre>
357      *
358      * @param entry zosDirectoryEntry
359      * @return null: entry was not parsed.
360      */
361     private FTPFile parseJeslevel2List(final String entry) {
362         return parseJeslevelList(entry, 4);
363     }
364 
365     private FTPFile parseJeslevelList(final String entry, final int matchNum) {
366         if (matches(entry)) {
367             final FTPFile file = new FTPFile();
368             if (group(matchNum).equalsIgnoreCase("OUTPUT")) {
369                 file.setRawListing(entry);
370                 final String name = group(2); /* Job Number, used by GET */
371                 file.setName(name);
372                 file.setType(FTPFile.FILE_TYPE);
373                 return file;
374             }
375         }
376         return null;
377     }
378 
379     /**
380      * Parses entries within a partitioned dataset.
381      *
382      * Format of a memberlist within a PDS:
383      *
384      * <pre>
385      *    0         1        2          3        4     5     6      7    8
386      *   Name      VV.MM   Created       Changed      Size  Init   Mod   Id
387      *   TBSHELF   01.03 2002/09/12 2002/10/11 09:37    11    11     0 KIL001
388      *   TBTOOL    01.12 2002/09/12 2004/11/26 19:54    51    28     0 KIL001
389      *
390      * -------------------------------------------
391      * [1] Name
392      * [2] VV.MM: Version . modification
393      * [3] Created: yyyy / MM / dd
394      * [4,5] Changed: yyyy / MM / dd HH:mm
395      * [6] Size: number of lines
396      * [7] Init: number of lines when first created
397      * [8] Mod: number of modified lines a last save
398      * [9] Id: User id for last update
399      * </pre>
400      *
401      * @param entry zosDirectoryEntry
402      * @return null: entry was not parsed.
403      */
404     private FTPFile parseMemberList(final String entry) {
405         final FTPFile file = new FTPFile();
406         if (matches(entry)) {
407             file.setRawListing(entry);
408             final String name = group(1);
409             final String datestr = group(2) + " " + group(3);
410             file.setName(name);
411             file.setType(FTPFile.FILE_TYPE);
412             try {
413                 file.setTimestamp(super.parseTimestamp(datestr));
414             } catch (final ParseException e) {
415                 // just ignore parsing errors.
416                 // TODO check this is ok
417                 // Drop thru to try simple parser
418             }
419             return file;
420         }
421         /*
422          * Assigns the name to the first word of the entry. Only to be used from a safe context, for example from a memberlist, where the regex for some reason
423          * fails. Then just assign the name field of FTPFile.
424          */
425         if (entry != null && !entry.trim().isEmpty()) {
426             file.setRawListing(entry);
427             final String name = entry.split(" ")[0];
428             file.setName(name);
429             file.setType(FTPFile.FILE_TYPE);
430             return file;
431         }
432         return null;
433     }
434 
435     /**
436      * Pre-parses is called as part of the interface. Per definition, it is called before the parsing takes place. Three kinds of lists are recognized:
437      * <ul>
438      *     <li>z/OS-MVS File lists,</li>
439      *     <li>z/OS-MVS Member lists,</li>
440      *     <li>Unix file lists.</li>
441      * </ul>
442      * @since 2.0
443      */
444     @Override
445     public List<String> preParse(final List<String> orig) {
446         // simply remove the header line. Composite logic will take care of the
447         // two different types of
448         // list in short order.
449         if (orig != null && !orig.isEmpty()) {
450             final String header = orig.get(0);
451             if (header.contains("Volume") && header.contains("Dsname")) {
452                 setType(FILE_LIST_TYPE);
453                 super.setRegex(FILE_LIST_REGEX);
454             } else if (header.contains("Name") && header.contains("Id")) {
455                 setType(MEMBER_LIST_TYPE);
456                 super.setRegex(MEMBER_LIST_REGEX);
457             } else if (header.startsWith("total")) {
458                 setType(UNIX_LIST_TYPE);
459                 unixFTPEntryParser = new UnixFTPEntryParser();
460             } else if (header.indexOf("Spool Files") >= 30) {
461                 setType(JES_LEVEL_1_LIST_TYPE);
462                 super.setRegex(JES_LEVEL_1_LIST_REGEX);
463             } else if (header.startsWith("JOBNAME") && header.indexOf("JOBID") > 8) { // header contains JOBNAME JOBID OWNER // STATUS CLASS
464                 setType(JES_LEVEL_2_LIST_TYPE);
465                 super.setRegex(JES_LEVEL_2_LIST_REGEX);
466             } else {
467                 setType(UNKNOWN_LIST_TYPE);
468             }
469             if (isType != JES_LEVEL_1_LIST_TYPE) { // remove header is necessary
470                 orig.remove(0);
471             }
472         }
473         return orig;
474     }
475 
476     /**
477      * Sets the type of listing being processed.
478      *
479      * @param type The listing type.
480      */
481     void setType(final int type) {
482         isType = type;
483     }
484 
485 }