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<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 }