View Javadoc
1   // Generated by delombok at Mon Apr 14 16:48:01 UTC 2025
2   package de.larssh.jes;
3   
4   import static de.larssh.utils.Collectors.toLinkedHashMap;
5   import static de.larssh.utils.Finals.constant;
6   import static de.larssh.utils.function.ThrowingFunction.throwing;
7   import static java.util.Arrays.asList;
8   import static java.util.Arrays.stream;
9   import static java.util.Collections.singletonList;
10  import static java.util.stream.Collectors.toList;
11  import java.io.ByteArrayOutputStream;
12  import java.io.Closeable;
13  import java.io.IOException;
14  import java.io.InputStream;
15  import java.io.StringReader;
16  import java.nio.charset.Charset;
17  import java.nio.charset.StandardCharsets;
18  import java.time.Duration;
19  import java.util.LinkedHashMap;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.Optional;
23  import java.util.function.Consumer;
24  import java.util.function.Function;
25  import java.util.regex.Matcher;
26  import java.util.regex.Pattern;
27  import org.apache.commons.io.input.ReaderInputStream;
28  import org.apache.commons.net.ftp.FTPClient;
29  import org.apache.commons.net.ftp.FTPFile;
30  import org.apache.commons.net.ftp.FTPReply;
31  import de.larssh.jes.parser.JesFtpFile;
32  import de.larssh.jes.parser.JesFtpFileEntryParserFactory;
33  import de.larssh.utils.Nullables;
34  import de.larssh.utils.Optionals;
35  import de.larssh.utils.annotations.SuppressJacocoGenerated;
36  import de.larssh.utils.function.ThrowingConsumer;
37  import de.larssh.utils.text.Patterns;
38  import de.larssh.utils.text.Strings;
39  import de.larssh.utils.time.Stopwatch;
40  import edu.umd.cs.findbugs.annotations.Nullable;
41  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
42  
43  /**
44   * This class allows to handle IBM z/OS JES spools using Java technologies. The
45   * used interface is the IBM z/OS FTP server, that should be available by
46   * default.
47   *
48   * <p>
49   * JES spool entries can be filtered and listed using
50   * {@link #list(String, JobStatus, String)} and
51   * {@link #listFilled(String, JobStatus, String)} methods, while the later one
52   * gathers more information, but takes some more time.
53   *
54   * <p>
55   * {@link #submit(String)} submits JCLs based on the FTP users permissions.
56   * {@link #waitFor(Job, Duration, Duration)} can be used to wait until a job
57   * terminates. Job outputs can be retrieved using {@link #retrieve(JobOutput)}
58   * and removed using {@link #delete(Job)}.
59   *
60   * <p>
61   * <b>Usage example:</b> The following shows the JesClient used inside a
62   * try-with-resource statement. The constructor descriptions describe further
63   * details.
64   *
65   * <pre>
66   * // Connect and login via simplified constructor
67   * try (JesClient jesClient = new JesClient(hostname, port, username, password)) {
68   *
69   *     // Submit JCL
70   *     Job job = jesClient.submit(jclContent);
71   *
72   *     // Wait for job to be finished
73   *     if (!jesClient.waitFor(job)) {
74   *         // Handle the case, a finished job cannot be found inside JES spool any longer
75   *         throw ...;
76   *     }
77   *
78   *     // Gather job status details
79   *     Job detailedJob = jesClient.getJobDetails(job);
80   *
81   *     // Gather finished jobs outputs
82   *     List&lt;JobOutput&gt; jobOutput = jesClient.get(job);
83   *
84   *     // Delete job from JES spool
85   *     jesClient.delete(job);
86   *
87   * // Logout and disconnect using try-with-resource (close method)
88   * }
89   * </pre>
90   *
91   * <p>
92   * In case filtering jobs does not work as expected, check the JES Interface
93   * Level of your server using {@link #getServerProperties()}. This class
94   * requires {@code JESINTERFACELEVEL = 2}. The JES Interface Level can be
95   * configured by a mainframe administrator inside {@code FTP.DATA}.
96   *
97   * @see <a href=
98   *      "https://www.ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/com.ibm.zos.v2r3.halu001/intfjes.htm">IBM
99   *      Knowledge Center - Interfacing with JES</a>
100  */
101 @SuppressWarnings({"PMD.ExcessiveImports", "PMD.GodClass"})
102 public class JesClient implements Closeable {
103 	/**
104 	 * Wildcard value to be used for name and owner filters, meaning "any" value.
105 	 */
106 	public static final String FILTER_WILDCARD = constant("*");
107 	/**
108 	 * Charset, that is used for submitting JCLs and retrieving job outputs.
109 	 */
110 	private static final Charset FTP_DATA_CHARSET = StandardCharsets.UTF_8;
111 	/**
112 	 * Maximum limit of spool entries (including)
113 	 */
114 	public static final int LIST_LIMIT_MAX = constant(1024);
115 	/**
116 	 * Limit of spool entries for {@link #exists(Job, JobStatus)}
117 	 *
118 	 * <p>
119 	 * Checking for existence does not need a limit, but using a limit allows to
120 	 * handle an additional error case.
121 	 */
122 	private static final int LIST_LIMIT_EXISTS = 2;
123 	/**
124 	 * Minimum limit of spool entries (including)
125 	 */
126 	private static final int LIST_LIMIT_MIN = 1;
127 	/**
128 	 * Pattern to find the job ID inside the FTP response after submitting a JCL.
129 	 */
130 	private static final Pattern PATTERN_FTP_SUBMIT_ID = Pattern.compile("^250-IT IS KNOWN TO JES AS (?<id>\\S+)", Pattern.CASE_INSENSITIVE);
131 	/**
132 	 * Pattern to find the job name inside a valid JCL.
133 	 */
134 	private static final Pattern PATTERN_JCL_JOB_NAME = Pattern.compile("^//\\s*(?<name>\\S+)");
135 	/**
136 	 * Pattern to check the response string for the spool entries limit warning.
137 	 */
138 	private static final Pattern PATTERN_LIST_LIMIT = Pattern.compile("^250-JESENTRYLIMIT OF \\d+ REACHED\\. +ADDITIONAL ENTRIES NOT DISPLAYED$", Pattern.CASE_INSENSITIVE);
139 	/**
140 	 * Pattern to check the response string for the empty list warning.
141 	 */
142 	private static final Pattern PATTERN_LIST_NAMES_NO_JOBS_FOUND = Pattern.compile("^550 NO JOBS FOUND FOR ", Pattern.CASE_INSENSITIVE);
143 	/**
144 	 * Pattern to retrieve status values from response strings.
145 	 */
146 	private static final Pattern PATTERN_STATUS = Pattern.compile("^211-(SERVER SITE VARIABLE |TIMER )?(?<key>\\S+)( VALUE)? IS (SET TO )?(?<value>\\S+?)\\.?$", Pattern.CASE_INSENSITIVE);
147 	/**
148 	 * Remote file name that is used when submitting a JCL.
149 	 */
150 	private static final String SUBMIT_REMOTE_FILE_NAME = JesClient.class.getSimpleName() + ".jcl";
151 	/**
152 	 * FTP Client used by the current JES client instance.
153 	 */
154 	private final FTPClient ftpClient;
155 	/**
156 	 * Current JES spool user
157 	 */
158 	private String jesOwner = FILTER_WILDCARD;
159 
160 	/**
161 	 * Expert constructor. This constructor creates a FTP client <b>without</b>
162 	 * connecting and logging in. It is meant to be used in scenarios, which require
163 	 * additional FTP configuration.
164 	 *
165 	 * <p>
166 	 * <b>Usage example 1</b> (using a simplified login)
167 	 *
168 	 * <pre>
169 	 * // Construct the JES client and its internal FTP client
170 	 * try (JesClient jesClient = new JesClient()) {
171 	 *
172 	 *     // Connect via FTP
173 	 *     jesClient.getFtpClient().connect(...);
174 	 *
175 	 *     // Simplified login using the JES client
176 	 *     jesClient.login(...);
177 	 *
178 	 *     ...
179 	 *
180 	 * // Logout and disconnect using try-with-resource (close method)
181 	 * }
182 	 * </pre>
183 	 *
184 	 * <p>
185 	 * <b>Usage example 2:</b> (using a custom login)
186 	 *
187 	 * <pre>
188 	 * // Construct the JES client and its internal FTP client
189 	 * try (JesClient jesClient = new JesClient()) {
190 	 *
191 	 *     // Connect via FTP
192 	 *     jesClient.getFtpClient().connect(...);
193 	 *
194 	 *     // Login via FTP client
195 	 *     jesClient.getFtpClient().login(...);
196 	 *
197 	 *     // Set the JES spool owner
198 	 *     jesClient.setJesOwner(...);
199 	 *
200 	 *     // Enter JES mode of the FTP connection
201 	 *     jesClient.enterJesMode();
202 	 *
203 	 *     ...
204 	 *
205 	 * // Logout and disconnect using try-with-resource (close method)
206 	 * }
207 	 * </pre>
208 	 */
209 	public JesClient() {
210 		ftpClient = new FTPClient();
211 		ftpClient.setParserFactory(new JesFtpFileEntryParserFactory());
212 	}
213 
214 	/**
215 	 * Simplified constructor. This constructor initiates a new FTP connection and
216 	 * logs in using the given credentials.
217 	 *
218 	 * <p>
219 	 * The JesClient can store a JES spool owner. This constructor initializes the
220 	 * JES spool owner using the given username.
221 	 *
222 	 * <p>
223 	 * The default port is {@link org.apache.commons.net.ftp.FTP#DEFAULT_PORT}.
224 	 *
225 	 * <p>
226 	 * <b>Warning:</b> This constructor calls the overridable method
227 	 * {@link #login(String, String)}, which might lead to uninitialized fields when
228 	 * overriding that method.
229 	 *
230 	 * @param hostname FTP hostname
231 	 * @param port     FTP port
232 	 * @param username FTP username and JES spool owner
233 	 * @param password FTP password
234 	 * @throws IOException  Technical FTP failure
235 	 * @throws JesException Logical JES failure
236 	 */
237 	@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
238 	@SuppressJacocoGenerated(justification = "this constructor cannot be mocked nicely")
239 	@SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "PCOA_PARTIALLY_CONSTRUCTED_OBJECT_ACCESS"}, justification = "see JavaDoc")
240 	public JesClient(final String hostname, final int port, final String username, final String password) throws IOException, JesException {
241 		this();
242 		ftpClient.connect(hostname, port);
243 		login(username, password);
244 	}
245 
246 	/**
247 	 * Logs out and disconnects the FTP connection.
248 	 */
249 	@Override
250 	public void close() throws IOException {
251 		try {
252 			if (getFtpClient().isAvailable()) {
253 				getFtpClient().logout();
254 			}
255 		} finally {
256 			if (getFtpClient().isConnected()) {
257 				getFtpClient().disconnect();
258 			}
259 		}
260 	}
261 
262 	/**
263 	 * Removes a given {@code job} from JES spool. This method cares only about the
264 	 * jobs ID.
265 	 *
266 	 * <p>
267 	 * In case you do not already have a {@link Job} object, deleting by job ID
268 	 * works as follows:
269 	 *
270 	 * <pre>
271 	 * String jobId = ...;
272 	 * jesClient.delete(new Job(jobId, JesClient.FILTER_WILDCARD, JobStatus.ALL, JesClient.FILTER_WILDCARD));
273 	 * </pre>
274 	 *
275 	 * @param job Job to be deleted
276 	 * @throws IOException  Technical FTP failure
277 	 * @throws JesException Logical JES failure
278 	 */
279 	public void delete(final Job job) throws IOException, JesException {
280 		if (!getFtpClient().deleteFile(job.getId())) {
281 			throw new JesException(getFtpClient(), "Job [%s] could not be deleted.", job.getId());
282 		}
283 	}
284 
285 	/**
286 	 * Enters the IBM z/OS FTP servers JES file type mode using a SITE command.
287 	 *
288 	 * @throws IOException  Technical FTP failure
289 	 * @throws JesException Logical JES failure
290 	 */
291 	public void enterJesMode() throws IOException, JesException {
292 		if (!getFtpClient().sendSiteCommand("FILEtype=JES")) {
293 			throw new JesException(getFtpClient(), "Failed setting JES mode.");
294 		}
295 	}
296 
297 	/**
298 	 * Reloads the job from server and returns {@code true} if the job is still
299 	 * available and matches the given job status.
300 	 *
301 	 * @param job    the job to search for
302 	 * @param status job status or ALL
303 	 * @return {@code true} if the job is still available
304 	 * @throws IOException  Technical FTP failure
305 	 * @throws JesException Logical JES failure
306 	 */
307 	public boolean exists(final Job job, final JobStatus status) throws IOException, JesException {
308 		setJesFilters(job.getName(), status, job.getOwner(), LIST_LIMIT_EXISTS);
309 		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()));
310 		return Optionals.ofSingle(ids).isPresent();
311 	}
312 
313 	/**
314 	 * Retrieves up-to-date job details for {@code job}. That includes all
315 	 * {@link Job} attributes, including a list of {@link JobOutput} instances for
316 	 * held jobs.
317 	 *
318 	 * @param job job to get up-to-date details for
319 	 * @return job details or {@link Optional#empty()} in case the job is no longer
320 	 *         available inside JES spool
321 	 * @throws IOException  Technical FTP failure
322 	 * @throws JesException Logical JES failure
323 	 */
324 	public Optional<Job> getJobDetails(final Job job) throws IOException, JesException {
325 		setJesFilters(job.getName(), JobStatus.ALL, job.getOwner(), LIST_LIMIT_MAX);
326 		return Optionals.ofSingle( //
327 		stream(getFtpClient().listFiles(job.getId())).filter(JesFtpFile.class::isInstance).map(JesFtpFile.class::cast).map(JesFtpFile::getJob));
328 	}
329 
330 	/**
331 	 * Corrects the result of {@link FTPClient#listNames()} and
332 	 * {@link FTPClient#listNames(String)} as the mainframe FTP server marks empty
333 	 * name listings as error.
334 	 *
335 	 * @param names result of {@link FTPClient#listNames()} and
336 	 *              {@link FTPClient#listNames(String)}
337 	 * @return array of names or {@link Optional#empty()} on real FTP error
338 	 */
339 	@SuppressWarnings("PMD.UseVarargs")
340 	@SuppressFBWarnings(value = "UVA_USE_VAR_ARGS", justification = "No varargs needed as this is for special technical reasons only.")
341 	private Optional<String[]> getListNameResults(@Nullable final String[] names) {
342 		if (names == null) {
343 			return Patterns.find(PATTERN_LIST_NAMES_NO_JOBS_FOUND, getFtpClient().getReplyString()).map(matcher -> new String[0]);
344 		}
345 		return Optional.of(names);
346 	}
347 
348 	/**
349 	 * Retrieves and parses a map of server properties, such as
350 	 * {@code "JESJOBNAME"}, {@code "JESSTATUS"}, {@code "JESOWNER"} and
351 	 * {@code "INTERFACELEVEL"}.
352 	 *
353 	 * @return map of server properties
354 	 * @throws IOException  Technical FTP failure
355 	 * @throws JesException Logical JES failure
356 	 */
357 	public Map<String, String> getServerProperties() throws IOException, JesException {
358 		// Execute STAT command
359 		if (!FTPReply.isPositiveCompletion(getFtpClient().stat())) {
360 			throw new JesException(getFtpClient(), "Failed executing STAT command.");
361 		}
362 		final String[] lines = getFtpClient().getReplyStrings();
363 		// Parse reply strings
364 		final Map<String, String> properties = new LinkedHashMap<>();
365 		for (final String line : lines) {
366 			final Optional<Matcher> matcher = Patterns.matches(PATTERN_STATUS, line);
367 			if (matcher.isPresent()) {
368 				// Key
369 				final String key = matcher.get().group("key");
370 				if (properties.containsKey(key)) {
371 					throw new JesException("Found duplicate status key \"%s\".", key);
372 				}
373 				// Value
374 				properties.put(key, matcher.get().group("value"));
375 			}
376 		}
377 		return properties;
378 	}
379 
380 	/**
381 	 * Returns a list of all job IDs boxed into {@link Job} objects matching the
382 	 * given filters. This method has a much higher performance compared to
383 	 * {@link #listFilled(String)}, though that method fills in additional
384 	 * {@link Job} fields.
385 	 *
386 	 * <p>
387 	 * {@code nameFilter} is allowed to end with the wildcard character "*".
388 	 *
389 	 * <p>
390 	 * JES does not list more than {@link #LIST_LIMIT_MAX} entries. In case more
391 	 * entries are available, a {@link JesLimitReachedException} is thrown,
392 	 * containing all entries up to the limit.
393 	 *
394 	 * @param nameFilter filter by job names
395 	 * @return list of jobs containing job IDs
396 	 * @throws IOException  Technical FTP failure
397 	 * @throws JesException Logical JES failure
398 	 */
399 	public List<Job> list(final String nameFilter) throws IOException, JesException {
400 		return list(nameFilter, JobStatus.ALL);
401 	}
402 
403 	/**
404 	 * Returns a list of all job IDs boxed into {@link Job} objects matching the
405 	 * given filters. This method has a much higher performance compared to
406 	 * {@link #listFilled(String, JobStatus)}, though that method fills in
407 	 * additional {@link Job} fields.
408 	 *
409 	 * <p>
410 	 * {@code nameFilter} is allowed to end with the wildcard character "*".
411 	 *
412 	 * <p>
413 	 * JES does not list more than {@link #LIST_LIMIT_MAX} entries. In case more
414 	 * entries are available, a {@link JesLimitReachedException} is thrown,
415 	 * containing all entries up to the limit.
416 	 *
417 	 * @param nameFilter filter by job names
418 	 * @param status     filter by job status
419 	 * @return list of jobs containing job IDs
420 	 * @throws IOException  Technical FTP failure
421 	 * @throws JesException Logical JES failure
422 	 */
423 	public List<Job> list(final String nameFilter, final JobStatus status) throws IOException, JesException {
424 		return list(nameFilter, status, FILTER_WILDCARD);
425 	}
426 
427 	/**
428 	 * Returns a list of all job IDs boxed into {@link Job} objects matching the
429 	 * given filters. This method has a much higher performance compared to
430 	 * {@link #listFilled(String, JobStatus, String)}, though that method fills in
431 	 * additional {@link Job} fields.
432 	 *
433 	 * <p>
434 	 * {@code nameFilter} and {@code ownerFilter} are allowed to end with the
435 	 * wildcard character "*".
436 	 *
437 	 * <p>
438 	 * JES does not list more than {@link #LIST_LIMIT_MAX} entries. In case more
439 	 * entries are available, a {@link JesLimitReachedException} is thrown,
440 	 * containing all entries up to the limit.
441 	 *
442 	 * @param nameFilter  filter by job names
443 	 * @param status      filter by job status
444 	 * @param ownerFilter filter by job owner
445 	 * @return list of jobs containing job IDs
446 	 * @throws IOException  Technical FTP failure
447 	 * @throws JesException Logical JES failure
448 	 */
449 	public List<Job> list(final String nameFilter, final JobStatus status, final String ownerFilter) throws IOException, JesException {
450 		return list(nameFilter, status, ownerFilter, LIST_LIMIT_MAX);
451 	}
452 
453 	/**
454 	 * Returns a list of all job IDs boxed into {@link Job} objects matching the
455 	 * given filters. This method has a much higher performance compared to
456 	 * {@link #listFilled(String, JobStatus, String, int)}, though that method fills
457 	 * in additional {@link Job} fields.
458 	 *
459 	 * <p>
460 	 * {@code nameFilter} and {@code ownerFilter} are allowed to end with the
461 	 * wildcard character "*".
462 	 *
463 	 * <p>
464 	 * JES does not list more than {@code limit} entries. In case more entries are
465 	 * available, a {@link JesLimitReachedException} is thrown, containing all
466 	 * entries up to the limit. {@code limit} can be from {@link #LIST_LIMIT_MIN}
467 	 * (including) to {@link #LIST_LIMIT_MAX} (including).
468 	 *
469 	 * @param nameFilter  filter by job names
470 	 * @param status      filter by job status
471 	 * @param ownerFilter filter by job owner
472 	 * @param limit       limit of spool entries
473 	 * @return list of jobs containing job IDs
474 	 * @throws IOException  Technical FTP failure
475 	 * @throws JesException Logical JES failure
476 	 */
477 	public List<Job> list(final String nameFilter, final JobStatus status, final String ownerFilter, final int limit) throws IOException, JesException {
478 		setJesFilters(nameFilter, status, ownerFilter, limit);
479 		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."));
480 		return throwIfLimitReached(limit, stream(ids).map(id -> new Job(id, nameFilter, status, ownerFilter)).collect(toList()));
481 	}
482 
483 	/**
484 	 * Returns a list of all {@link Job} objects matching the given filters. This
485 	 * method has a worse performance compared to {@link #list(String)}, though it
486 	 * fills in additional {@link Job} fields.
487 	 *
488 	 * <p>
489 	 * {@code nameFilter} is allowed to end with the wildcard character "*".
490 	 *
491 	 * <p>
492 	 * JES does not list more than {@link #LIST_LIMIT_MAX} entries. In case more
493 	 * entries are available, a {@link JesLimitReachedException} is thrown,
494 	 * containing all entries up to the limit.
495 	 *
496 	 * @param nameFilter filter by job names
497 	 * @return list of jobs
498 	 * @throws IOException  Technical FTP failure
499 	 * @throws JesException Logical JES failure
500 	 */
501 	public List<Job> listFilled(final String nameFilter) throws IOException, JesException {
502 		return listFilled(nameFilter, JobStatus.ALL);
503 	}
504 
505 	/**
506 	 * Returns a list of all {@link Job} objects matching the given filters. This
507 	 * method has a worse performance compared to {@link #list(String, JobStatus)},
508 	 * though it fills in additional {@link Job} fields.
509 	 *
510 	 * <p>
511 	 * {@code nameFilter} is allowed to end with the wildcard character "*".
512 	 *
513 	 * <p>
514 	 * JES does not list more than {@link #LIST_LIMIT_MAX} entries. In case more
515 	 * entries are available, a {@link JesLimitReachedException} is thrown,
516 	 * containing all entries up to the limit.
517 	 *
518 	 * @param nameFilter filter by job names
519 	 * @param status     filter by job status
520 	 * @return list of jobs
521 	 * @throws IOException  Technical FTP failure
522 	 * @throws JesException Logical JES failure
523 	 */
524 	public List<Job> listFilled(final String nameFilter, final JobStatus status) throws IOException, JesException {
525 		return listFilled(nameFilter, status, FILTER_WILDCARD);
526 	}
527 
528 	/**
529 	 * Returns a list of all {@link Job} objects matching the given filters. This
530 	 * method has a worse performance compared to
531 	 * {@link #list(String, JobStatus, String)}, though it fills in additional
532 	 * {@link Job} fields.
533 	 *
534 	 * <p>
535 	 * {@code nameFilter} and {@code ownerFilter} are allowed to end with the
536 	 * wildcard character "*".
537 	 *
538 	 * <p>
539 	 * JES does not list more than {@link #LIST_LIMIT_MAX} entries. In case more
540 	 * entries are available, a {@link JesLimitReachedException} is thrown,
541 	 * containing all entries up to the limit.
542 	 *
543 	 * @param nameFilter  filter by job names
544 	 * @param status      filter by job status
545 	 * @param ownerFilter filter by job owner
546 	 * @return list of jobs
547 	 * @throws IOException  Technical FTP failure
548 	 * @throws JesException Logical JES failure
549 	 */
550 	public List<Job> listFilled(final String nameFilter, final JobStatus status, final String ownerFilter) throws IOException, JesException {
551 		return listFilled(nameFilter, status, ownerFilter, LIST_LIMIT_MAX);
552 	}
553 
554 	/**
555 	 * Returns a list of all {@link Job} objects matching the given filters. This
556 	 * method has a worse performance compared to
557 	 * {@link #list(String, JobStatus, String)}, though it fills in additional
558 	 * {@link Job} fields.
559 	 *
560 	 * <p>
561 	 * {@code nameFilter} and {@code ownerFilter} are allowed to end with the
562 	 * wildcard character "*".
563 	 *
564 	 * <p>
565 	 * JES does not list more than {@code limit} entries. In case more entries are
566 	 * available, a {@link JesLimitReachedException} is thrown, containing all
567 	 * entries up to the limit. {@code limit} can be from {@link #LIST_LIMIT_MIN}
568 	 * (including) to {@link #LIST_LIMIT_MAX} (including).
569 	 *
570 	 * @param nameFilter  filter by job names
571 	 * @param status      filter by job status
572 	 * @param ownerFilter filter by job owner
573 	 * @param limit       limit of spool entries
574 	 * @return list of jobs
575 	 * @throws IOException  Technical FTP failure
576 	 * @throws JesException Logical JES failure
577 	 */
578 	public List<Job> listFilled(final String nameFilter, final JobStatus status, final String ownerFilter, final int limit) throws IOException, JesException {
579 		setJesFilters(nameFilter, status, ownerFilter, limit);
580 		final FTPFile[] files = getFtpClient().listFiles();
581 		return throwIfLimitReached(limit, stream(files).filter(JesFtpFile.class::isInstance).map(JesFtpFile.class::cast).map(JesFtpFile::getJob).collect(toList()));
582 	}
583 
584 	/**
585 	 * Shortcut method to perform a FTP login, set the internal JES owner and enter
586 	 * JES mode.
587 	 *
588 	 * <p>
589 	 * Is similar to the following lines of code.
590 	 *
591 	 * <pre>
592 	 * // Login via FTP client
593 	 * jesClient.getFtpClient().login(...);
594 	 *
595 	 * // Set the JES spool owner
596 	 * jesClient.setJesOwner(...);
597 	 *
598 	 * // Enter JES mode of the FTP connection
599 	 * jesClient.enterJesMode();
600 	 * </pre>
601 	 *
602 	 * @param username the user id to be used for FTP login and internal JES owner
603 	 * @param password the users password
604 	 * @throws IOException  Technical FTP failure
605 	 * @throws JesException Logical JES failure
606 	 */
607 	public void login(final String username, final String password) throws IOException, JesException {
608 		if (!getFtpClient().login(username, password)) {
609 			throw new JesException(getFtpClient(), "Could not login user [%s].", username);
610 		}
611 		setJesOwner(username);
612 		enterJesMode();
613 	}
614 
615 	/**
616 	 * Retrieves the content of {@code jobOutput}.
617 	 *
618 	 * @param jobOutput job output to be requested
619 	 * @return content of the specified job output
620 	 * @throws IOException  Technical FTP failure
621 	 * @throws JesException Logical JES failure
622 	 */
623 	public String retrieve(final JobOutput jobOutput) throws IOException, JesException {
624 		final String fileName = Strings.format("%s.%d", jobOutput.getJob().getId(), jobOutput.getIndex());
625 		try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
626 			if (!getFtpClient().retrieveFile(fileName, outputStream)) {
627 				throw new JesException(getFtpClient(), "Could not retrieve data of job output [%s.%s].", jobOutput.getJob().getId(), jobOutput.getStep());
628 			}
629 			return new String(outputStream.toByteArray(), FTP_DATA_CHARSET);
630 		}
631 	}
632 
633 	/**
634 	 * Retrieves all job outputs of {@code job}.
635 	 *
636 	 * @param job job to request all outputs of
637 	 * @return map with job output details and the corresponding content in specific
638 	 *         order
639 	 * @throws IOException  Technical FTP failure
640 	 * @throws JesException Logical JES failure
641 	 */
642 	public Map<JobOutput, String> retrieveOutputs(final Job job) throws IOException, JesException {
643 		if (job.getOutputs().isEmpty()) {
644 			return retrieveOutputs(getJobDetails(job).orElseThrow(() -> new JesException("Job [%s] is not available.", job.getId())));
645 		}
646 		return job.getOutputs().stream().collect(toLinkedHashMap(Function.identity(), throwing(this::retrieve)));
647 	}
648 
649 	/**
650 	 * Sends {@link org.apache.commons.net.ftp.FTPCmd#SITE} commands to set the
651 	 * given filter values.
652 	 *
653 	 * <p>
654 	 * {@code nameFilter} and {@code ownerFilter} are allowed to end with the
655 	 * wildcard character "*".
656 	 *
657 	 * <p>
658 	 * {@code limit} can be from {@link #LIST_LIMIT_MIN} (including) to
659 	 * {@link #LIST_LIMIT_MAX} (including). While that restriction is not checked by
660 	 * this method, values outside that range might result in a server side error
661 	 * message thrown as {@link JesException}.
662 	 *
663 	 * @param nameFilter  filter by job names
664 	 * @param status      filter by job status
665 	 * @param ownerFilter filter by job owner
666 	 * @param limit       limit of spool entries
667 	 * @throws IOException  Technical FTP failure
668 	 * @throws JesException Logical JES failure
669 	 */
670 	protected void setJesFilters(final String nameFilter, final JobStatus status, final String ownerFilter, final int limit) throws IOException, JesException {
671 		if (!getFtpClient().sendSiteCommand("JESJOBName=" + nameFilter)) {
672 			throw new JesException(getFtpClient(), "Failed setting JES job name filter to [%s].", nameFilter);
673 		}
674 		if (!getFtpClient().sendSiteCommand("JESOwner=" + ownerFilter)) {
675 			throw new JesException(getFtpClient(), "Failed setting JES job owner filter to [%s].", ownerFilter);
676 		}
677 		if (!getFtpClient().sendSiteCommand("JESSTatus=" + status.getValue())) {
678 			throw new JesException(getFtpClient(), "Failed setting JES job status filter to [%s].", status.getValue());
679 		}
680 		if (!getFtpClient().sendSiteCommand("JESENTRYLIMIT=" + limit)) {
681 			throw new JesException(getFtpClient(), "Failed setting JES entry limit to %d. Minimum/Maximum: %d/%d", limit, LIST_LIMIT_MIN, LIST_LIMIT_MAX);
682 		}
683 	}
684 
685 	/**
686 	 * Current JES spool user
687 	 *
688 	 * @param jesOwner JES spool owner
689 	 */
690 	public void setJesOwner(final String jesOwner) {
691 		this.jesOwner = Strings.toUpperCaseNeutral(jesOwner).trim();
692 	}
693 
694 	/**
695 	 * Submits the given JCL and returns a related {@link Job} object containing the
696 	 * started jobs ID.
697 	 *
698 	 * <p>
699 	 * In addition to the jobs ID this method tries to extract the jobs name from
700 	 * the given JCL. The returned owner is set to the internal JES owner, which can
701 	 * be set using {@link #setJesOwner(String)}.
702 	 *
703 	 * @param jclContent JCL to submit
704 	 * @return {@link Job} object containing the started jobs ID
705 	 * @throws IOException  Technical FTP failure
706 	 * @throws JesException Logical JES failure
707 	 */
708 	public Job submit(final String jclContent) throws IOException, JesException {
709 		try (InputStream inputStream = ReaderInputStream.builder().setReader(new StringReader(jclContent)).setCharset(FTP_DATA_CHARSET).get()) {
710 			if (!getFtpClient().storeUniqueFile(SUBMIT_REMOTE_FILE_NAME, inputStream)) {
711 				throw new JesException(getFtpClient(), "Submitting JCL failed.");
712 			}
713 		}
714 		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."));
715 		final String name = Patterns.find(PATTERN_JCL_JOB_NAME, jclContent).map(matcher -> matcher.group("name")).orElse(FILTER_WILDCARD);
716 		return new Job(jobId, name, JobStatus.INPUT, getJesOwner());
717 	}
718 
719 	/**
720 	 * In case the last FTP responses string contains the spool entries limit
721 	 * warning, a {@link JesLimitReachedException} is thrown, else {@code jobs} are
722 	 * returned.
723 	 *
724 	 * <p>
725 	 * The thrown exception contains the current spool entries limit and all
726 	 * entries, which were read already.
727 	 *
728 	 * @param limit current spool entries limit
729 	 * @param jobs  list of jobs
730 	 * @return {@code jobs} in case the spool entries limit is not reached
731 	 * @throws JesLimitReachedException if the last FTP responses string contains
732 	 *                                  the spool entries limit warning
733 	 */
734 	protected List<Job> throwIfLimitReached(final int limit, final List<Job> jobs) throws JesLimitReachedException {
735 		if (Strings.find(getFtpClient().getReplyString(), PATTERN_LIST_LIMIT)) {
736 			throw new JesLimitReachedException(limit, jobs, getFtpClient());
737 		}
738 		return jobs;
739 	}
740 
741 	/**
742 	 * Waits for {@code job} to be finished using {@code Thread#sleep(long)} for
743 	 * waiting between {@link #exists(Job, JobStatus)} calls and timing out after a
744 	 * given duration. {@code waiting} allows to specify the duration to wait.
745 	 *
746 	 * <p>
747 	 * The given jobs status specifies, which status are waited for:
748 	 * <ul>
749 	 * <li>{@link JobStatus#ALL}: waiting for {@link JobStatus#INPUT} and
750 	 * {@link JobStatus#ACTIVE}
751 	 * <li>{@link JobStatus#INPUT}: waiting for {@link JobStatus#INPUT} and
752 	 * {@link JobStatus#ACTIVE}
753 	 * <li>{@link JobStatus#ACTIVE}: waiting for {@link JobStatus#ACTIVE} only
754 	 * <li>{@link JobStatus#OUTPUT}: returning {@code true} with no checks and
755 	 * without waiting
756 	 * </ul>
757 	 *
758 	 * @param job     the job to wait for
759 	 * @param waiting duration to wait
760 	 * @param timeout timeout duration
761 	 * @return {@code true} if the job finished and {@code false} if the timeout has
762 	 *         been reached
763 	 * @throws InterruptedException if any thread has interrupted the current thread
764 	 * @throws IOException          Technical FTP failure
765 	 * @throws JesException         Logical JES failure
766 	 */
767 	@SuppressWarnings({"unused", "PMD.DoNotUseThreads"})
768 	public boolean waitFor(final Job job, final Duration waiting, final Duration timeout) throws InterruptedException, IOException, JesException {
769 		return waitFor(job, waiting, timeout, ThrowingConsumer.throwing(duration -> Thread.sleep(Nullables.orElseThrow(duration).toMillis())));
770 	}
771 
772 	/**
773 	 * Waits for {@code job} to be finished using {@code wait} for waiting between
774 	 * {@link #exists(Job, JobStatus)} calls and timing out after a given duration.
775 	 * {@code waiting} allows to specify the duration to wait.
776 	 *
777 	 * <p>
778 	 * The given jobs status specifies, which status are waited for:
779 	 * <ul>
780 	 * <li>{@link JobStatus#ALL}: waiting for {@link JobStatus#INPUT} and
781 	 * {@link JobStatus#ACTIVE}
782 	 * <li>{@link JobStatus#INPUT}: waiting for {@link JobStatus#INPUT} and
783 	 * {@link JobStatus#ACTIVE}
784 	 * <li>{@link JobStatus#ACTIVE}: waiting for {@link JobStatus#ACTIVE} only
785 	 * <li>{@link JobStatus#OUTPUT}: returning {@code true} with no checks and
786 	 * without waiting
787 	 * </ul>
788 	 *
789 	 * @param job     the job to wait for
790 	 * @param waiting duration to wait
791 	 * @param timeout timeout duration
792 	 * @param wait    method to use for waiting
793 	 * @return {@code true} if the job finished and {@code false} if the timeout has
794 	 *         been reached
795 	 * @throws IOException  Technical FTP failure
796 	 * @throws JesException Logical JES failure
797 	 */
798 	public boolean waitFor(final Job job, final Duration waiting, final Duration timeout, final Consumer<Duration> wait) throws IOException, JesException {
799 		if (job.getStatus() == JobStatus.OUTPUT) {
800 			return true;
801 		}
802 		// Status INPUT and ACTIVE might need to be waited for
803 		final List<JobStatus> stati = job.getStatus() == JobStatus.ACTIVE ? singletonList(JobStatus.ACTIVE) : asList(JobStatus.INPUT, JobStatus.ACTIVE);
804 		// Waiting for the status
805 		final Stopwatch stopwatch = new Stopwatch();
806 		for (final JobStatus status : stati) {
807 			while (exists(job, status)) {
808 				if (!stopwatch.waitFor(waiting, timeout, wait)) {
809 					return false;
810 				}
811 			}
812 		}
813 		return true;
814 	}
815 
816 	/**
817 	 * FTP Client used by the current JES client instance.
818 	 *
819 	 * @return FTP client
820 	 */
821 	@java.lang.SuppressWarnings("all")
822 	@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
823 	@lombok.Generated
824 	public FTPClient getFtpClient() {
825 		return this.ftpClient;
826 	}
827 
828 	/**
829 	 * Current JES spool user
830 	 *
831 	 * @return JES spool owner
832 	 */
833 	@java.lang.SuppressWarnings("all")
834 	@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
835 	@lombok.Generated
836 	public String getJesOwner() {
837 		return this.jesOwner;
838 	}
839 }