1
2 package de.larssh.jes.parser;
3
4 import static de.larssh.utils.text.Strings.NEW_LINE;
5 import static java.util.stream.Collectors.toList;
6 import java.io.BufferedReader;
7 import java.io.IOException;
8 import java.util.ArrayList;
9 import java.util.Arrays;
10 import java.util.List;
11 import java.util.Optional;
12 import java.util.OptionalInt;
13 import java.util.regex.Matcher;
14 import java.util.regex.Pattern;
15 import org.apache.commons.net.ftp.FTPFileEntryParser;
16 import de.larssh.jes.Job;
17 import de.larssh.jes.JobFlag;
18 import de.larssh.jes.JobOutput;
19 import de.larssh.jes.JobStatus;
20 import de.larssh.utils.Nullables;
21 import de.larssh.utils.Optionals;
22 import de.larssh.utils.text.Lines;
23 import de.larssh.utils.text.Patterns;
24 import de.larssh.utils.text.Strings;
25 import edu.umd.cs.findbugs.annotations.NonNull;
26 import edu.umd.cs.findbugs.annotations.Nullable;
27 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
28
29
30
31
32
33 public class JesFtpFileEntryParser implements FTPFileEntryParser {
34
35
36
37 private static final Pattern PATTERN_TITLE = Pattern.compile("^JOBNAME +JOBID +OWNER +STATUS +CLASS *$", Pattern.CASE_INSENSITIVE);
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 private static final Pattern PATTERN_JOB = Pattern.compile("^(?<name>[^ ]+) +(?<id>[^ ]+) +(?<owner>[^ ]+) +(?<status>(INPUT|ACTIVE|OUTPUT)) +(?<class>[^ ]+)( +(?<rest>.*))?$", Pattern.CASE_INSENSITIVE);
53
54
55
56
57
58
59
60
61
62 private static final Pattern PATTERN_JOB_ABEND = Pattern.compile("ABEND=(?<abend>\\S+)", Pattern.CASE_INSENSITIVE);
63
64
65
66
67
68
69
70
71
72 private static final Pattern PATTERN_JOB_RETURN_CODE = Pattern.compile("RC=(?<returnCode>\\d+)", Pattern.CASE_INSENSITIVE);
73
74
75
76 private static final Pattern PATTERN_GARBAGE = Pattern.compile("^(-+|STEP, PROC, CPUT, and ELAPT unknown) *$", Pattern.CASE_INSENSITIVE);
77
78
79
80 private static final Pattern PATTERN_SUB_TITLE = Pattern.compile("^ {9}ID STEPNAME PROCSTEP C DDNAME (BYTE|REC)-COUNT( COMMENT)? *$", Pattern.CASE_INSENSITIVE);
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95 private static final Pattern PATTERN_JOB_OUTPUT = Pattern.compile("^ {9}(?<index>\\d{3}) (?<step>.{8}) (?<procedureStep>.{8}) (?<class>.) (?<name>.{8}) +(?<length>\\d+) *$");
96
97
98
99 private static final Pattern PATTERN_SPOOL_FILES = Pattern.compile("^\\d+ spool files *$", Pattern.CASE_INSENSITIVE);
100
101
102
103
104
105
106
107 @SuppressWarnings("checkstyle:MultipleStringLiterals")
108 private Job createJob(final String line) {
109 final Matcher matcher = Patterns.matches(PATTERN_JOB, line).orElseThrow(() -> new JesFtpFileEntryParserException("Expected [%s] as job details line, got [%s].", PATTERN_JOB.pattern(), line));
110 final String jobId = matcher.group("id");
111 final String name = matcher.group("name");
112 final JobStatus status = JobStatus.valueOf(matcher.group("status"));
113 final String owner = matcher.group("owner");
114 final Optional<String> jesClass = Optionals.ofNonBlank(matcher.group("class"));
115 final Optional<String> rest = Optionals.ofNonBlank(matcher.group("rest"));
116 final OptionalInt resultCode = Optionals.mapToInt(rest.flatMap(r -> Patterns.find(PATTERN_JOB_RETURN_CODE, r)).map(m -> m.group("returnCode")), Integer::parseInt);
117 final Optional<String> abendCode = rest.flatMap(r -> Patterns.find(PATTERN_JOB_ABEND, r)).map(m -> m.group("abend"));
118 final List<JobFlag> flags = Arrays.stream(JobFlag.values()).filter(flag -> rest.map(r -> Patterns.find(flag.getRestPattern(), r).isPresent()).orElse(Boolean.FALSE)).collect(toList());
119 return new Job(jobId, name, status, owner, jesClass, resultCode, abendCode, flags.toArray(new JobFlag[0]));
120 }
121
122
123
124
125
126
127
128
129
130
131
132
133
134 @SuppressWarnings("PMD.CyclomaticComplexity")
135 private Job createJobAndOutputs(final String listEntry) {
136 final List<String> lines = new ArrayList<>(Lines.lines(listEntry));
137 if (lines.isEmpty()) {
138 throw new JesFtpFileEntryParserException("Expected [%s] as job details line, got no line.", PATTERN_JOB.pattern());
139 }
140
141 final Job job = createJob(lines.remove(0));
142
143 if (!lines.isEmpty()) {
144 final String spoolFiles = lines.get(lines.size() - 1);
145 if (Strings.matches(spoolFiles, PATTERN_SPOOL_FILES)) {
146 lines.remove(lines.size() - 1);
147 }
148 }
149
150 while (!lines.isEmpty() && Strings.matches(lines.get(0), PATTERN_GARBAGE)) {
151 lines.remove(0);
152 }
153
154 if (lines.isEmpty()) {
155 return job;
156 }
157 final String subTitle = lines.remove(0);
158 if (!Strings.matches(subTitle, PATTERN_SUB_TITLE)) {
159 throw new JesFtpFileEntryParserException("Expected [%s] as sub title line, got [%s].", PATTERN_SUB_TITLE.pattern(), subTitle);
160 }
161
162 for (final String line : lines) {
163 createJobOutput(job, line);
164 }
165 return job;
166 }
167
168
169
170
171
172
173
174
175
176 @SuppressWarnings("checkstyle:MultipleStringLiterals")
177 private JobOutput createJobOutput(final Job job, final String line) {
178 final Matcher matcher = Patterns.matches(PATTERN_JOB_OUTPUT, line).orElseThrow(() -> new JesFtpFileEntryParserException("Expected [%s] as job output details line, got [%s].", PATTERN_JOB_OUTPUT.pattern(), line));
179 final int index = Integer.parseInt(matcher.group("index"));
180 final String name = matcher.group("name");
181 final int length = Integer.parseInt(matcher.group("length"));
182 final Optional<String> step = Optionals.ofNonBlank(matcher.group("step"));
183 final Optional<String> procedureStep = Optionals.ofNonBlank(matcher.group("procedureStep"));
184 final Optional<String> jesClass = Optionals.ofNonBlank(matcher.group("class"));
185 return job.createOutput(index, name, length, step, procedureStep, jesClass);
186 }
187
188
189
190
191 @Nullable
192 @Override
193 public JesFtpFile parseFTPEntry(@Nullable final String listEntryNullable) {
194 final String listEntry = Nullables.orElseThrow(listEntryNullable);
195 return new JesFtpFile(createJobAndOutputs(listEntry), listEntry);
196 }
197
198
199
200
201 @Nullable
202 @Override
203 @SuppressWarnings({"checkstyle:SuppressWarnings", "resource"})
204 public String readNextEntry(@Nullable final BufferedReader reader) throws IOException {
205 return Nullables.orElseThrow(reader).readLine();
206 }
207
208
209
210
211 @NonNull
212 @Override
213 @SuppressWarnings("PMD.CyclomaticComplexity")
214 @SuppressFBWarnings(value = "CFS_CONFUSING_FUNCTION_SEMANTICS", justification = "returning input variable as required by interface contract")
215 public List<String> preParse(@Nullable final List<String> originalNullable) {
216 final List<String> original = Nullables.orElseThrow(originalNullable);
217
218 if (original.isEmpty()) {
219 throw new JesFtpFileEntryParserException("Parsing JES job details failed. No line found.");
220 }
221
222 if (!Strings.matches(original.get(0), PATTERN_TITLE)) {
223 throw new JesFtpFileEntryParserException("Parsing JES job details failed. Unexpected first line: [%s].", original.get(0));
224 }
225
226
227
228
229 final List<String> lines = new ArrayList<>();
230 final List<String> linesOfCurrentJob = new ArrayList<>();
231 final int size = original.size();
232 for (int index = 1; index <= size; index += 1) {
233 final boolean isLast = index >= size;
234 if ((isLast || Patterns.matches(PATTERN_JOB, original.get(index)).isPresent()) && !linesOfCurrentJob.isEmpty()) {
235 lines.add(String.join(NEW_LINE, linesOfCurrentJob));
236 linesOfCurrentJob.clear();
237 }
238 if (!isLast) {
239 linesOfCurrentJob.add(original.get(index));
240 }
241 }
242
243 original.clear();
244 original.addAll(lines);
245 return original;
246 }
247
248 @java.lang.SuppressWarnings("all")
249 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
250 @lombok.Generated
251 public JesFtpFileEntryParser() {
252 }
253 }