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