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 com.entwinemedia.fn.data.json.Jsons.arr;
24 import static com.entwinemedia.fn.data.json.Jsons.f;
25 import static com.entwinemedia.fn.data.json.Jsons.obj;
26 import static com.entwinemedia.fn.data.json.Jsons.v;
27 import static java.time.ZoneOffset.UTC;
28 import static org.apache.commons.lang3.StringUtils.isBlank;
29 import static org.apache.commons.lang3.StringUtils.isNotBlank;
30
31 import org.opencastproject.capture.CaptureParameters;
32 import org.opencastproject.capture.admin.api.Agent;
33 import org.opencastproject.capture.admin.api.CaptureAgentStateService;
34 import org.opencastproject.elasticsearch.api.SearchIndexException;
35 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
36 import org.opencastproject.elasticsearch.index.objects.event.Event;
37 import org.opencastproject.index.service.api.IndexService;
38 import org.opencastproject.mediapackage.MediaPackage;
39 import org.opencastproject.scheduler.api.SchedulerException;
40 import org.opencastproject.scheduler.api.SchedulerService;
41 import org.opencastproject.scheduler.api.TechnicalMetadata;
42 import org.opencastproject.security.api.UnauthorizedException;
43 import org.opencastproject.util.NotFoundException;
44
45 import com.entwinemedia.fn.data.Opt;
46 import com.entwinemedia.fn.data.json.Field;
47 import com.entwinemedia.fn.data.json.JObject;
48 import com.entwinemedia.fn.data.json.JValue;
49
50 import net.fortuna.ical4j.model.property.RRule;
51
52 import org.apache.commons.lang3.StringUtils;
53 import org.json.simple.JSONArray;
54 import org.json.simple.JSONObject;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 import java.time.Instant;
59 import java.time.format.DateTimeFormatter;
60 import java.util.ArrayList;
61 import java.util.Date;
62 import java.util.List;
63 import java.util.Objects;
64 import java.util.Optional;
65 import java.util.TimeZone;
66
67 public final class SchedulingUtils {
68
69
70 private static final Logger logger = LoggerFactory.getLogger(SchedulingUtils.class);
71
72 private static final String JSON_KEY_AGENT_ID = "agent_id";
73 private static final String JSON_KEY_START_DATE = "start";
74 private static final String JSON_KEY_END_DATE = "end";
75 private static final String JSON_KEY_DURATION = "duration";
76 private static final String JSON_KEY_INPUTS = "inputs";
77 private static final String JSON_KEY_RRULE = "rrule";
78
79
80 private SchedulingUtils() {
81 }
82
83 public static class SchedulingInfo {
84 private Opt<Date> startDate = Opt.none();
85 private Opt<Date> endDate = Opt.none();
86 private Opt<Long> duration = Opt.none();
87 private Opt<String> agentId = Opt.none();
88 private Opt<String> inputs = Opt.none();
89 private Opt<RRule> rrule = Opt.none();
90
91 public SchedulingInfo() {
92 }
93
94
95
96
97
98
99
100 public SchedulingInfo(SchedulingInfo other) {
101 this.startDate = other.startDate;
102 this.endDate = other.endDate;
103 this.duration = other.duration;
104 this.agentId = other.agentId;
105 this.inputs = other.inputs;
106 this.rrule = other.rrule;
107 }
108
109 public Opt<Date> getStartDate() {
110 return startDate;
111 }
112
113 public void setStartDate(Opt<Date> startDate) {
114 this.startDate = startDate;
115 }
116
117 public Opt<Date> getEndDate() {
118 if (endDate.isSome()) {
119 return endDate;
120 } else if (startDate.isSome() && duration.isSome()) {
121 return Opt.some(Date.from(startDate.get().toInstant().plusMillis(duration.get())));
122 } else {
123 return Opt.none();
124 }
125 }
126
127 public void setEndDate(Opt<Date> endDate) {
128 this.endDate = endDate;
129 }
130
131 public Opt<Long> getDuration() {
132 if (duration.isSome()) {
133 return duration;
134 } else if (startDate.isSome() && endDate.isSome()) {
135 return Opt.some(endDate.get().getTime() - startDate.get().getTime());
136 } else {
137 return Opt.none();
138 }
139 }
140
141 public void setDuration(Opt<Long> duration) {
142 this.duration = duration;
143 }
144
145 public Opt<String> getAgentId() {
146 return agentId;
147 }
148
149 public void setAgentId(Opt<String> agentId) {
150 this.agentId = agentId;
151 }
152
153 public Opt<String> getInputs() {
154 return inputs;
155 }
156
157 public void setInputs(Opt<String> inputs) {
158 this.inputs = inputs;
159 }
160
161 public Opt<RRule> getRrule() {
162 return rrule;
163 }
164
165 public void setRrule(Opt<RRule> rrule) {
166 this.rrule = rrule;
167 }
168
169
170
171
172 public JObject toJson() {
173 final List<Field> fields = new ArrayList<>();
174 final DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_DATE_TIME;
175 if (startDate.isSome()) {
176 fields.add(f(JSON_KEY_START_DATE, dateFormatter.format(startDate.get().toInstant().atZone(UTC))));
177 }
178 if (endDate.isSome()) {
179 fields.add(f(JSON_KEY_END_DATE, dateFormatter.format(endDate.get().toInstant().atZone(UTC))));
180 }
181 if (agentId.isSome()) {
182 fields.add(f(JSON_KEY_AGENT_ID, agentId.get()));
183 }
184 if (inputs.isSome()) {
185 fields.add(f(JSON_KEY_INPUTS, arr(inputs.get().split(","))));
186 }
187 return obj(fields);
188 }
189
190
191
192
193 @SuppressWarnings("unchecked")
194 public JSONObject toSource() {
195 final JSONObject source = new JSONObject();
196 if (rrule.isSome()) {
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.isSome()) {
204 sourceMetadata.put("start", dateFormatter.format(startDate.get().toInstant().atZone(UTC)));
205 }
206 if (endDate.isSome()) {
207 sourceMetadata.put("end", dateFormatter.format(endDate.get().toInstant().atZone(UTC)));
208 }
209 if (agentId.isSome()) {
210 sourceMetadata.put("device", agentId.get());
211 }
212 if (getDuration().isSome()) {
213 sourceMetadata.put("duration", String.valueOf(getDuration().get()));
214 }
215 if (rrule.isSome()) {
216 sourceMetadata.put("rrule", rrule.get().getValue());
217 }
218 sourceMetadata.put("inputs", inputs.getOr(""));
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.isNone()) {
236 result.startDate = Opt.some(metadata.getStartDate());
237 }
238 if (result.endDate.isNone()) {
239 result.endDate = Opt.some(metadata.getEndDate());
240 }
241 if (result.agentId.isNone()) {
242 result.agentId = Opt.some(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 = Opt.some(Date.from(Instant.from(dateFormatter.parse(startDate))));
269 }
270 if (isNotBlank(endDate)) {
271 schedulingInfo.endDate = Opt.some(Date.from(Instant.from(dateFormatter.parse(endDate))));
272 }
273 if (isNotBlank(agentId)) {
274 schedulingInfo.agentId = Opt.some(agentId);
275 }
276 if (isNotBlank(durationString)) {
277 try {
278 schedulingInfo.duration = Opt.some(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 = Opt.some(String.join(",", inputs));
290 }
291 if (isNotBlank(rrule)) {
292 try {
293 RRule parsedRrule = new RRule(rrule);
294 parsedRrule.validate();
295 schedulingInfo.rrule = Opt.some(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 = Opt.some(technicalMetadata.getStartDate());
327 result.endDate = Opt.some(technicalMetadata.getEndDate());
328 result.agentId = Opt.some(technicalMetadata.getAgentId());
329 String inputs = technicalMetadata.getCaptureAgentConfiguration().get(CaptureParameters.CAPTURE_DEVICE_NAMES);
330 if (isNotBlank(inputs)) {
331 result.inputs = Opt.some(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<JValue> convertConflictingEvents(
358 Optional<String> checkedEventId,
359 List<MediaPackage> mediaPackages,
360 IndexService indexService,
361 ElasticsearchIndex elasticsearchIndex
362 ) throws SearchIndexException {
363 final List<JValue> result = new ArrayList<>();
364 for (MediaPackage mediaPackage : mediaPackages) {
365 final Opt<Event> eventOpt = indexService.getEvent(mediaPackage.getIdentifier().toString(), elasticsearchIndex);
366 if (eventOpt.isSome()) {
367 final Event event = eventOpt.get();
368 if (checkedEventId.isPresent() && checkedEventId.equals(event.getIdentifier())) {
369 continue;
370 }
371 result.add(obj(f("start", v(event.getTechnicalStartTime())), f("end", v(event.getTechnicalEndTime())),
372 f("title", v(event.getTitle()))));
373 } else {
374 logger.warn("Index out of sync! Conflicting event catalog {} not found on event index!",
375 mediaPackage.getIdentifier().toString());
376 }
377 }
378 return result;
379 }
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400 public static List<MediaPackage> getConflictingEvents(
401 SchedulingInfo schedulingInfo,
402 CaptureAgentStateService agentStateService,
403 SchedulerService schedulerService
404 ) throws NotFoundException, UnauthorizedException, SchedulerException {
405
406 if (schedulingInfo.getRrule().isSome()) {
407 final Agent agent = agentStateService.getAgent(schedulingInfo.getAgentId().get());
408 String timezone = agent.getConfiguration().getProperty("capture.device.timezone");
409 if (StringUtils.isBlank(timezone)) {
410 timezone = TimeZone.getDefault().getID();
411 logger.warn("No 'capture.device.timezone' set on agent {}. The default server timezone {} will be used.",
412 schedulingInfo.getAgentId().get(), timezone);
413 }
414 return schedulerService.findConflictingEvents(
415 schedulingInfo.getAgentId().get(),
416 schedulingInfo.getRrule().get(),
417 schedulingInfo.getStartDate().get(),
418 schedulingInfo.getEndDate().get(),
419 schedulingInfo.getDuration().get(),
420 TimeZone.getTimeZone(timezone)
421 );
422 }
423
424 return schedulerService.findConflictingEvents(
425 schedulingInfo.getAgentId().get(),
426 schedulingInfo.getStartDate().get(),
427 schedulingInfo.getEndDate().get()
428 );
429 }
430
431 }