JesClient.java
// Generated by delombok at Mon Apr 14 16:48:01 UTC 2025
package de.larssh.jes;
import static de.larssh.utils.Collectors.toLinkedHashMap;
import static de.larssh.utils.Finals.constant;
import static de.larssh.utils.function.ThrowingFunction.throwing;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.input.ReaderInputStream;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import de.larssh.jes.parser.JesFtpFile;
import de.larssh.jes.parser.JesFtpFileEntryParserFactory;
import de.larssh.utils.Nullables;
import de.larssh.utils.Optionals;
import de.larssh.utils.annotations.SuppressJacocoGenerated;
import de.larssh.utils.function.ThrowingConsumer;
import de.larssh.utils.text.Patterns;
import de.larssh.utils.text.Strings;
import de.larssh.utils.time.Stopwatch;
import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* This class allows to handle IBM z/OS JES spools using Java technologies. The
* used interface is the IBM z/OS FTP server, that should be available by
* default.
*
* <p>
* JES spool entries can be filtered and listed using
* {@link #list(String, JobStatus, String)} and
* {@link #listFilled(String, JobStatus, String)} methods, while the later one
* gathers more information, but takes some more time.
*
* <p>
* {@link #submit(String)} submits JCLs based on the FTP users permissions.
* {@link #waitFor(Job, Duration, Duration)} can be used to wait until a job
* terminates. Job outputs can be retrieved using {@link #retrieve(JobOutput)}
* and removed using {@link #delete(Job)}.
*
* <p>
* <b>Usage example:</b> The following shows the JesClient used inside a
* try-with-resource statement. The constructor descriptions describe further
* details.
*
* <pre>
* // Connect and login via simplified constructor
* try (JesClient jesClient = new JesClient(hostname, port, username, password)) {
*
* // Submit JCL
* Job job = jesClient.submit(jclContent);
*
* // Wait for job to be finished
* if (!jesClient.waitFor(job)) {
* // Handle the case, a finished job cannot be found inside JES spool any longer
* throw ...;
* }
*
* // Gather job status details
* Job detailedJob = jesClient.getJobDetails(job);
*
* // Gather finished jobs outputs
* List<JobOutput> jobOutput = jesClient.get(job);
*
* // Delete job from JES spool
* jesClient.delete(job);
*
* // Logout and disconnect using try-with-resource (close method)
* }
* </pre>
*
* <p>
* In case filtering jobs does not work as expected, check the JES Interface
* Level of your server using {@link #getServerProperties()}. This class
* requires {@code JESINTERFACELEVEL = 2}. The JES Interface Level can be
* configured by a mainframe administrator inside {@code FTP.DATA}.
*
* @see <a href=
* "https://www.ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/com.ibm.zos.v2r3.halu001/intfjes.htm">IBM
* Knowledge Center - Interfacing with JES</a>
*/
@SuppressWarnings({"PMD.ExcessiveImports", "PMD.GodClass"})
public class JesClient implements Closeable {
/**
* Wildcard value to be used for name and owner filters, meaning "any" value.
*/
public static final String FILTER_WILDCARD = constant("*");
/**
* Charset, that is used for submitting JCLs and retrieving job outputs.
*/
private static final Charset FTP_DATA_CHARSET = StandardCharsets.UTF_8;
/**
* Maximum limit of spool entries (including)
*/
public static final int LIST_LIMIT_MAX = constant(1024);
/**
* Limit of spool entries for {@link #exists(Job, JobStatus)}
*
* <p>
* Checking for existence does not need a limit, but using a limit allows to
* handle an additional error case.
*/
private static final int LIST_LIMIT_EXISTS = 2;
/**
* Minimum limit of spool entries (including)
*/
private static final int LIST_LIMIT_MIN = 1;
/**
* Pattern to find the job ID inside the FTP response after submitting a JCL.
*/
private static final Pattern PATTERN_FTP_SUBMIT_ID = Pattern.compile("^250-IT IS KNOWN TO JES AS (?<id>\\S+)", Pattern.CASE_INSENSITIVE);
/**
* Pattern to find the job name inside a valid JCL.
*/
private static final Pattern PATTERN_JCL_JOB_NAME = Pattern.compile("^//\\s*(?<name>\\S+)");
/**
* Pattern to check the response string for the spool entries limit warning.
*/
private static final Pattern PATTERN_LIST_LIMIT = Pattern.compile("^250-JESENTRYLIMIT OF \\d+ REACHED\\. +ADDITIONAL ENTRIES NOT DISPLAYED$", Pattern.CASE_INSENSITIVE);
/**
* Pattern to check the response string for the empty list warning.
*/
private static final Pattern PATTERN_LIST_NAMES_NO_JOBS_FOUND = Pattern.compile("^550 NO JOBS FOUND FOR ", Pattern.CASE_INSENSITIVE);
/**
* Pattern to retrieve status values from response strings.
*/
private static final Pattern PATTERN_STATUS = Pattern.compile("^211-(SERVER SITE VARIABLE |TIMER )?(?<key>\\S+)( VALUE)? IS (SET TO )?(?<value>\\S+?)\\.?$", Pattern.CASE_INSENSITIVE);
/**
* Remote file name that is used when submitting a JCL.
*/
private static final String SUBMIT_REMOTE_FILE_NAME = JesClient.class.getSimpleName() + ".jcl";
/**
* FTP Client used by the current JES client instance.
*/
private final FTPClient ftpClient;
/**
* Current JES spool user
*/
private String jesOwner = FILTER_WILDCARD;
/**
* Expert constructor. This constructor creates a FTP client <b>without</b>
* connecting and logging in. It is meant to be used in scenarios, which require
* additional FTP configuration.
*
* <p>
* <b>Usage example 1</b> (using a simplified login)
*
* <pre>
* // Construct the JES client and its internal FTP client
* try (JesClient jesClient = new JesClient()) {
*
* // Connect via FTP
* jesClient.getFtpClient().connect(...);
*
* // Simplified login using the JES client
* jesClient.login(...);
*
* ...
*
* // Logout and disconnect using try-with-resource (close method)
* }
* </pre>
*
* <p>
* <b>Usage example 2:</b> (using a custom login)
*
* <pre>
* // Construct the JES client and its internal FTP client
* try (JesClient jesClient = new JesClient()) {
*
* // Connect via FTP
* jesClient.getFtpClient().connect(...);
*
* // Login via FTP client
* jesClient.getFtpClient().login(...);
*
* // Set the JES spool owner
* jesClient.setJesOwner(...);
*
* // Enter JES mode of the FTP connection
* jesClient.enterJesMode();
*
* ...
*
* // Logout and disconnect using try-with-resource (close method)
* }
* </pre>
*/
public JesClient() {
ftpClient = new FTPClient();
ftpClient.setParserFactory(new JesFtpFileEntryParserFactory());
}
/**
* Simplified constructor. This constructor initiates a new FTP connection and
* logs in using the given credentials.
*
* <p>
* The JesClient can store a JES spool owner. This constructor initializes the
* JES spool owner using the given username.
*
* <p>
* The default port is {@link org.apache.commons.net.ftp.FTP#DEFAULT_PORT}.
*
* <p>
* <b>Warning:</b> This constructor calls the overridable method
* {@link #login(String, String)}, which might lead to uninitialized fields when
* overriding that method.
*
* @param hostname FTP hostname
* @param port FTP port
* @param username FTP username and JES spool owner
* @param password FTP password
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
@SuppressJacocoGenerated(justification = "this constructor cannot be mocked nicely")
@SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "PCOA_PARTIALLY_CONSTRUCTED_OBJECT_ACCESS"}, justification = "see JavaDoc")
public JesClient(final String hostname, final int port, final String username, final String password) throws IOException, JesException {
this();
ftpClient.connect(hostname, port);
login(username, password);
}
/**
* Logs out and disconnects the FTP connection.
*/
@Override
public void close() throws IOException {
try {
if (getFtpClient().isAvailable()) {
getFtpClient().logout();
}
} finally {
if (getFtpClient().isConnected()) {
getFtpClient().disconnect();
}
}
}
/**
* Removes a given {@code job} from JES spool. This method cares only about the
* jobs ID.
*
* <p>
* In case you do not already have a {@link Job} object, deleting by job ID
* works as follows:
*
* <pre>
* String jobId = ...;
* jesClient.delete(new Job(jobId, JesClient.FILTER_WILDCARD, JobStatus.ALL, JesClient.FILTER_WILDCARD));
* </pre>
*
* @param job Job to be deleted
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public void delete(final Job job) throws IOException, JesException {
if (!getFtpClient().deleteFile(job.getId())) {
throw new JesException(getFtpClient(), "Job [%s] could not be deleted.", job.getId());
}
}
/**
* Enters the IBM z/OS FTP servers JES file type mode using a SITE command.
*
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public void enterJesMode() throws IOException, JesException {
if (!getFtpClient().sendSiteCommand("FILEtype=JES")) {
throw new JesException(getFtpClient(), "Failed setting JES mode.");
}
}
/**
* Reloads the job from server and returns {@code true} if the job is still
* available and matches the given job status.
*
* @param job the job to search for
* @param status job status or ALL
* @return {@code true} if the job is still available
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public boolean exists(final Job job, final JobStatus status) throws IOException, JesException {
setJesFilters(job.getName(), status, job.getOwner(), LIST_LIMIT_EXISTS);
final String[] ids = getListNameResults(getFtpClient().listNames(job.getId())).orElseThrow(() -> new JesException(getFtpClient(), "Retrieving job [%s] failed. Probably no FTP data connection socket could be opened.", job.getId()));
return Optionals.ofSingle(ids).isPresent();
}
/**
* Retrieves up-to-date job details for {@code job}. That includes all
* {@link Job} attributes, including a list of {@link JobOutput} instances for
* held jobs.
*
* @param job job to get up-to-date details for
* @return job details or {@link Optional#empty()} in case the job is no longer
* available inside JES spool
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public Optional<Job> getJobDetails(final Job job) throws IOException, JesException {
setJesFilters(job.getName(), JobStatus.ALL, job.getOwner(), LIST_LIMIT_MAX);
return Optionals.ofSingle( //
stream(getFtpClient().listFiles(job.getId())).filter(JesFtpFile.class::isInstance).map(JesFtpFile.class::cast).map(JesFtpFile::getJob));
}
/**
* Corrects the result of {@link FTPClient#listNames()} and
* {@link FTPClient#listNames(String)} as the mainframe FTP server marks empty
* name listings as error.
*
* @param names result of {@link FTPClient#listNames()} and
* {@link FTPClient#listNames(String)}
* @return array of names or {@link Optional#empty()} on real FTP error
*/
@SuppressWarnings("PMD.UseVarargs")
@SuppressFBWarnings(value = "UVA_USE_VAR_ARGS", justification = "No varargs needed as this is for special technical reasons only.")
private Optional<String[]> getListNameResults(@Nullable final String[] names) {
if (names == null) {
return Patterns.find(PATTERN_LIST_NAMES_NO_JOBS_FOUND, getFtpClient().getReplyString()).map(matcher -> new String[0]);
}
return Optional.of(names);
}
/**
* Retrieves and parses a map of server properties, such as
* {@code "JESJOBNAME"}, {@code "JESSTATUS"}, {@code "JESOWNER"} and
* {@code "INTERFACELEVEL"}.
*
* @return map of server properties
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public Map<String, String> getServerProperties() throws IOException, JesException {
// Execute STAT command
if (!FTPReply.isPositiveCompletion(getFtpClient().stat())) {
throw new JesException(getFtpClient(), "Failed executing STAT command.");
}
final String[] lines = getFtpClient().getReplyStrings();
// Parse reply strings
final Map<String, String> properties = new LinkedHashMap<>();
for (final String line : lines) {
final Optional<Matcher> matcher = Patterns.matches(PATTERN_STATUS, line);
if (matcher.isPresent()) {
// Key
final String key = matcher.get().group("key");
if (properties.containsKey(key)) {
throw new JesException("Found duplicate status key \"%s\".", key);
}
// Value
properties.put(key, matcher.get().group("value"));
}
}
return properties;
}
/**
* Returns a list of all job IDs boxed into {@link Job} objects matching the
* given filters. This method has a much higher performance compared to
* {@link #listFilled(String)}, though that method fills in additional
* {@link Job} fields.
*
* <p>
* {@code nameFilter} is allowed to end with the wildcard character "*".
*
* <p>
* JES does not list more than {@link #LIST_LIMIT_MAX} entries. In case more
* entries are available, a {@link JesLimitReachedException} is thrown,
* containing all entries up to the limit.
*
* @param nameFilter filter by job names
* @return list of jobs containing job IDs
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public List<Job> list(final String nameFilter) throws IOException, JesException {
return list(nameFilter, JobStatus.ALL);
}
/**
* Returns a list of all job IDs boxed into {@link Job} objects matching the
* given filters. This method has a much higher performance compared to
* {@link #listFilled(String, JobStatus)}, though that method fills in
* additional {@link Job} fields.
*
* <p>
* {@code nameFilter} is allowed to end with the wildcard character "*".
*
* <p>
* JES does not list more than {@link #LIST_LIMIT_MAX} entries. In case more
* entries are available, a {@link JesLimitReachedException} is thrown,
* containing all entries up to the limit.
*
* @param nameFilter filter by job names
* @param status filter by job status
* @return list of jobs containing job IDs
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public List<Job> list(final String nameFilter, final JobStatus status) throws IOException, JesException {
return list(nameFilter, status, FILTER_WILDCARD);
}
/**
* Returns a list of all job IDs boxed into {@link Job} objects matching the
* given filters. This method has a much higher performance compared to
* {@link #listFilled(String, JobStatus, String)}, though that method fills in
* additional {@link Job} fields.
*
* <p>
* {@code nameFilter} and {@code ownerFilter} are allowed to end with the
* wildcard character "*".
*
* <p>
* JES does not list more than {@link #LIST_LIMIT_MAX} entries. In case more
* entries are available, a {@link JesLimitReachedException} is thrown,
* containing all entries up to the limit.
*
* @param nameFilter filter by job names
* @param status filter by job status
* @param ownerFilter filter by job owner
* @return list of jobs containing job IDs
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public List<Job> list(final String nameFilter, final JobStatus status, final String ownerFilter) throws IOException, JesException {
return list(nameFilter, status, ownerFilter, LIST_LIMIT_MAX);
}
/**
* Returns a list of all job IDs boxed into {@link Job} objects matching the
* given filters. This method has a much higher performance compared to
* {@link #listFilled(String, JobStatus, String, int)}, though that method fills
* in additional {@link Job} fields.
*
* <p>
* {@code nameFilter} and {@code ownerFilter} are allowed to end with the
* wildcard character "*".
*
* <p>
* JES does not list more than {@code limit} entries. In case more entries are
* available, a {@link JesLimitReachedException} is thrown, containing all
* entries up to the limit. {@code limit} can be from {@link #LIST_LIMIT_MIN}
* (including) to {@link #LIST_LIMIT_MAX} (including).
*
* @param nameFilter filter by job names
* @param status filter by job status
* @param ownerFilter filter by job owner
* @param limit limit of spool entries
* @return list of jobs containing job IDs
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public List<Job> list(final String nameFilter, final JobStatus status, final String ownerFilter, final int limit) throws IOException, JesException {
setJesFilters(nameFilter, status, ownerFilter, limit);
final String[] ids = getListNameResults(getFtpClient().listNames()).orElseThrow(() -> new JesException(getFtpClient(), "Retrieving the list of job IDs failed. Probably no FTP data connection socket could be opened."));
return throwIfLimitReached(limit, stream(ids).map(id -> new Job(id, nameFilter, status, ownerFilter)).collect(toList()));
}
/**
* Returns a list of all {@link Job} objects matching the given filters. This
* method has a worse performance compared to {@link #list(String)}, though it
* fills in additional {@link Job} fields.
*
* <p>
* {@code nameFilter} is allowed to end with the wildcard character "*".
*
* <p>
* JES does not list more than {@link #LIST_LIMIT_MAX} entries. In case more
* entries are available, a {@link JesLimitReachedException} is thrown,
* containing all entries up to the limit.
*
* @param nameFilter filter by job names
* @return list of jobs
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public List<Job> listFilled(final String nameFilter) throws IOException, JesException {
return listFilled(nameFilter, JobStatus.ALL);
}
/**
* Returns a list of all {@link Job} objects matching the given filters. This
* method has a worse performance compared to {@link #list(String, JobStatus)},
* though it fills in additional {@link Job} fields.
*
* <p>
* {@code nameFilter} is allowed to end with the wildcard character "*".
*
* <p>
* JES does not list more than {@link #LIST_LIMIT_MAX} entries. In case more
* entries are available, a {@link JesLimitReachedException} is thrown,
* containing all entries up to the limit.
*
* @param nameFilter filter by job names
* @param status filter by job status
* @return list of jobs
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public List<Job> listFilled(final String nameFilter, final JobStatus status) throws IOException, JesException {
return listFilled(nameFilter, status, FILTER_WILDCARD);
}
/**
* Returns a list of all {@link Job} objects matching the given filters. This
* method has a worse performance compared to
* {@link #list(String, JobStatus, String)}, though it fills in additional
* {@link Job} fields.
*
* <p>
* {@code nameFilter} and {@code ownerFilter} are allowed to end with the
* wildcard character "*".
*
* <p>
* JES does not list more than {@link #LIST_LIMIT_MAX} entries. In case more
* entries are available, a {@link JesLimitReachedException} is thrown,
* containing all entries up to the limit.
*
* @param nameFilter filter by job names
* @param status filter by job status
* @param ownerFilter filter by job owner
* @return list of jobs
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public List<Job> listFilled(final String nameFilter, final JobStatus status, final String ownerFilter) throws IOException, JesException {
return listFilled(nameFilter, status, ownerFilter, LIST_LIMIT_MAX);
}
/**
* Returns a list of all {@link Job} objects matching the given filters. This
* method has a worse performance compared to
* {@link #list(String, JobStatus, String)}, though it fills in additional
* {@link Job} fields.
*
* <p>
* {@code nameFilter} and {@code ownerFilter} are allowed to end with the
* wildcard character "*".
*
* <p>
* JES does not list more than {@code limit} entries. In case more entries are
* available, a {@link JesLimitReachedException} is thrown, containing all
* entries up to the limit. {@code limit} can be from {@link #LIST_LIMIT_MIN}
* (including) to {@link #LIST_LIMIT_MAX} (including).
*
* @param nameFilter filter by job names
* @param status filter by job status
* @param ownerFilter filter by job owner
* @param limit limit of spool entries
* @return list of jobs
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public List<Job> listFilled(final String nameFilter, final JobStatus status, final String ownerFilter, final int limit) throws IOException, JesException {
setJesFilters(nameFilter, status, ownerFilter, limit);
final FTPFile[] files = getFtpClient().listFiles();
return throwIfLimitReached(limit, stream(files).filter(JesFtpFile.class::isInstance).map(JesFtpFile.class::cast).map(JesFtpFile::getJob).collect(toList()));
}
/**
* Shortcut method to perform a FTP login, set the internal JES owner and enter
* JES mode.
*
* <p>
* Is similar to the following lines of code.
*
* <pre>
* // Login via FTP client
* jesClient.getFtpClient().login(...);
*
* // Set the JES spool owner
* jesClient.setJesOwner(...);
*
* // Enter JES mode of the FTP connection
* jesClient.enterJesMode();
* </pre>
*
* @param username the user id to be used for FTP login and internal JES owner
* @param password the users password
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public void login(final String username, final String password) throws IOException, JesException {
if (!getFtpClient().login(username, password)) {
throw new JesException(getFtpClient(), "Could not login user [%s].", username);
}
setJesOwner(username);
enterJesMode();
}
/**
* Retrieves the content of {@code jobOutput}.
*
* @param jobOutput job output to be requested
* @return content of the specified job output
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public String retrieve(final JobOutput jobOutput) throws IOException, JesException {
final String fileName = Strings.format("%s.%d", jobOutput.getJob().getId(), jobOutput.getIndex());
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
if (!getFtpClient().retrieveFile(fileName, outputStream)) {
throw new JesException(getFtpClient(), "Could not retrieve data of job output [%s.%s].", jobOutput.getJob().getId(), jobOutput.getStep());
}
return new String(outputStream.toByteArray(), FTP_DATA_CHARSET);
}
}
/**
* Retrieves all job outputs of {@code job}.
*
* @param job job to request all outputs of
* @return map with job output details and the corresponding content in specific
* order
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public Map<JobOutput, String> retrieveOutputs(final Job job) throws IOException, JesException {
if (job.getOutputs().isEmpty()) {
return retrieveOutputs(getJobDetails(job).orElseThrow(() -> new JesException("Job [%s] is not available.", job.getId())));
}
return job.getOutputs().stream().collect(toLinkedHashMap(Function.identity(), throwing(this::retrieve)));
}
/**
* Sends {@link org.apache.commons.net.ftp.FTPCmd#SITE} commands to set the
* given filter values.
*
* <p>
* {@code nameFilter} and {@code ownerFilter} are allowed to end with the
* wildcard character "*".
*
* <p>
* {@code limit} can be from {@link #LIST_LIMIT_MIN} (including) to
* {@link #LIST_LIMIT_MAX} (including). While that restriction is not checked by
* this method, values outside that range might result in a server side error
* message thrown as {@link JesException}.
*
* @param nameFilter filter by job names
* @param status filter by job status
* @param ownerFilter filter by job owner
* @param limit limit of spool entries
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
protected void setJesFilters(final String nameFilter, final JobStatus status, final String ownerFilter, final int limit) throws IOException, JesException {
if (!getFtpClient().sendSiteCommand("JESJOBName=" + nameFilter)) {
throw new JesException(getFtpClient(), "Failed setting JES job name filter to [%s].", nameFilter);
}
if (!getFtpClient().sendSiteCommand("JESOwner=" + ownerFilter)) {
throw new JesException(getFtpClient(), "Failed setting JES job owner filter to [%s].", ownerFilter);
}
if (!getFtpClient().sendSiteCommand("JESSTatus=" + status.getValue())) {
throw new JesException(getFtpClient(), "Failed setting JES job status filter to [%s].", status.getValue());
}
if (!getFtpClient().sendSiteCommand("JESENTRYLIMIT=" + limit)) {
throw new JesException(getFtpClient(), "Failed setting JES entry limit to %d. Minimum/Maximum: %d/%d", limit, LIST_LIMIT_MIN, LIST_LIMIT_MAX);
}
}
/**
* Current JES spool user
*
* @param jesOwner JES spool owner
*/
public void setJesOwner(final String jesOwner) {
this.jesOwner = Strings.toUpperCaseNeutral(jesOwner).trim();
}
/**
* Submits the given JCL and returns a related {@link Job} object containing the
* started jobs ID.
*
* <p>
* In addition to the jobs ID this method tries to extract the jobs name from
* the given JCL. The returned owner is set to the internal JES owner, which can
* be set using {@link #setJesOwner(String)}.
*
* @param jclContent JCL to submit
* @return {@link Job} object containing the started jobs ID
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public Job submit(final String jclContent) throws IOException, JesException {
try (InputStream inputStream = ReaderInputStream.builder().setReader(new StringReader(jclContent)).setCharset(FTP_DATA_CHARSET).get()) {
if (!getFtpClient().storeUniqueFile(SUBMIT_REMOTE_FILE_NAME, inputStream)) {
throw new JesException(getFtpClient(), "Submitting JCL failed.");
}
}
final String jobId = Patterns.find(PATTERN_FTP_SUBMIT_ID, getFtpClient().getReplyString()).map(matcher -> matcher.group("id")).orElseThrow(() -> new JesException(getFtpClient(), "Started job, but could not extract its ID."));
final String name = Patterns.find(PATTERN_JCL_JOB_NAME, jclContent).map(matcher -> matcher.group("name")).orElse(FILTER_WILDCARD);
return new Job(jobId, name, JobStatus.INPUT, getJesOwner());
}
/**
* In case the last FTP responses string contains the spool entries limit
* warning, a {@link JesLimitReachedException} is thrown, else {@code jobs} are
* returned.
*
* <p>
* The thrown exception contains the current spool entries limit and all
* entries, which were read already.
*
* @param limit current spool entries limit
* @param jobs list of jobs
* @return {@code jobs} in case the spool entries limit is not reached
* @throws JesLimitReachedException if the last FTP responses string contains
* the spool entries limit warning
*/
protected List<Job> throwIfLimitReached(final int limit, final List<Job> jobs) throws JesLimitReachedException {
if (Strings.find(getFtpClient().getReplyString(), PATTERN_LIST_LIMIT)) {
throw new JesLimitReachedException(limit, jobs, getFtpClient());
}
return jobs;
}
/**
* Waits for {@code job} to be finished using {@code Thread#sleep(long)} for
* waiting between {@link #exists(Job, JobStatus)} calls and timing out after a
* given duration. {@code waiting} allows to specify the duration to wait.
*
* <p>
* The given jobs status specifies, which status are waited for:
* <ul>
* <li>{@link JobStatus#ALL}: waiting for {@link JobStatus#INPUT} and
* {@link JobStatus#ACTIVE}
* <li>{@link JobStatus#INPUT}: waiting for {@link JobStatus#INPUT} and
* {@link JobStatus#ACTIVE}
* <li>{@link JobStatus#ACTIVE}: waiting for {@link JobStatus#ACTIVE} only
* <li>{@link JobStatus#OUTPUT}: returning {@code true} with no checks and
* without waiting
* </ul>
*
* @param job the job to wait for
* @param waiting duration to wait
* @param timeout timeout duration
* @return {@code true} if the job finished and {@code false} if the timeout has
* been reached
* @throws InterruptedException if any thread has interrupted the current thread
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
@SuppressWarnings({"unused", "PMD.DoNotUseThreads"})
public boolean waitFor(final Job job, final Duration waiting, final Duration timeout) throws InterruptedException, IOException, JesException {
return waitFor(job, waiting, timeout, ThrowingConsumer.throwing(duration -> Thread.sleep(Nullables.orElseThrow(duration).toMillis())));
}
/**
* Waits for {@code job} to be finished using {@code wait} for waiting between
* {@link #exists(Job, JobStatus)} calls and timing out after a given duration.
* {@code waiting} allows to specify the duration to wait.
*
* <p>
* The given jobs status specifies, which status are waited for:
* <ul>
* <li>{@link JobStatus#ALL}: waiting for {@link JobStatus#INPUT} and
* {@link JobStatus#ACTIVE}
* <li>{@link JobStatus#INPUT}: waiting for {@link JobStatus#INPUT} and
* {@link JobStatus#ACTIVE}
* <li>{@link JobStatus#ACTIVE}: waiting for {@link JobStatus#ACTIVE} only
* <li>{@link JobStatus#OUTPUT}: returning {@code true} with no checks and
* without waiting
* </ul>
*
* @param job the job to wait for
* @param waiting duration to wait
* @param timeout timeout duration
* @param wait method to use for waiting
* @return {@code true} if the job finished and {@code false} if the timeout has
* been reached
* @throws IOException Technical FTP failure
* @throws JesException Logical JES failure
*/
public boolean waitFor(final Job job, final Duration waiting, final Duration timeout, final Consumer<Duration> wait) throws IOException, JesException {
if (job.getStatus() == JobStatus.OUTPUT) {
return true;
}
// Status INPUT and ACTIVE might need to be waited for
final List<JobStatus> stati = job.getStatus() == JobStatus.ACTIVE ? singletonList(JobStatus.ACTIVE) : asList(JobStatus.INPUT, JobStatus.ACTIVE);
// Waiting for the status
final Stopwatch stopwatch = new Stopwatch();
for (final JobStatus status : stati) {
while (exists(job, status)) {
if (!stopwatch.waitFor(waiting, timeout, wait)) {
return false;
}
}
}
return true;
}
/**
* FTP Client used by the current JES client instance.
*
* @return FTP client
*/
@java.lang.SuppressWarnings("all")
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
@lombok.Generated
public FTPClient getFtpClient() {
return this.ftpClient;
}
/**
* Current JES spool user
*
* @return JES spool owner
*/
@java.lang.SuppressWarnings("all")
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
@lombok.Generated
public String getJesOwner() {
return this.jesOwner;
}
}