1 // Generated by delombok at Fri Sep 19 22:55:48 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<JobOutput> 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 }