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  package org.opencastproject.index.service.impl.util;
22  
23  import org.opencastproject.elasticsearch.index.objects.event.Event;
24  import org.opencastproject.index.service.exception.IndexServiceException;
25  import org.opencastproject.index.service.util.RequestUtils;
26  import org.opencastproject.ingest.api.IngestException;
27  import org.opencastproject.ingest.api.IngestService;
28  import org.opencastproject.mediapackage.MediaPackage;
29  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
30  import org.opencastproject.mediapackage.MediaPackageElements;
31  import org.opencastproject.mediapackage.MediaPackageException;
32  import org.opencastproject.metadata.dublincore.DublinCore;
33  import org.opencastproject.metadata.dublincore.DublinCoreMetadataCollection;
34  import org.opencastproject.metadata.dublincore.EventCatalogUIAdapter;
35  import org.opencastproject.metadata.dublincore.MetadataField;
36  import org.opencastproject.metadata.dublincore.MetadataJson;
37  import org.opencastproject.metadata.dublincore.MetadataList;
38  import org.opencastproject.security.api.AccessControlEntry;
39  import org.opencastproject.security.api.AccessControlList;
40  import org.opencastproject.util.NotFoundException;
41  
42  import org.apache.commons.fileupload.FileItemIterator;
43  import org.apache.commons.fileupload.FileItemStream;
44  import org.apache.commons.fileupload.FileUploadException;
45  import org.apache.commons.fileupload.servlet.ServletFileUpload;
46  import org.apache.commons.fileupload.util.Streams;
47  import org.apache.commons.lang3.StringUtils;
48  import org.joda.time.DateTime;
49  import org.joda.time.DateTimeZone;
50  import org.json.simple.JSONArray;
51  import org.json.simple.JSONObject;
52  import org.json.simple.parser.JSONParser;
53  import org.json.simple.parser.ParseException;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  import java.io.IOException;
58  import java.text.SimpleDateFormat;
59  import java.util.ArrayList;
60  import java.util.List;
61  import java.util.ListIterator;
62  import java.util.Map;
63  import java.util.Optional;
64  import java.util.TimeZone;
65  
66  import javax.servlet.http.HttpServletRequest;
67  
68  public class EventHttpServletRequest {
69    /** The logging facility */
70    private static final Logger logger = LoggerFactory.getLogger(EventHttpServletRequest.class);
71  
72    private static final String ACTION_JSON_KEY = "action";
73    private static final String ALLOW_JSON_KEY = "allow";
74    private static final String METADATA_JSON_KEY = "metadata";
75    private static final String ROLE_JSON_KEY = "role";
76  
77    private Optional<AccessControlList> acl = Optional.empty();
78    private Optional<MediaPackage> mediaPackage = Optional.empty();
79    private Optional<MetadataList> metadataList = Optional.empty();
80    private Optional<JSONObject> processing = Optional.empty();
81    private Optional<JSONObject> source = Optional.empty();
82    private Optional<JSONObject> scheduling = Optional.empty();
83  
84    public void setAcl(AccessControlList acl) {
85      this.acl = Optional.of(acl);
86    }
87  
88    public void setMediaPackage(MediaPackage mediaPackage) {
89      this.mediaPackage = Optional.of(mediaPackage);
90    }
91  
92    public void setMetadataList(MetadataList metadataList) {
93      this.metadataList = Optional.of(metadataList);
94    }
95  
96    public void setProcessing(JSONObject processing) {
97      this.processing = Optional.of(processing);
98    }
99  
100   public void setScheduling(JSONObject scheduling) {
101     this.scheduling = Optional.of(scheduling);
102   }
103 
104   public void setSource(JSONObject source) {
105     this.source = Optional.of(source);
106   }
107 
108   public Optional<AccessControlList> getAcl() {
109     return acl;
110   }
111 
112   public Optional<MediaPackage> getMediaPackage() {
113     return mediaPackage;
114   }
115 
116   public Optional<MetadataList> getMetadataList() {
117     return metadataList;
118   }
119 
120   public Optional<JSONObject> getProcessing() {
121     return processing;
122   }
123 
124   public Optional<JSONObject> getScheduling() {
125     return scheduling;
126   }
127 
128   public Optional<JSONObject> getSource() {
129     return source;
130   }
131 
132   /**
133    * Create a {@link EventHttpServletRequest} from a {@link HttpServletRequest} to create a new {@link Event}.
134    *
135    * @param request
136    *          The multipart request that should result in a new {@link Event}
137    * @param ingestService
138    *          The {@link IngestService} to use to ingest {@link Event} media.
139    * @param eventCatalogUIAdapters
140    *          The catalog ui adapters to use for getting the event metadata.
141    * @param startDatePattern
142    *          The pattern to use to parse the start date from the request.
143    * @param startTimePattern
144    *          The pattern to use to parse the start time from the request.
145    * @return An {@link EventHttpServletRequest} populated from the request.
146    * @throws IndexServiceException
147    *           Thrown if unable to create the event for an internal reason.
148    * @throws IllegalArgumentException
149    *           Thrown if the multi part request doesn't have the necessary data.
150    */
151   public static EventHttpServletRequest createFromHttpServletRequest(
152           HttpServletRequest request,
153           IngestService ingestService,
154           List<EventCatalogUIAdapter> eventCatalogUIAdapters,
155           String startDatePattern,
156           String startTimePattern)
157                   throws IndexServiceException {
158     EventHttpServletRequest eventHttpServletRequest = new EventHttpServletRequest();
159     try {
160       if (ServletFileUpload.isMultipartContent(request)) {
161         eventHttpServletRequest.setMediaPackage(ingestService.createMediaPackage());
162         if (eventHttpServletRequest.getMediaPackage().isEmpty()) {
163           throw new IndexServiceException("Unable to create a new mediapackage to store the new event's media.");
164         }
165 
166         for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
167           FileItemStream item = iter.next();
168           String fieldName = item.getFieldName();
169           if (item.isFormField()) {
170             setFormField(eventCatalogUIAdapters, eventHttpServletRequest, item, fieldName, startDatePattern, startTimePattern);
171           } else {
172             if (!item.getName().isBlank()) {
173               ingestFile(ingestService, eventHttpServletRequest, item);
174             } else {
175               logger.debug("Skipping field {} due to missing filename", item.getFieldName());
176             }
177           }
178         }
179       } else {
180         throw new IllegalArgumentException("No multipart content");
181       }
182 
183       return eventHttpServletRequest;
184 
185     } catch (Exception e) {
186       throw new IndexServiceException("Unable to parse new event.", e);
187     }
188   }
189 
190   /**
191    * Ingest a file from a multi part request for a new event.
192    *
193    * @param ingestService
194    *          The {@link IngestService} to use to ingest the file.
195    * @param eventHttpServletRequest
196    *          The {@link EventHttpServletRequest} that has the ingest mediapackage.
197    * @param item
198    *          The representation of the file.
199    * @throws MediaPackageException
200    *           Thrown if unable to add the track to the mediapackage.
201    * @throws IOException
202    *           Thrown if unable to upload the file into the mediapackage.
203    * @throws IngestException
204    *           Thrown if unable to ingest the file.
205    */
206   private static void ingestFile(IngestService ingestService, EventHttpServletRequest eventHttpServletRequest,
207           FileItemStream item) throws MediaPackageException, IOException, IngestException {
208     MediaPackage mp = eventHttpServletRequest.getMediaPackage().get();
209     if ("presenter".equals(item.getFieldName())) {
210       eventHttpServletRequest.setMediaPackage(
211               ingestService.addTrack(item.openStream(), item.getName(), MediaPackageElements.PRESENTER_SOURCE, mp));
212     } else if ("presentation".equals(item.getFieldName())) {
213       eventHttpServletRequest.setMediaPackage(
214               ingestService.addTrack(item.openStream(), item.getName(), MediaPackageElements.PRESENTATION_SOURCE, mp));
215     } else if ("audio".equals(item.getFieldName())) {
216       eventHttpServletRequest.setMediaPackage(ingestService.addTrack(item.openStream(), item.getName(),
217               new MediaPackageElementFlavor("presenter-audio", "source"), mp));
218     } else {
219       logger.warn("Unknown field name found {}", item.getFieldName());
220     }
221   }
222 
223   /**
224    * Set a value for creating a new event from a form field.
225    *
226    * @param eventCatalogUIAdapters
227    *          The list of event catalog ui adapters used for loading the metadata for the new event.
228    * @param eventHttpServletRequest
229    *          The current details of the request that have been loaded.
230    * @param item
231    *          The content of the field.
232    * @param fieldName
233    *          The key of the field.
234    * @param startDatePattern
235    *          The pattern to use to parse the start date from the request.
236    * @param startTimePattern
237    *          The pattern to use to parse the start time from the request.
238    * @throws IOException
239    *           Thrown if unable to laod the content of the field.
240    * @throws NotFoundException
241    *           Thrown if unable to find a metadata catalog or field that matches an input catalog or field.
242    */
243   private static void setFormField(List<EventCatalogUIAdapter> eventCatalogUIAdapters,
244                                    EventHttpServletRequest eventHttpServletRequest,
245                                    FileItemStream item,
246                                    String fieldName,
247                                    String startDatePattern,
248                                    String startTimePattern)
249                   throws IOException, NotFoundException {
250     if (METADATA_JSON_KEY.equals(fieldName)) {
251       String metadata = Streams.asString(item.openStream());
252       if (StringUtils.isNotEmpty(metadata)) {
253         try {
254           MetadataList metadataList = deserializeMetadataList(metadata, eventCatalogUIAdapters, startDatePattern,
255                   startTimePattern);
256           eventHttpServletRequest.setMetadataList(metadataList);
257         } catch (IllegalArgumentException e) {
258           throw e;
259         } catch (ParseException e) {
260           throw new IllegalArgumentException(String.format("Unable to parse event metadata because: '%s'", e.toString()));
261         } catch (NotFoundException e) {
262           throw e;
263         } catch (java.text.ParseException e) {
264           throw new IllegalArgumentException(String.format("Unable to parse event metadata because: '%s'", e.toString()));
265         }
266       }
267     } else if ("acl".equals(item.getFieldName())) {
268       String access = Streams.asString(item.openStream());
269       if (StringUtils.isNotEmpty(access)) {
270         try {
271           AccessControlList acl = deserializeJsonToAcl(access, true);
272           eventHttpServletRequest.setAcl(acl);
273         } catch (Exception e) {
274           logger.warn("Unable to parse acl {}", access);
275           throw new IllegalArgumentException("Unable to parse acl");
276         }
277       }
278     } else if ("processing".equals(item.getFieldName())) {
279       String processing = Streams.asString(item.openStream());
280       if (StringUtils.isNotEmpty(processing)) {
281         JSONParser parser = new JSONParser();
282         try {
283           eventHttpServletRequest.setProcessing((JSONObject) parser.parse(processing));
284         } catch (Exception e) {
285           logger.warn("Unable to parse processing configuration {}", processing);
286           throw new IllegalArgumentException("Unable to parse processing configuration");
287         }
288       }
289     } else if ("scheduling".equals(item.getFieldName())) {
290       String scheduling = Streams.asString(item.openStream());
291       if (StringUtils.isNotEmpty(scheduling)) {
292         JSONParser parser = new JSONParser();
293         try {
294           eventHttpServletRequest.setScheduling((JSONObject) parser.parse(scheduling));
295         } catch (Exception e) {
296           logger.warn("Unable to parse scheduling information {}", scheduling);
297           throw new IllegalArgumentException("Unable to parse scheduling information");
298         }
299       }
300     }
301   }
302 
303   /**
304    * Load the details of updating an event.
305    *
306    * @param event
307    *          The event to update.
308    * @param request
309    *          The multipart request that has the data to load the updated event.
310    * @param eventCatalogUIAdapters
311    *          The list of catalog ui adapters to use to load the event metadata.
312    * @param startDatePattern
313    *          The pattern to use to parse the start date from the request.
314    * @param startTimePattern
315    *          The pattern to use to parse the start time from the request.
316    * @return The data for the event update
317    * @throws IllegalArgumentException
318    *           Thrown if the request to update the event is malformed.
319    * @throws IndexServiceException
320    *           Thrown if something is unable to load the event data.
321    * @throws NotFoundException
322    *           Thrown if unable to find a metadata catalog or field that matches an input catalog or field.
323    */
324   public static EventHttpServletRequest updateFromHttpServletRequest(
325           Event event,
326           HttpServletRequest request,
327           List<EventCatalogUIAdapter> eventCatalogUIAdapters,
328           String startDatePattern,
329           String startTimePattern)
330                   throws IllegalArgumentException, IndexServiceException, NotFoundException {
331     EventHttpServletRequest eventHttpServletRequest = new EventHttpServletRequest();
332     if (ServletFileUpload.isMultipartContent(request)) {
333       try {
334         for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
335           FileItemStream item = iter.next();
336           String fieldName = item.getFieldName();
337           if (item.isFormField()) {
338             setFormField(eventCatalogUIAdapters, eventHttpServletRequest, item, fieldName, startDatePattern, startTimePattern);
339           }
340         }
341       } catch (IOException e) {
342         throw new IndexServiceException("Unable to update event", e);
343       } catch (FileUploadException e) {
344         throw new IndexServiceException("Unable to update event", e);
345       }
346     } else {
347       throw new IllegalArgumentException("No multipart content");
348     }
349     return eventHttpServletRequest;
350   }
351 
352   /**
353    * De-serialize an JSON into an {@link AccessControlList}.
354    *
355    * @param json
356    *          The {@link AccessControlList} to serialize.
357    * @param assumeAllow
358    *          Assume that all entries are allows.
359    * @return An {@link AccessControlList} representation of the Json
360    * @throws ParseException
361    */
362   protected static AccessControlList deserializeJsonToAcl(String json, boolean assumeAllow) throws ParseException {
363     JSONParser parser = new JSONParser();
364     JSONArray aclJson = (JSONArray) parser.parse(json);
365     @SuppressWarnings("unchecked")
366     ListIterator<Object> iterator = aclJson.listIterator();
367     JSONObject aceJson;
368     List<AccessControlEntry> entries = new ArrayList<AccessControlEntry>();
369     while (iterator.hasNext()) {
370       aceJson = (JSONObject) iterator.next();
371       String action = aceJson.get(ACTION_JSON_KEY) != null ? aceJson.get(ACTION_JSON_KEY).toString() : "";
372       String allow;
373       if (assumeAllow) {
374         allow = "true";
375       } else {
376         allow = aceJson.get(ALLOW_JSON_KEY) != null ? aceJson.get(ALLOW_JSON_KEY).toString() : "";
377       }
378       String role = aceJson.get(ROLE_JSON_KEY) != null ? aceJson.get(ROLE_JSON_KEY).toString() : "";
379       if (StringUtils.trimToNull(action) != null && StringUtils.trimToNull(allow) != null
380               && StringUtils.trimToNull(role) != null) {
381         AccessControlEntry ace = new AccessControlEntry(role, action, Boolean.parseBoolean(allow));
382         entries.add(ace);
383       } else {
384         throw new IllegalArgumentException(String.format(
385                 "One of the access control elements is missing a property. The action was '%s', allow was '%s' and the role was '%s'",
386                 action, allow, role));
387       }
388     }
389     return new AccessControlList(entries);
390   }
391 
392   /**
393    * Change the simplified fields of key values provided to the external api into a {@link MetadataList}.
394    *
395    * @param json
396    *          The json string that contains an array of metadata field lists for the different catalogs.
397    * @param startDatePattern
398    *          The pattern to use to parse the start date from the json payload.
399    * @param startTimePattern
400    *          The pattern to use to parse the start time from the json payload.
401    * @return A {@link MetadataList} with the fields populated with the values provided.
402    * @throws ParseException
403    *           Thrown if unable to parse the json string.
404    * @throws NotFoundException
405    *           Thrown if unable to find the catalog or field that the json refers to.
406    */
407   protected static MetadataList deserializeMetadataList(
408           String json,
409           List<EventCatalogUIAdapter> catalogAdapters,
410           String startDatePattern,
411           String startTimePattern)
412           throws ParseException, NotFoundException, java.text.ParseException {
413     MetadataList metadataList = new MetadataList();
414     JSONParser parser = new JSONParser();
415     JSONArray jsonCatalogs = (JSONArray) parser.parse(json);
416     for (int i = 0; i < jsonCatalogs.size(); i++) {
417       JSONObject catalog = (JSONObject) jsonCatalogs.get(i);
418       if (catalog.get("flavor") == null || StringUtils.isBlank(catalog.get("flavor").toString())) {
419         throw new IllegalArgumentException(
420                 "Unable to create new event as no flavor was given for one of the metadata collections");
421       }
422       String flavorString = catalog.get("flavor").toString();
423       MediaPackageElementFlavor flavor = MediaPackageElementFlavor.parseFlavor(flavorString);
424 
425       DublinCoreMetadataCollection collection = null;
426       EventCatalogUIAdapter adapter = null;
427       for (EventCatalogUIAdapter eventCatalogUIAdapter : catalogAdapters) {
428         if (eventCatalogUIAdapter.getFlavor().equals(flavor)) {
429           adapter = eventCatalogUIAdapter;
430           collection = eventCatalogUIAdapter.getRawFields();
431         }
432       }
433 
434       if (collection == null) {
435         throw new IllegalArgumentException(
436                 String.format("Unable to find an EventCatalogUIAdapter with Flavor '%s'", flavorString));
437       }
438 
439       String fieldsJson = catalog.get("fields").toString();
440       if (StringUtils.trimToNull(fieldsJson) != null) {
441         Map<String, String> fields = RequestUtils.getKeyValueMap(fieldsJson);
442         for (String key : fields.keySet()) {
443           if ("subjects".equals(key)) {
444             // Handle the special case of allowing subjects to be an array.
445             MetadataField field = collection.getOutputFields().get(DublinCore.PROPERTY_SUBJECT.getLocalName());
446             if (field == null) {
447               throw new NotFoundException(String.format(
448                       "Cannot find a metadata field with id 'subject' from Catalog with Flavor '%s'.", flavorString));
449             }
450             collection.removeField(field);
451             try {
452               JSONArray subjects = (JSONArray) parser.parse(fields.get(key));
453               collection.addField(MetadataJson
454                       .copyWithDifferentJsonValue(field, StringUtils.join(subjects.iterator(), ",")));
455             } catch (ParseException e) {
456               throw new IllegalArgumentException(
457                       String.format("Unable to parse the 'subjects' metadata array field because: %s", e.toString()));
458             }
459           } else if ("startDate".equals(key)) {
460             // Special handling for start date since in API v1 we expect start date and start time to be separate fields.
461             MetadataField field = collection.getOutputFields().get(key);
462             if (field == null) {
463               throw new NotFoundException(String.format(
464                       "Cannot find a metadata field with id '%s' from Catalog with Flavor '%s'.", key, flavorString));
465             }
466             SimpleDateFormat apiSdf = MetadataField.getSimpleDateFormatter(startDatePattern == null ? field.getPattern() : startDatePattern);
467             SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(field.getPattern());
468             DateTime newStartDate = new DateTime(apiSdf.parse(fields.get(key)), DateTimeZone.UTC);
469             if (field.getValue() != null) {
470               DateTime oldStartDate = new DateTime(sdf.parse((String) field.getValue()), DateTimeZone.UTC);
471               newStartDate = oldStartDate.withDate(newStartDate.year().get(), newStartDate.monthOfYear().get(), newStartDate.dayOfMonth().get());
472             }
473             collection.removeField(field);
474             collection.addField(MetadataJson.copyWithDifferentJsonValue(field, sdf.format(newStartDate.toDate())));
475           } else if ("startTime".equals(key)) {
476             // Special handling for start time since in API v1 we expect start date and start time to be separate fields.
477             MetadataField field = collection.getOutputFields().get("startDate");
478             if (field == null) {
479               throw new NotFoundException(String.format(
480                       "Cannot find a metadata field with id '%s' from Catalog with Flavor '%s'.", "startDate", flavorString));
481             }
482             SimpleDateFormat apiSdf = MetadataField.getSimpleDateFormatter(startTimePattern == null ? "HH:mm" : startTimePattern);
483             SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(field.getPattern());
484             DateTime newStartDate = new DateTime(apiSdf.parse(fields.get(key)), DateTimeZone.UTC);
485             if (field.getValue() != null) {
486               DateTime oldStartDate = new DateTime(sdf.parse((String) field.getValue()), DateTimeZone.UTC);
487               newStartDate = oldStartDate.withTime(
488                       newStartDate.hourOfDay().get(),
489                       newStartDate.minuteOfHour().get(),
490                       newStartDate.secondOfMinute().get(),
491                       newStartDate.millisOfSecond().get());
492             }
493             collection.removeField(field);
494             collection.addField(MetadataJson.copyWithDifferentJsonValue(field, sdf.format(newStartDate.toDate())));
495           } else {
496             MetadataField field = collection.getOutputFields().get(key);
497             if (field == null) {
498               throw new NotFoundException(String.format(
499                       "Cannot find a metadata field with id '%s' from Catalog with Flavor '%s'.", key, flavorString));
500             }
501             collection.removeField(field);
502             collection.addField(MetadataJson.copyWithDifferentJsonValue(field, fields.get(key)));
503           }
504         }
505       }
506       metadataList.add(adapter, collection);
507     }
508     setStartDateAndTimeIfUnset(metadataList);
509     return metadataList;
510   }
511 
512   /**
513    * Set the start date and time to the current date & time if it hasn't been set through the api call.
514    *
515    * @param metadataList
516    *          The metadata list created from the json request to create a new event
517    */
518   private static void setStartDateAndTimeIfUnset(MetadataList metadataList) {
519     final DublinCoreMetadataCollection commonEventCollection = metadataList
520             .getMetadataByFlavor(MediaPackageElements.EPISODE.toString());
521     if (commonEventCollection != null) {
522       MetadataField startDate = commonEventCollection.getOutputFields().get("startDate");
523       if (!startDate.isUpdated()) {
524         SimpleDateFormat utcDateFormat = new SimpleDateFormat(startDate.getPattern());
525         utcDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
526         String currentDate = utcDateFormat.format(new DateTime(DateTimeZone.UTC).toDate());
527         commonEventCollection.removeField(startDate);
528         commonEventCollection.addField(MetadataJson.copyWithDifferentJsonValue(startDate, currentDate));
529       }
530     }
531   }
532 }