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        return indexSvc.getEvent(id, index);
79      } catch (SearchIndexException e) {
80        throw new RuntimeException(e);
81      }
82    }
83  
84    /**
85     * Takes the given scheduling information and completes the event start and end dates as well as the duration for the
86     * given event. If the weekday shall be changed, the start and end dates are adjusted accordingly.
87     *
88     * @param event The event to complete the scheduling information for.
89     * @param scheduling The (yet incomplete) scheduling information to complete.
90     * @return The completed scheduling information, adjusted for the given event.
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     // The client only sends start time hours and/or minutes. We have to apply this to each event to get a full date.
101     if (result.containsKey(SCHEDULING_START_KEY)) {
102       startDate = adjustedSchedulingDate(result, SCHEDULING_START_KEY, startDate, timezone);
103     }
104     // The client only sends end time hours and/or minutes. We have to apply this to each event to get a full date.
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     // If duration is set, we have to adjust the end or start date.
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     // Setting the weekday means that the event should be moved to the new weekday within the same week
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    * Creates a json object containing meta data based on the given scheduling information.
157    *
158    * @param scheduling The scheduling information to extract meta data from.
159    * @return The meta data, consisting of location, startDate, and duration.
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    * Merges all fields of the given meta data json objects into one object.
197    *
198    * @param first The first meta data json object.
199    * @param second The second meta data json object.
200    * @return A new json meta data object, containing the field of both input objects.
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    * Model class for one group of update instructions
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      * Create a new group from parsed JSON data
281      *
282      * @param eventIds Event IDs in this group
283      * @param metadata Metadata for this group
284      * @param scheduling Scheduling for this group
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      * Get the list of IDs of events to apply the bulk update to.
294      *
295      * @return The list of IDs of the events to apply the bulk update to.
296      */
297     public List<String> getEventIds() {
298       return eventIds;
299     }
300 
301     /**
302      * Get the meta data update to apply.
303      *
304      * @return The meta data update to apply.
305      */
306     public JSONObject getMetadata() {
307       return metadata;
308     }
309 
310     /**
311      *  Get the scheduling information update to apply.
312      *
313      * @return The scheduling information update to apply.
314      */
315     public JSONObject getScheduling() {
316       return scheduling;
317     }
318   }
319 
320   /**
321    * Model class for the bulk update instructions which are sent by the UI.
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      * Create a new instance by parsing the given json String.
332      *
333      * @param json The json serialized version of the bulk update instructions sent by the UI.
334      *
335      * @throws IllegalArgumentException If the json string cannot be parsed.
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 }