/* * %Z%%W% %I% * * ========================================================================= * Licensed Materials - Property of IBM * "Restricted Materials of IBM" * Copyright IBM Corp. 2008. All Rights Reserved * * DISCLAIMER: * The following [enclosed] code is sample code created by IBM * Corporation. This sample code is not part of any standard IBM product * and is provided to you solely for the purpose of assisting you in the * development of your applications. The code is provided 'AS IS', * without warranty of any kind. IBM shall not be liable for any damages * arising out of your use of the sample code, even if they have been * advised of the possibility of such damages. * ========================================================================= */ package com.ibm.jzos.sample; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.util.Iterator; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import com.ibm.jzos.CatalogSearch; import com.ibm.jzos.CatalogSearchField; import com.ibm.jzos.PdsDirectory; import com.ibm.jzos.RcException; import com.ibm.jzos.ZFile; import com.ibm.jzos.ZUtil; /** * A class that is used by {@link ZipDatasets} to handle the creation of * zip file entries for z/OS datasets. * <p/> * Instances of this class are constructed using a dataset name or pattern, * which can include: * <ul> * <li>A sequential dataset or PDS name: //A.B.C </li> * <li>A dataset pattern name: //A.*.D * as defined by the z/OS Catalog Search facility (IGGCSI00). * See {@link CatalogSearch} for more information. </li> * <li>A PDS member name: //A.B.C(MEM) </li> * <li>A PDS member pattern: //A.B.C(D*X) </li> * <li>A DD name: //DD:XYZ which might refer to a sequential dataset, or PDS, * or concatenation. </li> * <li>A DD name and member: //DD:XYZ(MEM) </li> * <li>A DD name and member pattern: //DD:XYZ(D*X) </li> * </ul> * The leading "//" prefix may be omitted and names are case insensitive. * <p/> * Each dataset is zipped to the ZipOutputStream by reading the source * dataset as text encoded in the default * EBCDIC codepage ({@link ZUtil#getDefaultPlatformEncoding()}) * and then writing the text to ZipOutputStream encoded using * the supplied target encoding. * <p/> * The name given to each entry is the actual MVS dataset name in upper case. * If the entry is for a PDS member, then the dataset name is used as * a directory name followed by the member name as a file name. * <p/> * @see ZipDatasets ZipDatasets the main class used to zip z/OS datasets * @see #addTo(ZipOutputStream, String) * @since 2.3.0 */ public class ZipDatasetSource { // Some constants used to build regular expression static final String SLASH_SLASH_PREFIX = "//"; static final String DD_PREFIX = "DD:"; static final String DSNAME_CHAR = "[\\w[#\\$\\.]]"; static final String DSNAME_PATTERN_CHAR = "[\\w[#\\$\\.\\*]]"; static final String MEMBER_CHAR = "[\\w[#\\$]"; static final String MEMBER__PATTERN_CHAR = "[\\w[#\\$\\*]]"; static final String DSNAME_PIECE = DSNAME_CHAR + "{1,44}"; static final String DDNAME_PIECE = DD_PREFIX + "\\w{1,8}"; /** A dataset pattern has at least one asterisk, but may not start with an asterisk */ static final String DSNAME_PATTERN = "^" + DSNAME_CHAR + "+\\*" + DSNAME_PATTERN_CHAR + "*$"; /** A member pattern is a dataset(pat) or DD:name(pat) where mpat is a member name or pattern that includes an asterisk */ static final String DSNAME_WITH_MEMBER_OR_PATTERN = "^(" + DSNAME_PIECE + "|" + DDNAME_PIECE + ")" + "\\(" + "(" + MEMBER__PATTERN_CHAR + "+)" + "\\)$"; /** The buffer size used to read/write blocks of data */ static final int BUFSIZE = 64 * 1024; /** * The name of the input dataset source. * Uppercased, with any "//" prefix removed. */ private String name; /** * The name of the member or member pattern name, which is * extracted from the original dataset source name. * May be null if no member name or pattern was given */ private String memberPattern; /** * Construct an instance given a dataset/pattern name. * We also convert the name to uppercase and drop any * "//" prefix. */ public ZipDatasetSource(String nm) { name = nm.toUpperCase(); if (name.startsWith(SLASH_SLASH_PREFIX)) { name = name.substring(SLASH_SLASH_PREFIX.length()); } } /** * Answer the dataset/pattern name. */ public String getName() { return name; } /** * Add one or more entries to the given ZipOutputStream for the * dataset or datasets described by this ZipDatasetSource. * <p/> * @param zipOutStream the output ZipOutputStream * @param targetEncoding the codepage used to encode the data written to the zipOutStream * @throws IOException */ public void addTo(ZipOutputStream zipOutStream, String targetEncoding) throws IOException { if (name.matches(DSNAME_PATTERN)) { // Process a dataset name that includes a pattern character ('*') addMatchingDatasets(zipOutStream, targetEncoding); } else { // If a member name or member pattern was given, // split it out from the dataset name Pattern mempat = Pattern.compile(DSNAME_WITH_MEMBER_OR_PATTERN); Matcher matcher = mempat.matcher(name); if (matcher.matches()) { name = matcher.group(1); // get the dsname | dd:name memberPattern = matcher.group(2); // get the member/pattern } // Process the dataset, pds, or dataset(member) addDatasetOrPds(zipOutStream, targetEncoding); } } /** * Add a single dataset, single member, complete PDS, or a pattern of PDS members * to the given ZipOutputStream. * <p/> * @param zipOutStream the target ZipOutputStream * @param targetEncoding the target text encoding * @throws IOException */ private void addDatasetOrPds(ZipOutputStream zipOutStream, String targetEncoding) throws IOException { // allocate a DD to point to the base dsname (or return the name if //DD:name given) String ddname = allocDD(); // If we are given no member name or a member name pattern, // we try to process the dataset as a PDS directory. // This fails if the dataset was not a PDS. PdsDirectory pdsDir = null; if (memberPattern == null || memberPattern.indexOf('*') >= 0) { try { pdsDir = new PdsDirectory(SLASH_SLASH_PREFIX + DD_PREFIX + ddname); } catch (IOException ioe) { } // fall through with pdsDir == null } // If its not a PdsDirectory, then assume that it is a regular dataset or single member if (pdsDir == null) { addDatasetOrMember(zipOutStream, targetEncoding, ddname, memberPattern); return; } // Process a PDS directory... // If we are given a pattern string to filter members, then build // a regular expression to use. Pattern memberRegex = null; if (memberPattern != null) { memberRegex = makeRegexPattern(memberPattern); } // Loop over the entries in the directory and add all/matching // members to the zipOutStream try { for (Iterator i=pdsDir.iterator(); i.hasNext(); ) { PdsDirectory.MemberInfo member = (PdsDirectory.MemberInfo)i.next(); String memberName = member.getName(); if (memberRegex == null || memberRegex.matcher(memberName).matches()) { addDatasetOrMember(zipOutStream, targetEncoding, ddname, memberName); } } } finally { // Faithfully close the directory and free the DD when we are done, // even if an exception is thrown. try { pdsDir.close(); } catch (IOException ignore) {} freeDD(ddname); } } /** * Add an single {@link ZipEntry} for a single dataset or member. * * @param zipOutStream the target ZipOutputStream * @param targetEncoding the target text encoding * @param ddname the DD allocated to the dataset or member * @param memberName the member name; used to create the entry name * @throws IOException */ private void addDatasetOrMember(ZipOutputStream zipOutStream, String targetEncoding, String ddname, String memberName) throws IOException { Reader reader = null; try { reader = openInputFile(ddname, memberName); // Construct the name of the Zip entry that we will add String entryName = memberName == null ? name : name + "/" + memberName; // Start a new ZipEntry in the Zip file, // copy the dataset/member data into the Zip entry, // and close the entry ZipEntry entry = new ZipEntry(entryName); zipOutStream.putNextEntry(entry); copyData(reader, zipOutStream, targetEncoding); zipOutStream.closeEntry(); System.out.println(" added: " + entryName + " (" + entry.getSize() + " -> " + entry.getCompressedSize() + ")"); } finally { closeInputFile(reader); freeDD(ddname); } } /** * Given a dataset source name that included wild card ('*') characters, * use the z/OS CatalogSearch facility (IGGCSI00) to find and process all * of the matching sequential or GDS datasets that match. * @param zipOutStream * @param targetEncoding * @throws IOException */ private void addMatchingDatasets(ZipOutputStream zipOutStream, String targetEncoding) throws IOException { CatalogSearch catSearch = new CatalogSearch(name); catSearch.setEntryTypes("AH"); // only NON-VSAM and Generation Datasets catSearch.addFieldName("ENTNAME"); catSearch.search(); while (catSearch.hasNext()) { CatalogSearch.Entry entry = (CatalogSearch.Entry)catSearch.next(); if (entry.isDatasetEntry()) { CatalogSearchField field = entry.getField("ENTNAME"); String dsn = field.getFString().trim(); // make a new ZipSource with the next dsn and add it ZipDatasetSource source = new ZipDatasetSource(dsn); source.addTo(zipOutStream, targetEncoding); } } } /** * Make a regular expression pattern than matches the given * member name pattern that includes literal characters and * zero or more asterisks. */ private Pattern makeRegexPattern(String memberPattern) { StringBuffer patBuf = new StringBuffer("^"); for (int i=0; i<memberPattern.length(); i++) { char c = memberPattern.charAt(i); switch (c) { case '*': patBuf.append(".*"); break; case '$': patBuf.append("\\$"); break; default: patBuf.append(c); } } patBuf.append('$'); return Pattern.compile(patBuf.toString()); } /** * Copy data from a reader to a ZipOutputStream. * @param reader a Reader open on the input dataset/member in the default EBCDIC encoding * @param zipOutStream the target ZipOutputStram * @param targetEncoding the target encoding for the ZipOutputStream * @throws IOException * @throws UnsupportedEncodingException */ private void copyData(Reader reader, ZipOutputStream zipOutStream, String targetEncoding) throws IOException, UnsupportedEncodingException { char[] cbuf = new char[BUFSIZE]; int nRead; // wrap the zipOutputStream in a Writer that encodes to the target encoding OutputStreamWriter osw = new OutputStreamWriter(zipOutStream, targetEncoding); while ((nRead = reader.read(cbuf)) != -1) { osw.write(cbuf, 0, nRead); } osw.flush(); // flush any buffered data to the ZipOutputStream } /** * Allocate a new DD with DISP=SHR to point to the source dataset, * or of a DD:name was given, return the ddname. * @return the ddname given/allocated * @throws IOException */ private String allocDD() throws IOException { String ddname = null; // See if a DD:name was given if (name.startsWith(DD_PREFIX)) { ddname = name.substring(DD_PREFIX.length()); return ddname; } // Otherwise we allocate a temporary DD to the given dataset // using DISP=SHR try { // get a new SYSnnnnnn DD name allocated to dummy ddname = ZFile.allocDummyDDName(); // reallocate it to the dataset with DISP=SHR ZFile.bpxwdyn("alloc fi("+ddname+") da("+name+") shr reuse msg(2)"); return ddname; } catch (RcException rce) { freeDD(ddname); // free the temp dd throw new IOException("Unable to allocate input dataset: " + name + " - " + rce); } } /** * Do our best to free a DD that we allocated * @param ddname */ private void freeDD(String ddname) { if (ddname == null || name.startsWith(DD_PREFIX)) { return; } try { // Omit the 'msg' keyword to suppress error messages. // We might not actually be able to free the DD if // if is still open as a PDS directory ZFile.bpxwdyn("free fi("+ddname+")"); } catch(RcException ignore) {} } /** * Open a Reader to point to the previously allocated * DD and optionally a given member. The encoding for the * reader is set to the default EBCDIC encoding * (see {@link ZUtil#getDefaultPlatformEncoding()}) * <p/> * @param ddname the DD allocated to the dataset * @param memberName if not null, the member name to open * @return a Reader * @throws IOException */ private Reader openInputFile(String ddname, String memberName) throws IOException { String sourceEncoding = ZUtil.getDefaultPlatformEncoding(); String filename = SLASH_SLASH_PREFIX + DD_PREFIX + ddname + (memberName==null ? "": "("+memberName+")"); // We open the file in text mode, so that new-line characters are inserted // at record boundaries and trailing spaces are removed from records. ZFile zFile = new ZFile(filename, "rt"); // If the open file's actual filename can be determined, use it as our name String actualFileName = zFile.getActualFilename(); if (actualFileName != null) { name = actualFileName; } // Strip the member name off of the actual file name int ilparen = name.indexOf('('); if (ilparen > 0) { name = name.substring(0,ilparen); } InputStream is = zFile.getInputStream(); return new InputStreamReader(is, sourceEncoding); } /** * Close the input reader. */ private void closeInputFile(Reader reader) { if (reader == null) return; try { reader.close(); } catch (IOException ignore) {} } }