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 com.entwinemedia.fn.data.Opt;
43  
44  import org.apache.commons.fileupload.FileItemIterator;
45  import org.apache.commons.fileupload.FileItemStream;
46  import org.apache.commons.fileupload.FileUploadException;
47  import org.apache.commons.fileupload.servlet.ServletFileUpload;
48  import org.apache.commons.fileupload.util.Streams;
49  import org.apache.commons.lang3.StringUtils;
50  import org.joda.time.DateTime;
51  import org.joda.time.DateTimeZone;
52  import org.json.simple.JSONArray;
53  import org.json.simple.JSONObject;
54  import org.json.simple.parser.JSONParser;
55  import org.json.simple.parser.ParseException;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  import java.io.IOException;
60  import java.text.SimpleDateFormat;
61  import java.util.ArrayList;
62  import java.util.List;
63  import java.util.ListIterator;
64  import java.util.Map;
65  import java.util.TimeZone;
66  
67  import javax.servlet.http.HttpServletRequest;
68  
69  public class EventHttpServletRequest {
70    /** The logging facility */
71    private static final Logger logger = LoggerFactory.getLogger(EventHttpServletRequest.class);
72  
73    private static final String ACTION_JSON_KEY = "action";
74    private static final String ALLOW_JSON_KEY = "allow";
75    private static final String METADATA_JSON_KEY = "metadata";
76    private static final String ROLE_JSON_KEY = "role";
77  
78    private Opt<AccessControlList> acl = Opt.none();
79    private Opt<MediaPackage> mediaPackage = Opt.none();
80    private Opt<MetadataList> metadataList = Opt.none();
81    private Opt<JSONObject> processing = Opt.none();
82    private Opt<JSONObject> source = Opt.none();
83    private Opt<JSONObject> scheduling = Opt.none();
84  
85    public void setAcl(AccessControlList acl) {
86      this.acl = Opt.some(acl);
87    }
88  
89    public void setMediaPackage(MediaPackage mediaPackage) {
90      this.mediaPackage = Opt.some(mediaPackage);
91    }
92  
93    public void setMetadataList(MetadataList metadataList) {
94      this.metadataList = Opt.some(metadataList);
95    }
96  
97    public void setProcessing(JSONObject processing) {
98      this.processing = Opt.some(processing);
99    }
100 
101   public void setScheduling(JSONObject scheduling) {
102     this.scheduling = Opt.some(scheduling);
103   }
104 
105   public void setSource(JSONObject source) {
106     this.source = Opt.some(source);
107   }
108 
109   public Opt<AccessControlList> getAcl() {
110     return acl;
111   }
112 
113   public Opt<MediaPackage> getMediaPackage() {
114     return mediaPackage;
115   }
116 
117   public Opt<MetadataList> getMetadataList() {
118     return metadataList;
119   }
120 
121   public Opt<JSONObject> getProcessing() {
122     return processing;
123   }
124 
125   public Opt<JSONObject> getScheduling() {
126     return scheduling;
127   }
128 
129   public Opt<JSONObject> getSource() {
130     return source;
131   }
132 
133   /**
134    * Create a {@link EventHttpServletRequest} from a {@link HttpServletRequest} to create a new {@link Event}.
135    *
136    * @param request
137    *          The multipart request that should result in a new {@link Event}
138    * @param ingestService
139    *          The {@link IngestService} to use to ingest {@link Event} media.
140    * @param eventCatalogUIAdapters
141    *          The catalog ui adapters to use for getting the event metadata.
142    * @param startDatePattern
143    *          The pattern to use to parse the start date from the request.
144    * @param startTimePattern
145    *          The pattern to use to parse the start time from the request.
146    * @return An {@link EventHttpServletRequest} populated from the request.
147    * @throws IndexServiceException
148    *           Thrown if unable to create the event for an internal reason.
149    * @throws IllegalArgumentException
150    *           Thrown if the multi part request doesn't have the necessary data.
151    */
152   public static EventHttpServletRequest createFromHttpServletRequest(
153           HttpServletRequest request,
154           IngestService ingestService,
155           List<EventCatalogUIAdapter> eventCatalogUIAdapters,
156           String startDatePattern,
157           String startTimePattern)
158                   throws IndexServiceException {
159     EventHttpServletRequest eventHttpServletRequest = new EventHttpServletRequest();
160     try {
161       if (ServletFileUpload.isMultipartContent(request)) {
162         eventHttpServletRequest.setMediaPackage(ingestService.createMediaPackage());
163         if (eventHttpServletRequest.getMediaPackage().isNone()) {
164           throw new IndexServiceException("Unable to create a new mediapackage to store the new event's media.");
165         }
166 
167         for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
168           FileItemStream item = iter.next();
169           String fieldName = item.getFieldName();
170           if (item.isFormField()) {
171             setFormField(eventCatalogUIAdapters, eventHttpServletRequest, item, fieldName, startDatePattern, 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.toString()));
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.toString()));
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, startTimePattern);
340           }
341         }
342       } catch (IOException e) {
343         throw new IndexServiceException("Unable to update event", e);
344       } catch (FileUploadException e) {
345         throw new IndexServiceException("Unable to update event", e);
346       }
347     } else {
348       throw new IllegalArgumentException("No multipart content");
349     }
350     return eventHttpServletRequest;
351   }
352 
353   /**
354    * De-serialize an JSON into an {@link AccessControlList}.
355    *
356    * @param json
357    *          The {@link AccessControlList} to serialize.
358    * @param assumeAllow
359    *          Assume that all entries are allows.
360    * @return An {@link AccessControlList} representation of the Json
361    * @throws ParseException
362    */
363   protected static AccessControlList deserializeJsonToAcl(String json, boolean assumeAllow) throws ParseException {
364     JSONParser parser = new JSONParser();
365     JSONArray aclJson = (JSONArray) parser.parse(json);
366     @SuppressWarnings("unchecked")
367     ListIterator<Object> iterator = aclJson.listIterator();
368     JSONObject aceJson;
369     List<AccessControlEntry> entries = new ArrayList<AccessControlEntry>();
370     while (iterator.hasNext()) {
371       aceJson = (JSONObject) iterator.next();
372       String action = aceJson.get(ACTION_JSON_KEY) != null ? aceJson.get(ACTION_JSON_KEY).toString() : "";
373       String allow;
374       if (assumeAllow) {
375         allow = "true";
376       } else {
377         allow = aceJson.get(ALLOW_JSON_KEY) != null ? aceJson.get(ALLOW_JSON_KEY).toString() : "";
378       }
379       String role = aceJson.get(ROLE_JSON_KEY) != null ? aceJson.get(ROLE_JSON_KEY).toString() : "";
380       if (StringUtils.trimToNull(action) != null && StringUtils.trimToNull(allow) != null
381               && StringUtils.trimToNull(role) != null) {
382         AccessControlEntry ace = new AccessControlEntry(role, action, Boolean.parseBoolean(allow));
383         entries.add(ace);
384       } else {
385         throw new IllegalArgumentException(String.format(
386                 "One of the access control elements is missing a property. The action was '%s', allow was '%s' and the role was '%s'",
387                 action, allow, role));
388       }
389     }
390     return new AccessControlList(entries);
391   }
392 
393   /**
394    * Change the simplified fields of key values provided to the external api into a {@link MetadataList}.
395    *
396    * @param json
397    *          The json string that contains an array of metadata field lists for the different catalogs.
398    * @param startDatePattern
399    *          The pattern to use to parse the start date from the json payload.
400    * @param startTimePattern
401    *          The pattern to use to parse the start time from the json payload.
402    * @return A {@link MetadataList} with the fields populated with the values provided.
403    * @throws ParseException
404    *           Thrown if unable to parse the json string.
405    * @throws NotFoundException
406    *           Thrown if unable to find the catalog or field that the json refers to.
407    */
408   protected static MetadataList deserializeMetadataList(
409           String json,
410           List<EventCatalogUIAdapter> catalogAdapters,
411           String startDatePattern,
412           String startTimePattern)
413           throws ParseException, NotFoundException, java.text.ParseException {
414     MetadataList metadataList = new MetadataList();
415     JSONParser parser = new JSONParser();
416     JSONArray jsonCatalogs = (JSONArray) parser.parse(json);
417     for (int i = 0; i < jsonCatalogs.size(); i++) {
418       JSONObject catalog = (JSONObject) jsonCatalogs.get(i);
419       if (catalog.get("flavor") == null || StringUtils.isBlank(catalog.get("flavor").toString())) {
420         throw new IllegalArgumentException(
421                 "Unable to create new event as no flavor was given for one of the metadata collections");
422       }
423       String flavorString = catalog.get("flavor").toString();
424       MediaPackageElementFlavor flavor = MediaPackageElementFlavor.parseFlavor(flavorString);
425 
426       DublinCoreMetadataCollection collection = null;
427       EventCatalogUIAdapter adapter = null;
428       for (EventCatalogUIAdapter eventCatalogUIAdapter : catalogAdapters) {
429         if (eventCatalogUIAdapter.getFlavor().equals(flavor)) {
430           adapter = eventCatalogUIAdapter;
431           collection = eventCatalogUIAdapter.getRawFields();
432         }
433       }
434 
435       if (collection == null) {
436         throw new IllegalArgumentException(
437                 String.format("Unable to find an EventCatalogUIAdapter with Flavor '%s'", flavorString));
438       }
439 
440       String fieldsJson = catalog.get("fields").toString();
441       if (StringUtils.trimToNull(fieldsJson) != null) {
442         Map<String, String> fields = RequestUtils.getKeyValueMap(fieldsJson);
443         for (String key : fields.keySet()) {
444           if ("subjects".equals(key)) {
445             // Handle the special case of allowing subjects to be an array.
446             MetadataField field = collection.getOutputFields().get(DublinCore.PROPERTY_SUBJECT.getLocalName());
447             if (field == null) {
448               throw new NotFoundException(String.format(
449                       "Cannot find a metadata field with id 'subject' from Catalog with Flavor '%s'.", flavorString));
450             }
451             collection.removeField(field);
452             try {
453               JSONArray subjects = (JSONArray) parser.parse(fields.get(key));
454               collection.addField(MetadataJson
455                       .copyWithDifferentJsonValue(field, StringUtils.join(subjects.iterator(), ",")));
456             } catch (ParseException e) {
457               throw new IllegalArgumentException(
458                       String.format("Unable to parse the 'subjects' metadata array field because: %s", e.toString()));
459             }
460           } else if ("startDate".equals(key)) {
461             // Special handling for start date since in API v1 we expect start date and start time to be separate fields.
462             MetadataField field = collection.getOutputFields().get(key);
463             if (field == null) {
464               throw new NotFoundException(String.format(
465                       "Cannot find a metadata field with id '%s' from Catalog with Flavor '%s'.", key, flavorString));
466             }
467             SimpleDateFormat apiSdf = MetadataField.getSimpleDateFormatter(startDatePattern == null ? field.getPattern() : startDatePattern);
468             SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(field.getPattern());
469             DateTime newStartDate = new DateTime(apiSdf.parse(fields.get(key)), DateTimeZone.UTC);
470             if (field.getValue() != null) {
471               DateTime oldStartDate = new DateTime(sdf.parse((String) field.getValue()), DateTimeZone.UTC);
472               newStartDate = oldStartDate.withDate(newStartDate.year().get(), newStartDate.monthOfYear().get(), newStartDate.dayOfMonth().get());
473             }
474             collection.removeField(field);
475             collection.addField(MetadataJson.copyWithDifferentJsonValue(field, sdf.format(newStartDate.toDate())));
476           } else if ("startTime".equals(key)) {
477             // Special handling for start time since in API v1 we expect start date and start time to be separate fields.
478             MetadataField field = collection.getOutputFields().get("startDate");
479             if (field == null) {
480               throw new NotFoundException(String.format(
481                       "Cannot find a metadata field with id '%s' from Catalog with Flavor '%s'.", "startDate", flavorString));
482             }
483             SimpleDateFormat apiSdf = MetadataField.getSimpleDateFormatter(startTimePattern == null ? "HH:mm" : startTimePattern);
484             SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(field.getPattern());
485             DateTime newStartDate = new DateTime(apiSdf.parse(fields.get(key)), DateTimeZone.UTC);
486             if (field.getValue() != null) {
487               DateTime oldStartDate = new DateTime(sdf.parse((String) field.getValue()), DateTimeZone.UTC);
488               newStartDate = oldStartDate.withTime(
489                       newStartDate.hourOfDay().get(),
490                       newStartDate.minuteOfHour().get(),
491                       newStartDate.secondOfMinute().get(),
492                       newStartDate.millisOfSecond().get());
493             }
494             collection.removeField(field);
495             collection.addField(MetadataJson.copyWithDifferentJsonValue(field, sdf.format(newStartDate.toDate())));
496           } else {
497             MetadataField field = collection.getOutputFields().get(key);
498             if (field == null) {
499               throw new NotFoundException(String.format(
500                       "Cannot find a metadata field with id '%s' from Catalog with Flavor '%s'.", key, flavorString));
501             }
502             collection.removeField(field);
503             collection.addField(MetadataJson.copyWithDifferentJsonValue(field, fields.get(key)));
504           }
505         }
506       }
507       metadataList.add(adapter, collection);
508     }
509     setStartDateAndTimeIfUnset(metadataList);
510     return metadataList;
511   }
512 
513   /**
514    * Set the start date and time to the current date & time if it hasn't been set through the api call.
515    *
516    * @param metadataList
517    *          The metadata list created from the json request to create a new event
518    */
519   private static void setStartDateAndTimeIfUnset(MetadataList metadataList) {
520     final DublinCoreMetadataCollection commonEventCollection = metadataList
521             .getMetadataByFlavor(MediaPackageElements.EPISODE.toString());
522     if (commonEventCollection != null) {
523       MetadataField startDate = commonEventCollection.getOutputFields().get("startDate");
524       if (!startDate.isUpdated()) {
525         SimpleDateFormat utcDateFormat = new SimpleDateFormat(startDate.getPattern());
526         utcDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
527         String currentDate = utcDateFormat.format(new DateTime(DateTimeZone.UTC).toDate());
528         commonEventCollection.removeField(startDate);
529         commonEventCollection.addField(MetadataJson.copyWithDifferentJsonValue(startDate, currentDate));
530       }
531     }
532   }
533 }