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,
171                 startTimePattern);
172           } else {
173             if (!item.getName().isBlank()) {
174               ingestFile(ingestService, eventHttpServletRequest, item);
175             } else {
176               logger.debug("Skipping field {} due to missing filename", item.getFieldName());
177             }
178           }
179         }
180       } else {
181         throw new IllegalArgumentException("No multipart content");
182       }
183 
184       return eventHttpServletRequest;
185 
186     } catch (Exception e) {
187       throw new IndexServiceException("Unable to parse new event.", e);
188     }
189   }
190 
191   /**
192    * Ingest a file from a multi part request for a new event.
193    *
194    * @param ingestService
195    *          The {@link IngestService} to use to ingest the file.
196    * @param eventHttpServletRequest
197    *          The {@link EventHttpServletRequest} that has the ingest mediapackage.
198    * @param item
199    *          The representation of the file.
200    * @throws MediaPackageException
201    *           Thrown if unable to add the track to the mediapackage.
202    * @throws IOException
203    *           Thrown if unable to upload the file into the mediapackage.
204    * @throws IngestException
205    *           Thrown if unable to ingest the file.
206    */
207   private static void ingestFile(IngestService ingestService, EventHttpServletRequest eventHttpServletRequest,
208           FileItemStream item) throws MediaPackageException, IOException, IngestException {
209     MediaPackage mp = eventHttpServletRequest.getMediaPackage().get();
210     if ("presenter".equals(item.getFieldName())) {
211       eventHttpServletRequest.setMediaPackage(
212               ingestService.addTrack(item.openStream(), item.getName(), MediaPackageElements.PRESENTER_SOURCE, mp));
213     } else if ("presentation".equals(item.getFieldName())) {
214       eventHttpServletRequest.setMediaPackage(
215               ingestService.addTrack(item.openStream(), item.getName(), MediaPackageElements.PRESENTATION_SOURCE, mp));
216     } else if ("audio".equals(item.getFieldName())) {
217       eventHttpServletRequest.setMediaPackage(ingestService.addTrack(item.openStream(), item.getName(),
218               new MediaPackageElementFlavor("presenter-audio", "source"), mp));
219     } else {
220       logger.warn("Unknown field name found {}", item.getFieldName());
221     }
222   }
223 
224   /**
225    * Set a value for creating a new event from a form field.
226    *
227    * @param eventCatalogUIAdapters
228    *          The list of event catalog ui adapters used for loading the metadata for the new event.
229    * @param eventHttpServletRequest
230    *          The current details of the request that have been loaded.
231    * @param item
232    *          The content of the field.
233    * @param fieldName
234    *          The key of the field.
235    * @param startDatePattern
236    *          The pattern to use to parse the start date from the request.
237    * @param startTimePattern
238    *          The pattern to use to parse the start time from the request.
239    * @throws IOException
240    *           Thrown if unable to laod the content of the field.
241    * @throws NotFoundException
242    *           Thrown if unable to find a metadata catalog or field that matches an input catalog or field.
243    */
244   private static void setFormField(List<EventCatalogUIAdapter> eventCatalogUIAdapters,
245                                    EventHttpServletRequest eventHttpServletRequest,
246                                    FileItemStream item,
247                                    String fieldName,
248                                    String startDatePattern,
249                                    String startTimePattern)
250                   throws IOException, NotFoundException {
251     if (METADATA_JSON_KEY.equals(fieldName)) {
252       String metadata = Streams.asString(item.openStream());
253       if (StringUtils.isNotEmpty(metadata)) {
254         try {
255           MetadataList metadataList = deserializeMetadataList(metadata, eventCatalogUIAdapters, startDatePattern,
256                   startTimePattern);
257           eventHttpServletRequest.setMetadataList(metadataList);
258         } catch (IllegalArgumentException e) {
259           throw e;
260         } catch (ParseException e) {
261           throw new IllegalArgumentException(String.format("Unable to parse event metadata because: '%s'", e));
262         } catch (NotFoundException e) {
263           throw e;
264         } catch (java.text.ParseException e) {
265           throw new IllegalArgumentException(String.format("Unable to parse event metadata because: '%s'", e));
266         }
267       }
268     } else if ("acl".equals(item.getFieldName())) {
269       String access = Streams.asString(item.openStream());
270       if (StringUtils.isNotEmpty(access)) {
271         try {
272           AccessControlList acl = deserializeJsonToAcl(access, true);
273           eventHttpServletRequest.setAcl(acl);
274         } catch (Exception e) {
275           logger.warn("Unable to parse acl {}", access);
276           throw new IllegalArgumentException("Unable to parse acl");
277         }
278       }
279     } else if ("processing".equals(item.getFieldName())) {
280       String processing = Streams.asString(item.openStream());
281       if (StringUtils.isNotEmpty(processing)) {
282         JSONParser parser = new JSONParser();
283         try {
284           eventHttpServletRequest.setProcessing((JSONObject) parser.parse(processing));
285         } catch (Exception e) {
286           logger.warn("Unable to parse processing configuration {}", processing);
287           throw new IllegalArgumentException("Unable to parse processing configuration");
288         }
289       }
290     } else if ("scheduling".equals(item.getFieldName())) {
291       String scheduling = Streams.asString(item.openStream());
292       if (StringUtils.isNotEmpty(scheduling)) {
293         JSONParser parser = new JSONParser();
294         try {
295           eventHttpServletRequest.setScheduling((JSONObject) parser.parse(scheduling));
296         } catch (Exception e) {
297           logger.warn("Unable to parse scheduling information {}", scheduling);
298           throw new IllegalArgumentException("Unable to parse scheduling information");
299         }
300       }
301     }
302   }
303 
304   /**
305    * Load the details of updating an event.
306    *
307    * @param event
308    *          The event to update.
309    * @param request
310    *          The multipart request that has the data to load the updated event.
311    * @param eventCatalogUIAdapters
312    *          The list of catalog ui adapters to use to load the event metadata.
313    * @param startDatePattern
314    *          The pattern to use to parse the start date from the request.
315    * @param startTimePattern
316    *          The pattern to use to parse the start time from the request.
317    * @return The data for the event update
318    * @throws IllegalArgumentException
319    *           Thrown if the request to update the event is malformed.
320    * @throws IndexServiceException
321    *           Thrown if something is unable to load the event data.
322    * @throws NotFoundException
323    *           Thrown if unable to find a metadata catalog or field that matches an input catalog or field.
324    */
325   public static EventHttpServletRequest updateFromHttpServletRequest(
326           Event event,
327           HttpServletRequest request,
328           List<EventCatalogUIAdapter> eventCatalogUIAdapters,
329           String startDatePattern,
330           String startTimePattern)
331                   throws IllegalArgumentException, IndexServiceException, NotFoundException {
332     EventHttpServletRequest eventHttpServletRequest = new EventHttpServletRequest();
333     if (ServletFileUpload.isMultipartContent(request)) {
334       try {
335         for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
336           FileItemStream item = iter.next();
337           String fieldName = item.getFieldName();
338           if (item.isFormField()) {
339             setFormField(eventCatalogUIAdapters, eventHttpServletRequest, item, fieldName, startDatePattern,
340                 startTimePattern);
341           }
342         }
343       } catch (IOException e) {
344         throw new IndexServiceException("Unable to update event", e);
345       } catch (FileUploadException e) {
346         throw new IndexServiceException("Unable to update event", e);
347       }
348     } else {
349       throw new IllegalArgumentException("No multipart content");
350     }
351     return eventHttpServletRequest;
352   }
353 
354   /**
355    * De-serialize an JSON into an {@link AccessControlList}.
356    *
357    * @param json
358    *          The {@link AccessControlList} to serialize.
359    * @param assumeAllow
360    *          Assume that all entries are allows.
361    * @return An {@link AccessControlList} representation of the Json
362    * @throws ParseException
363    */
364   protected static AccessControlList deserializeJsonToAcl(String json, boolean assumeAllow) throws ParseException {
365     JSONParser parser = new JSONParser();
366     JSONArray aclJson = (JSONArray) parser.parse(json);
367     @SuppressWarnings("unchecked")
368     ListIterator<Object> iterator = aclJson.listIterator();
369     JSONObject aceJson;
370     List<AccessControlEntry> entries = new ArrayList<AccessControlEntry>();
371     while (iterator.hasNext()) {
372       aceJson = (JSONObject) iterator.next();
373       String action = aceJson.get(ACTION_JSON_KEY) != null ? aceJson.get(ACTION_JSON_KEY).toString() : "";
374       String allow;
375       if (assumeAllow) {
376         allow = "true";
377       } else {
378         allow = aceJson.get(ALLOW_JSON_KEY) != null ? aceJson.get(ALLOW_JSON_KEY).toString() : "";
379       }
380       String role = aceJson.get(ROLE_JSON_KEY) != null ? aceJson.get(ROLE_JSON_KEY).toString() : "";
381       if (StringUtils.trimToNull(action) != null && StringUtils.trimToNull(allow) != null
382               && StringUtils.trimToNull(role) != null) {
383         AccessControlEntry ace = new AccessControlEntry(role, action, Boolean.parseBoolean(allow));
384         entries.add(ace);
385       } else {
386         throw new IllegalArgumentException(String.format(
387                 "One of the access control elements is missing a property. The action was '%s', allow was '%s' and "
388                     + "the role was '%s'",
389                 action, allow, role));
390       }
391     }
392     return new AccessControlList(entries);
393   }
394 
395   /**
396    * Change the simplified fields of key values provided to the external api into a {@link MetadataList}.
397    *
398    * @param json
399    *          The json string that contains an array of metadata field lists for the different catalogs.
400    * @param startDatePattern
401    *          The pattern to use to parse the start date from the json payload.
402    * @param startTimePattern
403    *          The pattern to use to parse the start time from the json payload.
404    * @return A {@link MetadataList} with the fields populated with the values provided.
405    * @throws ParseException
406    *           Thrown if unable to parse the json string.
407    * @throws NotFoundException
408    *           Thrown if unable to find the catalog or field that the json refers to.
409    */
410   protected static MetadataList deserializeMetadataList(
411           String json,
412           List<EventCatalogUIAdapter> catalogAdapters,
413           String startDatePattern,
414           String startTimePattern)
415           throws ParseException, NotFoundException, java.text.ParseException {
416     MetadataList metadataList = new MetadataList();
417     JSONParser parser = new JSONParser();
418     JSONArray jsonCatalogs = (JSONArray) parser.parse(json);
419     for (int i = 0; i < jsonCatalogs.size(); i++) {
420       JSONObject catalog = (JSONObject) jsonCatalogs.get(i);
421       if (catalog.get("flavor") == null || StringUtils.isBlank(catalog.get("flavor").toString())) {
422         throw new IllegalArgumentException(
423                 "Unable to create new event as no flavor was given for one of the metadata collections");
424       }
425       String flavorString = catalog.get("flavor").toString();
426       MediaPackageElementFlavor flavor = MediaPackageElementFlavor.parseFlavor(flavorString);
427 
428       DublinCoreMetadataCollection collection = null;
429       EventCatalogUIAdapter adapter = null;
430       for (EventCatalogUIAdapter eventCatalogUIAdapter : catalogAdapters) {
431         if (eventCatalogUIAdapter.getFlavor().equals(flavor)) {
432           adapter = eventCatalogUIAdapter;
433           collection = eventCatalogUIAdapter.getRawFields();
434         }
435       }
436 
437       if (collection == null) {
438         throw new IllegalArgumentException(
439                 String.format("Unable to find an EventCatalogUIAdapter with Flavor '%s'", flavorString));
440       }
441 
442       String fieldsJson = catalog.get("fields").toString();
443       if (StringUtils.trimToNull(fieldsJson) != null) {
444         Map<String, String> fields = RequestUtils.getKeyValueMap(fieldsJson);
445         for (String key : fields.keySet()) {
446           if ("subjects".equals(key)) {
447             // Handle the special case of allowing subjects to be an array.
448             MetadataField field = collection.getOutputFields().get(DublinCore.PROPERTY_SUBJECT.getLocalName());
449             if (field == null) {
450               throw new NotFoundException(String.format(
451                       "Cannot find a metadata field with id 'subject' from Catalog with Flavor '%s'.", flavorString));
452             }
453             collection.removeField(field);
454             try {
455               JSONArray subjects = (JSONArray) parser.parse(fields.get(key));
456               collection.addField(MetadataJson
457                       .copyWithDifferentJsonValue(field, StringUtils.join(subjects.iterator(), ",")));
458             } catch (ParseException e) {
459               throw new IllegalArgumentException(
460                       String.format("Unable to parse the 'subjects' metadata array field because: %s", e.toString()));
461             }
462           } else if ("startDate".equals(key)) {
463             // Special handling for start date since in API v1 we expect start date and start time to be separate
464             // fields.
465             MetadataField field = collection.getOutputFields().get(key);
466             if (field == null) {
467               throw new NotFoundException(String.format(
468                       "Cannot find a metadata field with id '%s' from Catalog with Flavor '%s'.", key, flavorString));
469             }
470             SimpleDateFormat apiSdf = MetadataField.getSimpleDateFormatter(startDatePattern == null
471                 ? field.getPattern() : startDatePattern);
472             SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(field.getPattern());
473             DateTime newStartDate = new DateTime(apiSdf.parse(fields.get(key)), DateTimeZone.UTC);
474             if (field.getValue() != null) {
475               DateTime oldStartDate = new DateTime(sdf.parse((String) field.getValue()), DateTimeZone.UTC);
476               newStartDate = oldStartDate.withDate(newStartDate.year().get(), newStartDate.monthOfYear().get(),
477                   newStartDate.dayOfMonth().get());
478             }
479             collection.removeField(field);
480             collection.addField(MetadataJson.copyWithDifferentJsonValue(field, sdf.format(newStartDate.toDate())));
481           } else if ("startTime".equals(key)) {
482             // Special handling for start time since in API v1 we expect start date and start time to be separate
483             // fields.
484             MetadataField field = collection.getOutputFields().get("startDate");
485             if (field == null) {
486               throw new NotFoundException(String.format(
487                       "Cannot find a metadata field with id '%s' from Catalog with Flavor '%s'.", "startDate",
488                   flavorString));
489             }
490             SimpleDateFormat apiSdf = MetadataField.getSimpleDateFormatter(startTimePattern == null
491                 ? "HH:mm" : startTimePattern);
492             SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(field.getPattern());
493             DateTime newStartDate = new DateTime(apiSdf.parse(fields.get(key)), DateTimeZone.UTC);
494             if (field.getValue() != null) {
495               DateTime oldStartDate = new DateTime(sdf.parse((String) field.getValue()), DateTimeZone.UTC);
496               newStartDate = oldStartDate.withTime(
497                       newStartDate.hourOfDay().get(),
498                       newStartDate.minuteOfHour().get(),
499                       newStartDate.secondOfMinute().get(),
500                       newStartDate.millisOfSecond().get());
501             }
502             collection.removeField(field);
503             collection.addField(MetadataJson.copyWithDifferentJsonValue(field, sdf.format(newStartDate.toDate())));
504           } else {
505             MetadataField field = collection.getOutputFields().get(key);
506             if (field == null) {
507               throw new NotFoundException(String.format(
508                       "Cannot find a metadata field with id '%s' from Catalog with Flavor '%s'.", key, flavorString));
509             }
510             collection.removeField(field);
511             collection.addField(MetadataJson.copyWithDifferentJsonValue(field, fields.get(key)));
512           }
513         }
514       }
515       metadataList.add(adapter, collection);
516     }
517     setStartDateAndTimeIfUnset(metadataList);
518     return metadataList;
519   }
520 
521   /**
522    * Set the start date and time to the current date & time if it hasn't been set through the api call.
523    *
524    * @param metadataList
525    *          The metadata list created from the json request to create a new event
526    */
527   private static void setStartDateAndTimeIfUnset(MetadataList metadataList) {
528     final DublinCoreMetadataCollection commonEventCollection = metadataList
529             .getMetadataByFlavor(MediaPackageElements.EPISODE.toString());
530     if (commonEventCollection != null) {
531       MetadataField startDate = commonEventCollection.getOutputFields().get("startDate");
532       if (!startDate.isUpdated()) {
533         SimpleDateFormat utcDateFormat = new SimpleDateFormat(startDate.getPattern());
534         utcDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
535         String currentDate = utcDateFormat.format(new DateTime(DateTimeZone.UTC).toDate());
536         commonEventCollection.removeField(startDate);
537         commonEventCollection.addField(MetadataJson.copyWithDifferentJsonValue(startDate, currentDate));
538       }
539     }
540   }
541 }