View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
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   * This class holds utility functions which are related to the bulk update feature for events.
56   */
57  public final class BulkUpdateUtil {
58  
59    private static final JSONParser parser = new JSONParser();
60  
61    private BulkUpdateUtil() {
62    }
63  
64    /**
65     * Wraps the IndexService.getEvent() method to convert SearchIndexExceptions into RuntimeExceptions. Useful when
66     * using Java's functional programming features.
67     *
68     * @param indexSvc The IndexService instance.
69     * @param index The index to get the event from.
70     * @param id The id of the event to get.
71     * @return An optional holding the event or nothing, if not found.
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     * Takes the given scheduling information and completes the event start and end dates as well as the duration for the
87     * given event. If the weekday shall be changed, the start and end dates are adjusted accordingly.
88     *
89     * @param event The event to complete the scheduling information for.
90     * @param scheduling The (yet incomplete) scheduling information to complete.
91     * @return The completed scheduling information, adjusted for the given event.
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     // The client only sends start time hours and/or minutes. We have to apply this to each event to get a full date.
102     if (result.containsKey(SCHEDULING_START_KEY)) {
103       startDate = adjustedSchedulingDate(result, SCHEDULING_START_KEY, startDate, timezone);
104     }
105     // The client only sends end time hours and/or minutes. We have to apply this to each event to get a full date.
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     // If duration is set, we have to adjust the end or start date.
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     // Setting the weekday means that the event should be moved to the new weekday within the same week
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    * Creates a json object containing meta data based on the given scheduling information.
158    *
159    * @param scheduling The scheduling information to extract meta data from.
160    * @return The meta data, consisting of location, startDate, and duration.
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    * Merges all fields of the given meta data json objects into one object.
198    *
199    * @param first The first meta data json object.
200    * @param second The second meta data json object.
201    * @return A new json meta data object, containing the field of both input objects.
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    * Model class for one group of update instructions
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      * Create a new group from parsed JSON data
282      *
283      * @param eventIds Event IDs in this group
284      * @param metadata Metadata for this group
285      * @param scheduling Scheduling for this group
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      * Get the list of IDs of events to apply the bulk update to.
295      *
296      * @return The list of IDs of the events to apply the bulk update to.
297      */
298     public List<String> getEventIds() {
299       return eventIds;
300     }
301 
302     /**
303      * Get the meta data update to apply.
304      *
305      * @return The meta data update to apply.
306      */
307     public JSONObject getMetadata() {
308       return metadata;
309     }
310 
311     /**
312      *  Get the scheduling information update to apply.
313      *
314      * @return The scheduling information update to apply.
315      */
316     public JSONObject getScheduling() {
317       return scheduling;
318     }
319   }
320 
321   /**
322    * Model class for the bulk update instructions which are sent by the UI.
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      * Create a new instance by parsing the given json String.
333      *
334      * @param json The json serialized version of the bulk update instructions sent by the UI.
335      *
336      * @throws IllegalArgumentException If the json string cannot be parsed.
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 }