/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.ingest.endpoint; import org.opencastproject.ingest.api.IngestService; import org.opencastproject.job.api.JobProducer; import org.opencastproject.mediapackage.EName; import org.opencastproject.mediapackage.MediaPackage; import org.opencastproject.mediapackage.MediaPackageBuilderFactory; import org.opencastproject.mediapackage.MediaPackageElement; import org.opencastproject.mediapackage.MediaPackageElementFlavor; import org.opencastproject.mediapackage.MediaPackageElements; import org.opencastproject.mediapackage.MediaPackageException; import org.opencastproject.mediapackage.MediaPackageParser; import org.opencastproject.mediapackage.MediaPackageSupport; import org.opencastproject.mediapackage.identifier.IdImpl; import org.opencastproject.metadata.dublincore.DublinCore; import org.opencastproject.metadata.dublincore.DublinCoreCatalog; import org.opencastproject.metadata.dublincore.DublinCoreCatalogService; import org.opencastproject.metadata.dublincore.DublinCores; import org.opencastproject.rest.AbstractJobProducerEndpoint; import org.opencastproject.security.api.TrustedHttpClient; import org.opencastproject.serviceregistry.api.ServiceRegistry; import org.opencastproject.util.NotFoundException; import org.opencastproject.util.UploadJob; import org.opencastproject.util.UploadProgressListener; import org.opencastproject.util.doc.rest.RestParameter; import org.opencastproject.util.doc.rest.RestQuery; import org.opencastproject.util.doc.rest.RestResponse; import org.opencastproject.util.doc.rest.RestService; import org.opencastproject.workflow.api.WorkflowInstance; import org.opencastproject.workflow.api.WorkflowParser; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.apache.commons.fileupload.FileItemIterator; import org.apache.commons.fileupload.FileItemStream; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.fileupload.util.Streams; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.json.simple.JSONObject; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.persistence.NoResultException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; /** * Creates and augments Matterhorn MediaPackages using the api. Stores media into the Working File Repository. */ @Path("/") @RestService(name = "ingestservice", title = "Ingest Service", abstractText = "This service creates and augments Matterhorn media packages that include media tracks, metadata " + "catalogs and attachments.", notes = { "All paths above are relative to the REST endpoint base (something like http://your.server/files)", "If the service is down or not working it will return a status 503, this means the the underlying service is " + "not working and is either restarting or has failed", "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In " + "other words, there is a bug! You should file an error report with your server logs from the time when the " + "error occurred: <a href=\"https://opencast.jira.com\">Opencast Issue Tracker</a>" }) public class IngestRestService extends AbstractJobProducerEndpoint { private static final Logger logger = LoggerFactory.getLogger(IngestRestService.class); /** Key for the default workflow definition in config.properties */ protected static final String DEFAULT_WORKFLOW_DEFINITION = "org.opencastproject.workflow.default.definition"; /** Key for the default maximum number of ingests in config.properties */ protected static final String MAX_INGESTS_KEY = "org.opencastproject.ingest.max.concurrent"; /** The http request parameter used to provide the workflow instance id */ protected static final String WORKFLOW_INSTANCE_ID_PARAM = "workflowInstanceId"; /** The http request parameter used to provide the workflow definition id */ protected static final String WORKFLOW_DEFINITION_ID_PARAM = "workflowDefinitionId"; /** The default workflow definition */ private String defaultWorkflowDefinitionId = null; /** The http client */ private TrustedHttpClient httpClient; /** Dublin Core Terms: http://purl.org/dc/terms/ */ private static List<String> dcterms = Arrays.asList("abstract", "accessRights", "accrualMethod", "accrualPeriodicity", "accrualPolicy", "alternative", "audience", "available", "bibliographicCitation", "conformsTo", "contributor", "coverage", "created", "creator", "date", "dateAccepted", "dateCopyrighted", "dateSubmitted", "description", "educationLevel", "extent", "format", "hasFormat", "hasPart", "hasVersion", "identifier", "instructionalMethod", "isFormatOf", "isPartOf", "isReferencedBy", "isReplacedBy", "isRequiredBy", "issued", "isVersionOf", "language", "license", "mediator", "medium", "modified", "provenance", "publisher", "references", "relation", "replaces", "requires", "rights", "rightsHolder", "source", "spatial", "subject", "tableOfContents", "temporal", "title", "type", "valid"); private MediaPackageBuilderFactory factory = null; private IngestService ingestService = null; private ServiceRegistry serviceRegistry = null; private DublinCoreCatalogService dublinCoreService; // For the progress bar -1 bug workaround, keeping UploadJobs in memory rather than saving them using JPA private HashMap<String, UploadJob> jobs; // The number of ingests this service can handle concurrently. private int ingestLimit = -1; /* Stores a map workflow ID and date to update the ingest start times post-hoc */ private Cache<String, Date> startCache = null; /* Formatter to for the date into a string */ private DateFormat formatter = new SimpleDateFormat(IngestService.UTC_DATE_FORMAT); public IngestRestService() { factory = MediaPackageBuilderFactory.newInstance(); jobs = new HashMap<String, UploadJob>(); startCache = CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.DAYS).build(); } /** * Returns the maximum number of concurrent ingest operations or <code>-1</code> if no limit is enforced. * * @return the maximum number of concurrent ingest operations * @see #isIngestLimitEnabled() */ protected synchronized int getIngestLimit() { return ingestLimit; } /** * Sets the maximum number of concurrent ingest operations. Use <code>-1</code> to indicate no limit. * * @param ingestLimit * the limit */ private synchronized void setIngestLimit(int ingestLimit) { this.ingestLimit = ingestLimit; } /** * Returns <code>true</code> if a maximum number of concurrent ingest operations has been defined. * * @return <code>true</code> if there is a maximum number of concurrent ingests */ protected synchronized boolean isIngestLimitEnabled() { return ingestLimit >= 0; } /** * Callback for activation of this component. */ public void activate(ComponentContext cc) { if (cc != null) { defaultWorkflowDefinitionId = StringUtils.trimToNull(cc.getBundleContext().getProperty( DEFAULT_WORKFLOW_DEFINITION)); if (defaultWorkflowDefinitionId == null) { throw new IllegalStateException("Default workflow definition is null: " + DEFAULT_WORKFLOW_DEFINITION); } if (cc.getBundleContext().getProperty(MAX_INGESTS_KEY) != null) { try { ingestLimit = Integer.parseInt(StringUtils.trimToNull(cc.getBundleContext().getProperty(MAX_INGESTS_KEY))); if (ingestLimit == 0) { ingestLimit = -1; } } catch (NumberFormatException e) { logger.warn("Max ingest property with key " + MAX_INGESTS_KEY + " isn't defined so no ingest limit will be used."); ingestLimit = -1; } } } } @PUT @Produces(MediaType.TEXT_XML) @Path("createMediaPackageWithID/{id}") @RestQuery(name = "createMediaPackageWithID", description = "Create an empty media package with ID /n Overrides Existing Mediapackage ", pathParameters = { @RestParameter(description = "The Id for the new Mediapackage", isRequired = true, name = "id", type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns media package", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response createMediaPackage(@PathParam("id") String mediaPackageId) { MediaPackage mp; try { mp = ingestService.createMediaPackage(mediaPackageId); startCache.put(mp.getIdentifier().toString(), new Date()); return Response.ok(mp).build(); } catch (Exception e) { logger.warn(e.getMessage(), e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } } @GET @Produces(MediaType.TEXT_XML) @Path("createMediaPackage") @RestQuery(name = "createMediaPackage", description = "Create an empty media package", restParameters = { }, reponses = { @RestResponse(description = "Returns media package", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response createMediaPackage() { MediaPackage mp; try { mp = ingestService.createMediaPackage(); startCache.put(mp.getIdentifier().toString(), new Date()); return Response.ok(mp).build(); } catch (Exception e) { logger.warn(e.getMessage(), e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } } @POST @Path("discardMediaPackage") @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) }, reponses = { @RestResponse(description = "", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response discardMediaPackage(@FormParam("mediaPackage") String mpx) { logger.debug("discardMediaPackage(MediaPackage): {}", mpx); try { MediaPackage mp = factory.newMediaPackageBuilder().loadFromXml(mpx); ingestService.discardMediaPackage(mp); return Response.ok().build(); } catch (Exception e) { logger.warn(e.getMessage(), e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } } @POST @Produces(MediaType.TEXT_XML) @Path("addTrack") @RestQuery(name = "addTrackURL", description = "Add a media track to a given media package using an URL", restParameters = { @RestParameter(description = "The location of the media", isRequired = true, name = "url", type = RestParameter.Type.STRING), @RestParameter(description = "The kind of media", isRequired = true, name = "flavor", type = RestParameter.Type.STRING), @RestParameter(description = "The Tags of the media track", isRequired = false, name = "tags", type = RestParameter.Type.STRING), @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage", type = RestParameter.Type.TEXT) }, reponses = { @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response addMediaPackageTrack(@FormParam("url") String url, @FormParam("flavor") String flavor, @FormParam("tags") String tags, @FormParam("mediaPackage") String mpx) { try { MediaPackage mp = factory.newMediaPackageBuilder().loadFromXml(mpx); if (MediaPackageSupport.sanityCheck(mp).isSome()) return Response.serverError().status(Status.BAD_REQUEST).build(); String[] tagsArray = null; if (tags != null) { tagsArray = tags.split(","); } mp = ingestService.addTrack(new URI(url), MediaPackageElementFlavor.parseFlavor(flavor), tagsArray, mp); return Response.ok(mp).build(); } catch (Exception e) { logger.warn(e.getMessage(), e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } } @POST @Produces(MediaType.TEXT_XML) @Consumes(MediaType.MULTIPART_FORM_DATA) @Path("addTrack") @RestQuery( name = "addTrackInputStream", description = "Add a media track to a given media package using an input stream", restParameters = { @RestParameter(description = "The kind of media track", isRequired = true, name = "flavor", type = RestParameter.Type.STRING), @RestParameter(description = "The Tags of the media track", isRequired = false, name = "tags", type = RestParameter.Type.STRING), @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), reponses = { @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response addMediaPackageTrack(@Context HttpServletRequest request) { return addMediaPackageElement(request, MediaPackageElement.Type.Track); } @POST @Produces(MediaType.TEXT_XML) @Path("addPartialTrack") @RestQuery(name = "addPartialTrackURL", description = "Add a partial media track to a given media package using an URL", restParameters = { @RestParameter(description = "The location of the media", isRequired = true, name = "url", type = RestParameter.Type.STRING), @RestParameter(description = "The kind of media", isRequired = true, name = "flavor", type = RestParameter.Type.STRING), @RestParameter(description = "The start time in milliseconds", isRequired = true, name = "startTime", type = RestParameter.Type.INTEGER), @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage", type = RestParameter.Type.TEXT) }, reponses = { @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response addMediaPackagePartialTrack(@FormParam("url") String url, @FormParam("flavor") String flavor, @FormParam("startTime") Long startTime, @FormParam("mediaPackage") String mpx) { try { MediaPackage mp = factory.newMediaPackageBuilder().loadFromXml(mpx); if (MediaPackageSupport.sanityCheck(mp).isSome()) return Response.serverError().status(Status.BAD_REQUEST).build(); mp = ingestService.addPartialTrack(new URI(url), MediaPackageElementFlavor.parseFlavor(flavor), startTime, mp); return Response.ok(mp).build(); } catch (Exception e) { logger.warn(e.getMessage(), e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } } @POST @Produces(MediaType.TEXT_XML) @Consumes(MediaType.MULTIPART_FORM_DATA) @Path("addPartialTrack") @RestQuery(name = "addPartialTrackInputStream", description = "Add a partial media track to a given media package using an input stream", restParameters = { @RestParameter(description = "The kind of media track", isRequired = true, name = "flavor", type = RestParameter.Type.STRING), @RestParameter(description = "The start time in milliseconds", isRequired = true, name = "startTime", type = RestParameter.Type.INTEGER), @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), reponses = { @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response addMediaPackagePartialTrack(@Context HttpServletRequest request) { return addMediaPackageElement(request, MediaPackageElement.Type.Track); } @POST @Produces(MediaType.TEXT_XML) @Path("addCatalog") @RestQuery(name = "addCatalogURL", description = "Add a metadata catalog to a given media package using an URL", restParameters = { @RestParameter(description = "The location of the catalog", isRequired = true, name = "url", type = RestParameter.Type.STRING), @RestParameter(description = "The kind of catalog", isRequired = true, name = "flavor", type = RestParameter.Type.STRING), @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage", type = RestParameter.Type.TEXT) }, reponses = { @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response addMediaPackageCatalog(@FormParam("url") String url, @FormParam("flavor") String flavor, @FormParam("mediaPackage") String mpx) { try { MediaPackage mp = factory.newMediaPackageBuilder().loadFromXml(mpx); if (MediaPackageSupport.sanityCheck(mp).isSome()) return Response.serverError().status(Status.BAD_REQUEST).build(); MediaPackage resultingMediaPackage = ingestService.addCatalog(new URI(url), MediaPackageElementFlavor.parseFlavor(flavor), mp); return Response.ok(resultingMediaPackage).build(); } catch (Exception e) { logger.warn(e.getMessage(), e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } } @POST @Produces(MediaType.TEXT_XML) @Consumes(MediaType.MULTIPART_FORM_DATA) @Path("addCatalog") @RestQuery(name = "addCatalogInputStream", description = "Add a metadata catalog to a given media package using an input stream", restParameters = { @RestParameter(description = "The kind of media catalog", isRequired = true, name = "flavor", type = RestParameter.Type.STRING), @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), reponses = { @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response addMediaPackageCatalog(@Context HttpServletRequest request) { return addMediaPackageElement(request, MediaPackageElement.Type.Catalog); } @POST @Produces(MediaType.TEXT_XML) @Path("addAttachment") @RestQuery(name = "addAttachmentURL", description = "Add an attachment to a given media package using an URL", restParameters = { @RestParameter(description = "The location of the attachment", isRequired = true, name = "url", type = RestParameter.Type.STRING), @RestParameter(description = "The kind of attachment", isRequired = true, name = "flavor", type = RestParameter.Type.STRING), @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage", type = RestParameter.Type.TEXT) }, reponses = { @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response addMediaPackageAttachment(@FormParam("url") String url, @FormParam("flavor") String flavor, @FormParam("mediaPackage") String mpx) { try { MediaPackage mp = factory.newMediaPackageBuilder().loadFromXml(mpx); if (MediaPackageSupport.sanityCheck(mp).isSome()) return Response.serverError().status(Status.BAD_REQUEST).build(); mp = ingestService.addAttachment(new URI(url), MediaPackageElementFlavor.parseFlavor(flavor), mp); return Response.ok(mp).build(); } catch (Exception e) { logger.warn(e.getMessage(), e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } } @POST @Produces(MediaType.TEXT_XML) @Consumes(MediaType.MULTIPART_FORM_DATA) @Path("addAttachment") @RestQuery(name = "addAttachmentInputStream", description = "Add an attachment to a given media package using an input stream", restParameters = { @RestParameter(description = "The kind of attachment", isRequired = true, name = "flavor", type = RestParameter.Type.STRING), @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), reponses = { @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response addMediaPackageAttachment(@Context HttpServletRequest request) { return addMediaPackageElement(request, MediaPackageElement.Type.Attachment); } protected Response addMediaPackageElement(HttpServletRequest request, MediaPackageElement.Type type) { MediaPackageElementFlavor flavor = null; InputStream in = null; try { String fileName = null; MediaPackage mp = null; Long startTime = null; String[] tags = null; /* Only accept multipart/form-data */ if (!ServletFileUpload.isMultipartContent(request)) { return Response.serverError().status(Status.BAD_REQUEST).build(); } boolean isDone = false; for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) { FileItemStream item = iter.next(); String fieldName = item.getFieldName(); if (item.isFormField()) { if ("flavor".equals(fieldName)) { String flavorString = Streams.asString(item.openStream()); if (flavorString != null) { flavor = MediaPackageElementFlavor.parseFlavor(flavorString); } } else if ("tags".equals(fieldName)) { tags = Streams.asString(item.openStream()).split(","); } else if ("mediaPackage".equals(fieldName)) { try { mp = factory.newMediaPackageBuilder().loadFromXml(item.openStream()); } catch (MediaPackageException e) { return Response.serverError().status(Status.BAD_REQUEST).build(); } } else if ("startTime".equals(fieldName)) { try { startTime = Long.parseLong(IOUtils.toString(item.openStream())); } catch (Exception e) { logger.info("Unable to parse the 'startTime' parameter: {}", ExceptionUtils.getMessage(e)); return Response.serverError().status(Status.BAD_REQUEST).build(); } } } else { if (flavor == null) { /* A flavor has to be specified in the request prior the video file */ return Response.serverError().status(Status.BAD_REQUEST).build(); } fileName = item.getName(); in = item.openStream(); isDone = true; } if (isDone) { break; } } /* * Check if we actually got a valid request including a message body and a valid mediapackage to attach the * element to */ if (in == null || mp == null || MediaPackageSupport.sanityCheck(mp).isSome()) { return Response.serverError().status(Status.BAD_REQUEST).build(); } switch (type) { case Attachment: mp = ingestService.addAttachment(in, fileName, flavor, tags, mp); break; case Catalog: mp = ingestService.addCatalog(in, fileName, flavor, tags, mp); break; case Track: if (startTime == null) { mp = ingestService.addTrack(in, fileName, flavor, tags, mp); } else { mp = ingestService.addPartialTrack(in, fileName, flavor, startTime, mp); } break; default: throw new IllegalStateException("Type must be one of track, catalog, or attachment"); } // ingestService.ingest(mp); return Response.ok(MediaPackageParser.getAsXml(mp)).build(); } catch (Exception e) { logger.warn(e.getMessage(), e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } finally { IOUtils.closeQuietly(in); } } @POST @Produces(MediaType.TEXT_XML) @Consumes(MediaType.MULTIPART_FORM_DATA) @Path("addMediaPackage") @RestQuery(name = "addMediaPackage", description = "<p>Create and ingest media package from media tracks with additional Dublin Core metadata. It is " + "mandatory to set a title for the recording. This can be done with the 'title' form field or by supplying a DC " + "catalog with a title included. The identifier of the newly created media package will be taken from the " + "<em>identifier</em> field or the episode DublinCore catalog (deprecated<sup>*</sup>). If no identifier is " + "set, a new random UUIDv4 will be generated. This endpoint is not meant to be used by capture agents for " + "scheduled recordings. Its primary use is for manual ingests with command line tools like curl.</p> " + "<p>Multiple tracks can be ingested by using multiple form fields. It is important to always set the " + "flavor of the next media file <em>before</em> sending the media file itself.</p>" + "<b>(*)</b> The special treatment of the identifier field is deprecated and may be removed in future versions " + "without further notice in favor of a random UUID generation to ensure uniqueness of identifiers. " + "<h3>Example curl command:</h3>" + "<p>Ingest one video file:</p>" + "<p><pre>\n" + "curl -f -i --digest -u opencast_system_account:CHANGE_ME -H 'X-Requested-Auth: Digest' \\\n" + " http://localhost:8080/ingest/addMediaPackage -F creator='John Doe' -F title='Test Recording' \\\n" + " -F 'flavor=presentation/source' -F 'BODY=@test-recording.mp4' \n" + "</pre></p>" + "<p>Ingest two video files:</p>" + "<p><pre>\n" + "curl -f -i --digest -u opencast_system_account:CHANGE_ME -H 'X-Requested-Auth: Digest' \\\n" + " http://localhost:8080/ingest/addMediaPackage -F creator='John Doe' -F title='Test Recording' \\\n" + " -F 'flavor=presentation/source' -F 'BODY=@test-recording-vga.mp4' \\\n" + " -F 'flavor=presenter/source' -F 'BODY=@test-recording-camera.mp4' \n" + "</pre></p>", restParameters = { @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), @RestParameter(description = "Episode metadata value", isRequired = false, name = "abstract", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "accessRights", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "available", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "contributor", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "coverage", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "created", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "creator", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "date", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "description", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "extent", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "format", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "identifier", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "isPartOf", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "isReferencedBy", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "isReplacedBy", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "language", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "license", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "publisher", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "relation", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "replaces", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "rights", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "rightsHolder", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "source", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "spatial", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "subject", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "temporal", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "title", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "type", type = RestParameter.Type.STRING), @RestParameter(description = "URL of episode DublinCore Catalog", isRequired = false, name = "episodeDCCatalogUri", type = RestParameter.Type.STRING), @RestParameter(description = "Episode DublinCore Catalog", isRequired = false, name = "episodeDCCatalog", type = RestParameter.Type.STRING), @RestParameter(description = "URL of series DublinCore Catalog", isRequired = false, name = "seriesDCCatalogUri", type = RestParameter.Type.STRING), @RestParameter(description = "Series DublinCore Catalog", isRequired = false, name = "seriesDCCatalog", type = RestParameter.Type.STRING), @RestParameter(description = "URL of a media track file", isRequired = false, name = "mediaUri", type = RestParameter.Type.STRING) }, bodyParameter = @RestParameter(description = "The media track file", isRequired = true, name = "BODY", type = RestParameter.Type.FILE), reponses = { @RestResponse(description = "Ingest successfull. Returns workflow instance as xml", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Ingest failed due to invalid requests.", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "Ingest failed. Something went wrong internally. Please have a look at the log files", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response addMediaPackage(@Context HttpServletRequest request) { return addMediaPackage(request, null); } @POST @Produces(MediaType.TEXT_XML) @Consumes(MediaType.MULTIPART_FORM_DATA) @Path("addMediaPackage/{wdID}") @RestQuery(name = "addMediaPackage", description = "<p>Create and ingest media package from media tracks with additional Dublin Core metadata. It is " + "mandatory to set a title for the recording. This can be done with the 'title' form field or by supplying a DC " + "catalog with a title included. The identifier of the newly created media package will be taken from the " + "<em>identifier</em> field or the episode DublinCore catalog (deprecated<sup>*</sup>). If no identifier is " + "set, a newa randumm UUIDv4 will be generated. This endpoint is not meant to be used by capture agents for " + "scheduled recordings. It's primary use is for manual ingests with command line tools like curl.</p> " + "<p>Multiple tracks can be ingested by using multiple form fields. It's important, however, to always set the " + "flavor of the next media file <em>before</em> sending the media file itself.</p>" + "<b>(*)</b> The special treatment of the identifier field is deprecated any may be removed in future versions " + "without further notice in favor of a random UUID generation to ensure uniqueness of identifiers. " + "<h3>Example curl command:</h3>" + "<p>Ingest one video file:</p>" + "<p><pre>\n" + "curl -f -i --digest -u opencast_system_account:CHANGE_ME -H 'X-Requested-Auth: Digest' \\\n" + " http://localhost:8080/ingest/addMediaPackage/fast -F creator='John Doe' -F title='Test Recording' \\\n" + " -F 'flavor=presentation/source' -F 'BODY=@test-recording.mp4' \n" + "</pre></p>" + "<p>Ingest two video files:</p>" + "<p><pre>\n" + "curl -f -i --digest -u opencast_system_account:CHANGE_ME -H 'X-Requested-Auth: Digest' \\\n" + " http://localhost:8080/ingest/addMediaPackage/fast -F creator='John Doe' -F title='Test Recording' \\\n" + " -F 'flavor=presentation/source' -F 'BODY=@test-recording-vga.mp4' \\\n" + " -F 'flavor=presenter/source' -F 'BODY=@test-recording-camera.mp4' \n" + "</pre></p>", pathParameters = { @RestParameter(description = "Workflow definition id", isRequired = true, name = "wdID", type = RestParameter.Type.STRING) }, restParameters = { @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), @RestParameter(description = "Episode metadata value", isRequired = false, name = "abstract", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "accessRights", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "available", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "contributor", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "coverage", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "created", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "creator", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "date", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "description", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "extent", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "format", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "identifier", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "isPartOf", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "isReferencedBy", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "isReplacedBy", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "language", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "license", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "publisher", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "relation", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "replaces", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "rights", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "rightsHolder", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "source", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "spatial", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "subject", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "temporal", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "title", type = RestParameter.Type.STRING), @RestParameter(description = "Episode metadata value", isRequired = false, name = "type", type = RestParameter.Type.STRING), @RestParameter(description = "URL of episode DublinCore Catalog", isRequired = false, name = "episodeDCCatalogUri", type = RestParameter.Type.STRING), @RestParameter(description = "Episode DublinCore Catalog", isRequired = false, name = "episodeDCCatalog", type = RestParameter.Type.STRING), @RestParameter(description = "URL of series DublinCore Catalog", isRequired = false, name = "seriesDCCatalogUri", type = RestParameter.Type.STRING), @RestParameter(description = "Series DublinCore Catalog", isRequired = false, name = "seriesDCCatalog", type = RestParameter.Type.STRING), @RestParameter(description = "URL of a media track file", isRequired = false, name = "mediaUri", type = RestParameter.Type.STRING) }, bodyParameter = @RestParameter(description = "The media track file", isRequired = true, name = "BODY", type = RestParameter.Type.FILE), reponses = { @RestResponse(description = "Ingest successfull. Returns workflow instance as XML", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Ingest failed due to invalid requests.", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "Ingest failed. Something went wrong internally. Please have a look at the log files", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response addMediaPackage(@Context HttpServletRequest request, @PathParam("wdID") String wdID) { MediaPackageElementFlavor flavor = null; try { MediaPackage mp = ingestService.createMediaPackage(); DublinCoreCatalog dcc = null; Map<String, String> workflowProperties = new HashMap<String, String>(); int seriesDCCatalogNumber = 0; int episodeDCCatalogNumber = 0; boolean hasMedia = false; if (ServletFileUpload.isMultipartContent(request)) { for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) { FileItemStream item = iter.next(); if (item.isFormField()) { String fieldName = item.getFieldName(); String value = Streams.asString(item.openStream(), "UTF-8"); /* Ignore empty fields */ if ("".equals(value)) { continue; } /* “Remember” the flavor for the next media. */ if ("flavor".equals(fieldName)) { flavor = MediaPackageElementFlavor.parseFlavor(value); /* Fields for DC catalog */ } else if (dcterms.contains(fieldName)) { if ("identifier".equals(fieldName)) { /* Use the identifier for the mediapackage */ mp.setIdentifier(new IdImpl(value)); } EName en = new EName(DublinCore.TERMS_NS_URI, fieldName); if (dcc == null) { dcc = dublinCoreService.newInstance(); } dcc.add(en, value); /* Episode metadata by URL */ } else if ("episodeDCCatalogUri".equals(fieldName)) { try { URI dcurl = new URI(value); updateMediaPackageID(mp, dcurl); ingestService.addCatalog(dcurl, MediaPackageElements.EPISODE, mp); episodeDCCatalogNumber += 1; } catch (java.net.URISyntaxException e) { /* Parameter was not a valid URL: Return 400 Bad Request */ logger.warn(e.getMessage(), e); return Response.serverError().status(Status.BAD_REQUEST).build(); } /* Episode metadata DC catalog (XML) as string */ } else if ("episodeDCCatalog".equals(fieldName)) { InputStream is = new ByteArrayInputStream(value.getBytes("UTF-8")); updateMediaPackageID(mp, is); is.reset(); String fileName = "episode" + episodeDCCatalogNumber + ".xml"; episodeDCCatalogNumber += 1; ingestService.addCatalog(is, fileName, MediaPackageElements.EPISODE, mp); /* Series by URL */ } else if ("seriesDCCatalogUri".equals(fieldName)) { try { URI dcurl = new URI(value); ingestService.addCatalog(dcurl, MediaPackageElements.SERIES, mp); } catch (java.net.URISyntaxException e) { /* Parameter was not a valid URL: Return 400 Bad Request */ logger.warn(e.getMessage(), e); return Response.serverError().status(Status.BAD_REQUEST).build(); } /* Series DC catalog (XML) as string */ } else if ("seriesDCCatalog".equals(fieldName)) { String fileName = "series" + seriesDCCatalogNumber + ".xml"; seriesDCCatalogNumber += 1; InputStream is = new ByteArrayInputStream(value.getBytes("UTF-8")); ingestService.addCatalog(is, fileName, MediaPackageElements.SERIES, mp); /* Add media files by URL */ } else if ("mediaUri".equals(fieldName)) { if (flavor == null) { /* A flavor has to be specified in the request prior the media file */ return Response.serverError().status(Status.BAD_REQUEST).build(); } URI mediaUrl; try { mediaUrl = new URI(value); } catch (java.net.URISyntaxException e) { /* Parameter was not a valid URL: Return 400 Bad Request */ logger.warn(e.getMessage(), e); return Response.serverError().status(Status.BAD_REQUEST).build(); } ingestService.addTrack(mediaUrl, flavor, mp); hasMedia = true; } else { /* Tread everything else as workflow properties */ workflowProperties.put(fieldName, value); } /* Media files as request parameter */ } else { if (flavor == null) { /* A flavor has to be specified in the request prior the video file */ return Response.serverError().status(Status.BAD_REQUEST).build(); } ingestService.addTrack(item.openStream(), item.getName(), flavor, mp); hasMedia = true; } } /* Check if we got any media. Fail if not. */ if (!hasMedia) { logger.warn("Rejected ingest without actual media."); return Response.serverError().status(Status.BAD_REQUEST).build(); } /* Add episode mediapackage if metadata were send separately */ if (dcc != null) { ByteArrayOutputStream out = new ByteArrayOutputStream(); dcc.toXml(out, true); InputStream in = new ByteArrayInputStream(out.toByteArray()); ingestService.addCatalog(in, "dublincore.xml", MediaPackageElements.EPISODE, mp); /* Check if we have metadata for the episode */ } else if (episodeDCCatalogNumber == 0) { logger.warn("Rejected ingest without episode metadata. At least provide a title."); return Response.serverError().status(Status.BAD_REQUEST).build(); } WorkflowInstance workflow = (wdID == null) ? ingestService.ingest(mp) : ingestService.ingest(mp, wdID, workflowProperties); return Response.ok(workflow).build(); } return Response.serverError().status(Status.BAD_REQUEST).build(); } catch (Exception e) { logger.warn(e.getMessage(), e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } } /** * Try updating the identifier of a mediapackage with the identifier from a episode DublinCore catalog. * * @param mp * MediaPackage to modify * @param is * InputStream containing the episode DublinCore catalog */ private void updateMediaPackageID(MediaPackage mp, InputStream is) throws IOException { DublinCoreCatalog dc = DublinCores.read(is); EName en = new EName(DublinCore.TERMS_NS_URI, "identifier"); String id = dc.getFirst(en); if (id != null) { mp.setIdentifier(new IdImpl(id)); } } /** * Try updating the identifier of a mediapackage with the identifier from a episode DublinCore catalog. * * @param mp * MediaPackage to modify * @param uri * URI to get the episode DublinCore catalog from */ private void updateMediaPackageID(MediaPackage mp, URI uri) throws IOException { InputStream in = null; HttpResponse response = null; try { if (uri.toString().startsWith("http")) { HttpGet get = new HttpGet(uri); response = httpClient.execute(get); int httpStatusCode = response.getStatusLine().getStatusCode(); if (httpStatusCode != 200) { throw new IOException(uri + " returns http " + httpStatusCode); } in = response.getEntity().getContent(); } else { in = uri.toURL().openStream(); } updateMediaPackageID(mp, in); in.close(); } finally { IOUtils.closeQuietly(in); httpClient.close(response); } } @POST @Path("addZippedMediaPackage/{workflowDefinitionId}") @Produces(MediaType.TEXT_XML) @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), reponses = { @RestResponse(description = "", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "", responseCode = HttpServletResponse.SC_NOT_FOUND), @RestResponse(description = "", responseCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE) }, returnDescription = "") public Response addZippedMediaPackage(@Context HttpServletRequest request, @PathParam("workflowDefinitionId") String wdID, @QueryParam("id") String wiID) { logger.debug("addZippedMediaPackage(HttpRequest)"); if (!isIngestLimitEnabled() || getIngestLimit() > 0) { return ingestZippedMediaPackage(request, wdID, wiID); } else { logger.warn("Delaying ingest because we have exceeded the maximum number of ingests this server is setup to do concurrently."); return Response.status(Status.SERVICE_UNAVAILABLE).build(); } } @POST @Path("addZippedMediaPackage") @Produces(MediaType.TEXT_XML) @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 = { @RestParameter(description = "The workflow definition ID to run on this mediapackage. " + "This parameter has to be set in the request prior to the zipped mediapackage " + "(This parameter is deprecated. Please use /addZippedMediaPackage/{workflowDefinitionId} instead)", isRequired = false, name = WORKFLOW_DEFINITION_ID_PARAM, type = RestParameter.Type.STRING), @RestParameter(description = "The workflow instance ID to associate with this zipped mediapackage. " + "This parameter has to be set in the request prior to the zipped mediapackage " + "(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), reponses = { @RestResponse(description = "", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "", responseCode = HttpServletResponse.SC_NOT_FOUND), @RestResponse(description = "", responseCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE) }, returnDescription = "") public Response addZippedMediaPackage(@Context HttpServletRequest request) { logger.debug("addZippedMediaPackage(HttpRequest)"); if (!isIngestLimitEnabled() || getIngestLimit() > 0) { return ingestZippedMediaPackage(request, null, null); } else { logger.warn("Delaying ingest because we have exceeded the maximum number of ingests this server is setup to do concurrently."); return Response.status(Status.SERVICE_UNAVAILABLE).build(); } } private Response ingestZippedMediaPackage(HttpServletRequest request, String wdID, String wiID) { if (isIngestLimitEnabled()) { setIngestLimit(getIngestLimit() - 1); logger.debug("An ingest has started so remaining ingest limit is " + getIngestLimit()); } InputStream in = null; Date started = new Date(); logger.info("Received new request from {} to ingest a zipped mediapackage", request.getRemoteHost()); try { String workflowDefinitionId = wdID; String workflowIdAsString = wiID; Long workflowInstanceIdAsLong = null; Map<String, String> workflowConfig = new HashMap<String, String>(); if (ServletFileUpload.isMultipartContent(request)) { boolean isDone = false; for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) { FileItemStream item = iter.next(); if (item.isFormField()) { if (WORKFLOW_INSTANCE_ID_PARAM.equals(item.getFieldName())) { workflowIdAsString = IOUtils.toString(item.openStream(), "UTF-8"); continue; } else if (WORKFLOW_DEFINITION_ID_PARAM.equals(item.getFieldName())) { workflowDefinitionId = IOUtils.toString(item.openStream(), "UTF-8"); continue; } else { logger.debug("Processing form field: " + item.getFieldName()); workflowConfig.put(item.getFieldName(), IOUtils.toString(item.openStream(), "UTF-8")); } } else { logger.debug("Processing file item"); // once the body gets read iter.hasNext must not be invoked or the stream can not be read // MH-9579 in = item.openStream(); isDone = true; } if (isDone) break; } } else { logger.debug("Processing file item"); in = request.getInputStream(); } DateFormat formatter = new SimpleDateFormat(IngestService.UTC_DATE_FORMAT); workflowConfig.put(IngestService.START_DATE_KEY, formatter.format(started)); /* Try to convert the workflowId to integer */ if (!StringUtils.isBlank(workflowIdAsString)) { try { workflowInstanceIdAsLong = Long.parseLong(workflowIdAsString); } catch (NumberFormatException e) { logger.warn("{} '{}' is not numeric", WORKFLOW_INSTANCE_ID_PARAM, workflowIdAsString); } } if (StringUtils.isBlank(workflowDefinitionId)) { workflowDefinitionId = defaultWorkflowDefinitionId; } WorkflowInstance workflow = ingestService.addZippedMediaPackage(in, workflowDefinitionId, workflowConfig, workflowInstanceIdAsLong); return Response.ok(WorkflowParser.toXml(workflow)).build(); } catch (NotFoundException e) { logger.info(e.getMessage()); return Response.status(Status.NOT_FOUND).build(); } catch (MediaPackageException e) { logger.warn(e.getMessage()); return Response.serverError().status(Status.BAD_REQUEST).build(); } catch (Exception e) { logger.warn(e.getMessage(), e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } finally { IOUtils.closeQuietly(in); if (isIngestLimitEnabled()) { setIngestLimit(getIngestLimit() + 1); logger.debug("An ingest has finished so increased ingest limit to " + getIngestLimit()); } } } @POST @Produces(MediaType.TEXT_HTML) @Path("ingest") @RestQuery(name = "ingest", description = "Ingest the completed media package into the system, retrieving all URL-referenced files", restParameters = { @RestParameter(description = "The media package", isRequired = true, name = "mediaPackage", type = RestParameter.Type.TEXT), @RestParameter(description = "Workflow definition id", isRequired = false, name = WORKFLOW_DEFINITION_ID_PARAM, type = RestParameter.Type.STRING), @RestParameter(description = "The workflow instance ID to associate with this zipped mediapackage", isRequired = false, name = WORKFLOW_INSTANCE_ID_PARAM, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns the media package", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "") public Response ingest(MultivaluedMap<String, String> formData) { /** * Note: We use a MultivaluedMap here to ensure that we can get any arbitrary form parameters. This is required to * enable things like holding for trim or distributing to YouTube. */ logger.debug("ingest(MediaPackage)"); try { MediaPackage mp = null; Map<String, String> wfConfig = new HashMap<String, String>(); for (String key : formData.keySet()) { if (!"mediaPackage".equals(key)) { wfConfig.put(key, formData.getFirst(key)); } else { mp = factory.newMediaPackageBuilder().loadFromXml(formData.getFirst(key)); } } if (mp == null) { logger.warn("Rejected ingest without mediapackage."); return Response.status(Response.Status.BAD_REQUEST).build(); } return ingest(mp, wfConfig); } catch (Exception e) { logger.warn(e.getMessage(), e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } } @POST @Produces(MediaType.TEXT_HTML) @Path("ingest/{wdID}") @RestQuery(name = "ingest", description = "Ingest the completed media package into the system, retrieving all URL-referenced files, and starting a specified workflow", pathParameters = { @RestParameter(description = "Workflow definition id", isRequired = true, name = "wdID", type = RestParameter.Type.STRING) }, restParameters = { @RestParameter(description = "The ID of the given media package", isRequired = true, name = "mediaPackage", type = RestParameter.Type.TEXT) }, reponses = { @RestResponse(description = "Returns the media package", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "") public Response ingest(@PathParam("wdID") String wdID, MultivaluedMap<String, String> formData) { if (StringUtils.isBlank(wdID)) { return Response.status(Response.Status.BAD_REQUEST).build(); } try { MediaPackage mp = null; Map<String, String> wfConfig = new HashMap<String, String>(); wfConfig.put(WORKFLOW_DEFINITION_ID_PARAM, wdID); for (String key : formData.keySet()) { if (!"mediaPackage".equals(key)) { wfConfig.put(key, formData.getFirst(key)); } else { mp = factory.newMediaPackageBuilder().loadFromXml(formData.getFirst(key)); } } return ingest(mp, wfConfig); } catch (Exception e) { logger.warn(e.getMessage(), e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } } private Response ingest(MediaPackage mp, Map<String, String> wfConfig) { if (MediaPackageSupport.sanityCheck(mp).isSome()) return Response.serverError().status(Status.BAD_REQUEST).build(); try { String workflowInstance = wfConfig.get(WORKFLOW_INSTANCE_ID_PARAM); String workflowDefinition = wfConfig.get(WORKFLOW_DEFINITION_ID_PARAM); // Double check that the required params exist. if (mp == null) { return Response.status(Response.Status.BAD_REQUEST).build(); } WorkflowInstance workflow = null; wfConfig.put(IngestService.START_DATE_KEY, formatter.format(startCache.asMap().get(mp.getIdentifier().toString()))); // a workflow instance has been specified if (StringUtils.isNotBlank(workflowInstance)) { Long workflowInstanceId = null; try { workflowInstanceId = Long.parseLong(workflowInstance); } catch (NumberFormatException e) { /* * Eat the exception, we don't *really* care since the system will just make up a new ID if needed. This may * also be an unscheduled capture, which might not have a Long ID. */ } // a workflow defintion was specified if (StringUtils.isNotBlank(workflowDefinition)) { workflow = ingestService.ingest(mp, workflowDefinition, wfConfig, workflowInstanceId); } else { workflow = ingestService.ingest(mp, null, wfConfig, workflowInstanceId); } } // a workflow definition was specified, but not a workflow id else if (StringUtils.isNotBlank(workflowDefinition)) { workflow = ingestService.ingest(mp, workflowDefinition, wfConfig, null); } // nothing was specified, so we start a new workflow else { workflow = ingestService.ingest(mp, null, wfConfig, null); } startCache.asMap().remove(mp.getIdentifier().toString()); return Response.ok(WorkflowParser.toXml(workflow)).build(); } catch (Exception e) { logger.warn(e.getMessage(), e); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } } protected UploadJob createUploadJob() { UploadJob job = new UploadJob(); jobs.put(job.getId(), job); return job; } /** * Creates an upload job and returns an HTML form ready for uploading the file to the newly created upload job. * Returns 500 if something goes wrong unexpectedly * * @return HTML form ready for uploading the file */ @GET @Path("filechooser-local.html") @Produces(MediaType.TEXT_HTML) public Response createUploadJobHtml(@QueryParam("elementType") String elementType) { InputStream is = null; elementType = (elementType == null) ? "track" : elementType; try { UploadJob job = createUploadJob(); is = getClass().getResourceAsStream("/templates/uploadform.html"); String html = IOUtils.toString(is, "UTF-8"); // String uploadURL = serverURL + "/ingest/addElementMonitored/" + job.getId(); String uploadURL = "addElementMonitored/" + job.getId(); html = html.replaceAll("\\{uploadURL\\}", uploadURL); html = html.replaceAll("\\{jobId\\}", job.getId()); html = html.replaceAll("\\{elementType\\}", elementType); logger.debug("New upload job created: " + job.getId()); jobs.put(job.getId(), job); return Response.ok(html).build(); } catch (Exception ex) { logger.warn(ex.getMessage(), ex); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } finally { IOUtils.closeQuietly(is); } } @GET @Path("filechooser-inbox.html") @Produces(MediaType.TEXT_HTML) public Response createInboxHtml() { InputStream is = null; try { is = getClass().getResourceAsStream("/templates/inboxform.html"); String html = IOUtils.toString(is, "UTF-8"); return Response.ok(html).build(); } catch (Exception ex) { logger.warn(ex.getMessage(), ex); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } finally { IOUtils.closeQuietly(is); } } @GET @Path("filechooser-archive.html") @Produces(MediaType.TEXT_HTML) public Response createArchiveHtml(@QueryParam("elementType") String elementType) { InputStream is = null; elementType = (elementType == null) ? "track" : elementType; try { UploadJob job = createUploadJob(); is = getClass().getResourceAsStream("/templates/uploadform.html"); String html = IOUtils.toString(is, "UTF-8"); // String uploadURL = serverURL + "/ingest/addElementMonitored/" + job.getId(); String uploadURL = "/ingest/addZippedMediaPackage"; html = html.replaceAll("\\{uploadURL\\}", uploadURL); html = html.replaceAll("\\{jobId\\}", job.getId()); html = html.replaceAll("\\{elementType\\}", elementType); logger.debug("New upload job created: " + job.getId()); jobs.put(job.getId(), job); return Response.ok(html).build(); } catch (Exception ex) { logger.warn(ex.getMessage(), ex); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } finally { IOUtils.closeQuietly(is); } } /** * Add an elements to a MediaPackage and keeps track of the progress of the upload. Returns an HTML that triggers the * host sites UploadListener.uploadComplete javascript event Returns an HTML that triggers the host sites * UploadListener.uplaodFailed javascript event in case of error * * @param jobId * of the upload job * @param request * containing the file, the flavor and the MediaPackage to which it should be added * @return HTML that calls the UploadListener.uploadComplete javascript handler */ @POST @Path("addElementMonitored/{jobId}") @Produces(MediaType.TEXT_HTML) public Response addElementMonitored(@PathParam("jobId") String jobId, @Context HttpServletRequest request) { UploadJob job = null; MediaPackage mp = null; String fileName = null; MediaPackageElementFlavor flavor = null; String elementType = "track"; try { try { // try to get UploadJob if (jobs.containsKey(jobId)) { job = jobs.get(jobId); } else { throw new NoResultException("Job not found"); } } catch (NoResultException e) { logger.warn("Upload job not found for Id: " + jobId); return buildUploadFailedRepsonse(job); } if (ServletFileUpload.isMultipartContent(request)) { ServletFileUpload upload = new ServletFileUpload(); UploadProgressListener listener = new UploadProgressListener(job); upload.setProgressListener(listener); for (FileItemIterator iter = upload.getItemIterator(request); iter.hasNext();) { FileItemStream item = iter.next(); String fieldName = item.getFieldName(); if ("mediaPackage".equalsIgnoreCase(fieldName)) { mp = factory.newMediaPackageBuilder().loadFromXml(item.openStream()); } else if ("flavor".equals(fieldName)) { String flavorString = Streams.asString(item.openStream()); if (flavorString != null) { flavor = MediaPackageElementFlavor.parseFlavor(flavorString); } } else if ("elementType".equalsIgnoreCase(fieldName)) { String typeString = Streams.asString(item.openStream()); if (typeString != null) { elementType = typeString; } } else if ("file".equalsIgnoreCase(fieldName)) { fileName = item.getName(); job.setFilename(fileName); if ((mp != null) && (flavor != null) && (fileName != null)) { // decide which element type to add if ("TRACK".equalsIgnoreCase(elementType)) { mp = ingestService.addTrack(item.openStream(), fileName, flavor, mp); } else if ("CATALOG".equalsIgnoreCase(elementType)) { logger.info("Adding Catalog: " + fileName + " - " + flavor); mp = ingestService.addCatalog(item.openStream(), fileName, flavor, mp); } InputStream is = null; try { is = getClass().getResourceAsStream("/templates/complete.html"); String html = IOUtils.toString(is, "UTF-8"); html = html.replaceAll("\\{mediaPackage\\}", MediaPackageParser.getAsXml(mp)); html = html.replaceAll("\\{jobId\\}", job.getId()); return Response.ok(html).build(); } finally { IOUtils.closeQuietly(is); } } } } } else { logger.warn("Job " + job.getId() + ": message is not multipart/form-data encoded"); } return buildUploadFailedRepsonse(job); } catch (Exception ex) { logger.error(ex.getMessage()); return buildUploadFailedRepsonse(job); } } /** * Builds a Response containing an HTML that calls the UploadListener.uploadFailed javascript handler. * * @return HTML that calls the UploadListener.uploadFailed js function */ private Response buildUploadFailedRepsonse(UploadJob job) { InputStream is = null; try { is = getClass().getResourceAsStream("/templates/error.html"); String html = IOUtils.toString(is, "UTF-8"); html = html.replaceAll("\\{jobId\\}", job.getId()); return Response.ok(html).build(); } catch (IOException ex) { logger.error("Unable to build upload failed Response"); return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build(); } finally { IOUtils.closeQuietly(is); } } /** * Adds a dublinCore metadata catalog to the MediaPackage and returns the grown mediaPackage. JQuery Ajax functions * doesn't support multipart/form-data encoding. * * @param mp * MediaPackage * @param dc * DublinCoreCatalog * @return grown MediaPackage XML */ @POST @Produces(MediaType.TEXT_XML) @Path("addDCCatalog") @RestQuery(name = "addDCCatalog", description = "Add a dublincore episode catalog to a given media package using an url", restParameters = { @RestParameter(description = "The media package as XML", isRequired = true, name = "mediaPackage", type = RestParameter.Type.TEXT), @RestParameter(description = "DublinCore catalog as XML", isRequired = true, name = "dublinCore", type = RestParameter.Type.TEXT), @RestParameter(defaultValue = "dublincore/episode", description = "DublinCore Flavor", isRequired = false, name = "flavor", type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns augmented media package", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Media package not valid", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR) }, returnDescription = "") public Response addDCCatalog(@FormParam("mediaPackage") String mp, @FormParam("dublinCore") String dc, @FormParam("flavor") String flavor) { MediaPackageElementFlavor dcFlavor = MediaPackageElements.EPISODE; if (flavor != null) { try { dcFlavor = MediaPackageElementFlavor.parseFlavor(flavor); } catch (IllegalArgumentException e) { logger.warn("Unable to set dublin core flavor to {}, using {} instead", flavor, MediaPackageElements.EPISODE); } } MediaPackage mediaPackage; /* Check if we got a proper mediapackage and try to parse it */ try { mediaPackage = MediaPackageBuilderFactory.newInstance().newMediaPackageBuilder().loadFromXml(mp); } catch (MediaPackageException e) { return Response.serverError().status(Status.BAD_REQUEST).build(); } if (MediaPackageSupport.sanityCheck(mediaPackage).isSome()) { return Response.serverError().status(Status.BAD_REQUEST).build(); } /* Check if we got a proper catalog */ if (StringUtils.isBlank(dc)) { return Response.serverError().status(Status.BAD_REQUEST).build(); } InputStream in = null; try { in = IOUtils.toInputStream(dc, "UTF-8"); mediaPackage = ingestService.addCatalog(in, "dublincore.xml", dcFlavor, mediaPackage); } catch (MediaPackageException e) { return Response.serverError().status(Status.BAD_REQUEST).build(); } catch (IOException e) { /* Return an internal server error if we could not write to disk */ logger.error("Could not write catalog to disk: {}", e.getMessage()); return Response.serverError().build(); } catch (Exception e) { logger.error(e.getMessage()); return Response.serverError().build(); } finally { IOUtils.closeQuietly(in); } return Response.ok(mediaPackage).build(); } /** * Returns information about the progress of a file upload as a JSON string. Returns 404 if upload job id doesn't * exists Returns 500 if something goes wrong unexpectedly * * TODO cache UploadJobs because endpoint is asked periodically so that not each request yields a DB query operation * * @param jobId * @return progress JSON string */ @SuppressWarnings("unchecked") @GET @Produces(MediaType.APPLICATION_JSON) @Path("getProgress/{jobId}") public Response getProgress(@PathParam("jobId") String jobId) throws NotFoundException { // try to get UploadJob, responde 404 if not successful UploadJob job = null; if (jobs.containsKey(jobId)) { job = jobs.get(jobId); } else { throw new NotFoundException("Job not found"); } /* * String json = "{total:" + Long.toString(job.getBytesTotal()) + ", received:" + * Long.toString(job.getBytesReceived()) + "}"; return Response.ok(json).build(); */ JSONObject out = new JSONObject(); out.put("filename", job.getFilename()); out.put("total", Long.toString(job.getBytesTotal())); out.put("received", Long.toString(job.getBytesReceived())); return Response.ok(out.toJSONString()).header("Content-Type", MediaType.APPLICATION_JSON).build(); } @Override public JobProducer getService() { return ingestService; } @Override public ServiceRegistry getServiceRegistry() { return serviceRegistry; } /** * OSGi Declarative Services callback to set the reference to the ingest service. * * @param ingestService * the ingest service */ void setIngestService(IngestService ingestService) { this.ingestService = ingestService; } /** * OSGi Declarative Services callback to set the reference to the service registry. * * @param serviceRegistry * the service registry */ void setServiceRegistry(ServiceRegistry serviceRegistry) { this.serviceRegistry = serviceRegistry; } /** * OSGi Declarative Services callback to set the reference to the dublin core service. * * @param dcService * the dublin core service */ void setDublinCoreService(DublinCoreCatalogService dcService) { this.dublinCoreService = dcService; } /** * Sets the trusted http client * * @param httpClient * the http client */ public void setHttpClient(TrustedHttpClient httpClient) { this.httpClient = httpClient; } }