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