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