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