1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.opencastproject.external.util;
22
23 import static java.time.ZoneOffset.UTC;
24 import static org.apache.commons.lang3.StringUtils.isBlank;
25 import static org.apache.commons.lang3.StringUtils.isNotBlank;
26
27 import org.opencastproject.capture.CaptureParameters;
28 import org.opencastproject.capture.admin.api.Agent;
29 import org.opencastproject.capture.admin.api.CaptureAgentStateService;
30 import org.opencastproject.elasticsearch.api.SearchIndexException;
31 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
32 import org.opencastproject.elasticsearch.index.objects.event.Event;
33 import org.opencastproject.index.service.api.IndexService;
34 import org.opencastproject.mediapackage.MediaPackage;
35 import org.opencastproject.scheduler.api.SchedulerException;
36 import org.opencastproject.scheduler.api.SchedulerService;
37 import org.opencastproject.scheduler.api.TechnicalMetadata;
38 import org.opencastproject.security.api.UnauthorizedException;
39 import org.opencastproject.util.NotFoundException;
40
41 import com.google.gson.JsonArray;
42 import com.google.gson.JsonObject;
43 import com.google.gson.JsonPrimitive;
44
45 import net.fortuna.ical4j.model.property.RRule;
46
47 import org.apache.commons.lang3.StringUtils;
48 import org.json.simple.JSONArray;
49 import org.json.simple.JSONObject;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import java.time.Instant;
54 import java.time.ZoneOffset;
55 import java.time.format.DateTimeFormatter;
56 import java.util.ArrayList;
57 import java.util.Date;
58 import java.util.List;
59 import java.util.Objects;
60 import java.util.Optional;
61 import java.util.TimeZone;
62
63 public final class SchedulingUtils {
64
65
66 private static final Logger logger = LoggerFactory.getLogger(SchedulingUtils.class);
67
68 private static final String JSON_KEY_AGENT_ID = "agent_id";
69 private static final String JSON_KEY_START_DATE = "start";
70 private static final String JSON_KEY_END_DATE = "end";
71 private static final String JSON_KEY_DURATION = "duration";
72 private static final String JSON_KEY_INPUTS = "inputs";
73 private static final String JSON_KEY_RRULE = "rrule";
74
75
76 private SchedulingUtils() {
77 }
78
79 public static class SchedulingInfo {
80 private Optional<Date> startDate = Optional.empty();
81 private Optional<Date> endDate = Optional.empty();
82 private Optional<Long> duration = Optional.empty();
83 private Optional<String> agentId = Optional.empty();
84 private Optional<String> inputs = Optional.empty();
85 private Optional<RRule> rrule = Optional.empty();
86
87 public SchedulingInfo() {
88 }
89
90
91
92
93
94
95
96 public SchedulingInfo(SchedulingInfo other) {
97 this.startDate = other.startDate;
98 this.endDate = other.endDate;
99 this.duration = other.duration;
100 this.agentId = other.agentId;
101 this.inputs = other.inputs;
102 this.rrule = other.rrule;
103 }
104
105 public Optional<Date> getStartDate() {
106 return startDate;
107 }
108
109 public void setStartDate(Optional<Date> startDate) {
110 this.startDate = startDate;
111 }
112
113 public Optional<Date> getEndDate() {
114 if (endDate.isPresent()) {
115 return endDate;
116 } else if (startDate.isPresent() && duration.isPresent()) {
117 return Optional.of(Date.from(startDate.get().toInstant().plusMillis(duration.get())));
118 } else {
119 return Optional.empty();
120 }
121 }
122
123 public void setEndDate(Optional<Date> endDate) {
124 this.endDate = endDate;
125 }
126
127 public Optional<Long> getDuration() {
128 if (duration.isPresent()) {
129 return duration;
130 } else if (startDate.isPresent() && endDate.isPresent()) {
131 return Optional.of(endDate.get().getTime() - startDate.get().getTime());
132 } else {
133 return Optional.empty();
134 }
135 }
136
137 public void setDuration(Optional<Long> duration) {
138 this.duration = duration;
139 }
140
141 public Optional<String> getAgentId() {
142 return agentId;
143 }
144
145 public void setAgentId(Optional<String> agentId) {
146 this.agentId = agentId;
147 }
148
149 public Optional<String> getInputs() {
150 return inputs;
151 }
152
153 public void setInputs(Optional<String> inputs) {
154 this.inputs = inputs;
155 }
156
157 public Optional<RRule> getRrule() {
158 return rrule;
159 }
160
161 public void setRrule(Optional<RRule> rrule) {
162 this.rrule = rrule;
163 }
164
165
166
167
168 public JsonObject toJson() {
169 JsonObject json = new JsonObject();
170 DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_DATE_TIME;
171 if (startDate.isPresent()) {
172 json.addProperty(JSON_KEY_START_DATE, dateFormatter.format(startDate.get().toInstant().atZone(ZoneOffset.UTC)));
173 }
174 if (endDate.isPresent()) {
175 json.addProperty(JSON_KEY_END_DATE, dateFormatter.format(endDate.get().toInstant().atZone(ZoneOffset.UTC)));
176 }
177 if (agentId.isPresent()) {
178 json.addProperty(JSON_KEY_AGENT_ID, agentId.get());
179 }
180 if (inputs.isPresent()) {
181 JsonArray inputsArray = new JsonArray();
182 for (String input : inputs.get().split(",")) {
183 inputsArray.add(new JsonPrimitive(input.trim()));
184 }
185 json.add(JSON_KEY_INPUTS, inputsArray);
186 }
187 return json;
188 }
189
190
191
192
193 @SuppressWarnings("unchecked")
194 public JSONObject toSource() {
195 final JSONObject source = new JSONObject();
196 if (rrule.isPresent()) {
197 source.put("type", "SCHEDULE_MULTIPLE");
198 } else {
199 source.put("type", "SCHEDULE_SINGLE");
200 }
201 final JSONObject sourceMetadata = new JSONObject();
202 final DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_DATE_TIME;
203 if (startDate.isPresent()) {
204 sourceMetadata.put("start", dateFormatter.format(startDate.get().toInstant().atZone(UTC)));
205 }
206 if (endDate.isPresent()) {
207 sourceMetadata.put("end", dateFormatter.format(endDate.get().toInstant().atZone(UTC)));
208 }
209 if (agentId.isPresent()) {
210 sourceMetadata.put("device", agentId.get());
211 }
212 if (getDuration().isPresent()) {
213 sourceMetadata.put("duration", String.valueOf(getDuration().get()));
214 }
215 if (rrule.isPresent()) {
216 sourceMetadata.put("rrule", rrule.get().getValue());
217 }
218 sourceMetadata.put("inputs", inputs.orElse(""));
219
220 source.put("metadata", sourceMetadata);
221 return source;
222 }
223
224
225
226
227
228
229
230
231
232
233 public SchedulingInfo merge(TechnicalMetadata metadata) {
234 SchedulingInfo result = new SchedulingInfo(this);
235 if (result.startDate.isEmpty()) {
236 result.startDate = Optional.of(metadata.getStartDate());
237 }
238 if (result.endDate.isEmpty()) {
239 result.endDate = Optional.of(metadata.getEndDate());
240 }
241 if (result.agentId.isEmpty()) {
242 result.agentId = Optional.of(metadata.getAgentId());
243 }
244 return result;
245 }
246
247
248
249
250
251
252
253
254
255 public static SchedulingInfo of(JSONObject json) {
256 final DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_DATE_TIME;
257 final SchedulingInfo schedulingInfo = new SchedulingInfo();
258 final String startDate = (String) json.get(JSON_KEY_START_DATE);
259 final String endDate = (String) json.get(JSON_KEY_END_DATE);
260 final String agentId = (String) json.get(JSON_KEY_AGENT_ID);
261 final JSONArray inputs = (JSONArray) json.get(JSON_KEY_INPUTS);
262 final String rrule = (String) json.get(JSON_KEY_RRULE);
263
264
265 final String durationString = Objects.toString(json.get(JSON_KEY_DURATION), null);
266
267 if (isNotBlank(startDate)) {
268 schedulingInfo.startDate = Optional.of(Date.from(Instant.from(dateFormatter.parse(startDate))));
269 }
270 if (isNotBlank(endDate)) {
271 schedulingInfo.endDate = Optional.of(Date.from(Instant.from(dateFormatter.parse(endDate))));
272 }
273 if (isNotBlank(agentId)) {
274 schedulingInfo.agentId = Optional.of(agentId);
275 }
276 if (isNotBlank(durationString)) {
277 try {
278 schedulingInfo.duration = Optional.of(Long.parseLong(durationString));
279 } catch (Exception e) {
280 throw new IllegalArgumentException("Invalid format of field 'duration'");
281 }
282 }
283
284 if (isBlank(endDate) && isBlank(durationString)) {
285 throw new IllegalArgumentException("Either 'end' or 'duration' must be specified");
286 }
287
288 if (inputs != null) {
289 schedulingInfo.inputs = Optional.of(String.join(",", inputs));
290 }
291 if (isNotBlank(rrule)) {
292 try {
293 RRule parsedRrule = new RRule(rrule);
294 parsedRrule.validate();
295 schedulingInfo.rrule = Optional.of(parsedRrule);
296 } catch (Exception e) {
297 throw new IllegalArgumentException("Invalid RRule: " + rrule);
298 }
299 if (isBlank(durationString) || isBlank(startDate) || isBlank(endDate)) {
300 throw new IllegalArgumentException("'start', 'end' and 'duration' must be specified when 'rrule' is specified");
301 }
302 }
303 return schedulingInfo;
304 }
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321 public static SchedulingInfo of(String eventId, SchedulerService schedulerService)
322 throws UnauthorizedException, SchedulerException {
323 final SchedulingInfo result = new SchedulingInfo();
324 try {
325 final TechnicalMetadata technicalMetadata = schedulerService.getTechnicalMetadata(eventId);
326 result.startDate = Optional.of(technicalMetadata.getStartDate());
327 result.endDate = Optional.of(technicalMetadata.getEndDate());
328 result.agentId = Optional.of(technicalMetadata.getAgentId());
329 String inputs = technicalMetadata.getCaptureAgentConfiguration().get(CaptureParameters.CAPTURE_DEVICE_NAMES);
330 if (isNotBlank(inputs)) {
331 result.inputs = Optional.of(inputs);
332 }
333 return result;
334 } catch (NotFoundException e) {
335 return result;
336 }
337 }
338 }
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357 public static List<JsonObject> convertConflictingEvents(
358 Optional<String> checkedEventId,
359 List<MediaPackage> mediaPackages,
360 IndexService indexService,
361 ElasticsearchIndex elasticsearchIndex
362 ) throws SearchIndexException {
363 List<JsonObject> result = new ArrayList<>();
364 for (MediaPackage mediaPackage : mediaPackages) {
365 Optional<Event> eventOpt = indexService.getEvent(mediaPackage.getIdentifier().toString(), elasticsearchIndex);
366 if (eventOpt.isPresent()) {
367 final Event event = eventOpt.get();
368 if (checkedEventId.isPresent() && checkedEventId.get().equals(event.getIdentifier())) {
369 continue;
370 }
371
372 JsonObject eventJson = new JsonObject();
373 if (event.getTechnicalStartTime() != null)
374 eventJson.addProperty("start", event.getTechnicalStartTime().toString());
375 if (event.getTechnicalEndTime() != null)
376 eventJson.addProperty("end", event.getTechnicalEndTime().toString());
377 eventJson.addProperty("title", event.getTitle());
378
379 result.add(eventJson);
380 } else {
381 logger.warn("Index out of sync! Conflicting event catalog {} not found on event index!",
382 mediaPackage.getIdentifier().toString());
383 }
384 }
385 return result;
386 }
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407 public static List<MediaPackage> getConflictingEvents(
408 SchedulingInfo schedulingInfo,
409 CaptureAgentStateService agentStateService,
410 SchedulerService schedulerService
411 ) throws NotFoundException, UnauthorizedException, SchedulerException {
412
413 if (schedulingInfo.getRrule().isPresent()) {
414 final Agent agent = agentStateService.getAgent(schedulingInfo.getAgentId().get());
415 String timezone = agent.getConfiguration().getProperty("capture.device.timezone");
416 if (StringUtils.isBlank(timezone)) {
417 timezone = TimeZone.getDefault().getID();
418 logger.warn("No 'capture.device.timezone' set on agent {}. The default server timezone {} will be used.",
419 schedulingInfo.getAgentId().get(), timezone);
420 }
421 return schedulerService.findConflictingEvents(
422 schedulingInfo.getAgentId().get(),
423 schedulingInfo.getRrule().get(),
424 schedulingInfo.getStartDate().get(),
425 schedulingInfo.getEndDate().get(),
426 schedulingInfo.getDuration().get(),
427 TimeZone.getTimeZone(timezone)
428 );
429 }
430
431 return schedulerService.findConflictingEvents(
432 schedulingInfo.getAgentId().get(),
433 schedulingInfo.getStartDate().get(),
434 schedulingInfo.getEndDate().get()
435 );
436 }
437
438 }