1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.adminui.util;
23
24 import static org.opencastproject.adminui.endpoint.AbstractEventEndpoint.SCHEDULING_AGENT_ID_KEY;
25 import static org.opencastproject.adminui.endpoint.AbstractEventEndpoint.SCHEDULING_END_KEY;
26 import static org.opencastproject.adminui.endpoint.AbstractEventEndpoint.SCHEDULING_START_KEY;
27
28 import org.opencastproject.elasticsearch.api.SearchIndexException;
29 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
30 import org.opencastproject.elasticsearch.index.objects.event.Event;
31 import org.opencastproject.index.service.api.IndexService;
32 import org.opencastproject.index.service.catalog.adapter.events.CommonEventCatalogUIAdapter;
33 import org.opencastproject.mediapackage.MediaPackageElements;
34
35 import org.json.simple.JSONArray;
36 import org.json.simple.JSONObject;
37 import org.json.simple.parser.JSONParser;
38 import org.json.simple.parser.ParseException;
39
40 import java.time.DayOfWeek;
41 import java.time.Duration;
42 import java.time.Instant;
43 import java.time.ZoneId;
44 import java.time.ZoneOffset;
45 import java.time.ZonedDateTime;
46 import java.time.format.DateTimeFormatter;
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.Collection;
50 import java.util.List;
51 import java.util.Optional;
52
53
54
55
56
57 public final class BulkUpdateUtil {
58
59 private static final JSONParser parser = new JSONParser();
60
61 private BulkUpdateUtil() {
62 }
63
64
65
66
67
68
69
70
71
72
73 public static Optional<Event> getEvent(
74 final IndexService indexSvc,
75 final ElasticsearchIndex index,
76 final String id) {
77 try {
78 final Event event = indexSvc.getEvent(id, index).orNull();
79 return Optional.ofNullable(event);
80 } catch (SearchIndexException e) {
81 throw new RuntimeException(e);
82 }
83 }
84
85
86
87
88
89
90
91
92
93 @SuppressWarnings("unchecked")
94 public static JSONObject addSchedulingDates(final Event event, final JSONObject scheduling) {
95 final JSONObject result = deepCopy(scheduling);
96 ZonedDateTime startDate = ZonedDateTime.parse(event.getRecordingStartDate());
97 ZonedDateTime endDate = ZonedDateTime.parse(event.getRecordingEndDate());
98 final InternalDuration oldDuration = InternalDuration.of(startDate.toInstant(), endDate.toInstant());
99 final ZoneId timezone = ZoneId.of((String) result.get("timezone"));
100
101
102 if (result.containsKey(SCHEDULING_START_KEY)) {
103 startDate = adjustedSchedulingDate(result, SCHEDULING_START_KEY, startDate, timezone);
104 }
105
106 if (result.containsKey(SCHEDULING_END_KEY)) {
107 endDate = adjustedSchedulingDate(result, SCHEDULING_END_KEY, endDate, timezone);
108 }
109 if (endDate.isBefore(startDate)) {
110 endDate = endDate.plusDays(1);
111 }
112
113
114 if (result.containsKey("duration")) {
115 final JSONObject time = (JSONObject) result.get("duration");
116 final InternalDuration newDuration = new InternalDuration(oldDuration);
117 if (time.containsKey("hour")) {
118 newDuration.hours = (Long) time.get("hour");
119 }
120 if (time.containsKey("minute")) {
121 newDuration.minutes = (Long) time.get("minute");
122 }
123 if (time.containsKey("second")) {
124 newDuration.seconds = (Long) time.get("second");
125 }
126 if (result.containsKey(SCHEDULING_END_KEY)) {
127 startDate = endDate.minusHours(newDuration.hours)
128 .minusMinutes(newDuration.minutes)
129 .minusSeconds(newDuration.seconds);
130 } else {
131 endDate = startDate.plusHours(newDuration.hours)
132 .plusMinutes(newDuration.minutes)
133 .plusSeconds(newDuration.seconds);
134 }
135 }
136
137
138 if (result.containsKey("weekday")) {
139 final String weekdayAbbrev = ((String) result.get("weekday"));
140 if (weekdayAbbrev != null) {
141 final DayOfWeek newWeekDay = Arrays.stream(DayOfWeek.values())
142 .filter(d -> d.name().startsWith(weekdayAbbrev.toUpperCase()))
143 .findAny()
144 .orElseThrow(() -> new IllegalArgumentException("Cannot parse weekday: " + weekdayAbbrev));
145 final int daysDiff = newWeekDay.getValue() - startDate.getDayOfWeek().getValue();
146 startDate = startDate.plusDays(daysDiff);
147 endDate = endDate.plusDays(daysDiff);
148 }
149 }
150
151 result.put(SCHEDULING_START_KEY, startDate.format(DateTimeFormatter.ISO_INSTANT));
152 result.put(SCHEDULING_END_KEY, endDate.format(DateTimeFormatter.ISO_INSTANT));
153 return result;
154 }
155
156
157
158
159
160
161
162 @SuppressWarnings("unchecked")
163 public static JSONObject toNonTechnicalMetadataJson(final JSONObject scheduling) {
164 final List<JSONObject> fields = new ArrayList<>();
165 if (scheduling.containsKey(SCHEDULING_AGENT_ID_KEY)) {
166 final JSONObject locationJson = new JSONObject();
167 locationJson.put("id", "location");
168 locationJson.put("value", scheduling.get(SCHEDULING_AGENT_ID_KEY));
169 fields.add(locationJson);
170 }
171 if (scheduling.containsKey(SCHEDULING_START_KEY) && scheduling.containsKey(SCHEDULING_END_KEY)) {
172 final JSONObject startDateJson = new JSONObject();
173 startDateJson.put("id", "startDate");
174 final String startDate = Instant.parse((String) scheduling.get(SCHEDULING_START_KEY))
175 .atOffset(ZoneOffset.UTC)
176 .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + ".000Z";
177 startDateJson.put("value", startDate);
178 fields.add(startDateJson);
179
180 final JSONObject durationJson = new JSONObject();
181 durationJson.put("id", "duration");
182 final Instant start = Instant.parse((String) scheduling.get(SCHEDULING_START_KEY));
183 final Instant end = Instant.parse((String) scheduling.get(SCHEDULING_END_KEY));
184 final InternalDuration duration = InternalDuration.of(start, end);
185 durationJson.put("value", duration.toString());
186 fields.add(durationJson);
187 }
188
189 final JSONObject result = new JSONObject();
190 result.put("flavor", MediaPackageElements.EPISODE.toString());
191 result.put("title", CommonEventCatalogUIAdapter.EPISODE_TITLE);
192 result.put("fields", fields);
193 return result;
194 }
195
196
197
198
199
200
201
202
203 @SuppressWarnings("unchecked")
204 public static JSONObject mergeMetadataFields(final JSONObject first, final JSONObject second) {
205 if (first == null) {
206 return second;
207 }
208 if (second == null) {
209 return first;
210 }
211 final JSONObject result = deepCopy(first);
212 final Collection fields = (Collection) result.get("fields");
213 fields.addAll((Collection) second.get("fields"));
214 return result;
215 }
216
217 private static JSONObject deepCopy(final JSONObject o) {
218 try {
219 return (JSONObject) parser.parse(o.toJSONString());
220 } catch (ParseException e) {
221 throw new IllegalArgumentException(e);
222 }
223 }
224
225 private static class InternalDuration {
226 private long hours;
227 private long minutes;
228 private long seconds;
229
230 InternalDuration() {
231 }
232
233 InternalDuration(final InternalDuration other) {
234 this.hours = other.hours;
235 this.minutes = other.minutes;
236 this.seconds = other.seconds;
237 }
238
239 public static InternalDuration of(final Instant start, final Instant end) {
240 final InternalDuration result = new InternalDuration();
241 final Duration duration = Duration.between(start, end);
242 result.hours = duration.toHours();
243 result.minutes = duration.minusHours(result.hours).toMinutes();
244 result.seconds = duration.minusHours(result.hours).minusMinutes(result.minutes).getSeconds();
245 return result;
246 }
247
248 @Override
249 public String toString() {
250 return String.format("%02d:%02d:%02d", hours, minutes, seconds);
251 }
252 }
253
254 private static ZonedDateTime adjustedSchedulingDate(
255 final JSONObject scheduling,
256 final String dateKey,
257 final ZonedDateTime date,
258 final ZoneId timezone) {
259 final JSONObject time = (JSONObject) scheduling.get(dateKey);
260 ZonedDateTime result = date.withZoneSameInstant(timezone);
261 if (time.containsKey("hour")) {
262 final int hour = Math.toIntExact((Long) time.get("hour"));
263 result = result.withHour(hour);
264 }
265 if (time.containsKey("minute")) {
266 final int minute = Math.toIntExact((Long) time.get("minute"));
267 result = result.withMinute(minute);
268 }
269 return result.withZoneSameInstant(ZoneOffset.UTC);
270 }
271
272
273
274
275 public static class BulkUpdateInstructionGroup {
276 private final List<String> eventIds;
277 private final JSONObject metadata;
278 private final JSONObject scheduling;
279
280
281
282
283
284
285
286
287 public BulkUpdateInstructionGroup(final List<String> eventIds, final JSONObject metadata, final JSONObject scheduling) {
288 this.eventIds = eventIds;
289 this.metadata = metadata;
290 this.scheduling = scheduling;
291 }
292
293
294
295
296
297
298 public List<String> getEventIds() {
299 return eventIds;
300 }
301
302
303
304
305
306
307 public JSONObject getMetadata() {
308 return metadata;
309 }
310
311
312
313
314
315
316 public JSONObject getScheduling() {
317 return scheduling;
318 }
319 }
320
321
322
323
324 public static class BulkUpdateInstructions {
325 private static final String KEY_EVENTS = "events";
326 private static final String KEY_METADATA = "metadata";
327 private static final String KEY_SCHEDULING = "scheduling";
328
329 private final List<BulkUpdateInstructionGroup> groups;
330
331
332
333
334
335
336
337
338 @SuppressWarnings("unchecked")
339 public BulkUpdateInstructions(final String json) throws IllegalArgumentException {
340 try {
341 final JSONArray root = (JSONArray) parser.parse(json);
342 groups = new ArrayList<>(root.size());
343 for (final Object jsonGroup : root) {
344 final JSONObject jsonObject = (JSONObject) jsonGroup;
345 final JSONArray eventIds = (JSONArray) jsonObject.get(KEY_EVENTS);
346 final JSONObject metadata = (JSONObject) jsonObject.get(KEY_METADATA);
347 final JSONObject scheduling = (JSONObject) jsonObject.get(KEY_SCHEDULING);
348 groups.add(new BulkUpdateInstructionGroup(eventIds, metadata, scheduling));
349 }
350 } catch (final ParseException e) {
351 throw new IllegalArgumentException(e);
352 }
353 }
354
355 public List<BulkUpdateInstructionGroup> getGroups() {
356 return groups;
357 }
358 }
359
360 }