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.ingest.endpoint;
23  
24  import static org.apache.commons.lang3.StringUtils.trimToNull;
25  import static org.opencastproject.mediapackage.MediaPackageElements.XACML_POLICY_EPISODE;
26  
27  import org.opencastproject.authorization.xacml.XACMLUtils;
28  import org.opencastproject.capture.CaptureParameters;
29  import org.opencastproject.ingest.api.IngestException;
30  import org.opencastproject.ingest.api.IngestService;
31  import org.opencastproject.ingest.impl.IngestServiceImpl;
32  import org.opencastproject.job.api.JobProducer;
33  import org.opencastproject.mediapackage.EName;
34  import org.opencastproject.mediapackage.MediaPackage;
35  import org.opencastproject.mediapackage.MediaPackageBuilderFactory;
36  import org.opencastproject.mediapackage.MediaPackageElement;
37  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
38  import org.opencastproject.mediapackage.MediaPackageElements;
39  import org.opencastproject.mediapackage.MediaPackageException;
40  import org.opencastproject.mediapackage.MediaPackageParser;
41  import org.opencastproject.mediapackage.MediaPackageSupport;
42  import org.opencastproject.mediapackage.identifier.IdImpl;
43  import org.opencastproject.metadata.dublincore.DublinCore;
44  import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
45  import org.opencastproject.metadata.dublincore.DublinCoreCatalogService;
46  import org.opencastproject.metadata.dublincore.DublinCoreXmlFormat;
47  import org.opencastproject.metadata.dublincore.DublinCores;
48  import org.opencastproject.rest.AbstractJobProducerEndpoint;
49  import org.opencastproject.scheduler.api.SchedulerConflictException;
50  import org.opencastproject.scheduler.api.SchedulerException;
51  import org.opencastproject.security.api.AccessControlList;
52  import org.opencastproject.security.api.AccessControlParser;
53  import org.opencastproject.security.api.AccessControlParsingException;
54  import org.opencastproject.security.api.TrustedHttpClient;
55  import org.opencastproject.security.api.UnauthorizedException;
56  import org.opencastproject.serviceregistry.api.ServiceRegistry;
57  import org.opencastproject.util.NotFoundException;
58  import org.opencastproject.util.doc.rest.RestParameter;
59  import org.opencastproject.util.doc.rest.RestQuery;
60  import org.opencastproject.util.doc.rest.RestResponse;
61  import org.opencastproject.util.doc.rest.RestService;
62  import org.opencastproject.workflow.api.JaxbWorkflowInstance;
63  import org.opencastproject.workflow.api.WorkflowInstance;
64  import org.opencastproject.workflow.api.XmlWorkflowParser;
65  
66  import com.google.common.cache.Cache;
67  import com.google.common.cache.CacheBuilder;
68  
69  import org.apache.commons.fileupload.FileItemIterator;
70  import org.apache.commons.fileupload.FileItemStream;
71  import org.apache.commons.fileupload.FileUploadException;
72  import org.apache.commons.fileupload.servlet.ServletFileUpload;
73  import org.apache.commons.fileupload.util.Streams;
74  import org.apache.commons.io.IOUtils;
75  import org.apache.commons.lang3.StringUtils;
76  import org.apache.commons.lang3.exception.ExceptionUtils;
77  import org.apache.http.HttpResponse;
78  import org.apache.http.client.methods.HttpGet;
79  import org.osgi.service.component.ComponentContext;
80  import org.osgi.service.component.annotations.Activate;
81  import org.osgi.service.component.annotations.Component;
82  import org.osgi.service.component.annotations.Reference;
83  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
84  import org.slf4j.Logger;
85  import org.slf4j.LoggerFactory;
86  
87  import java.io.ByteArrayInputStream;
88  import java.io.ByteArrayOutputStream;
89  import java.io.IOException;
90  import java.io.InputStream;
91  import java.net.URI;
92  import java.nio.charset.StandardCharsets;
93  import java.text.DateFormat;
94  import java.text.SimpleDateFormat;
95  import java.util.ArrayList;
96  import java.util.Arrays;
97  import java.util.Date;
98  import java.util.HashMap;
99  import java.util.List;
100 import java.util.Map;
101 import java.util.concurrent.TimeUnit;
102 
103 import javax.servlet.http.HttpServletRequest;
104 import javax.servlet.http.HttpServletResponse;
105 import javax.ws.rs.Consumes;
106 import javax.ws.rs.FormParam;
107 import javax.ws.rs.GET;
108 import javax.ws.rs.POST;
109 import javax.ws.rs.PUT;
110 import javax.ws.rs.Path;
111 import javax.ws.rs.PathParam;
112 import javax.ws.rs.Produces;
113 import javax.ws.rs.QueryParam;
114 import javax.ws.rs.core.Context;
115 import javax.ws.rs.core.MediaType;
116 import javax.ws.rs.core.MultivaluedHashMap;
117 import javax.ws.rs.core.MultivaluedMap;
118 import javax.ws.rs.core.Response;
119 import javax.ws.rs.core.Response.Status;
120 
121 /**
122  * Creates and augments Opencast MediaPackages using the api. Stores media into the Working File Repository.
123  */
124 @Path("/ingest")
125 @RestService(
126     name = "ingestservice",
127     title = "Ingest Service",
128     abstractText = "This service creates and augments Opencast media packages that include media tracks, metadata "
129         + "catalogs and attachments.",
130     notes = {
131         "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
132         "If the service is down or not working it will return a status 503, this means the the underlying service is "
133                 + "not working and is either restarting or has failed",
134         "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
135                 + "other words, there is a bug! You should file an error report with your server logs from the time "
136                 + "when the error occurred: "
137                 + "<a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>" })
138 @Component(
139     immediate = true,
140     service = IngestRestService.class,
141     property = {
142         "service.description=Ingest REST Endpoint",
143         "opencast.service.type=org.opencastproject.ingest",
144         "opencast.service.path=/ingest",
145         "opencast.service.jobproducer=true"
146     }
147 )
148 @JaxrsResource
149 public class IngestRestService extends AbstractJobProducerEndpoint {
150 
151   private static final Logger logger = LoggerFactory.getLogger(IngestRestService.class);
152 
153   /** Key for the default workflow definition in config.properties */
154   protected static final String DEFAULT_WORKFLOW_DEFINITION = "org.opencastproject.workflow.default.definition";
155 
156   /** Key for the default maximum number of ingests in config.properties */
157   protected static final String MAX_INGESTS_KEY = "org.opencastproject.ingest.max.concurrent";
158 
159   /** The http request parameter used to provide the workflow instance id */
160   protected static final String WORKFLOW_INSTANCE_ID_PARAM = "workflowInstanceId";
161 
162   /** The http request parameter used to provide the workflow definition id */
163   protected static final String WORKFLOW_DEFINITION_ID_PARAM = "workflowDefinitionId";
164 
165   /** The default workflow definition */
166   private String defaultWorkflowDefinitionId = null;
167 
168   /** The http client */
169   private TrustedHttpClient httpClient;
170 
171   /** Dublin Core Terms: http://purl.org/dc/terms/ */
172   private static final List<String> dcterms = Arrays.asList("abstract", "accessRights", "accrualMethod",
173           "accrualPeriodicity", "accrualPolicy", "alternative", "audience", "available", "bibliographicCitation",
174           "conformsTo", "contributor", "coverage", "created", "creator", "date", "dateAccepted", "dateCopyrighted",
175           "dateSubmitted", "description", "educationLevel", "extent", "format", "hasFormat", "hasPart", "hasVersion",
176           "identifier", "instructionalMethod", "isFormatOf", "isPartOf", "isReferencedBy", "isReplacedBy",
177           "isRequiredBy", "issued", "isVersionOf", "language", "license", "mediator", "medium", "modified",
178           "provenance", "publisher", "references", "relation", "replaces", "requires", "rights", "rightsHolder",
179           "source", "spatial", "subject", "tableOfContents", "temporal", "title", "type", "valid");
180 
181   /** Formatter to for the date into a string */
182   private static final DateFormat DATE_FORMAT = new SimpleDateFormat(IngestService.UTC_DATE_FORMAT);
183 
184   /** Media package builder factory */
185   private static final MediaPackageBuilderFactory MP_FACTORY = MediaPackageBuilderFactory.newInstance();
186 
187   private IngestService ingestService = null;
188   private ServiceRegistry serviceRegistry = null;
189   private DublinCoreCatalogService dublinCoreService;
190   // The number of ingests this service can handle concurrently.
191   private int ingestLimit = -1;
192   /* Stores a map workflow ID and date to update the ingest start times post-hoc */
193   private final Cache<String, Date> startCache = CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.DAYS).build();
194 
195   /**
196    * Returns the maximum number of concurrent ingest operations or <code>-1</code> if no limit is enforced.
197    *
198    * @return the maximum number of concurrent ingest operations
199    * @see #isIngestLimitEnabled()
200    */
201   protected synchronized int getIngestLimit() {
202     return ingestLimit;
203   }
204 
205   /**
206    * Sets the maximum number of concurrent ingest operations. Use <code>-1</code> to indicate no limit.
207    *
208    * @param ingestLimit
209    *          the limit
210    */
211   private synchronized void setIngestLimit(int ingestLimit) {
212     this.ingestLimit = ingestLimit;
213   }
214 
215   /**
216    * Returns <code>true</code> if a maximum number of concurrent ingest operations has been defined.
217    *
218    * @return <code>true</code> if there is a maximum number of concurrent ingests
219    */
220   protected synchronized boolean isIngestLimitEnabled() {
221     return ingestLimit >= 0;
222   }
223 
224   /**
225    * Callback for activation of this component.
226    */
227   @Activate
228   public void activate(ComponentContext cc) {
229     if (cc != null) {
230       defaultWorkflowDefinitionId = trimToNull(cc.getBundleContext().getProperty(DEFAULT_WORKFLOW_DEFINITION));
231       if (defaultWorkflowDefinitionId == null) {
232         defaultWorkflowDefinitionId = "schedule-and-upload";
233       }
234       if (cc.getBundleContext().getProperty(MAX_INGESTS_KEY) != null) {
235         try {
236           ingestLimit = Integer.parseInt(trimToNull(cc.getBundleContext().getProperty(MAX_INGESTS_KEY)));
237           if (ingestLimit == 0) {
238             ingestLimit = -1;
239           }
240         } catch (NumberFormatException e) {
241           logger.warn("Max ingest property with key " + MAX_INGESTS_KEY
242                   + " isn't defined so no ingest limit will be used.");
243           ingestLimit = -1;
244         }
245       }
246     }
247   }
248 
249   @PUT
250   @Produces(MediaType.TEXT_XML)
251   @Path("createMediaPackageWithID/{id}")
252   @RestQuery(
253       name = "createMediaPackageWithID",
254       description = "Create an empty media package with ID /n Overrides Existing Mediapackage ",
255       pathParameters = {
256           @RestParameter(description = "The Id for the new Mediapackage", isRequired = true, name = "id",
257               type = RestParameter.Type.STRING)
258       },
259       responses = {
260           @RestResponse(description = "Returns media package", responseCode = HttpServletResponse.SC_OK),
261           @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
262       },
263       returnDescription = "")
264   public Response createMediaPackage(@PathParam("id") String mediaPackageId) {
265     MediaPackage mp;
266     try {
267       mp = ingestService.createMediaPackage(mediaPackageId);
268 
269       startCache.put(mp.getIdentifier().toString(), new Date());
270       return Response.ok(mp).build();
271     } catch (Exception e) {
272       logger.warn("Unable to create mediapackage", e);
273       return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build();
274     }
275   }
276 
277   @GET
278   @Produces(MediaType.TEXT_XML)
279   @Path("createMediaPackage")
280   @RestQuery(
281       name = "createMediaPackage",
282       description = "Create an empty media package",
283       restParameters = {},
284       responses = {
285           @RestResponse(description = "Returns media package", responseCode = HttpServletResponse.SC_OK),
286           @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
287       },
288       returnDescription = "")
289   public Response createMediaPackage() {
290     MediaPackage mp;
291     try {
292       mp = ingestService.createMediaPackage();
293       startCache.put(mp.getIdentifier().toString(), new Date());
294       return Response.ok(mp).build();
295     } catch (Exception e) {
296       logger.warn("Unable to create empty mediapackage", e);
297       return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build();
298     }
299   }
300 
301   @POST
302   @Path("discardMediaPackage")
303   @RestQuery(
304       name = "discardMediaPackage",
305       description = "Discard a media package",
306       restParameters = {
307           @RestParameter(description = "Given media package to be destroyed", isRequired = true, name = "mediaPackage",
308               type = RestParameter.Type.TEXT)
309       },
310       responses = {
311           @RestResponse(description = "", responseCode = HttpServletResponse.SC_OK),
312           @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
313       },
314       returnDescription = "")
315   public Response discardMediaPackage(@FormParam("mediaPackage") String mpx) {
316     logger.debug("discardMediaPackage(MediaPackage): {}", mpx);
317     try {
318       MediaPackage mp = MP_FACTORY.newMediaPackageBuilder().loadFromXml(mpx);
319       ingestService.discardMediaPackage(mp);
320       return Response.ok().build();
321     } catch (Exception e) {
322       logger.warn("Unable to discard mediapackage {}", mpx, e);
323       return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build();
324     }
325   }
326 
327   @POST
328   @Produces(MediaType.TEXT_XML)
329   @Path("addTrack")
330   @RestQuery(
331       name = "addTrackURL",
332       description = "Add a media track to a given media package using an URL",
333       restParameters = {
334           @RestParameter(description = "The location of the media", isRequired = true, name = "url",
335               type = RestParameter.Type.STRING),
336           @RestParameter(description = "The kind of media", isRequired = true, name = "flavor",
337               type = RestParameter.Type.STRING),
338           @RestParameter(description = "The tags of the  media track", isRequired = false, name = "tags",
339               type = RestParameter.Type.STRING),
340           @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage",
341               type = RestParameter.Type.TEXT)
342       },
343       responses = {
344           @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK),
345           @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST),
346           @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
347       },
348       returnDescription = "")
349   public Response addMediaPackageTrack(@FormParam("url") String url, @FormParam("flavor") String flavor,
350       @FormParam("tags")  String tags, @FormParam("mediaPackage") String mpx) {
351     logger.trace("add media package from url: {} flavor: {} tags: {} mediaPackage: {}", url, flavor, tags, mpx);
352     try {
353       MediaPackage mp = MP_FACTORY.newMediaPackageBuilder().loadFromXml(mpx);
354       if (MediaPackageSupport.sanityCheck(mp).isPresent()) {
355         return Response.serverError().status(Status.BAD_REQUEST).build();
356       }
357       String[] tagsArray = null;
358       if (tags != null) {
359         tagsArray = tags.split(",");
360       }
361       mp = ingestService.addTrack(new URI(url), MediaPackageElementFlavor.parseFlavor(flavor), tagsArray, mp);
362       return Response.ok(mp).build();
363     } catch (Exception e) {
364       logger.warn("Unable to add mediapackage track", e);
365       return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build();
366     }
367   }
368 
369   @POST
370   @Produces(MediaType.TEXT_XML)
371   @Consumes(MediaType.MULTIPART_FORM_DATA)
372   @Path("addTrack")
373   @RestQuery(
374       name = "addTrackInputStream",
375       description = "Add a media track to a given media package using an input stream",
376       restParameters = {
377           @RestParameter(description = "The kind of media track", isRequired = true, name = "flavor",
378               type = RestParameter.Type.STRING),
379           @RestParameter(description = "The tags of the media track", isRequired = false, name = "tags",
380               type = RestParameter.Type.STRING),
381           @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage",
382               type = RestParameter.Type.TEXT) },
383       bodyParameter = @RestParameter(description = "The media track file", isRequired = true, name = "BODY",
384           type = RestParameter.Type.FILE),
385       responses = {
386           @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK),
387           @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST),
388           @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) },
389       returnDescription = "")
390   public Response addMediaPackageTrack(@Context HttpServletRequest request) {
391     logger.trace("add track as multipart-form-data");
392     return addMediaPackageElement(request, MediaPackageElement.Type.Track);
393   }
394 
395   @POST
396   @Produces(MediaType.TEXT_XML)
397   @Path("addPartialTrack")
398   @RestQuery(
399       name = "addPartialTrackURL",
400       description = "Add a partial media track to a given media package using an URL",
401       restParameters = {
402           @RestParameter(description = "The location of the media", isRequired = true, name = "url",
403               type = RestParameter.Type.STRING),
404           @RestParameter(description = "The kind of media", isRequired = true, name = "flavor",
405               type = RestParameter.Type.STRING),
406           @RestParameter(description = "The start time in milliseconds", isRequired = true, name = "startTime",
407               type = RestParameter.Type.INTEGER),
408           @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage",
409               type = RestParameter.Type.TEXT)
410       },
411       responses = {
412           @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK),
413           @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST),
414           @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
415       },
416       returnDescription = "")
417   public Response addMediaPackagePartialTrack(@FormParam("url") String url, @FormParam("flavor") String flavor,
418           @FormParam("startTime") Long startTime, @FormParam("mediaPackage") String mpx) {
419     logger.trace("add partial track with url: {} flavor: {} startTime: {} mediaPackage: {}",
420             url, flavor, startTime, mpx);
421     try {
422       MediaPackage mp = MP_FACTORY.newMediaPackageBuilder().loadFromXml(mpx);
423       if (MediaPackageSupport.sanityCheck(mp).isPresent()) {
424         return Response.serverError().status(Status.BAD_REQUEST).build();
425       }
426 
427       mp = ingestService.addPartialTrack(new URI(url), MediaPackageElementFlavor.parseFlavor(flavor), startTime, mp);
428       return Response.ok(mp).build();
429     } catch (Exception e) {
430       logger.warn("Unable to add partial track", e);
431       return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build();
432     }
433   }
434 
435   @POST
436   @Produces(MediaType.TEXT_XML)
437   @Consumes(MediaType.MULTIPART_FORM_DATA)
438   @Path("addPartialTrack")
439   @RestQuery(
440       name = "addPartialTrackInputStream",
441       description = "Add a partial media track to a given media package using an input stream",
442       restParameters = {
443           @RestParameter(description = "The kind of media track", isRequired = true, name = "flavor",
444               type = RestParameter.Type.STRING),
445           @RestParameter(description = "The start time in milliseconds", isRequired = true, name = "startTime",
446               type = RestParameter.Type.INTEGER),
447           @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage",
448               type = RestParameter.Type.TEXT)
449       },
450       bodyParameter = @RestParameter(description = "The media track file", isRequired = true, name = "BODY",
451           type = RestParameter.Type.FILE),
452       responses = {
453           @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK),
454           @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST),
455           @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
456       },
457       returnDescription = "")
458   public Response addMediaPackagePartialTrack(@Context HttpServletRequest request) {
459     logger.trace("add partial track as multipart-form-data");
460     return addMediaPackageElement(request, MediaPackageElement.Type.Track);
461   }
462 
463   @POST
464   @Produces(MediaType.TEXT_XML)
465   @Path("addCatalog")
466   @RestQuery(
467       name = "addCatalogURL",
468       description = "Add a metadata catalog to a given media package using an URL",
469       restParameters = {
470           @RestParameter(description = "The location of the catalog", isRequired = true, name = "url",
471               type = RestParameter.Type.STRING),
472           @RestParameter(description = "The kind of catalog", isRequired = true, name = "flavor",
473               type = RestParameter.Type.STRING),
474           @RestParameter(description = "The tags of the catalog", isRequired = false, name = "tags",
475               type = RestParameter.Type.STRING),
476           @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage",
477               type = RestParameter.Type.TEXT)
478       },
479       responses = {
480           @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK),
481           @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST),
482           @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
483       },
484       returnDescription = "")
485   public Response addMediaPackageCatalog(@FormParam("url") String url, @FormParam("flavor") String flavor,
486       @FormParam("tags") String tags, @FormParam("mediaPackage") String mpx) {
487     logger.trace("add catalog with url: {} flavor: {} tags: {} mediaPackage: {}", url, flavor, tags, mpx);
488     try {
489       MediaPackage mp = MP_FACTORY.newMediaPackageBuilder().loadFromXml(mpx);
490       if (MediaPackageSupport.sanityCheck(mp).isPresent()) {
491         return Response.serverError().status(Status.BAD_REQUEST).build();
492       }
493       String[] tagsArray = null;
494       if (tags != null) {
495         tagsArray = tags.split(",");
496       }
497       MediaPackage resultingMediaPackage = ingestService.addCatalog(new URI(url),
498               MediaPackageElementFlavor.parseFlavor(flavor), tagsArray, mp);
499       return Response.ok(resultingMediaPackage).build();
500     } catch (Exception e) {
501       logger.warn("Unable to add catalog", e);
502       return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build();
503     }
504   }
505 
506   @POST
507   @Produces(MediaType.TEXT_XML)
508   @Consumes(MediaType.MULTIPART_FORM_DATA)
509   @Path("addCatalog")
510   @RestQuery(
511       name = "addCatalogInputStream",
512       description = "Add a metadata catalog to a given media package using an input stream",
513       restParameters = {
514           @RestParameter(description = "The kind of media catalog", isRequired = true, name = "flavor",
515               type = RestParameter.Type.STRING),
516           @RestParameter(description = "The tags of the attachment", isRequired = false, name = "tags",
517               type = RestParameter.Type.STRING),
518           @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage",
519               type = RestParameter.Type.TEXT)
520       },
521       bodyParameter = @RestParameter(description = "The metadata catalog file", isRequired = true, name = "BODY",
522           type = RestParameter.Type.FILE),
523       responses = {
524           @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK),
525           @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST),
526           @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
527       },
528       returnDescription = "")
529   public Response addMediaPackageCatalog(@Context HttpServletRequest request) {
530     logger.trace("add catalog as multipart-form-data");
531     return addMediaPackageElement(request, MediaPackageElement.Type.Catalog);
532   }
533 
534   @POST
535   @Produces(MediaType.TEXT_XML)
536   @Path("addAttachment")
537   @RestQuery(
538       name = "addAttachmentURL",
539       description = "Add an attachment to a given media package using an URL",
540       restParameters = {
541           @RestParameter(description = "The location of the attachment", isRequired = true, name = "url",
542               type = RestParameter.Type.STRING),
543           @RestParameter(description = "The kind of attachment", isRequired = true, name = "flavor",
544               type = RestParameter.Type.STRING),
545           @RestParameter(description = "The tags of the attachment", isRequired = false, name = "tags",
546               type = RestParameter.Type.STRING),
547           @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage",
548               type = RestParameter.Type.TEXT)
549       },
550       responses = {
551           @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK),
552           @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST),
553           @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
554       },
555       returnDescription = "")
556   public Response addMediaPackageAttachment(@FormParam("url") String url, @FormParam("flavor") String flavor,
557       @FormParam("tags") String tags, @FormParam("mediaPackage") String mpx) {
558     logger.trace("add attachment with url: {} flavor: {} mediaPackage: {}", url, flavor, mpx);
559     try {
560       MediaPackage mp = MP_FACTORY.newMediaPackageBuilder().loadFromXml(mpx);
561       if (MediaPackageSupport.sanityCheck(mp).isPresent()) {
562         return Response.serverError().status(Status.BAD_REQUEST).build();
563       }
564       String[] tagsArray = null;
565       if (tags != null) {
566         tagsArray = tags.split(",");
567       }
568       mp = ingestService.addAttachment(new URI(url), MediaPackageElementFlavor.parseFlavor(flavor), tagsArray, mp);
569       return Response.ok(mp).build();
570     } catch (Exception e) {
571       logger.warn("Unable to add attachment", e);
572       return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build();
573     }
574   }
575 
576   @POST
577   @Produces(MediaType.TEXT_XML)
578   @Consumes(MediaType.MULTIPART_FORM_DATA)
579   @Path("addAttachment")
580   @RestQuery(
581       name = "addAttachmentInputStream",
582       description = "Add an attachment to a given media package using an input stream",
583       restParameters = {
584           @RestParameter(description = "The kind of attachment", isRequired = true, name = "flavor",
585               type = RestParameter.Type.STRING),
586           @RestParameter(description = "The tags of the attachment", isRequired = false, name = "tags",
587               type = RestParameter.Type.STRING),
588           @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage",
589               type = RestParameter.Type.TEXT)
590       },
591       bodyParameter = @RestParameter(description = "The attachment file", isRequired = true, name = "BODY",
592           type = RestParameter.Type.FILE),
593       responses = {
594           @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK),
595           @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST),
596           @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
597       },
598       returnDescription = "")
599   public Response addMediaPackageAttachment(@Context HttpServletRequest request) {
600     logger.trace("add attachment as multipart-form-data");
601     return addMediaPackageElement(request, MediaPackageElement.Type.Attachment);
602   }
603 
604   protected Response addMediaPackageElement(HttpServletRequest request, MediaPackageElement.Type type) {
605     MediaPackageElementFlavor flavor = null;
606     InputStream in = null;
607     try {
608       String fileName = null;
609       MediaPackage mp = null;
610       Long startTime = null;
611       String[] tags = null;
612       /* Only accept multipart/form-data */
613       if (!ServletFileUpload.isMultipartContent(request)) {
614         logger.trace("request isn't multipart-form-data");
615         return Response.serverError().status(Status.BAD_REQUEST).build();
616       }
617       boolean isDone = false;
618       for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
619         FileItemStream item = iter.next();
620         String fieldName = item.getFieldName();
621         if (item.isFormField()) {
622           if ("flavor".equals(fieldName)) {
623             String flavorString = Streams.asString(item.openStream(), "UTF-8");
624             logger.trace("flavor: {}", flavorString);
625             if (flavorString != null) {
626               try {
627                 flavor = MediaPackageElementFlavor.parseFlavor(flavorString);
628               } catch (IllegalArgumentException e) {
629                 String error = String.format("Could not parse flavor '%s'", flavorString);
630                 logger.debug(error, e);
631                 return Response.status(Status.BAD_REQUEST).entity(error).build();
632               }
633             }
634           } else if ("tags".equals(fieldName)) {
635             String tagsString = Streams.asString(item.openStream(), "UTF-8");
636             logger.trace("tags: {}", tagsString);
637             tags = tagsString.split(",");
638           } else if ("mediaPackage".equals(fieldName)) {
639             try {
640               String mediaPackageString = Streams.asString(item.openStream(), "UTF-8");
641               logger.trace("mediaPackage: {}", mediaPackageString);
642               mp = MP_FACTORY.newMediaPackageBuilder().loadFromXml(mediaPackageString);
643             } catch (MediaPackageException e) {
644               logger.debug("Unable to parse the 'mediaPackage' parameter: {}", ExceptionUtils.getMessage(e));
645               return Response.serverError().status(Status.BAD_REQUEST).build();
646             }
647           } else if ("startTime".equals(fieldName) && "/ingest/addPartialTrack".equals(request.getPathInfo())) {
648             String startTimeString = Streams.asString(item.openStream(), "UTF-8");
649             logger.trace("startTime: {}", startTime);
650             try {
651               startTime = Long.parseLong(startTimeString);
652             } catch (Exception e) {
653               logger.debug("Unable to parse the 'startTime' parameter: {}", ExceptionUtils.getMessage(e));
654               return Response.serverError().status(Status.BAD_REQUEST).build();
655             }
656           }
657         } else {
658           if (flavor == null) {
659             /* A flavor has to be specified in the request prior the video file */
660             logger.debug("A flavor has to be specified in the request prior to the content BODY");
661             return Response.serverError().status(Status.BAD_REQUEST).build();
662           }
663           fileName = item.getName();
664           in = item.openStream();
665           isDone = true;
666         }
667         if (isDone) {
668           break;
669         }
670       }
671       /*
672        * Check if we actually got a valid request including a message body and a valid mediapackage to attach the
673        * element to
674        */
675       if (in == null || mp == null || MediaPackageSupport.sanityCheck(mp).isPresent()) {
676         return Response.serverError().status(Status.BAD_REQUEST).build();
677       }
678       switch (type) {
679         case Attachment:
680           mp = ingestService.addAttachment(in, fileName, flavor, tags, mp);
681           break;
682         case Catalog:
683           try {
684             mp = ingestService.addCatalog(in, fileName, flavor, tags, mp);
685           } catch (IllegalArgumentException e) {
686             logger.debug("Invalid catalog data", e);
687             return Response.serverError().status(Status.BAD_REQUEST).build();
688           }
689           break;
690         case Track:
691           if (startTime == null) {
692             mp = ingestService.addTrack(in, fileName, flavor, tags, mp);
693           } else {
694             mp = ingestService.addPartialTrack(in, fileName, flavor, startTime, mp);
695           }
696           break;
697         default:
698           throw new IllegalStateException("Type must be one of track, catalog, or attachment");
699       }
700       return Response.ok(MediaPackageParser.getAsXml(mp)).build();
701     } catch (Exception e) {
702       logger.warn("Unable to add mediapackage element", e);
703       return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build();
704     } finally {
705       IOUtils.closeQuietly(in);
706     }
707   }
708 
709   @POST
710   @Produces(MediaType.TEXT_XML)
711   @Consumes(MediaType.MULTIPART_FORM_DATA)
712   @Path("addMediaPackage")
713   @RestQuery(
714       name = "addMediaPackage",
715       description = "<p>Create and ingest media package from media tracks with additional Dublin Core metadata. It is "
716         + "mandatory to set a title for the recording. This can be done with the 'title' form field or by supplying a "
717         + "DC catalog with a title included.  The identifier of the newly created media package will be taken from the "
718         + "<em>identifier</em> field or the episode DublinCore catalog (deprecated<sup>*</sup>). If no identifier is "
719         + "set, a new random UUIDv4 will be generated. This endpoint is not meant to be used by capture agents for "
720         + "scheduled recordings. Its primary use is for manual ingests with command line tools like cURL.</p> "
721         + "<p>Multiple tracks can be ingested by using multiple form fields. It is important to always set the "
722         + "flavor of the next media file <em>before</em> sending the media file itself.</p>"
723         + "<b>(*)</b> The special treatment of the identifier field is deprecated and may be removed in future "
724         + "versions without further notice in favor of a random UUID generation to ensure uniqueness of identifiers. "
725         + "<h3>Example cURL command:</h3>"
726         + "<p>Ingest one video file:</p>"
727         + "<p><pre>\n"
728         + "curl -i -u admin:opencast http://localhost:8080/ingest/addMediaPackage \\\n"
729         + "    -F creator='John Doe' -F title='Test Recording' \\\n"
730         + "    -F 'flavor=presentation/source' -F 'BODY=@test-recording.mp4' \n"
731         + "</pre></p>"
732         + "<p>Ingest two video files:</p>"
733         + "<p><pre>\n"
734         + "curl -i -u admin:opencast http://localhost:8080/ingest/addMediaPackage \\\n"
735         + "    -F creator='John Doe' -F title='Test Recording' \\\n"
736         + "    -F 'flavor=presentation/source' -F 'BODY=@test-recording-vga.mp4' \\\n"
737         + "    -F 'flavor=presenter/source' -F 'BODY=@test-recording-camera.mp4' \n"
738         + "</pre></p>",
739       restParameters = {
740           @RestParameter(description = "The kind of media track. This has to be specified prior to each media track",
741               isRequired = true, name = "flavor", type = RestParameter.Type.STRING),
742           @RestParameter(description = "Episode metadata value", isRequired = false, name = "abstract",
743               type = RestParameter.Type.STRING),
744           @RestParameter(description = "Episode metadata value", isRequired = false, name = "accessRights",
745               type = RestParameter.Type.STRING),
746           @RestParameter(description = "Episode metadata value", isRequired = false, name = "available",
747               type = RestParameter.Type.STRING),
748           @RestParameter(description = "Episode metadata value", isRequired = false, name = "contributor",
749               type = RestParameter.Type.STRING),
750           @RestParameter(description = "Episode metadata value", isRequired = false, name = "coverage",
751               type = RestParameter.Type.STRING),
752           @RestParameter(description = "Episode metadata value", isRequired = false, name = "created",
753               type = RestParameter.Type.STRING),
754           @RestParameter(description = "Episode metadata value", isRequired = false, name = "creator",
755               type = RestParameter.Type.STRING),
756           @RestParameter(description = "Episode metadata value", isRequired = false, name = "date",
757               type = RestParameter.Type.STRING),
758           @RestParameter(description = "Episode metadata value", isRequired = false, name = "description",
759               type = RestParameter.Type.STRING),
760           @RestParameter(description = "Episode metadata value", isRequired = false, name = "extent",
761               type = RestParameter.Type.STRING),
762           @RestParameter(description = "Episode metadata value", isRequired = false, name = "format",
763               type = RestParameter.Type.STRING),
764           @RestParameter(description = "Episode metadata value", isRequired = false, name = "identifier",
765               type = RestParameter.Type.STRING),
766           @RestParameter(description = "Episode metadata value", isRequired = false, name = "isPartOf",
767               type = RestParameter.Type.STRING),
768           @RestParameter(description = "Episode metadata value", isRequired = false, name = "isReferencedBy",
769               type = RestParameter.Type.STRING),
770           @RestParameter(description = "Episode metadata value", isRequired = false, name = "isReplacedBy",
771               type = RestParameter.Type.STRING),
772           @RestParameter(description = "Episode metadata value", isRequired = false, name = "language",
773               type = RestParameter.Type.STRING),
774           @RestParameter(description = "Episode metadata value", isRequired = false, name = "license",
775               type = RestParameter.Type.STRING),
776           @RestParameter(description = "Episode metadata value", isRequired = false, name = "publisher",
777               type = RestParameter.Type.STRING),
778           @RestParameter(description = "Episode metadata value", isRequired = false, name = "relation",
779               type = RestParameter.Type.STRING),
780           @RestParameter(description = "Episode metadata value", isRequired = false, name = "replaces",
781               type = RestParameter.Type.STRING),
782           @RestParameter(description = "Episode metadata value", isRequired = false, name = "rights",
783               type = RestParameter.Type.STRING),
784           @RestParameter(description = "Episode metadata value", isRequired = false, name = "rightsHolder",
785               type = RestParameter.Type.STRING),
786           @RestParameter(description = "Episode metadata value", isRequired = false, name = "source",
787               type = RestParameter.Type.STRING),
788           @RestParameter(description = "Episode metadata value", isRequired = false, name = "spatial",
789               type = RestParameter.Type.STRING),
790           @RestParameter(description = "Episode metadata value", isRequired = false, name = "subject",
791               type = RestParameter.Type.STRING),
792           @RestParameter(description = "Episode metadata value", isRequired = false, name = "temporal",
793               type = RestParameter.Type.STRING),
794           @RestParameter(description = "Episode metadata value", isRequired = false, name = "title",
795               type = RestParameter.Type.STRING),
796           @RestParameter(description = "Episode metadata value", isRequired = false, name = "type",
797               type = RestParameter.Type.STRING),
798           @RestParameter(description = "URL of episode DublinCore Catalog", isRequired = false,
799               name = "episodeDCCatalogUri", type = RestParameter.Type.STRING),
800           @RestParameter(description = "Episode DublinCore Catalog", isRequired = false, name = "episodeDCCatalog",
801               type = RestParameter.Type.STRING),
802           @RestParameter(description = "URL of series DublinCore Catalog", isRequired = false,
803               name = "seriesDCCatalogUri", type = RestParameter.Type.STRING),
804           @RestParameter(description = "Series DublinCore Catalog", isRequired = false, name = "seriesDCCatalog",
805               type = RestParameter.Type.STRING),
806           @RestParameter(description = "Access control list in XACML or JSON form", isRequired = false, name = "acl",
807               type = RestParameter.Type.STRING),
808           @RestParameter(description = "Tag of the next media file", isRequired = false, name = "tag",
809               type = RestParameter.Type.STRING),
810           @RestParameter(description = "URL of a media track file", isRequired = false, name = "mediaUri",
811               type = RestParameter.Type.STRING) },
812       bodyParameter = @RestParameter(description = "The media track file", isRequired = true, name = "BODY",
813           type = RestParameter.Type.FILE),
814       responses = {
815           @RestResponse(description = "Ingest successful. Returns workflow instance as xml",
816               responseCode = HttpServletResponse.SC_OK),
817           @RestResponse(description = "Ingest failed due to invalid requests.",
818               responseCode = HttpServletResponse.SC_BAD_REQUEST),
819           @RestResponse(description = "Ingest failed. Something went wrong internally. Please have a look at the "
820               + "log files", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) },
821       returnDescription = "")
822   public Response addMediaPackage(@Context HttpServletRequest request) {
823     logger.trace("add mediapackage as multipart-form-data");
824     return addMediaPackage(request, null);
825   }
826 
827   @POST
828   @Produces(MediaType.TEXT_XML)
829   @Consumes(MediaType.MULTIPART_FORM_DATA)
830   @Path("addMediaPackage/{wdID}")
831   @RestQuery(name = "addMediaPackage",
832       description = "<p>Create and ingest media package from media tracks with additional Dublin Core metadata. It is "
833         + "mandatory to set a title for the recording. This can be done with the 'title' form field or by supplying a "
834         + "DC catalog with a title included.  The identifier of the newly created media package will be taken from the "
835         + "<em>identifier</em> field or the episode DublinCore catalog (deprecated<sup>*</sup>). If no identifier is "
836         + "set, a newa randumm UUIDv4 will be generated. This endpoint is not meant to be used by capture agents for "
837         + "scheduled recordings. It's primary use is for manual ingests with command line tools like cURL.</p> "
838         + "<p>Multiple tracks can be ingested by using multiple form fields. It's important, however, to always set "
839         + "the flavor of the next media file <em>before</em> sending the media file itself.</p>"
840         + "<b>(*)</b> The special treatment of the identifier field is deprecated any may be removed in future "
841         + "versions without further notice in favor of a random UUID generation to ensure uniqueness of identifiers. "
842         + "<h3>Example cURL command:</h3>"
843         + "<p>Ingest one video file:</p>"
844         + "<p><pre>\n"
845         + "curl -i -u admin:opencast http://localhost:8080/ingest/addMediaPackage/fast \\\n"
846         + "    -F creator='John Doe' -F title='Test Recording' \\\n"
847         + "    -F 'flavor=presentation/source' -F 'BODY=@test-recording.mp4' \n"
848         + "</pre></p>"
849         + "<p>Ingest two video files:</p>"
850         + "<p><pre>\n"
851         + "curl -i -u admin:opencast http://localhost:8080/ingest/addMediaPackage/fast \\\n"
852         + "    -F creator='John Doe' -F title='Test Recording' \\\n"
853         + "    -F 'flavor=presentation/source' -F 'BODY=@test-recording-vga.mp4' \\\n"
854         + "    -F 'flavor=presenter/source' -F 'BODY=@test-recording-camera.mp4' \n"
855         + "</pre></p>",
856       pathParameters = {
857           @RestParameter(description = "Workflow definition id", isRequired = true, name = "wdID",
858               type = RestParameter.Type.STRING) },
859       restParameters = {
860           @RestParameter(description = "The kind of media track. This has to be specified prior to each media track",
861               isRequired = true, name = "flavor", type = RestParameter.Type.STRING),
862           @RestParameter(description = "Episode metadata value", isRequired = false, name = "abstract",
863               type = RestParameter.Type.STRING),
864           @RestParameter(description = "Episode metadata value", isRequired = false, name = "accessRights",
865               type = RestParameter.Type.STRING),
866           @RestParameter(description = "Episode metadata value", isRequired = false, name = "available",
867               type = RestParameter.Type.STRING),
868           @RestParameter(description = "Episode metadata value", isRequired = false, name = "contributor",
869               type = RestParameter.Type.STRING),
870           @RestParameter(description = "Episode metadata value", isRequired = false, name = "coverage",
871               type = RestParameter.Type.STRING),
872           @RestParameter(description = "Episode metadata value", isRequired = false, name = "created",
873               type = RestParameter.Type.STRING),
874           @RestParameter(description = "Episode metadata value", isRequired = false, name = "creator",
875               type = RestParameter.Type.STRING),
876           @RestParameter(description = "Episode metadata value", isRequired = false, name = "date",
877               type = RestParameter.Type.STRING),
878           @RestParameter(description = "Episode metadata value", isRequired = false, name = "description",
879               type = RestParameter.Type.STRING),
880           @RestParameter(description = "Episode metadata value", isRequired = false, name = "extent",
881               type = RestParameter.Type.STRING),
882           @RestParameter(description = "Episode metadata value", isRequired = false, name = "format",
883               type = RestParameter.Type.STRING),
884           @RestParameter(description = "Episode metadata value", isRequired = false, name = "identifier",
885               type = RestParameter.Type.STRING),
886           @RestParameter(description = "Episode metadata value", isRequired = false, name = "isPartOf",
887               type = RestParameter.Type.STRING),
888           @RestParameter(description = "Episode metadata value", isRequired = false, name = "isReferencedBy",
889               type = RestParameter.Type.STRING),
890           @RestParameter(description = "Episode metadata value", isRequired = false, name = "isReplacedBy",
891               type = RestParameter.Type.STRING),
892           @RestParameter(description = "Episode metadata value", isRequired = false, name = "language",
893               type = RestParameter.Type.STRING),
894           @RestParameter(description = "Episode metadata value", isRequired = false, name = "license",
895               type = RestParameter.Type.STRING),
896           @RestParameter(description = "Episode metadata value", isRequired = false, name = "publisher",
897               type = RestParameter.Type.STRING),
898           @RestParameter(description = "Episode metadata value", isRequired = false, name = "relation",
899               type = RestParameter.Type.STRING),
900           @RestParameter(description = "Episode metadata value", isRequired = false, name = "replaces",
901               type = RestParameter.Type.STRING),
902           @RestParameter(description = "Episode metadata value", isRequired = false, name = "rights",
903               type = RestParameter.Type.STRING),
904           @RestParameter(description = "Episode metadata value", isRequired = false, name = "rightsHolder",
905               type = RestParameter.Type.STRING),
906           @RestParameter(description = "Episode metadata value", isRequired = false, name = "source",
907               type = RestParameter.Type.STRING),
908           @RestParameter(description = "Episode metadata value", isRequired = false, name = "spatial",
909               type = RestParameter.Type.STRING),
910           @RestParameter(description = "Episode metadata value", isRequired = false, name = "subject",
911               type = RestParameter.Type.STRING),
912           @RestParameter(description = "Episode metadata value", isRequired = false, name = "temporal",
913               type = RestParameter.Type.STRING),
914           @RestParameter(description = "Episode metadata value", isRequired = false, name = "title",
915               type = RestParameter.Type.STRING),
916           @RestParameter(description = "Episode metadata value", isRequired = false, name = "type",
917               type = RestParameter.Type.STRING),
918           @RestParameter(description = "URL of episode DublinCore Catalog", isRequired = false,
919               name = "episodeDCCatalogUri", type = RestParameter.Type.STRING),
920           @RestParameter(description = "Episode DublinCore Catalog", isRequired = false, name = "episodeDCCatalog",
921               type = RestParameter.Type.STRING),
922           @RestParameter(description = "URL of series DublinCore Catalog", isRequired = false,
923               name = "seriesDCCatalogUri", type = RestParameter.Type.STRING),
924           @RestParameter(description = "Series DublinCore Catalog", isRequired = false, name = "seriesDCCatalog",
925               type = RestParameter.Type.STRING),
926           @RestParameter(description = "Access control list in XACML or JSON form", isRequired = false, name = "acl",
927               type = RestParameter.Type.STRING),
928           @RestParameter(description = "Tag of the next media file", isRequired = false, name = "tag",
929               type = RestParameter.Type.STRING),
930           @RestParameter(description = "URL of a media track file", isRequired = false, name = "mediaUri",
931               type = RestParameter.Type.STRING) },
932       bodyParameter = @RestParameter(description = "The media track file", isRequired = true, name = "BODY",
933           type = RestParameter.Type.FILE),
934       responses = {
935           @RestResponse(description = "Ingest successful. Returns workflow instance as XML",
936               responseCode = HttpServletResponse.SC_OK),
937           @RestResponse(description = "Ingest failed due to invalid requests.",
938               responseCode = HttpServletResponse.SC_BAD_REQUEST),
939           @RestResponse(description = "Ingest failed. A workflow is currently active on the media package",
940               responseCode = HttpServletResponse.SC_CONFLICT),
941           @RestResponse(description = "Ingest failed. Something went wrong internally. Please have a look at the "
942               + "log files", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) },
943       returnDescription = "")
944   public Response addMediaPackage(@Context HttpServletRequest request, @PathParam("wdID") String wdID) {
945     logger.trace("add mediapackage as multipart-form-data with workflow definition id: {}", wdID);
946     // For compatibility, we will support re-use of flavors for now but will print a warning.
947     // If there are no major complaints, we will remove this with Opencast 11 or 12
948     boolean flavorAlreadyUsed = false;
949     MediaPackageElementFlavor flavor = null;
950     List<String> tags = new ArrayList<>();
951     try {
952       MediaPackage mp = ingestService.createMediaPackage();
953       DublinCoreCatalog dcc = null;
954       Map<String, String> workflowProperties = new HashMap<>();
955       int seriesDCCatalogNumber = 0;
956       int episodeDCCatalogNumber = 0;
957       boolean hasMedia = false;
958       if (ServletFileUpload.isMultipartContent(request)) {
959         for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
960           FileItemStream item = iter.next();
961           if (item.isFormField()) {
962             String fieldName = item.getFieldName();
963             String value = Streams.asString(item.openStream(), "UTF-8");
964             logger.trace("form field {}: {}", fieldName, value);
965             /* Ignore empty fields */
966             if ("".equals(value)) {
967               continue;
968             }
969 
970             /* “Remember” the flavor for the next media. */
971             if ("flavor".equals(fieldName)) {
972               try {
973                 flavor = MediaPackageElementFlavor.parseFlavor(value);
974                 flavorAlreadyUsed = false;
975               } catch (IllegalArgumentException e) {
976                 return badRequest(String.format("Could not parse flavor '%s'", value), e);
977               }
978               /* “Remember” the tags for the next media. */
979             } else if ("tag".equals(fieldName)) {
980               tags.add(value);
981               /* Fields for DC catalog */
982             } else if (dcterms.contains(fieldName)) {
983               if ("identifier".equals(fieldName)) {
984                 /* Use the identifier for the mediapackage */
985                 mp.setIdentifier(new IdImpl(value));
986               }
987               if (dcc == null) {
988                 dcc = dublinCoreService.newInstance();
989               }
990               dcc.add(new EName(DublinCore.TERMS_NS_URI, fieldName), value);
991 
992               /* Episode metadata by URL */
993             } else if ("episodeDCCatalogUri".equals(fieldName)) {
994               try {
995                 URI dcUrl = new URI(value);
996                 ingestService.addCatalog(dcUrl, MediaPackageElements.EPISODE, null, mp);
997                 updateMediaPackageID(mp, dcUrl);
998                 episodeDCCatalogNumber += 1;
999               } catch (java.net.URISyntaxException e) {
1000                 return badRequest(String.format("Invalid URI %s for episodeDCCatalogUri", value), e);
1001               } catch (Exception e) {
1002                 return badRequest("Could not parse XML Dublin Core catalog", e);
1003               }
1004 
1005               /* Episode metadata DC catalog (XML) as string */
1006             } else if ("episodeDCCatalog".equals(fieldName)) {
1007               try (InputStream is = new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8))) {
1008                 final String fileName = "episode-" + episodeDCCatalogNumber + ".xml";
1009                 ingestService.addCatalog(is, fileName, MediaPackageElements.EPISODE, mp);
1010                 episodeDCCatalogNumber += 1;
1011                 is.reset();
1012                 updateMediaPackageID(mp, is);
1013               } catch (Exception e) {
1014                 return badRequest("Could not parse XML Dublin Core catalog", e);
1015               }
1016 
1017               /* Series by URL */
1018             } else if ("seriesDCCatalogUri".equals(fieldName)) {
1019               try {
1020                 URI dcUrl = new URI(value);
1021                 ingestService.addCatalog(dcUrl, MediaPackageElements.SERIES, null, mp);
1022               } catch (java.net.URISyntaxException e) {
1023                 return badRequest(String.format("Invalid URI %s for episodeDCCatalogUri", value), e);
1024               } catch (Exception e) {
1025                 return badRequest("Could not parse XML Dublin Core catalog", e);
1026               }
1027 
1028               /* Series DC catalog (XML) as string */
1029             } else if ("seriesDCCatalog".equals(fieldName)) {
1030               final String fileName = "series-" + seriesDCCatalogNumber + ".xml";
1031               seriesDCCatalogNumber += 1;
1032               try (InputStream is = new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8))) {
1033                 ingestService.addCatalog(is, fileName, MediaPackageElements.SERIES, mp);
1034               } catch (Exception e) {
1035                 return badRequest("Could not parse XML Dublin Core catalog", e);
1036               }
1037 
1038               // Add ACL in JSON, XML or XACML format
1039             } else if ("acl".equals(fieldName)) {
1040               InputStream inputStream = new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8));
1041               AccessControlList acl;
1042               try {
1043                 acl = AccessControlParser.parseAcl(inputStream);
1044                 inputStream = new ByteArrayInputStream(XACMLUtils.getXacml(mp, acl).getBytes(StandardCharsets.UTF_8));
1045               } catch (AccessControlParsingException e) {
1046                 // Couldn't parse this → already XACML. Why again are we using three different formats?
1047                 logger.debug("Unable to parse ACL, guessing that this is already XACML");
1048                 inputStream.reset();
1049               }
1050               ingestService.addAttachment(inputStream, "episode-security.xml", XACML_POLICY_EPISODE, mp);
1051 
1052               /* Add media files by URL */
1053             } else if ("mediaUri".equals(fieldName)) {
1054               if (flavor == null) {
1055                 return badRequest("A flavor has to be specified in the request prior to the media file", null);
1056               }
1057               URI mediaUrl;
1058               try {
1059                 mediaUrl = new URI(value);
1060               } catch (java.net.URISyntaxException e) {
1061                 return badRequest(String.format("Invalid URI %s for media", value), e);
1062               }
1063               warnIfFlavorAlreadyUsed(flavorAlreadyUsed);
1064               ingestService.addTrack(mediaUrl, flavor, tags.toArray(new String[0]), mp);
1065               flavorAlreadyUsed = true;
1066               tags.clear();
1067               hasMedia = true;
1068 
1069             } else {
1070               /* Tread everything else as workflow properties */
1071               workflowProperties.put(fieldName, value);
1072             }
1073 
1074             /* Media files as request parameter */
1075           } else {
1076             if (flavor == null) {
1077               /* A flavor has to be specified in the request prior the video file */
1078               return badRequest("A flavor has to be specified in the request prior to the content BODY", null);
1079             }
1080             warnIfFlavorAlreadyUsed(flavorAlreadyUsed);
1081             ingestService.addTrack(item.openStream(), item.getName(), flavor, tags.toArray(new String[0]), mp);
1082             flavorAlreadyUsed = true;
1083             tags.clear();
1084             hasMedia = true;
1085           }
1086         }
1087 
1088         /* Check if we got any media. Fail if not. */
1089         if (!hasMedia) {
1090           return badRequest("Rejected ingest without actual media.", null);
1091         }
1092 
1093         /* Add episode mediapackage if metadata were send separately */
1094         if (dcc != null) {
1095           ByteArrayOutputStream out = new ByteArrayOutputStream();
1096           try {
1097             dcc.toXml(out, true);
1098             try (InputStream in = new ByteArrayInputStream(out.toByteArray())) {
1099               ingestService.addCatalog(in, "dublincore.xml", MediaPackageElements.EPISODE, mp);
1100             }
1101           } catch (Exception e) {
1102             return badRequest("Could not create XML from ingested metadata", e);
1103           }
1104 
1105           /* Check if we have metadata for the episode */
1106         } else if (episodeDCCatalogNumber == 0) {
1107           return badRequest("Rejected ingest without episode metadata. At least provide a title.", null);
1108         }
1109 
1110         WorkflowInstance workflow = (wdID == null)
1111             ? ingestService.ingest(mp)
1112             : ingestService.ingest(mp, wdID, workflowProperties);
1113         return Response.ok(new JaxbWorkflowInstance(workflow)).build();
1114       }
1115       return Response.serverError().status(Status.BAD_REQUEST).build();
1116     } catch (IllegalArgumentException e) {
1117       return badRequest(e.getMessage(), e);
1118     } catch (IllegalStateException e) {
1119       return Response.status(Status.CONFLICT).entity(e.getMessage()).build();
1120     } catch (Exception e) {
1121       logger.warn("Unable to add mediapackage", e);
1122       return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build();
1123     }
1124   }
1125 
1126   @Deprecated
1127   private void warnIfFlavorAlreadyUsed(final boolean used) {
1128     if (used) {
1129       logger.warn("\n"
1130           + "********************************************\n"
1131           + "* Warning: Re-use of flavors during ingest *\n"
1132           + "*          is deprecated and will be       *\n"
1133           + "*          removed soon! Declare a flavor  *\n"
1134           + "*          for each media file or create   *\n"
1135           + "*          an issue if you need this.      *\n"
1136           + "********************************************"
1137       );
1138     }
1139   }
1140 
1141   /**
1142    * Try updating the identifier of a mediapackage with the identifier from a episode DublinCore catalog.
1143    *
1144    * @param mp
1145    *          MediaPackage to modify
1146    * @param is
1147    *          InputStream containing the episode DublinCore catalog
1148    */
1149   private void updateMediaPackageID(MediaPackage mp, InputStream is) throws IOException {
1150     DublinCoreCatalog dc = DublinCores.read(is);
1151     EName en = new EName(DublinCore.TERMS_NS_URI, "identifier");
1152     String id = dc.getFirst(en);
1153     if (id != null) {
1154       mp.setIdentifier(new IdImpl(id));
1155     }
1156   }
1157 
1158   /**
1159    * Try updating the identifier of a mediapackage with the identifier from a episode DublinCore catalog.
1160    *
1161    * @param mp
1162    *          MediaPackage to modify
1163    * @param uri
1164    *          URI to get the episode DublinCore catalog from
1165    */
1166   private void updateMediaPackageID(MediaPackage mp, URI uri) throws IOException {
1167     InputStream in = null;
1168     HttpResponse response = null;
1169     try {
1170       if (uri.toString().startsWith("http")) {
1171         HttpGet get = new HttpGet(uri);
1172         response = httpClient.execute(get);
1173         int httpStatusCode = response.getStatusLine().getStatusCode();
1174         if (httpStatusCode != 200) {
1175           throw new IOException(uri + " returns http " + httpStatusCode);
1176         }
1177         in = response.getEntity().getContent();
1178       } else {
1179         in = uri.toURL().openStream();
1180       }
1181       updateMediaPackageID(mp, in);
1182       in.close();
1183     } finally {
1184       IOUtils.closeQuietly(in);
1185       httpClient.close(response);
1186     }
1187   }
1188 
1189   @POST
1190   @Path("addZippedMediaPackage/{workflowDefinitionId}")
1191   @Produces(MediaType.TEXT_XML)
1192   @RestQuery(
1193       name = "addZippedMediaPackage",
1194       description = "Create media package from a compressed file containing a manifest.xml document and all media "
1195           + "tracks, metadata catalogs and attachments",
1196       pathParameters = {
1197           @RestParameter(description = "Workflow definition id", isRequired = true,
1198               name = WORKFLOW_DEFINITION_ID_PARAM, type = RestParameter.Type.STRING)
1199       },
1200       restParameters = {
1201           @RestParameter(description = "The workflow instance ID to associate with this zipped mediapackage",
1202               isRequired = false, name = WORKFLOW_INSTANCE_ID_PARAM, type = RestParameter.Type.STRING)
1203       },
1204       bodyParameter = @RestParameter(description = "The compressed (application/zip) media package file",
1205           isRequired = true, name = "BODY", type = RestParameter.Type.FILE),
1206       responses = {
1207           @RestResponse(description = "", responseCode = HttpServletResponse.SC_OK),
1208           @RestResponse(description = "", responseCode = HttpServletResponse.SC_BAD_REQUEST),
1209           @RestResponse(description = "", responseCode = HttpServletResponse.SC_NOT_FOUND),
1210           @RestResponse(description = "", responseCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE)
1211       },
1212       returnDescription = "")
1213   public Response addZippedMediaPackage(@Context HttpServletRequest request,
1214           @PathParam("workflowDefinitionId") String wdID, @QueryParam("id") String wiID) {
1215     logger.trace("add zipped media package with workflow definition id: {} and workflow instance id: {}", wdID, wiID);
1216     if (!isIngestLimitEnabled() || getIngestLimit() > 0) {
1217       return ingestZippedMediaPackage(request, wdID, wiID);
1218     } else {
1219       logger.warn("Delaying ingest because we have exceeded the maximum number of ingests this server is setup to do "
1220           + "concurrently.");
1221       return Response.status(Status.SERVICE_UNAVAILABLE).build();
1222     }
1223   }
1224 
1225   @POST
1226   @Path("addZippedMediaPackage")
1227   @Produces(MediaType.TEXT_XML)
1228   @RestQuery(
1229       name = "addZippedMediaPackage",
1230       description = "Create media package from a compressed file containing a manifest.xml document and all media "
1231           + "tracks, metadata catalogs and attachments",
1232       restParameters = {
1233           @RestParameter(description = "The workflow definition ID to run on this mediapackage. "
1234                   + "This parameter has to be set in the request prior to the zipped mediapackage "
1235                   + "(This parameter is deprecated. Please use /addZippedMediaPackage/{workflowDefinitionId} instead)",
1236               isRequired = false, name = WORKFLOW_DEFINITION_ID_PARAM, type = RestParameter.Type.STRING),
1237           @RestParameter(description = "The workflow instance ID to associate with this zipped mediapackage. "
1238                   + "This parameter has to be set in the request prior to the zipped mediapackage "
1239                   + "(This parameter is deprecated. Please use /addZippedMediaPackage/{workflowDefinitionId} with a "
1240                   + "path parameter instead)",
1241               isRequired = false, name = WORKFLOW_INSTANCE_ID_PARAM, type = RestParameter.Type.STRING)
1242       },
1243       bodyParameter = @RestParameter(description = "The compressed (application/zip) media package file",
1244           isRequired = true, name = "BODY", type = RestParameter.Type.FILE),
1245       responses = {
1246           @RestResponse(description = "", responseCode = HttpServletResponse.SC_OK),
1247           @RestResponse(description = "", responseCode = HttpServletResponse.SC_BAD_REQUEST),
1248           @RestResponse(description = "", responseCode = HttpServletResponse.SC_NOT_FOUND),
1249           @RestResponse(description = "", responseCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE)
1250       },
1251       returnDescription = "")
1252   public Response addZippedMediaPackage(@Context HttpServletRequest request) {
1253     logger.trace("add zipped media package");
1254     if (!isIngestLimitEnabled() || getIngestLimit() > 0) {
1255       return ingestZippedMediaPackage(request, null, null);
1256     } else {
1257       logger.warn("Delaying ingest because we have exceeded the maximum number of ingests this server is setup to do "
1258           + "concurrently.");
1259       return Response.status(Status.SERVICE_UNAVAILABLE).build();
1260     }
1261   }
1262 
1263   private Response ingestZippedMediaPackage(HttpServletRequest request, String wdID, String wiID) {
1264     if (isIngestLimitEnabled()) {
1265       setIngestLimit(getIngestLimit() - 1);
1266       logger.debug("An ingest has started so remaining ingest limit is " + getIngestLimit());
1267     }
1268     InputStream in = null;
1269     Date started = new Date();
1270 
1271     logger.info("Received new request from {} to ingest a zipped mediapackage", request.getRemoteHost());
1272 
1273     try {
1274       String workflowDefinitionId = wdID;
1275       String workflowIdAsString = wiID;
1276       Long workflowInstanceIdAsLong = null;
1277       Map<String, String> workflowConfig = new HashMap<>();
1278       if (ServletFileUpload.isMultipartContent(request)) {
1279         boolean isDone = false;
1280         for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
1281           FileItemStream item = iter.next();
1282           if (item.isFormField()) {
1283             String fieldName = item.getFieldName();
1284             String value = Streams.asString(item.openStream(), "UTF-8");
1285             logger.trace("{}: {}", fieldName, value);
1286             if (WORKFLOW_INSTANCE_ID_PARAM.equals(fieldName)) {
1287               workflowIdAsString = value;
1288               continue;
1289             } else if (WORKFLOW_DEFINITION_ID_PARAM.equals(fieldName)) {
1290               workflowDefinitionId = value;
1291               continue;
1292             } else {
1293               logger.debug("Processing form field: " + fieldName);
1294               workflowConfig.put(fieldName, value);
1295             }
1296           } else {
1297             logger.debug("Processing file item");
1298             // once the body gets read iter.hasNext must not be invoked or the stream can not be read
1299             // MH-9579
1300             in = item.openStream();
1301             isDone = true;
1302           }
1303           if (isDone) {
1304             break;
1305           }
1306         }
1307       } else {
1308         logger.debug("Processing file item");
1309         in = request.getInputStream();
1310       }
1311 
1312       // Adding ingest start time to workflow configuration
1313       DateFormat formatter = new SimpleDateFormat(IngestService.UTC_DATE_FORMAT);
1314       workflowConfig.put(IngestService.START_DATE_KEY, formatter.format(started));
1315 
1316       /* Legacy support: Try to convert the workflowId to integer */
1317       if (!StringUtils.isBlank(workflowIdAsString)) {
1318         try {
1319           workflowInstanceIdAsLong = Long.parseLong(workflowIdAsString);
1320         } catch (NumberFormatException e) {
1321           // The workflowId is not a long value and might be the media package identifier
1322           workflowConfig.put(IngestServiceImpl.LEGACY_MEDIAPACKAGE_ID_KEY, workflowIdAsString);
1323         }
1324       }
1325       if (StringUtils.isBlank(workflowDefinitionId)) {
1326         workflowDefinitionId = defaultWorkflowDefinitionId;
1327       }
1328 
1329       WorkflowInstance workflow;
1330       if (workflowInstanceIdAsLong != null) {
1331         workflow = ingestService.addZippedMediaPackage(in, workflowDefinitionId, workflowConfig,
1332                 workflowInstanceIdAsLong);
1333       } else {
1334         workflow = ingestService.addZippedMediaPackage(in, workflowDefinitionId, workflowConfig);
1335       }
1336       return Response.ok(XmlWorkflowParser.toXml(workflow)).build();
1337     } catch (NotFoundException e) {
1338       logger.info("Not found: {}", e.getMessage());
1339       return Response.status(Status.NOT_FOUND).build();
1340     } catch (MediaPackageException e) {
1341       logger.warn("Unable to ingest mediapackage: {}", e.getMessage());
1342       return Response.serverError().status(Status.BAD_REQUEST).build();
1343     } catch (Exception e) {
1344       logger.warn("Unable to ingest mediapackage", e);
1345       return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build();
1346     } finally {
1347       IOUtils.closeQuietly(in);
1348       if (isIngestLimitEnabled()) {
1349         setIngestLimit(getIngestLimit() + 1);
1350         logger.debug("An ingest has finished so increased ingest limit to " + getIngestLimit());
1351       }
1352     }
1353   }
1354 
1355   @POST
1356   @Produces(MediaType.TEXT_XML)
1357   @Path("ingest/{wdID}")
1358   @RestQuery(
1359       name = "ingest",
1360       description = "<p>Ingest the completed media package into the system and start a specified workflow.</p>"
1361              + "<p>In addition to the documented form parameters, workflow parameters are accepted as well.</p>",
1362       pathParameters = {
1363           @RestParameter(description = "Workflow definition id", isRequired = true, name = "wdID",
1364               type = RestParameter.Type.STRING)
1365       },
1366       restParameters = {
1367           @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage",
1368               type = RestParameter.Type.TEXT)
1369       },
1370       responses = {
1371           @RestResponse(description = "Returns the workflow instance", responseCode = HttpServletResponse.SC_OK),
1372           @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST)
1373       },
1374       returnDescription = "")
1375   public Response ingest(@Context HttpServletRequest request, @PathParam("wdID") String wdID) {
1376     logger.trace("ingest media package with workflow definition id: {}", wdID);
1377     if (StringUtils.isBlank(wdID)) {
1378       return Response.status(Response.Status.BAD_REQUEST).build();
1379     }
1380     return ingest(wdID, request);
1381   }
1382 
1383   @POST
1384   @Produces(MediaType.TEXT_XML)
1385   @Path("ingest")
1386   @RestQuery(
1387       name = "ingest",
1388       description = "<p>Ingest the completed media package into the system</p>"
1389              + "<p>In addition to the documented form parameters, workflow parameters are accepted as well.</p>",
1390       restParameters = {
1391           @RestParameter(description = "The media package", isRequired = true, name = "mediaPackage",
1392               type = RestParameter.Type.TEXT),
1393           @RestParameter(description = "Workflow definition id", isRequired = false,
1394               name = WORKFLOW_DEFINITION_ID_PARAM, type = RestParameter.Type.STRING),
1395           @RestParameter(description = "The workflow instance ID to associate this ingest with scheduled events.",
1396               isRequired = false, name = WORKFLOW_INSTANCE_ID_PARAM, type = RestParameter.Type.STRING)
1397       },
1398       responses = {
1399           @RestResponse(description = "Returns the workflow instance", responseCode = HttpServletResponse.SC_OK),
1400           @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST)
1401       },
1402       returnDescription = "")
1403   public Response ingest(@Context HttpServletRequest request) {
1404     return ingest(null, request);
1405   }
1406 
1407   private Map<String, String> getWorkflowConfig(MultivaluedMap<String, String> formData) {
1408     Map<String, String> wfConfig = new HashMap<>();
1409     for (String key : formData.keySet()) {
1410       if (!"mediaPackage".equals(key)) {
1411         wfConfig.put(key, formData.getFirst(key));
1412       }
1413     }
1414     return wfConfig;
1415   }
1416 
1417   private Response ingest(final String wdID, final HttpServletRequest request) {
1418     /* Note: We use a MultivaluedMap here to ensure that we can get any arbitrary form parameters. This is required to
1419      * enable things like holding for trim or distributing to YouTube. */
1420     final MultivaluedMap<String, String> formData = new MultivaluedHashMap<>();
1421     if (ServletFileUpload.isMultipartContent(request)) {
1422       // parse form fields
1423       try {
1424         for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
1425           FileItemStream item = iter.next();
1426           if (item.isFormField()) {
1427             final String value = Streams.asString(item.openStream(), "UTF-8");
1428             formData.putSingle(item.getFieldName(), value);
1429           }
1430         }
1431       } catch (FileUploadException | IOException e) {
1432         return Response.status(Response.Status.BAD_REQUEST).build();
1433       }
1434     } else {
1435       request.getParameterMap().forEach((key, value) -> formData.put(key, Arrays.asList(value)));
1436     }
1437 
1438     final Map<String, String> wfConfig = getWorkflowConfig(formData);
1439     if (StringUtils.isNotBlank(wdID)) {
1440       wfConfig.put(WORKFLOW_DEFINITION_ID_PARAM, wdID);
1441     }
1442 
1443     final MediaPackage mp;
1444     try {
1445       mp = MP_FACTORY.newMediaPackageBuilder().loadFromXml(formData.getFirst("mediaPackage"));
1446       if (MediaPackageSupport.sanityCheck(mp).isPresent()) {
1447         logger.warn("Rejected ingest with invalid mediapackage {}", mp);
1448         return Response.status(Status.BAD_REQUEST).build();
1449       }
1450     } catch (Exception e) {
1451       logger.warn("Rejected ingest without mediapackage");
1452       return Response.status(Status.BAD_REQUEST).build();
1453     }
1454 
1455     final String workflowInstance = wfConfig.get(WORKFLOW_INSTANCE_ID_PARAM);
1456     final String workflowDefinition = wfConfig.get(WORKFLOW_DEFINITION_ID_PARAM);
1457 
1458     // Adding ingest start time to workflow configuration
1459     final Date ingestDate = startCache.getIfPresent(mp.getIdentifier().toString());
1460     wfConfig.put(IngestService.START_DATE_KEY, DATE_FORMAT.format(ingestDate != null ? ingestDate : new Date()));
1461 
1462     try {
1463       /* Legacy support: Try to convert the workflowInstance to integer */
1464       Long workflowInstanceId = null;
1465       if (StringUtils.isNotBlank(workflowInstance)) {
1466         try {
1467           workflowInstanceId = Long.parseLong(workflowInstance);
1468         } catch (NumberFormatException e) {
1469           // The workflowId is not a long value and might be the media package identifier
1470           wfConfig.put(IngestServiceImpl.LEGACY_MEDIAPACKAGE_ID_KEY, workflowInstance);
1471         }
1472       }
1473 
1474       WorkflowInstance workflow;
1475       if (workflowInstanceId != null) {
1476         workflow = ingestService.ingest(mp, trimToNull(workflowDefinition), wfConfig, workflowInstanceId);
1477       } else {
1478         workflow = ingestService.ingest(mp, trimToNull(workflowDefinition), wfConfig);
1479       }
1480 
1481       startCache.asMap().remove(mp.getIdentifier().toString());
1482       return Response.ok(XmlWorkflowParser.toXml(workflow)).build();
1483 
1484     } catch (Exception e) {
1485       Throwable cause = e.getCause();
1486       if (cause instanceof NotFoundException) {
1487         return badRequest("Could not retrieve all media package elements", e);
1488       }
1489       logger.warn("Unable to ingest mediapackage", e);
1490       return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build();
1491     }
1492   }
1493 
1494   @POST
1495   @Path("schedule")
1496   @RestQuery(
1497       name = "schedule",
1498       description = "Schedule an event based on the given media package",
1499       restParameters = {
1500           @RestParameter(description = "The media package", isRequired = true, name = "mediaPackage",
1501               type = RestParameter.Type.TEXT)
1502       },
1503       responses = {
1504           @RestResponse(description = "Event scheduled", responseCode = HttpServletResponse.SC_CREATED),
1505           @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST)
1506       },
1507       returnDescription = "")
1508   public Response schedule(MultivaluedMap<String, String> formData) {
1509     logger.trace("pass schedule with default workflow definition id {}", defaultWorkflowDefinitionId);
1510     return this.schedule(defaultWorkflowDefinitionId, formData);
1511   }
1512 
1513   @POST
1514   @Path("schedule/{wdID}")
1515   @RestQuery(
1516       name = "schedule",
1517       description = "Schedule an event based on the given media package",
1518       pathParameters = {
1519           @RestParameter(description = "Workflow definition id", isRequired = true, name = "wdID",
1520               type = RestParameter.Type.STRING)
1521       },
1522       restParameters = {
1523           @RestParameter(description = "The media package", isRequired = true, name = "mediaPackage",
1524               type = RestParameter.Type.TEXT)
1525       },
1526       responses = {
1527           @RestResponse(description = "Event scheduled", responseCode = HttpServletResponse.SC_CREATED),
1528           @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST)
1529       },
1530       returnDescription = "")
1531   public Response schedule(@PathParam("wdID") String wdID, MultivaluedMap<String, String> formData) {
1532     if (StringUtils.isBlank(wdID)) {
1533       logger.trace("workflow definition id is not specified");
1534       return Response.status(Response.Status.BAD_REQUEST).build();
1535     }
1536 
1537     Map<String, String> wfConfig = getWorkflowConfig(formData);
1538     if (StringUtils.isNotBlank(wdID)) {
1539       wfConfig.put(CaptureParameters.INGEST_WORKFLOW_DEFINITION, wdID);
1540     }
1541     logger.debug("Schedule with workflow definition '{}'", wfConfig.get(WORKFLOW_DEFINITION_ID_PARAM));
1542 
1543     String mediaPackageXml = formData.getFirst("mediaPackage");
1544     if (StringUtils.isBlank(mediaPackageXml)) {
1545       logger.debug("Rejected schedule without media package");
1546       return Response.status(Status.BAD_REQUEST).build();
1547     }
1548 
1549     MediaPackage mp = null;
1550     try {
1551       mp = MP_FACTORY.newMediaPackageBuilder().loadFromXml(mediaPackageXml);
1552       if (MediaPackageSupport.sanityCheck(mp).isPresent()) {
1553         throw new MediaPackageException("Insane media package");
1554       }
1555     } catch (MediaPackageException e) {
1556       logger.debug("Rejected ingest with invalid media package {}", mp);
1557       return Response.status(Status.BAD_REQUEST).build();
1558     }
1559 
1560     MediaPackageElement[] mediaPackageElements = mp.getElementsByFlavor(MediaPackageElements.EPISODE);
1561     if (mediaPackageElements.length != 1) {
1562       logger.debug("There can be only one (and exactly one) episode dublin core catalog: https://youtu.be/_J3VeogFUOs");
1563       return Response.status(Status.BAD_REQUEST).build();
1564     }
1565 
1566     try {
1567       ingestService.schedule(mp, wdID, wfConfig);
1568       return Response.status(Status.CREATED).build();
1569     } catch (IngestException e) {
1570       return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build();
1571     } catch (SchedulerConflictException e) {
1572       return Response.status(Status.CONFLICT).entity(e.getMessage()).build();
1573     } catch (NotFoundException | UnauthorizedException | SchedulerException e) {
1574       return Response.serverError().build();
1575     }
1576   }
1577 
1578   /**
1579    * Adds a dublinCore metadata catalog to the MediaPackage and returns the grown mediaPackage. JQuery Ajax functions
1580    * doesn't support multipart/form-data encoding.
1581    *
1582    * @param mp
1583    *          MediaPackage
1584    * @param dc
1585    *          DublinCoreCatalog
1586    * @return grown MediaPackage XML
1587    */
1588   @POST
1589   @Produces(MediaType.TEXT_XML)
1590   @Path("addDCCatalog")
1591   @RestQuery(
1592       name = "addDCCatalog",
1593       description = "Add a dublincore episode catalog to a given media package using an url",
1594       restParameters = {
1595           @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage",
1596               type = RestParameter.Type.TEXT),
1597           @RestParameter(description = "DublinCore catalog as XML", isRequired = true, name = "dublinCore",
1598               type = RestParameter.Type.TEXT),
1599           @RestParameter(defaultValue = "dublincore/episode", description = "DublinCore Flavor", isRequired = false,
1600               name = "flavor", type = RestParameter.Type.STRING)
1601       },
1602       responses = {
1603           @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK),
1604           @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST),
1605           @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
1606       },
1607       returnDescription = "")
1608   public Response addDCCatalog(@FormParam("mediaPackage") String mp, @FormParam("dublinCore") String dc,
1609           @FormParam("flavor") String flavor) {
1610     logger.trace("add DC catalog: {} with flavor: {} to media package: {}", dc, flavor, mp);
1611     MediaPackageElementFlavor dcFlavor = MediaPackageElements.EPISODE;
1612     if (flavor != null) {
1613       try {
1614         dcFlavor = MediaPackageElementFlavor.parseFlavor(flavor);
1615       } catch (IllegalArgumentException e) {
1616         return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build();
1617       }
1618     }
1619     MediaPackage mediaPackage;
1620     /* Check if we got a proper mediapackage and try to parse it */
1621     try {
1622       mediaPackage = MediaPackageBuilderFactory.newInstance().newMediaPackageBuilder().loadFromXml(mp);
1623     } catch (MediaPackageException e) {
1624       return Response.serverError().status(Status.BAD_REQUEST).build();
1625     }
1626     if (MediaPackageSupport.sanityCheck(mediaPackage).isPresent()) {
1627       return Response.status(Status.BAD_REQUEST).build();
1628     }
1629 
1630     /* Check if we got a proper catalog */
1631     try {
1632       DublinCoreXmlFormat.read(dc);
1633     } catch (Exception e) {
1634       return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build();
1635     }
1636 
1637     try (InputStream in = IOUtils.toInputStream(dc, "UTF-8")) {
1638       mediaPackage = ingestService.addCatalog(in, "dublincore.xml", dcFlavor, mediaPackage);
1639     } catch (MediaPackageException e) {
1640       return Response.serverError().status(Status.BAD_REQUEST).entity(e.getMessage()).build();
1641     } catch (IOException e) {
1642       logger.error("Could not write catalog to disk", e);
1643       return Response.serverError().build();
1644     } catch (Exception e) {
1645       logger.error("Unable to add catalog", e);
1646       return Response.serverError().build();
1647     }
1648     return Response.ok(mediaPackage).build();
1649   }
1650 
1651   /**
1652    * Return a bad request response but log additional details in debug mode.
1653    *
1654    * @param message
1655    *          Message to send
1656    * @param e
1657    *          Exception to log. If <pre>null</pre>, a new exception is created to log a stack trace.
1658    * @return 400 BAD REQUEST HTTP response
1659    */
1660   private Response badRequest(final String message, final Exception e) {
1661     logger.debug(message, e == null && logger.isDebugEnabled() ? new IngestException(message) : e);
1662     return Response.status(Status.BAD_REQUEST)
1663         .entity(message)
1664         .build();
1665   }
1666 
1667   @Override
1668   public JobProducer getService() {
1669     return ingestService;
1670   }
1671 
1672   @Override
1673   public ServiceRegistry getServiceRegistry() {
1674     return serviceRegistry;
1675   }
1676 
1677   /**
1678    * OSGi Declarative Services callback to set the reference to the ingest service.
1679    *
1680    * @param ingestService
1681    *          the ingest service
1682    */
1683   @Reference
1684   void setIngestService(IngestService ingestService) {
1685     this.ingestService = ingestService;
1686   }
1687 
1688   /**
1689    * OSGi Declarative Services callback to set the reference to the service registry.
1690    *
1691    * @param serviceRegistry
1692    *          the service registry
1693    */
1694   @Reference
1695   void setServiceRegistry(ServiceRegistry serviceRegistry) {
1696     this.serviceRegistry = serviceRegistry;
1697   }
1698 
1699   /**
1700    * OSGi Declarative Services callback to set the reference to the dublin core service.
1701    *
1702    * @param dcService
1703    *          the dublin core service
1704    */
1705   @Reference
1706   void setDublinCoreService(DublinCoreCatalogService dcService) {
1707     this.dublinCoreService = dcService;
1708   }
1709 
1710   /**
1711    * Sets the trusted http client
1712    *
1713    * @param httpClient
1714    *          the http client
1715    */
1716   @Reference
1717   public void setHttpClient(TrustedHttpClient httpClient) {
1718     this.httpClient = httpClient;
1719   }
1720 
1721 }