Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.opencastproject.external.endpoint.EventsEndpoint Maven / Gradle / Ivy
/*
* 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.external.endpoint;
import static com.entwinemedia.fn.Stream.$;
import static com.entwinemedia.fn.data.json.Jsons.BLANK;
import static com.entwinemedia.fn.data.json.Jsons.NULL;
import static com.entwinemedia.fn.data.json.Jsons.arr;
import static com.entwinemedia.fn.data.json.Jsons.f;
import static com.entwinemedia.fn.data.json.Jsons.obj;
import static com.entwinemedia.fn.data.json.Jsons.v;
import static org.apache.commons.lang3.StringUtils.trimToNull;
import static org.opencastproject.external.common.ApiVersion.VERSION_1_11_0;
import static org.opencastproject.external.common.ApiVersion.VERSION_1_1_0;
import static org.opencastproject.external.common.ApiVersion.VERSION_1_4_0;
import static org.opencastproject.external.common.ApiVersion.VERSION_1_7_0;
import static org.opencastproject.external.util.SchedulingUtils.SchedulingInfo;
import static org.opencastproject.external.util.SchedulingUtils.convertConflictingEvents;
import static org.opencastproject.external.util.SchedulingUtils.getConflictingEvents;
import static org.opencastproject.util.RestUtil.getEndpointUrl;
import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
import org.opencastproject.assetmanager.api.AssetManager;
import org.opencastproject.assetmanager.api.AssetManagerException;
import org.opencastproject.capture.CaptureParameters;
import org.opencastproject.capture.admin.api.CaptureAgentStateService;
import org.opencastproject.elasticsearch.api.SearchIndexException;
import org.opencastproject.elasticsearch.api.SearchResult;
import org.opencastproject.elasticsearch.api.SearchResultItem;
import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
import org.opencastproject.elasticsearch.index.QueryPreprocessor;
import org.opencastproject.elasticsearch.index.objects.IndexObject;
import org.opencastproject.elasticsearch.index.objects.event.Event;
import org.opencastproject.elasticsearch.index.objects.event.EventIndexSchema;
import org.opencastproject.elasticsearch.index.objects.event.EventSearchQuery;
import org.opencastproject.external.common.ApiMediaType;
import org.opencastproject.external.common.ApiResponses;
import org.opencastproject.external.common.ApiVersion;
import org.opencastproject.external.util.AclUtils;
import org.opencastproject.external.util.ExternalMetadataUtils;
import org.opencastproject.index.service.api.IndexService;
import org.opencastproject.index.service.catalog.adapter.DublinCoreMetadataUtil;
import org.opencastproject.index.service.exception.IndexServiceException;
import org.opencastproject.index.service.impl.util.EventHttpServletRequest;
import org.opencastproject.index.service.impl.util.EventUtils;
import org.opencastproject.index.service.util.RequestUtils;
import org.opencastproject.index.service.util.RestUtils;
import org.opencastproject.ingest.api.IngestException;
import org.opencastproject.ingest.api.IngestService;
import org.opencastproject.list.impl.EmptyResourceListQuery;
import org.opencastproject.mediapackage.Attachment;
import org.opencastproject.mediapackage.AudioStream;
import org.opencastproject.mediapackage.Catalog;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.mediapackage.Publication;
import org.opencastproject.mediapackage.Stream;
import org.opencastproject.mediapackage.Track;
import org.opencastproject.mediapackage.TrackSupport;
import org.opencastproject.mediapackage.VideoStream;
import org.opencastproject.mediapackage.track.TrackImpl;
import org.opencastproject.metadata.dublincore.DublinCore;
import org.opencastproject.metadata.dublincore.DublinCoreMetadataCollection;
import org.opencastproject.metadata.dublincore.EventCatalogUIAdapter;
import org.opencastproject.metadata.dublincore.MetadataField;
import org.opencastproject.metadata.dublincore.MetadataJson;
import org.opencastproject.metadata.dublincore.MetadataList;
import org.opencastproject.metadata.dublincore.MetadataList.Locked;
import org.opencastproject.rest.RestConstants;
import org.opencastproject.scheduler.api.SchedulerConflictException;
import org.opencastproject.scheduler.api.SchedulerException;
import org.opencastproject.scheduler.api.SchedulerService;
import org.opencastproject.scheduler.api.TechnicalMetadata;
import org.opencastproject.security.api.AccessControlEntry;
import org.opencastproject.security.api.AccessControlList;
import org.opencastproject.security.api.AccessControlParser;
import org.opencastproject.security.api.Permissions;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.security.urlsigning.exception.UrlSigningException;
import org.opencastproject.security.urlsigning.service.UrlSigningService;
import org.opencastproject.systems.OpencastConstants;
import org.opencastproject.util.DateTimeSupport;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.RestUtil;
import org.opencastproject.util.RestUtil.R;
import org.opencastproject.util.UrlSupport;
import org.opencastproject.util.data.Option;
import org.opencastproject.util.data.Tuple;
import org.opencastproject.util.doc.rest.RestParameter;
import org.opencastproject.util.doc.rest.RestParameter.Type;
import org.opencastproject.util.doc.rest.RestQuery;
import org.opencastproject.util.doc.rest.RestResponse;
import org.opencastproject.util.doc.rest.RestService;
import org.opencastproject.util.requests.SortCriterion;
import org.opencastproject.workflow.api.WorkflowDatabaseException;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowService;
import com.entwinemedia.fn.Fn;
import com.entwinemedia.fn.data.Opt;
import com.entwinemedia.fn.data.json.Field;
import com.entwinemedia.fn.data.json.JObject;
import com.entwinemedia.fn.data.json.JValue;
import com.entwinemedia.fn.data.json.Jsons;
import com.entwinemedia.fn.data.json.Jsons.Functions;
import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
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.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
@Path("/")
@Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_0_0, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0,
ApiMediaType.VERSION_1_3_0, ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0,
ApiMediaType.VERSION_1_6_0, ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0,
ApiMediaType.VERSION_1_9_0, ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
@RestService(name = "externalapievents", title = "External API Events Service", notes = {},
abstractText = "Provides resources and operations related to the events")
@Component(
immediate = true,
service = { EventsEndpoint.class,ManagedService.class },
property = {
"service.description=External API - Events Endpoint",
"opencast.service.type=org.opencastproject.external.events",
"opencast.service.path=/api/events"
}
)
public class EventsEndpoint implements ManagedService {
protected static final String URL_SIGNING_EXPIRES_DURATION_SECONDS_KEY = "url.signing.expires.seconds";
/** The default time before a piece of signed content expires. 2 Hours. */
protected static final Long DEFAULT_URL_SIGNING_EXPIRE_DURATION = 2 * 60 * 60L;
/** Subtype of previews required by the video editor */
private static final String PREVIEW_SUBTYPE = "preview.subtype";
/** Subtype of previews required by the video editor */
private static final String DEFAULT_PREVIEW_SUBTYPE = "preview";
/** ID of the workflow used to retract published events */
private static final String RETRACT_WORKFLOW = "retract.workflow.id";
/** Default ID of the workflow used to retract published events */
private static final String DEFAULT_RETRACT_WORKFLOW = "delete";
/** The logging facility */
private static final Logger logger = LoggerFactory.getLogger(EventsEndpoint.class);
/** Base URL of this endpoint */
protected String endpointBaseUrl;
private static long expireSeconds = DEFAULT_URL_SIGNING_EXPIRE_DURATION;
private String previewSubtype = DEFAULT_PREVIEW_SUBTYPE;
private Map configuredMetadataFields = new TreeMap<>();
private String retractWorkflowId = DEFAULT_RETRACT_WORKFLOW;
/** The resolutions */
private enum CommentResolution {
ALL, UNRESOLVED, RESOLVED;
};
/* OSGi service references */
private AssetManager assetManager;
private ElasticsearchIndex elasticsearchIndex;
private IndexService indexService;
private IngestService ingestService;
private SecurityService securityService;
private final List catalogUIAdapters = new CopyOnWriteArrayList<>();
private final Map> orgCatalogUIAdaptersMap = new ConcurrentHashMap<>();
private UrlSigningService urlSigningService;
private SchedulerService schedulerService;
private CaptureAgentStateService agentStateService;
private WorkflowService workflowService;
/** OSGi DI */
@Reference
public void setAssetManager(AssetManager assetManager) {
this.assetManager = assetManager;
}
/** OSGi DI */
@Reference
void setElasticsearchIndex(ElasticsearchIndex elasticsearchIndex) {
this.elasticsearchIndex = elasticsearchIndex;
}
/** OSGi DI */
@Reference
public void setIndexService(IndexService indexService) {
this.indexService = indexService;
}
/** OSGi DI */
@Reference
public void setIngestService(IngestService ingestService) {
this.ingestService = ingestService;
}
/** OSGi DI */
@Reference
void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
/** OSGi DI */
@Reference
public void setUrlSigningService(UrlSigningService urlSigningService) {
this.urlSigningService = urlSigningService;
}
public SecurityService getSecurityService() {
return securityService;
}
public SchedulerService getSchedulerService() {
return schedulerService;
}
@Reference
public void setSchedulerService(SchedulerService schedulerService) {
this.schedulerService = schedulerService;
}
/** OSGi DI. */
@Reference(
cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC,
unbind = "removeCatalogUIAdapter"
)
public void addCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) {
catalogUIAdapters.add(catalogUIAdapter);
invalidateOrgCatalogUIAdaptersMapFor(catalogUIAdapter);
}
/** OSGi DI. */
public void removeCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) {
catalogUIAdapters.remove(catalogUIAdapter);
invalidateOrgCatalogUIAdaptersMapFor(catalogUIAdapter);
}
/**
* Invalidates caches for organizations that are handled by given catalog.
*
* @param catalogUIAdapter catalog used to identify affected organizations.
*/
private void invalidateOrgCatalogUIAdaptersMapFor(EventCatalogUIAdapter catalogUIAdapter) {
// clean cached org to catalog map
for (String orgName : orgCatalogUIAdaptersMap.keySet()) {
if (catalogUIAdapter.handlesOrganization(orgName)) {
orgCatalogUIAdaptersMap.remove(orgName);
}
}
}
/** OSGi DI */
public CaptureAgentStateService getAgentStateService() {
return agentStateService;
}
/** OSGi DI */
@Reference
public void setAgentStateService(CaptureAgentStateService agentStateService) {
this.agentStateService = agentStateService;
}
/** OSGi DI */
@Reference
public void setWorkflowService(WorkflowService workflowService) {
this.workflowService = workflowService;
}
private List getEventCatalogUIAdapters() {
return getEventCatalogUIAdapters(getSecurityService().getOrganization().getId());
}
public List getEventCatalogUIAdapters(String organization) {
List cachedCatalogUIAdapters = orgCatalogUIAdaptersMap.computeIfAbsent(organization,
org -> new ArrayList<>(catalogUIAdapters.stream()
.filter(a -> a.handlesOrganization(org))
.collect(Collectors.toList())));
// create a shallow copy as callers may change it
return new ArrayList<>(cachedCatalogUIAdapters);
}
/** OSGi activation method */
@Activate
void activate(ComponentContext cc) {
logger.info("Activating External API - Events Endpoint");
final Tuple endpointUrl = getEndpointUrl(cc, OpencastConstants.EXTERNAL_API_URL_ORG_PROPERTY,
RestConstants.SERVICE_PATH_PROPERTY);
endpointBaseUrl = UrlSupport.concat(endpointUrl.getA(), endpointUrl.getB());
logger.debug("Configured service endpoint is {}", endpointBaseUrl);
}
/** OSGi callback if properties file is present */
@Override
public void updated(Dictionary properties) throws ConfigurationException {
// Ensure properties is not null
if (properties == null) {
properties = new Hashtable<>();
logger.debug("No configuration set");
}
// Read URL Signing Expiration duration
// Default to DEFAULT_URL_SIGNING_EXPIRE_DURATION.toString()));
expireSeconds = Long.parseLong(Objects.toString(
properties.get(URL_SIGNING_EXPIRES_DURATION_SECONDS_KEY),
DEFAULT_URL_SIGNING_EXPIRE_DURATION.toString()));
logger.debug("URLs signatures are configured to expire in {}.", DateTimeSupport.humanReadableTime(expireSeconds));
// Read preview subtype configuration
// Default to DEFAULT_PREVIEW_SUBTYPE
previewSubtype = StringUtils.defaultString((String) properties.get(PREVIEW_SUBTYPE), DEFAULT_PREVIEW_SUBTYPE);
logger.debug("Preview subtype is '{}'", previewSubtype);
configuredMetadataFields = DublinCoreMetadataUtil.getDublinCoreProperties(properties);
retractWorkflowId = StringUtils.defaultString((String) properties.get(RETRACT_WORKFLOW), DEFAULT_RETRACT_WORKFLOW);
logger.debug("Retract Workflow is '{}'", retractWorkflowId);
}
public static boolean isNullOrEmpty(List list) {
return list == null || list.isEmpty();
}
@GET
@Path("{eventId}")
@RestQuery(name = "getevent", description = "Returns a single event. By setting the optional sign parameter to true, the method will pre-sign distribution urls if signing is turned on in Opencast. Remember to consider the maximum validity of signed URLs when caching this response.", returnDescription = "", pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, restParameters = {
@RestParameter(name = "sign", isRequired = false, description = "Whether public distribution urls should be signed.", type = Type.BOOLEAN),
@RestParameter(name = "withacl", isRequired = false, description = "Whether the acl metadata should be included in the response.", type = Type.BOOLEAN),
@RestParameter(name = "withmetadata", isRequired = false, description = "Whether the metadata catalogs should be included in the response.", type = Type.BOOLEAN),
@RestParameter(name = "withscheduling", isRequired = false, description = "Whether the scheduling information should be included in the response.", type = Type.BOOLEAN),
@RestParameter(name = "withpublications", isRequired = false, description = "Whether the publication ids and urls should be included in the response.", type = Type.BOOLEAN),
@RestParameter(name = "includeInternalPublication", isRequired = false, description = "Whether internal publications should be included.", type = Type.BOOLEAN)}, responses = {
@RestResponse(description = "The event is returned.", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response getEvent(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
@QueryParam("sign") boolean sign, @QueryParam("withacl") Boolean withAcl,
@QueryParam("withmetadata") Boolean withMetadata, @QueryParam("withscheduling") Boolean withScheduling,
@QueryParam("withpublications") Boolean withPublications, @QueryParam("includeInternalPublication") Boolean includeInternalPublication)
throws Exception {
final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
if (requestedVersion.isSmallerThan(VERSION_1_1_0)) {
// withScheduling was added in version 1.1.0 and should be ignored for smaller versions
withScheduling = false;
}
for (final Event event : indexService.getEvent(id, elasticsearchIndex)) {
event.updatePreview(previewSubtype);
return ApiResponses.Json.ok(
requestedVersion, eventToJSON(event, withAcl, withMetadata, withScheduling, withPublications, includeInternalPublication, sign, requestedVersion));
}
return ApiResponses.notFound("Cannot find an event with id '%s'.", id);
}
@GET
@Path("{eventId}/media")
@RestQuery(name = "geteventmedia", description = "Returns media tracks of specific single event.", returnDescription = "", pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, responses = {
@RestResponse(description = "The event's media is returned.", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response getEventMedia(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id)
throws Exception {
final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
ArrayList tracks = new ArrayList<>();
for (final Event event : indexService.getEvent(id, elasticsearchIndex)) {
final MediaPackage mp = indexService.getEventMediapackage(event);
for (Track track : mp.getTracks()) {
if (track instanceof TrackImpl) {
tracks.add((TrackImpl) track);
}
}
List tracksJson = new ArrayList<>();
for (Track track : tracks) {
List fields = new ArrayList<>();
if (track.getChecksum() != null)
fields.add(f("checksum", v(track.getChecksum().toString())));
if (track.getDescription() != null)
fields.add(f("description", v(track.getDescription())));
if (track.getDuration() != null)
fields.add(f("duration", v(track.getDuration())));
if (track.getElementDescription() != null)
fields.add(f("element-description", v(track.getElementDescription())));
if (track.getFlavor() != null)
fields.add(f("flavor", v(track.getFlavor().toString())));
if (track.getIdentifier() != null)
fields.add(f("identifier", v(track.getIdentifier())));
if (track.getMimeType() != null)
fields.add(f("mimetype", v(track.getMimeType().toString())));
fields.add(f("size", v(track.getSize())));
if (!requestedVersion.isSmallerThan(VERSION_1_7_0)) {
fields.add(f("has_video", v(track.hasVideo())));
fields.add(f("has_audio", v(track.hasAudio())));
fields.add(f("is_master_playlist", v(track.isMaster())));
fields.add(f("is_live", v(track.isLive())));
}
if (track.getStreams() != null) {
List streams = new ArrayList<>();
for (Stream stream : track.getStreams()) {
streams.add(f(stream.getIdentifier(), getJsonStream(stream)));
}
fields.add(f("streams", obj(streams)));
}
if (track.getTags() != null) {
List tags = new ArrayList<>();
for (String tag : track.getTags()) {
tags.add(v(tag));
}
fields.add(f("tags", arr(tags)));
}
if (track.getURI() != null)
fields.add(f("uri", v(track.getURI().toString())));
tracksJson.add(obj(fields));
}
return ApiResponses.Json.ok(acceptHeader, arr(tracksJson));
}
return ApiResponses.notFound("Cannot find an event with id '%s'.", id);
}
@DELETE
@Path("{eventId}")
@RestQuery(name = "deleteevent", description = "Deletes an event.", returnDescription = "", pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, responses = {
@RestResponse(description = "The event has been deleted.", responseCode = HttpServletResponse.SC_NO_CONTENT),
@RestResponse(description = "The retraction of publications has started.", responseCode = HttpServletResponse.SC_ACCEPTED),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response deleteEvent(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id)
throws SearchIndexException, UnauthorizedException {
final Opt event = indexService.getEvent(id, elasticsearchIndex);
if (event.isNone()) {
return RestUtil.R.notFound(id);
}
final IndexService.EventRemovalResult result;
try {
result = indexService.removeEvent(event.get(), retractWorkflowId);
} catch (WorkflowDatabaseException e) {
logger.error("Workflow database is not reachable. This may be a temporary problem.");
return RestUtil.R.serverError();
} catch (NotFoundException e) {
logger.error("Configured retract workflow not found. Check your configuration.");
return RestUtil.R.serverError();
}
switch (result) {
case SUCCESS:
return Response.noContent().build();
case RETRACTING:
return Response.accepted().build();
case GENERAL_FAILURE:
return Response.serverError().build();
case NOT_FOUND:
return RestUtil.R.notFound(id);
default:
throw new RuntimeException("Unknown EventRemovalResult type: " + result.name());
}
}
@POST
@Path("{eventId}")
@RestQuery(name = "updateeventmetadata", description = "Updates an event.", returnDescription = "", pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, restParameters = {
@RestParameter(name = "acl", isRequired = false, description = "A collection of roles with their possible action", type = Type.STRING),
@RestParameter(name = "metadata", isRequired = false, description = "Event metadata as Form param", type = Type.STRING),
@RestParameter(name = "scheduling", isRequired = false, description = "Scheduling information as Form param", type = Type.STRING),
@RestParameter(name = "presenter", isRequired = false, description = "Presenter movie track", type = Type.FILE),
@RestParameter(name = "presentation", isRequired = false, description = "Presentation movie track", type = Type.FILE),
@RestParameter(name = "audio", isRequired = false, description = "Audio track", type = Type.FILE),
@RestParameter(name = "processing", isRequired = false, description = "Processing instructions task configuration", type = Type.STRING), }, responses = {
@RestResponse(description = "The event has been updated.", responseCode = HttpServletResponse.SC_NO_CONTENT),
@RestResponse(description = "The event could not be updated due to a scheduling conflict.", responseCode = HttpServletResponse.SC_CONFLICT),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response updateEventMetadata(@HeaderParam("Accept") String acceptHeader, @Context HttpServletRequest request,
@PathParam("eventId") String eventId) {
final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
try {
String startDatePattern = configuredMetadataFields.containsKey("startDate") ? configuredMetadataFields.get("startDate").getPattern() : null;
String startTimePattern = configuredMetadataFields.containsKey("startTime") ? configuredMetadataFields.get("startTime").getPattern() : null;
for (final Event event : indexService.getEvent(eventId, elasticsearchIndex)) {
EventHttpServletRequest eventHttpServletRequest = EventHttpServletRequest.updateFromHttpServletRequest(event,
request, getEventCatalogUIAdapters(), startDatePattern, startTimePattern);
// FIXME: All of these update operations should be a part of a transaction to avoid a partially updated event.
if (eventHttpServletRequest.getMetadataList().isSome()) {
indexService.updateEventMetadata(eventId, eventHttpServletRequest.getMetadataList().get(), elasticsearchIndex);
}
if (eventHttpServletRequest.getAcl().isSome()) {
indexService.updateEventAcl(eventId, eventHttpServletRequest.getAcl().get(), elasticsearchIndex);
}
if (eventHttpServletRequest.getProcessing().isSome()) {
if (!event.isScheduledEvent() || event.hasRecordingStarted()) {
return RestUtil.R.badRequest("Processing can't be updated for events that are already uploaded.");
}
JSONObject processing = eventHttpServletRequest.getProcessing().get();
String workflowId = (String) processing.get("workflow");
if (workflowId == null)
throw new IllegalArgumentException("No workflow template in metadata");
Map configuration = new HashMap<>();
if (eventHttpServletRequest.getProcessing().get().get("configuration") != null) {
configuration = new HashMap<>((JSONObject) eventHttpServletRequest.getProcessing().get().get("configuration"));
}
Opt> caMetadataOpt = Opt.none();
Opt> workflowConfigOpt = Opt.none();
Map caMetadata = new HashMap<>(getSchedulerService().getCaptureAgentConfiguration(eventId));
if (!workflowId.equals(caMetadata.get(CaptureParameters.INGEST_WORKFLOW_DEFINITION))) {
caMetadata.put(CaptureParameters.INGEST_WORKFLOW_DEFINITION, workflowId);
caMetadataOpt = Opt.some(caMetadata);
}
Map oldWorkflowConfig = new HashMap<>(getSchedulerService().getWorkflowConfig(eventId));
if (!oldWorkflowConfig.equals(configuration))
workflowConfigOpt = Opt.some(configuration);
if (!caMetadataOpt.isNone() || !workflowConfigOpt.isNone()) {
getSchedulerService().updateEvent(eventId, Opt.none(), Opt.none(), Opt.none(),
Opt.none(), Opt.none(), workflowConfigOpt, caMetadataOpt);
}
}
if (eventHttpServletRequest.getScheduling().isSome() && !requestedVersion.isSmallerThan(VERSION_1_1_0)) {
// Scheduling is only available for version 1.1.0 and above
Optional clientError = updateSchedulingInformation(
eventHttpServletRequest.getScheduling().get(), eventId, requestedVersion, false);
if (clientError.isPresent()) {
return clientError.get();
}
}
return Response.noContent().build();
}
return ApiResponses.notFound("Cannot find an event with id '%s'.", eventId);
} catch (NotFoundException e) {
return ApiResponses.notFound("Cannot find an event with id '%s'.", eventId);
} catch (UnauthorizedException e) {
return Response.status(Status.UNAUTHORIZED).build();
} catch (IllegalArgumentException e) {
logger.debug("Unable to update event '{}'", eventId, e);
return RestUtil.R.badRequest(e.getMessage());
} catch (IndexServiceException e) {
logger.error("Unable to get multi part fields or file for event '{}'", eventId, e);
throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
} catch (SearchIndexException e) {
logger.error("Unable to update event '{}'", eventId, e);
throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
} catch (Exception e) {
throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
}
}
@POST
@Path("/")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@RestQuery(name = "createevent", description = "Creates an event by sending metadata, access control list, processing instructions and files in a multipart request.", returnDescription = "", restParameters = {
@RestParameter(name = "acl", isRequired = false, description = "A collection of roles with their possible action", type = STRING),
@RestParameter(name = "metadata", description = "Event metadata as Form param", isRequired = false, type = STRING),
@RestParameter(name = "scheduling", description = "Scheduling information as Form param", isRequired = false, type = STRING),
@RestParameter(name = "presenter", description = "Presenter movie track", isRequired = false, type = Type.FILE),
@RestParameter(name = "presentation", description = "Presentation movie track", isRequired = false, type = Type.FILE),
@RestParameter(name = "audio", description = "Audio track", isRequired = false, type = Type.FILE),
@RestParameter(name = "processing", description = "Processing instructions task configuration", isRequired = false, type = STRING) }, responses = {
@RestResponse(description = "A new event is created and its identifier is returned in the Location header.", responseCode = HttpServletResponse.SC_CREATED),
@RestResponse(description = "The event could not be created due to a scheduling conflict.", responseCode = HttpServletResponse.SC_CONFLICT),
@RestResponse(description = "The request is invalid or inconsistent..", responseCode = HttpServletResponse.SC_BAD_REQUEST) })
public Response createNewEvent(@HeaderParam("Accept") String acceptHeader, @Context HttpServletRequest request) {
final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
try {
String startDatePattern = configuredMetadataFields.containsKey("startDate") ? configuredMetadataFields.get("startDate").getPattern() : null;
String startTimePattern = configuredMetadataFields.containsKey("startTime") ? configuredMetadataFields.get("startTime").getPattern() : null;
EventHttpServletRequest eventHttpServletRequest = EventHttpServletRequest.createFromHttpServletRequest(request,
ingestService, getEventCatalogUIAdapters(), startDatePattern, startTimePattern);
// If scheduling information is provided, the source has to be "SCHEDULE_SINGLE" or "SCHEDULE_MULTIPLE".
if (eventHttpServletRequest.getScheduling().isSome() && !requestedVersion.isSmallerThan(VERSION_1_1_0)) {
// Scheduling is only available for version 1.1.0 and above
return scheduleNewEvent(eventHttpServletRequest, eventHttpServletRequest.getScheduling().get(), requestedVersion);
}
JSONObject source = new JSONObject();
source.put("type", "UPLOAD");
eventHttpServletRequest.setSource(source);
String eventId = indexService.createEvent(eventHttpServletRequest);
return ApiResponses.Json.created(requestedVersion, URI.create(getEventUrl(eventId)), obj(f("identifier", v(eventId))));
} catch (IllegalArgumentException | DateTimeParseException e) {
logger.debug("Unable to create event", e);
return RestUtil.R.badRequest(e.getMessage());
} catch (SchedulerException | IndexServiceException e) {
if (e.getCause() != null && e.getCause() instanceof NotFoundException
|| e.getCause() instanceof IllegalArgumentException) {
logger.debug("Unable to create event", e);
return RestUtil.R.badRequest(e.getCause().getMessage());
} else {
logger.error("Unable to create event", e);
throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
}
} catch (Exception e) {
logger.error("Unable to create event", e);
throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
}
}
private Response scheduleNewEvent(EventHttpServletRequest request, JSONObject scheduling, ApiVersion requestedVersion) throws
MediaPackageException, IOException, IngestException, SchedulerException,
NotFoundException, UnauthorizedException, SearchIndexException, java.text.ParseException {
final SchedulingInfo schedulingInfo = SchedulingInfo.of(scheduling);
final JSONObject source = schedulingInfo.toSource();
request.setSource(source);
try {
final String eventId = indexService.createEvent(request);
if (StringUtils.isEmpty(eventId)) {
return RestUtil.R.badRequest("The date range provided did not include any events");
}
if (eventId.contains(",")) {
// This the case when SCHEDULE_MULTIPLE is performed.
return ApiResponses.Json.ok(requestedVersion, arr(
Arrays.stream(eventId.split(","))
.map(s -> obj(f("identifier", v(s))))
.collect(Collectors.toList()))
);
}
return ApiResponses.Json.created(requestedVersion, URI.create(getEventUrl(eventId)), obj(f("identifier", v(eventId))));
} catch (SchedulerConflictException e) {
final List conflictingEvents =
getConflictingEvents(schedulingInfo, agentStateService, schedulerService);
logger.debug("Client tried to schedule conflicting event(s).");
return ApiResponses.Json.conflict(requestedVersion,
arr(convertConflictingEvents(Optional.empty(), conflictingEvents, indexService, elasticsearchIndex)));
}
}
@GET
@Path("/")
@RestQuery(name = "getevents", description = "Returns a list of events. By setting the optional sign parameter to true, the method will pre-sign distribution urls if signing is turned on in Opencast. Remember to consider the maximum validity of signed URLs when caching this response.", returnDescription = "", restParameters = {
@RestParameter(name = "sign", isRequired = false, description = "Whether public distribution urls should be signed.", type = Type.BOOLEAN),
@RestParameter(name = "withacl", isRequired = false, description = "Whether the acl metadata should be included in the response.", type = Type.BOOLEAN),
@RestParameter(name = "withmetadata", isRequired = false, description = "Whether the metadata catalogs should be included in the response.", type = Type.BOOLEAN),
@RestParameter(name = "withscheduling", isRequired = false, description = "Whether the scheduling information should be included in the response.", type = Type.BOOLEAN),
@RestParameter(name = "withpublications", isRequired = false, description = "Whether the publication ids and urls should be included in the response.", type = Type.BOOLEAN),
@RestParameter(name = "includeInternalPublication", description = "Whether internal publications should be included.", isRequired = false, type = Type.BOOLEAN),
@RestParameter(name = "onlyWithWriteAccess", isRequired = false, description = "Whether only to get the events to which we have write access.", type = Type.BOOLEAN),
@RestParameter(name = "filter", isRequired = false, description = "Usage [Filter Name]:[Value to Filter With]. Multiple filters can be used by combining them with commas \",\". Available Filters: presenters, contributors, location, textFilter, series, subject. If API ver > 1.1.0 also: identifier, title, description, series_name, language, created, license, rightsholder, is_part_of, source, status, agent_id, start, technical_start.", type = STRING),
@RestParameter(name = "sort", description = "Sort the results based upon a list of comma seperated sorting criteria. In the comma seperated list each type of sorting is specified as a pair such as: :ASC or :DESC. Adding the suffix ASC or DESC sets the order as ascending or descending order and is mandatory.", isRequired = false, type = STRING),
@RestParameter(name = "limit", description = "The maximum number of results to return for a single request.", isRequired = false, type = RestParameter.Type.INTEGER),
@RestParameter(name = "offset", description = "The index of the first result to return.", isRequired = false, type = RestParameter.Type.INTEGER) }, responses = {
@RestResponse(description = "A (potentially empty) list of events is returned.", responseCode = HttpServletResponse.SC_OK) })
public Response getEvents(@HeaderParam("Accept") String acceptHeader, @QueryParam("id") String id,
@QueryParam("commentReason") String reasonFilter, @QueryParam("commentResolution") String resolutionFilter,
@QueryParam("filter") List filter, @QueryParam("sort") String sort, @QueryParam("offset") Integer offset,
@QueryParam("limit") Integer limit, @QueryParam("sign") boolean sign, @QueryParam("withacl") Boolean withAcl,
@QueryParam("withmetadata") Boolean withMetadata, @QueryParam("withscheduling") Boolean withScheduling,
@QueryParam("onlyWithWriteAccess") Boolean onlyWithWriteAccess, @QueryParam("withpublications") Boolean withPublications, @QueryParam("includeInternalPublication") Boolean includeInternalPublication) {
final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
if (requestedVersion.isSmallerThan(VERSION_1_1_0)) {
// withscheduling was added for version 1.1.0 and should be ignored for smaller versions.
withScheduling = false;
}
Option optLimit = Option.option(limit);
Option optOffset = Option.option(offset);
Option optSort = Option.option(trimToNull(sort));
EventSearchQuery query = new EventSearchQuery(getSecurityService().getOrganization().getId(),
getSecurityService().getUser());
// If the limit is set to 0, this is not taken into account
if (optLimit.isSome() && limit == 0) {
optLimit = Option.none();
}
//List of all events from the filters
List allEvents = new ArrayList<>();
if (!isNullOrEmpty(filter)) {
// API version 1.5.0: Additive filter
if (!requestedVersion.isSmallerThan(ApiVersion.VERSION_1_5_0)) {
filter = filter.subList(0,1);
}
for (String filterPart : filter) {
// Parse the filters
for (String f : filterPart.split(",")) {
String[] filterTuple = f.split(":");
if (filterTuple.length < 2) {
logger.debug("No value for filter {} in filters list: {}", filterTuple[0], filter);
continue;
}
String name = filterTuple[0];
String value;
if (!requestedVersion.isSmallerThan(ApiVersion.VERSION_1_1_0)) {
// MH-13038 - 1.1.0 and higher support colons in values
value = f.substring(name.length() + 1);
} else {
value = filterTuple[1];
}
if ("presenters".equals(name)) {
query.withPresenter(value);
} else if ("contributors".equals(name)) {
query.withContributor(value);
} else if ("location".equals(name)) {
query.withLocation(value);
} else if ("textFilter".equals(name)) {
query.withText(QueryPreprocessor.sanitize(value));
} else if ("series".equals(name)) {
query.withSeriesId(value);
} else if ("subject".equals(name)) {
query.withSubject(value);
} else if (!requestedVersion.isSmallerThan(ApiVersion.VERSION_1_1_0)) {
// additional filters only available with Version 1.1.0 or higher
if ("identifier".equals(name)) {
query.withIdentifier(value);
} else if ("title".equals(name)) {
query.withTitle(value);
} else if ("description".equals(name)) {
query.withDescription(value);
} else if ("series_name".equals(name)) {
query.withSeriesName(value);
} else if ("language".equals(name)) {
query.withLanguage(value);
} else if ("created".equals(name)) {
query.withCreated(value);
} else if ("license".equals(name)) {
query.withLicense(value);
} else if ("rightsholder".equals(name)) {
query.withRights(value);
} else if ("is_part_of".equals(name)) {
query.withSeriesId(value);
} else if ("source".equals(name)) {
query.withSource(value);
} else if ("status".equals(name)) {
query.withEventStatus(value);
} else if ("agent_id".equals(name)) {
query.withAgentId(value);
} else if ("start".equals(name)) {
try {
Tuple fromAndToCreationRange = RestUtils.getFromAndToDateRange(value);
query.withStartFrom(fromAndToCreationRange.getA());
query.withStartTo(fromAndToCreationRange.getB());
} catch (Exception e) {
return RestUtil.R
.badRequest(String.format("Filter 'start' could not be parsed: %s", e.getMessage()));
}
} else if ("technical_start".equals(name)) {
try {
Tuple fromAndToCreationRange = RestUtils.getFromAndToDateRange(value);
query.withTechnicalStartFrom(fromAndToCreationRange.getA());
query.withTechnicalStartTo(fromAndToCreationRange.getB());
} catch (Exception e) {
return RestUtil.R
.badRequest(String.format("Filter 'technical_start' could not be parsed: %s", e.getMessage()));
}
} else {
logger.warn("Unknown filter criteria {}", name);
return RestUtil.R.badRequest(String.format("Unknown filter criterion in request: %s", name));
}
}
}
if (optSort.isSome()) {
Set sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
for (SortCriterion criterion : sortCriteria) {
switch (criterion.getFieldName()) {
case EventIndexSchema.TITLE:
query.sortByTitle(criterion.getOrder());
break;
case EventIndexSchema.PRESENTER:
query.sortByPresenter(criterion.getOrder());
break;
case EventIndexSchema.TECHNICAL_START:
case "technical_date":
query.sortByTechnicalStartDate(criterion.getOrder());
break;
case EventIndexSchema.TECHNICAL_END:
query.sortByTechnicalEndDate(criterion.getOrder());
break;
case EventIndexSchema.START_DATE:
case "date":
query.sortByStartDate(criterion.getOrder());
break;
case EventIndexSchema.END_DATE:
query.sortByEndDate(criterion.getOrder());
break;
case EventIndexSchema.WORKFLOW_STATE:
query.sortByWorkflowState(criterion.getOrder());
break;
case EventIndexSchema.SERIES_NAME:
query.sortBySeriesName(criterion.getOrder());
break;
case EventIndexSchema.LOCATION:
query.sortByLocation(criterion.getOrder());
break;
// For compatibility, we mimic to support the old review_status and scheduling_status sort criteria (MH-13407)
case "review_status":
case "scheduling_status":
break;
default:
return RestUtil.R.badRequest(String.format("Unknown sort criterion in request: %s", criterion.getFieldName()));
}
}
}
// TODO: Add the comment resolution filter to the query
if (StringUtils.isNotBlank(resolutionFilter)) {
try {
CommentResolution.valueOf(resolutionFilter);
} catch (Exception e) {
logger.debug("Unable to parse comment resolution filter {}", resolutionFilter);
return Response.status(Status.BAD_REQUEST).build();
}
}
if (optLimit.isSome())
query.withLimit(optLimit.get());
if (optOffset.isSome())
query.withOffset(offset);
// TODO: Add other filters to the query
SearchResult results = null;
try {
results = elasticsearchIndex.getByQuery(query);
} catch (SearchIndexException e) {
logger.error("The External Search Index was not able to get the events list", e);
throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
}
SearchResultItem[] items = results.getItems();
List events = new ArrayList<>();
for (SearchResultItem item : items) {
Event source = item.getSource();
source.updatePreview(previewSubtype);
events.add(source);
}
//Append filtered results to the list
allEvents.addAll(events);
}
} else {
if (optSort.isSome()) {
Set sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
for (SortCriterion criterion : sortCriteria) {
switch (criterion.getFieldName()) {
case EventIndexSchema.TITLE:
query.sortByTitle(criterion.getOrder());
break;
case EventIndexSchema.PRESENTER:
query.sortByPresenter(criterion.getOrder());
break;
case EventIndexSchema.TECHNICAL_START:
case "technical_date":
query.sortByTechnicalStartDate(criterion.getOrder());
break;
case EventIndexSchema.TECHNICAL_END:
query.sortByTechnicalEndDate(criterion.getOrder());
break;
case EventIndexSchema.START_DATE:
case "date":
query.sortByStartDate(criterion.getOrder());
break;
case EventIndexSchema.END_DATE:
query.sortByEndDate(criterion.getOrder());
break;
case EventIndexSchema.WORKFLOW_STATE:
query.sortByWorkflowState(criterion.getOrder());
break;
case EventIndexSchema.SERIES_NAME:
query.sortBySeriesName(criterion.getOrder());
break;
case EventIndexSchema.LOCATION:
query.sortByLocation(criterion.getOrder());
break;
// For compatibility, we mimic to support the old review_status and scheduling_status sort criteria (MH-13407)
case "review_status":
case "scheduling_status":
break;
default:
return RestUtil.R.badRequest(String.format("Unknown sort criterion in request: %s", criterion.getFieldName()));
}
}
}
// TODO: Add the comment resolution filter to the query
if (StringUtils.isNotBlank(resolutionFilter)) {
try {
CommentResolution.valueOf(resolutionFilter);
} catch (Exception e) {
logger.debug("Unable to parse comment resolution filter {}", resolutionFilter);
return Response.status(Status.BAD_REQUEST).build();
}
}
if (optLimit.isSome())
query.withLimit(optLimit.get());
if (optOffset.isSome())
query.withOffset(offset);
if (onlyWithWriteAccess != null && onlyWithWriteAccess) {
query.withoutActions();
query.withAction(Permissions.Action.WRITE);
}
// TODO: Add other filters to the query
SearchResult results = null;
try {
results = elasticsearchIndex.getByQuery(query);
} catch (SearchIndexException e) {
logger.error("The External Search Index was not able to get the events list", e);
throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
}
SearchResultItem[] items = results.getItems();
List events = new ArrayList<>();
for (SearchResultItem item : items) {
Event source = item.getSource();
source.updatePreview(previewSubtype);
events.add(source);
}
//Append filtered results to the list
allEvents.addAll(events);
}
try {
return getJsonEvents(
acceptHeader, allEvents, withAcl, withMetadata, withScheduling, withPublications, includeInternalPublication, sign, requestedVersion);
} catch (Exception e) {
logger.error("Unable to get events", e);
throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
}
}
/**
* Render a collection of {@link Event}s into a json array.
*
* @param acceptHeader
* The accept header to return to the client.
* @param events
* The {@link List} of {@link Event}s to render into json.
* @param withAcl
* Whether to include the events' ACLs.
* @param withMetadata
* Whether to include the events' metadata.
* @param withScheduling
* Whether to include the events' scheduling information.
* @param withPublications
* Whether to include the events' publications.
* @param withSignedUrls
* Whether to sign the included urls.
* @return A {@link Response} with the accept header and body as the Json array of {@link Event}s.
* @throws IndexServiceException
* @throws SchedulerException
* @throws UnauthorizedException
*/
protected Response getJsonEvents(String acceptHeader, List events, Boolean withAcl, Boolean withMetadata,
Boolean withScheduling, Boolean withPublications,Boolean includeInternalPublication, Boolean withSignedUrls, ApiVersion requestedVersion)
throws IndexServiceException, UnauthorizedException, SchedulerException {
List eventsList = new ArrayList<>();
for (IndexObject item : events) {
eventsList.add(eventToJSON((Event) item, withAcl, withMetadata, withScheduling, withPublications, includeInternalPublication, withSignedUrls,
requestedVersion));
}
return ApiResponses.Json.ok(requestedVersion, arr(eventsList));
}
/**
* Transform an {@link Event} to Json
*
* @param event
* The event to transform into json
* @param withAcl
* Whether to add the acl information for the event
* @param withMetadata
* Whether to add all the metadata for the event
* @param withScheduling
* Whether to add the scheduling information for the event
* @param withPublications
* Whether to add the publications
* @param withSignedUrls
* Whether to sign the urls if they are protected by stream security.
* @return The event in json format.
* @throws IndexServiceException
* Thrown if unable to get the metadata for the event.
* @throws SchedulerException
* @throws UnauthorizedException
*/
protected JValue eventToJSON(Event event, Boolean withAcl, Boolean withMetadata, Boolean withScheduling,
Boolean withPublications, Boolean includeInternalPublication, Boolean withSignedUrls, ApiVersion requestedVersion) throws IndexServiceException, SchedulerException, UnauthorizedException {
List fields = new ArrayList<>();
if (event.getArchiveVersion() != null)
fields.add(f("archive_version", v(event.getArchiveVersion())));
fields.add(f("created", v(event.getCreated(), Jsons.BLANK)));
fields.add(f("creator", v(event.getCreator(), Jsons.BLANK)));
fields.add(f("contributor", arr($(event.getContributors()).map(Functions.stringToJValue))));
fields.add(f("description", v(event.getDescription(), Jsons.BLANK)));
fields.add(f("has_previews", v(event.hasPreview())));
fields.add(f("identifier", v(event.getIdentifier(), BLANK)));
fields.add(f("location", v(event.getLocation(), BLANK)));
fields.add(f("presenter", arr($(event.getPresenters()).map(Functions.stringToJValue))));
if (!requestedVersion.isSmallerThan(VERSION_1_1_0)) {
fields.add(f("language", v(event.getLanguage(), BLANK)));
fields.add(f("rightsholder", v(event.getRights(), BLANK)));
fields.add(f("license", v(event.getLicense(), BLANK)));
fields.add(f("is_part_of", v(event.getSeriesId(), BLANK)));
fields.add(f("series", v(event.getSeriesName(),BLANK)));
fields.add(f("source", v(event.getSource(), BLANK)));
fields.add(f("status", v(event.getEventStatus(), BLANK)));
}
List publicationIds = new ArrayList<>();
if (event.getPublications() != null) {
for (Publication publication : event.getPublications()) {
publicationIds.add(v(publication.getChannel()));
}
}
fields.add(f("publication_status", arr(publicationIds)));
fields.add(f("processing_state", v(event.getWorkflowState(), BLANK)));
if (requestedVersion.isSmallerThan(VERSION_1_4_0)) {
fields.add(f("start", v(event.getTechnicalStartTime(), BLANK)));
if (event.getTechnicalEndTime() != null) {
long duration = new DateTime(event.getTechnicalEndTime()).getMillis()
- new DateTime(event.getTechnicalStartTime()).getMillis();
fields.add(f("duration", v(duration)));
}
} else {
fields.add(f("start", v(event.getRecordingStartDate(), BLANK)));
fields.add(f("duration", v(event.getDuration(), NULL)));
}
if (StringUtils.trimToNull(event.getSubject()) != null) {
fields.add(f("subjects", arr(splitSubjectIntoArray(event.getSubject()))));
} else {
fields.add(f("subjects", arr()));
}
fields.add(f("title", v(event.getTitle(), BLANK)));
if (withAcl != null && withAcl) {
AccessControlList acl = getAclFromEvent(event);
fields.add(f("acl", arr(AclUtils.serializeAclToJson(acl))));
}
if (withMetadata != null && withMetadata) {
try {
Opt metadata = getEventMetadata(event);
if (metadata.isSome()) {
fields.add(f("metadata", MetadataJson.listToJson(metadata.get(), true)));
}
} catch (Exception e) {
logger.error("Unable to get metadata for event '{}'", event.getIdentifier(), e);
throw new IndexServiceException("Unable to add metadata to event", e);
}
}
if (withScheduling != null && withScheduling) {
fields.add(f("scheduling", SchedulingInfo.of(event.getIdentifier(), schedulerService).toJson()));
}
if (withPublications != null && withPublications) {
List publications = getPublications(event, withSignedUrls, includeInternalPublication,requestedVersion);
fields.add(f("publications", arr(publications)));
}
return obj(fields);
}
private List splitSubjectIntoArray(final String subject) {
return com.entwinemedia.fn.Stream.$(subject.split(",")).map(new Fn() {
@Override
public JValue apply(String a) {
return v(a.trim());
}
}).toList();
}
@GET
@Path("{eventId}/acl")
@RestQuery(name = "geteventacl", description = "Returns an event's access policy.", returnDescription = "", pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, responses = {
@RestResponse(description = "The access control list for the specified event is returned.", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response getEventAcl(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id)
throws Exception {
for (final Event event : indexService.getEvent(id, elasticsearchIndex)) {
AccessControlList acl = getAclFromEvent(event);
return ApiResponses.Json.ok(acceptHeader, arr(AclUtils.serializeAclToJson(acl)));
}
return ApiResponses.notFound("Cannot find an event with id '%s'.", id);
}
@PUT
@Path("{eventId}/acl")
@RestQuery(name = "updateeventacl", description = "Update an event's access policy.", returnDescription = "", pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, restParameters = {
@RestParameter(name = "acl", isRequired = true, description = "Access policy", type = STRING) }, responses = {
@RestResponse(description = "The access control list for the specified event is updated.", responseCode = HttpServletResponse.SC_NO_CONTENT),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response updateEventAcl(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
@FormParam("acl") String acl) throws Exception {
if (indexService.getEvent(id, elasticsearchIndex).isSome()) {
AccessControlList accessControlList;
try {
accessControlList = AclUtils.deserializeJsonToAcl(acl, false);
} catch (ParseException e) {
logger.debug("Unable to update event acl to '{}'", acl, e);
return R.badRequest(String.format("Unable to parse acl '%s' because '%s'", acl, e.getMessage()));
} catch (IllegalArgumentException e) {
logger.debug("Unable to update event acl to '{}'", acl, e);
return R.badRequest(e.getMessage());
}
try {
accessControlList = indexService.updateEventAcl(id, accessControlList, elasticsearchIndex);
} catch (IllegalArgumentException e) {
logger.error("Unable to update event '{}' acl with '{}'", id, acl, e);
return Response.status(Status.FORBIDDEN).build();
}
return Response.noContent().build();
} else {
return ApiResponses.notFound("Cannot find an event with id '%s'.", id);
}
}
@POST
@Path("{eventId}/acl/{action}")
@RestQuery(name = "addeventace", description = "Grants permission to execute action on the specified event to any user with role role. Note that this is a convenience method to avoid having to build and post a complete access control list.", returnDescription = "", pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING),
@RestParameter(name = "action", description = "The action that is allowed to be executed", isRequired = true, type = STRING) }, restParameters = {
@RestParameter(name = "role", isRequired = true, description = "The role that is granted permission", type = STRING) }, responses = {
@RestResponse(description = "The permission has been created in the access control list of the specified event.", responseCode = HttpServletResponse.SC_NO_CONTENT),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response addEventAce(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
@PathParam("action") String action, @FormParam("role") String role) throws Exception {
List entries = new ArrayList<>();
for (final Event event : indexService.getEvent(id, elasticsearchIndex)) {
AccessControlList accessControlList = getAclFromEvent(event);
AccessControlEntry newAce = new AccessControlEntry(role, action, true);
boolean alreadyInAcl = false;
for (AccessControlEntry ace : accessControlList.getEntries()) {
if (ace.equals(newAce)) {
// We have found an identical access control entry so just return.
entries = accessControlList.getEntries();
alreadyInAcl = true;
break;
} else if (ace.getAction().equals(newAce.getAction()) && ace.getRole().equals(newAce.getRole())
&& !ace.isAllow()) {
entries.add(newAce);
alreadyInAcl = true;
} else {
entries.add(ace);
}
}
if (!alreadyInAcl) {
entries.add(newAce);
}
AccessControlList withNewAce = new AccessControlList(entries);
try {
withNewAce = indexService.updateEventAcl(id, withNewAce, elasticsearchIndex);
} catch (IllegalArgumentException e) {
logger.error("Unable to update event '{}' acl entry with action '{}' and role '{}'", id, action, role, e);
return Response.status(Status.FORBIDDEN).build();
}
return Response.noContent().build();
}
return ApiResponses.notFound("Cannot find an event with id '%s'.", id);
}
@DELETE
@Path("{eventId}/acl/{action}/{role}")
@RestQuery(name = "deleteeventace", description = "Revokes permission to execute action on the specified event from any user with role role.", returnDescription = "", pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING),
@RestParameter(name = "action", description = "The action that is no longer allowed to be executed", isRequired = true, type = STRING),
@RestParameter(name = "role", description = "The role that is no longer granted permission", isRequired = true, type = STRING) }, responses = {
@RestResponse(description = "The permission has been revoked from the access control list of the specified event.", responseCode = HttpServletResponse.SC_NO_CONTENT),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response deleteEventAce(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
@PathParam("action") String action, @PathParam("role") String role) throws Exception {
List entries = new ArrayList<>();
for (final Event event : indexService.getEvent(id, elasticsearchIndex)) {
AccessControlList accessControlList = getAclFromEvent(event);
boolean foundDelete = false;
for (AccessControlEntry ace : accessControlList.getEntries()) {
if (ace.getAction().equals(action) && ace.getRole().equals(role)) {
foundDelete = true;
} else {
entries.add(ace);
}
}
if (!foundDelete) {
return ApiResponses.notFound("Unable to find an access control entry with action '%s' and role '%s'", action,
role);
}
AccessControlList withoutDeleted = new AccessControlList(entries);
try {
withoutDeleted = indexService.updateEventAcl(id, withoutDeleted, elasticsearchIndex);
} catch (IllegalArgumentException e) {
logger.error("Unable to delete event's '{}' acl entry with action '{}' and role '{}'", id, action, role, e);
return Response.status(Status.FORBIDDEN).build();
}
return Response.noContent().build();
}
return ApiResponses.notFound("Cannot find an event with id '%s'.", id);
}
@GET
@Path("{eventId}/metadata")
@RestQuery(name = "geteventmetadata", description = "Returns the event's metadata of the specified type. For a metadata catalog there is the flavor such as 'dublincore/episode' and this is the unique type.", returnDescription = "", pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, restParameters = {
@RestParameter(name = "type", isRequired = false, description = "The type of metadata to get", type = STRING) }, responses = {
@RestResponse(description = "The metadata collection is returned.", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response getAllEventMetadata(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
@QueryParam("type") String type) throws Exception {
final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
if (StringUtils.trimToNull(type) == null) {
Opt metadataList = getEventMetadataById(id);
if (metadataList.isSome()) {
MetadataList actualList = metadataList.get();
// API v1 should return a two separate fields for start date and start time. Since those fields were merged in index service, we have to split them up.
final DublinCoreMetadataCollection collection = actualList.getMetadataByFlavor("dublincore/episode");
final boolean withOrderedText = collection == null;
if (collection != null) {
convertStartDateTimeToApiV1(collection);
}
return ApiResponses.Json.ok(requestedVersion, MetadataJson.listToJson(actualList, withOrderedText));
}
else
return ApiResponses.notFound("Cannot find an event with id '%s'.", id);
} else {
return getEventMetadataByType(id, type, requestedVersion);
}
}
private void convertStartDateTimeToApiV1(DublinCoreMetadataCollection collection) throws java.text.ParseException {
if (!collection.getOutputFields().containsKey("startDate")) return;
MetadataField oldStartDateField = collection.getOutputFields().get("startDate");
SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(oldStartDateField.getPattern());
Date startDate = sdf.parse((String) oldStartDateField.getValue());
if (configuredMetadataFields.containsKey("startDate")) {
MetadataField startDateField = configuredMetadataFields.get("startDate");
final String pattern = startDateField.getPattern() == null ? "yyyy-MM-dd" : startDateField.getPattern();
startDateField = new MetadataField(startDateField);
startDateField.setPattern(pattern);
sdf.applyPattern(startDateField.getPattern());
startDateField.setValue(sdf.format(startDate));
collection.removeField(oldStartDateField);
collection.addField(startDateField);
}
if (configuredMetadataFields.containsKey("startTime")) {
MetadataField startTimeField = configuredMetadataFields.get("startTime");
final String pattern = startTimeField.getPattern() == null ? "HH:mm" : startTimeField.getPattern();
startTimeField = new MetadataField(startTimeField);
startTimeField.setPattern(pattern);
sdf.applyPattern(startTimeField.getPattern());
startTimeField.setValue(sdf.format(startDate));
collection.addField(startTimeField);
}
}
protected Opt getEventMetadataById(String id) throws IndexServiceException, Exception {
for (final Event event : indexService.getEvent(id, elasticsearchIndex)) {
return getEventMetadata(event);
}
return Opt. none();
}
protected Opt getEventMetadata(Event event) throws IndexServiceException, Exception {
MetadataList metadataList = new MetadataList();
List catalogUIAdapters = getEventCatalogUIAdapters();
EventCatalogUIAdapter eventCatalogUIAdapter = indexService.getCommonEventCatalogUIAdapter();
catalogUIAdapters.remove(eventCatalogUIAdapter);
if (catalogUIAdapters.size() > 0) {
MediaPackage mediaPackage = indexService.getEventMediapackage(event);
for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
// TODO: This is very slow:
DublinCoreMetadataCollection fields = catalogUIAdapter.getFields(mediaPackage);
if (fields != null) {
ExternalMetadataUtils.removeCollectionList(fields);
metadataList.add(catalogUIAdapter, fields);
}
}
}
DublinCoreMetadataCollection collection = EventUtils.getEventMetadata(event, eventCatalogUIAdapter,
new EmptyResourceListQuery());
ExternalMetadataUtils.changeSubjectToSubjects(collection);
ExternalMetadataUtils.removeCollectionList(collection);
metadataList.add(eventCatalogUIAdapter, collection);
if (WorkflowInstance.WorkflowState.RUNNING.toString().equals(event.getWorkflowState())) {
metadataList.setLocked(Locked.WORKFLOW_RUNNING);
}
return Opt.some(metadataList);
}
private Opt getFlavor(String flavorString) {
try {
MediaPackageElementFlavor flavor = MediaPackageElementFlavor.parseFlavor(flavorString);
return Opt.some(flavor);
} catch (IllegalArgumentException e) {
return Opt.none();
}
}
private Response getEventMetadataByType(String id, String type, ApiVersion requestedVersion) throws Exception {
for (final Event event : indexService.getEvent(id, elasticsearchIndex)) {
Opt flavor = getFlavor(type);
if (flavor.isNone()) {
return R.badRequest(
String.format("Unable to parse type '%s' as a flavor so unable to find the matching catalog.", type));
}
// Try the main catalog first as we load it from the index.
EventCatalogUIAdapter eventCatalogUIAdapter = indexService.getCommonEventCatalogUIAdapter();
if (flavor.get().equals(eventCatalogUIAdapter.getFlavor())) {
DublinCoreMetadataCollection collection = EventUtils.getEventMetadata(event, eventCatalogUIAdapter,
new EmptyResourceListQuery());
ExternalMetadataUtils.changeSubjectToSubjects(collection);
ExternalMetadataUtils.removeCollectionList(collection);
convertStartDateTimeToApiV1(collection);
return ApiResponses.Json.ok(requestedVersion, MetadataJson.collectionToJson(collection, false));
}
// Try the other catalogs
List catalogUIAdapters = getEventCatalogUIAdapters();
catalogUIAdapters.remove(eventCatalogUIAdapter);
if (catalogUIAdapters.size() > 0) {
MediaPackage mediaPackage = indexService.getEventMediapackage(event);
for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
if (flavor.get().equals(catalogUIAdapter.getFlavor())) {
DublinCoreMetadataCollection fields = catalogUIAdapter.getFields(mediaPackage);
ExternalMetadataUtils.removeCollectionList(fields);
convertStartDateTimeToApiV1(fields);
return ApiResponses.Json.ok(requestedVersion, MetadataJson.collectionToJson(fields, false));
}
}
}
return ApiResponses.notFound("Cannot find a catalog with type '%s' for event with id '%s'.", type, id);
}
return ApiResponses.notFound("Cannot find an event with id '%s'.", id);
}
@PUT
@Path("{eventId}/metadata")
@RestQuery(name = "updateeventmetadata", description = "Update the metadata with the matching type of the specified event. For a metadata catalog there is the flavor such as 'dublincore/episode' and this is the unique type.", returnDescription = "", pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, restParameters = {
@RestParameter(name = "type", isRequired = true, description = "The type of metadata to update", type = STRING),
@RestParameter(name = "metadata", description = "Metadata catalog in JSON format", isRequired = true, type = STRING) }, responses = {
@RestResponse(description = "The metadata of the given namespace has been updated.", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response updateEventMetadataByType(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
@QueryParam("type") String type, @FormParam("metadata") String metadataJSON) throws Exception {
Map updatedFields;
JSONParser parser = new JSONParser();
try {
updatedFields = RequestUtils.getKeyValueMap(metadataJSON);
} catch (ParseException e) {
logger.debug("Unable to update event '{}' with metadata type '{}' and content '{}'", id, type, metadataJSON, e);
return RestUtil.R.badRequest(String.format("Unable to parse metadata fields as json from '%s'", metadataJSON));
} catch (IllegalArgumentException e) {
logger.debug("Unable to update event '{}' with metadata type '{}' and content '{}'", id, type, metadataJSON, e);
return RestUtil.R.badRequest(e.getMessage());
}
if (updatedFields == null || updatedFields.size() == 0) {
return RestUtil.R.badRequest(
String.format("Unable to parse metadata fields as json from '%s' because there were no fields to update.",
metadataJSON));
}
Opt flavor = getFlavor(type);
if (flavor.isNone()) {
return R.badRequest(
String.format("Unable to parse type '%s' as a flavor so unable to find the matching catalog.", type));
}
DublinCoreMetadataCollection collection = null;
EventCatalogUIAdapter adapter = null;
for (final Event event : indexService.getEvent(id, elasticsearchIndex)) {
MetadataList metadataList = new MetadataList();
// Try the main catalog first as we load it from the index.
EventCatalogUIAdapter eventCatalogUIAdapter = indexService.getCommonEventCatalogUIAdapter();
if (flavor.get().equals(eventCatalogUIAdapter.getFlavor())) {
collection = EventUtils.getEventMetadata(event, eventCatalogUIAdapter);
adapter = eventCatalogUIAdapter;
} else {
metadataList.add(eventCatalogUIAdapter, EventUtils.getEventMetadata(event, eventCatalogUIAdapter));
}
// Try the other catalogs
List catalogUIAdapters = getEventCatalogUIAdapters();
catalogUIAdapters.remove(eventCatalogUIAdapter);
if (catalogUIAdapters.size() > 0) {
MediaPackage mediaPackage = indexService.getEventMediapackage(event);
for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
if (flavor.get().equals(catalogUIAdapter.getFlavor())) {
collection = catalogUIAdapter.getFields(mediaPackage);
adapter = eventCatalogUIAdapter;
} else {
metadataList.add(catalogUIAdapter, catalogUIAdapter.getFields(mediaPackage));
}
}
}
if (collection == null) {
return ApiResponses.notFound("Cannot find a catalog with type '%s' for event with id '%s'.", type, id);
}
for (String key : updatedFields.keySet()) {
if ("subjects".equals(key)) {
MetadataField field = collection.getOutputFields().get(DublinCore.PROPERTY_SUBJECT.getLocalName());
Opt error = validateField(field, key, id, type, updatedFields);
if (error.isSome()) {
return error.get();
}
collection.removeField(field);
JSONArray subjectArray = (JSONArray) parser.parse(updatedFields.get(key));
collection.addField(
MetadataJson.copyWithDifferentJsonValue(field, StringUtils.join(subjectArray.iterator(), ",")));
} else if ("startDate".equals(key)) {
// Special handling for start date since in API v1 we expect start date and start time to be separate fields.
MetadataField field = collection.getOutputFields().get(key);
Opt error = validateField(field, key, id, type, updatedFields);
if (error.isSome()) {
return error.get();
}
String apiPattern = field.getPattern();
if (configuredMetadataFields.containsKey("startDate")) {
final String startDate = configuredMetadataFields.get("startDate").getPattern();
apiPattern = startDate == null ? apiPattern : startDate;
}
SimpleDateFormat apiSdf = MetadataField.getSimpleDateFormatter(apiPattern);
SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(field.getPattern());
DateTime oldStartDate = new DateTime(sdf.parse((String) field.getValue()), DateTimeZone.UTC);
DateTime newStartDate = new DateTime(apiSdf.parse(updatedFields.get(key)), DateTimeZone.UTC);
DateTime updatedStartDate = oldStartDate.withDate(newStartDate.year().get(), newStartDate.monthOfYear().get(), newStartDate.dayOfMonth().get());
collection.removeField(field);
collection.addField(
MetadataJson.copyWithDifferentJsonValue(field, sdf.format(updatedStartDate.toDate())));
} else if ("startTime".equals(key)) {
// Special handling for start time since in API v1 we expect start date and start time to be separate fields.
MetadataField field = collection.getOutputFields().get("startDate");
Opt error = validateField(field, "startDate", id, type, updatedFields);
if (error.isSome()) {
return error.get();
}
String apiPattern = "HH:mm";
if (configuredMetadataFields.containsKey("startTime")) {
final String startTime = configuredMetadataFields.get("startTime").getPattern();
apiPattern = startTime == null ? apiPattern : startTime;
}
SimpleDateFormat apiSdf = MetadataField.getSimpleDateFormatter(apiPattern);
SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(field.getPattern());
DateTime oldStartDate = new DateTime(sdf.parse((String) field.getValue()), DateTimeZone.UTC);
DateTime newStartDate = new DateTime(apiSdf.parse(updatedFields.get(key)), DateTimeZone.UTC);
DateTime updatedStartDate = oldStartDate.withTime(
newStartDate.hourOfDay().get(),
newStartDate.minuteOfHour().get(),
newStartDate.secondOfMinute().get(),
newStartDate.millisOfSecond().get());
collection.removeField(field);
collection.addField(
MetadataJson.copyWithDifferentJsonValue(field, sdf.format(updatedStartDate.toDate())));
} else {
MetadataField field = collection.getOutputFields().get(key);
Opt error = validateField(field, key, id, type, updatedFields);
if (error.isSome()) {
return error.get();
}
collection.removeField(field);
collection.addField(
MetadataJson.copyWithDifferentJsonValue(field, updatedFields.get(key)));
}
}
metadataList.add(adapter, collection);
indexService.updateEventMetadata(id, metadataList, elasticsearchIndex);
return Response.noContent().build();
}
return ApiResponses.notFound("Cannot find an event with id '%s'.", id);
}
private Opt validateField(MetadataField field, String key, String id, String type, Map updatedFields) {
if (field == null) {
return Opt.some(ApiResponses.notFound(
"Cannot find a metadata field with id '%s' from event with id '%s' and the metadata type '%s'.",
key, id, type));
} else if (field.isRequired() && StringUtils.isBlank(updatedFields.get(key))) {
return Opt.some(R.badRequest(String.format(
"The event metadata field with id '%s' and the metadata type '%s' is required and can not be empty!.",
key, type)));
}
return Opt.none();
}
@DELETE
@Path("{eventId}/metadata")
@RestQuery(name = "deleteeventmetadata", description = "Delete the metadata namespace catalog of the specified event. This will remove all fields and values of the catalog.", returnDescription = "", pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, restParameters = {
@RestParameter(name = "type", isRequired = true, description = "The type of metadata to delete", type = STRING) }, responses = {
@RestResponse(description = "The metadata of the given namespace has been updated.", responseCode = HttpServletResponse.SC_NO_CONTENT),
@RestResponse(description = "The main metadata catalog dublincore/episode cannot be deleted as it has mandatory fields.", responseCode = HttpServletResponse.SC_FORBIDDEN),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response deleteEventMetadataByType(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
@QueryParam("type") String type) throws SearchIndexException {
for (final Event event : indexService.getEvent(id, elasticsearchIndex)) {
Opt flavor = getFlavor(type);
if (flavor.isNone()) {
return R.badRequest(
String.format("Unable to parse type '%s' as a flavor so unable to find the matching catalog.", type));
}
EventCatalogUIAdapter eventCatalogUIAdapter = indexService.getCommonEventCatalogUIAdapter();
if (flavor.get().equals(eventCatalogUIAdapter.getFlavor())) {
return Response
.status(Status.FORBIDDEN).entity(String
.format("Unable to delete mandatory metadata catalog with type '%s' for event '%s'", type, id))
.build();
}
try {
indexService.removeCatalogByFlavor(event, flavor.get());
} catch (NotFoundException e) {
return ApiResponses.notFound(e.getMessage());
} catch (IndexServiceException e) {
logger.error("Unable to remove metadata catalog with type '{}' from event '{}'", type, id, e);
throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
} catch (IllegalStateException e) {
logger.debug("Unable to remove metadata catalog with type '{}' from event '{}'", type, id, e);
throw new WebApplicationException(e, Status.BAD_REQUEST);
} catch (UnauthorizedException e) {
return Response.status(Status.UNAUTHORIZED).build();
}
return Response.noContent().build();
}
return ApiResponses.notFound("Cannot find an event with id '%s'.", id);
}
@GET
@Path("{eventId}/publications")
@RestQuery(name = "geteventpublications", description = "Returns an event's list of publications.",
returnDescription = "",
pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING)
},
restParameters = {
@RestParameter(name = "sign", description = "Whether public distribution urls should be signed.",
isRequired = false, type = Type.BOOLEAN),
@RestParameter(name = "includeInternalPublication", description = "Whether internal publications should be included.",
isRequired = false, type = Type.BOOLEAN)
},
responses = {
@RestResponse(description = "The list of publications is returned.", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response getEventPublications(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
@QueryParam("sign") boolean sign, @QueryParam("includeInternalPublication") boolean includeInternalPublication) throws Exception {
try {
final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
final Opt event = indexService.getEvent(id, elasticsearchIndex);
if (event.isSome()) {
return ApiResponses.Json.ok(acceptHeader, arr(getPublications(event.get(), sign, includeInternalPublication, requestedVersion)));
} else {
return ApiResponses.notFound(String.format("Unable to find event with id '%s'", id));
}
} catch (SearchIndexException e) {
logger.error("Unable to get list of publications from event with id '{}'", id, e);
throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
}
}
private List getPublications(Event event, Boolean withSignedUrls, Boolean includeInternalPublication, ApiVersion requestedVersion) {
return event.getPublications().stream()
.filter(publication -> ((includeInternalPublication != null && includeInternalPublication && !requestedVersion.isSmallerThan(VERSION_1_11_0)) || EventUtils.internalChannelFilter.apply(publication)))
.map(p -> getPublication(p, withSignedUrls, requestedVersion))
.collect(Collectors.toList());
}
public JObject getPublication(Publication publication, Boolean sign, ApiVersion requestedVersion) {
// signing publication URLs was introduced in 1.7.0
URI publicationUrl = publication.getURI();
if (!requestedVersion.isSmallerThan(VERSION_1_7_0)) {
publicationUrl = getSignedUrl(publicationUrl, sign);
}
return obj(f("id", v(publication.getIdentifier())), f("channel", v(publication.getChannel())),
f("mediatype", v(publication.getMimeType(), BLANK)),
f("url", v(publicationUrl, BLANK)),
f("media", arr(getPublicationTracksJson(publication, sign, requestedVersion))),
f("attachments", arr(getPublicationAttachmentsJson(publication, sign))),
f("metadata", arr(getPublicationCatalogsJson(publication, sign))));
}
private URI getSignedUrl(URI url, boolean sign) {
if (url == null || !sign) {
return url;
}
if (urlSigningService.accepts(url.toString())) {
try {
return URI.create(urlSigningService.sign(url.toString(), expireSeconds, null, null));
} catch (UrlSigningException e) {
logger.error("Unable to sign URI {}", url, e);
}
}
return url;
}
private List getPublicationTracksJson(Publication publication, Boolean sign, ApiVersion requestedVersion) {
List tracks = new ArrayList<>();
for (Track track : publication.getTracks()) {
VideoStream[] videoStreams = TrackSupport.byType(track.getStreams(), VideoStream.class);
List trackInfo = new ArrayList<>();
if (videoStreams.length > 0) {
// Only supporting one stream, like in many other places...
final VideoStream videoStream = videoStreams[0];
if (videoStream.getBitRate() != null)
trackInfo.add(f("bitrate", v(videoStream.getBitRate())));
if (videoStream.getFrameRate() != null)
trackInfo.add(f("framerate", v(videoStream.getFrameRate())));
if (videoStream.getFrameCount() != null)
trackInfo.add(f("framecount", v(videoStream.getFrameCount())));
if (videoStream.getFrameWidth() != null)
trackInfo.add(f("width", v(videoStream.getFrameWidth())));
if (videoStream.getFrameHeight() != null)
trackInfo.add(f("height", v(videoStream.getFrameHeight())));
}
if (!requestedVersion.isSmallerThan(VERSION_1_7_0)) {
trackInfo.add(f("is_master_playlist", v(track.isMaster())));
trackInfo.add(f("is_live", v(track.isLive())));
}
tracks.add(obj(f("id", v(track.getIdentifier(), BLANK)), f("mediatype", v(track.getMimeType(), BLANK)),
f("url", v(getSignedUrl(track.getURI(), sign), BLANK)), f("flavor", v(track.getFlavor(), BLANK)),
f("size", v(track.getSize())), f("checksum", v(track.getChecksum(), BLANK)),
f("tags", arr(track.getTags())), f("has_audio", v(track.hasAudio())),
f("has_video", v(track.hasVideo())), f("duration", v(track.getDuration(), NULL)),
f("description", v(track.getDescription(), BLANK))).merge(trackInfo));
}
return tracks;
}
private List getPublicationAttachmentsJson(Publication publication, Boolean sign) {
List attachments = new ArrayList<>();
for (Attachment attachment : publication.getAttachments()) {
attachments.add(
obj(f("id", v(attachment.getIdentifier(), BLANK)), f("mediatype", v(attachment.getMimeType(), BLANK)),
f("url", v(getSignedUrl(attachment.getURI(), sign), BLANK)),
f("flavor", v(attachment.getFlavor(), BLANK)), f("ref", v(attachment.getReference(), BLANK)),
f("size", v(attachment.getSize())), f("checksum", v(attachment.getChecksum(), BLANK)),
f("tags", arr(attachment.getTags()))));
}
return attachments;
}
private List getPublicationCatalogsJson(Publication publication, Boolean sign) {
List catalogs = new ArrayList<>();
for (Catalog catalog : publication.getCatalogs()) {
catalogs.add(obj(f("id", v(catalog.getIdentifier(), BLANK)), f("mediatype", v(catalog.getMimeType(), BLANK)),
f("url", v(getSignedUrl(catalog.getURI(), sign), BLANK)),
f("flavor", v(catalog.getFlavor(), BLANK)), f("size", v(catalog.getSize())),
f("checksum", v(catalog.getChecksum(), BLANK)), f("tags", arr(catalog.getTags()))));
}
return catalogs;
}
@GET
@Path("{eventId}/publications/{publicationId}")
@RestQuery(name = "geteventpublication", description = "Returns a single publication.", returnDescription = "",
pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING),
@RestParameter(name = "publicationId", description = "The publication id", isRequired = true, type = STRING)
},
restParameters = {
@RestParameter(name = "sign", description = "Whether public distribution urls should be signed.",
isRequired = false, type = Type.BOOLEAN)
},
responses = {
@RestResponse(description = "The track details are returned.", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "The specified event or publication does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response getEventPublication(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String eventId,
@PathParam("publicationId") String publicationId, @QueryParam("sign") boolean sign) throws Exception {
try {
final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
return ApiResponses.Json.ok(acceptHeader, getPublication(eventId, publicationId, sign, requestedVersion));
} catch (NotFoundException e) {
return ApiResponses.notFound(e.getMessage());
} catch (SearchIndexException e) {
logger.error("Unable to get list of publications from event with id '{}'", eventId, e);
throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
}
}
private JObject getPublication(String eventId, String publicationId, Boolean withSignedUrls, ApiVersion requestedVersion)
throws SearchIndexException, NotFoundException {
for (final Event event : indexService.getEvent(eventId, elasticsearchIndex)) {
List publications;
publications = event.getPublications().stream().filter(publication -> (!requestedVersion.isSmallerThan(VERSION_1_11_0) || EventUtils.internalChannelFilter.apply(publication))).collect(Collectors.toList());
for (Publication publication : publications) {
if (publicationId.equals(publication.getIdentifier())) {
return getPublication(publication, withSignedUrls, requestedVersion);
}
}
throw new NotFoundException(
String.format("Unable to find publication with id '%s' in event with id '%s'", publicationId, eventId));
}
throw new NotFoundException(String.format("Unable to find event with id '%s'", eventId));
}
/**
* Get an {@link AccessControlList} from an {@link Event}.
*
* @param event
* The {@link Event} to get the ACL from.
* @return The {@link AccessControlList} stored in the {@link Event}
*/
protected static AccessControlList getAclFromEvent(Event event) {
AccessControlList activeAcl = new AccessControlList();
try {
if (event.getAccessPolicy() != null)
activeAcl = AccessControlParser.parseAcl(event.getAccessPolicy());
} catch (Exception e) {
logger.error("Unable to parse access policy", e);
}
return activeAcl;
}
private JValue getJsonStream(Stream stream) {
List fields = new ArrayList<>();
if (stream instanceof AudioStream) {
AudioStream audioStream = (AudioStream) stream;
if (audioStream.getBitDepth() != null)
fields.add(f("bitdepth", v(audioStream.getBitDepth())));
if (audioStream.getBitRate() != null)
fields.add(f("bitrate", v(audioStream.getBitRate())));
if (audioStream.getCaptureDevice() != null)
fields.add(f("capturedevice", v(audioStream.getCaptureDevice())));
if (audioStream.getCaptureDeviceVendor() != null)
fields.add(f("capturedevicevendor", v(audioStream.getCaptureDeviceVendor())));
if (audioStream.getCaptureDeviceVersion() != null)
fields.add(f("capturedeviceversion", v(audioStream.getCaptureDeviceVersion())));
if (audioStream.getChannels() != null)
fields.add(f("channels", v(audioStream.getChannels())));
if (audioStream.getEncoderLibraryVendor() != null)
fields.add(f("encoderlibraryvendor", v(audioStream.getEncoderLibraryVendor())));
if (audioStream.getFormat() != null)
fields.add(f("format", v(audioStream.getFormat())));
if (audioStream.getFormatVersion() != null)
fields.add(f("formatversion", v(audioStream.getFormatVersion())));
if (audioStream.getFrameCount() != null)
fields.add(f("framecount", v(audioStream.getFrameCount())));
if (audioStream.getIdentifier() != null)
fields.add(f("identifier", v(audioStream.getIdentifier())));
if (audioStream.getPkLevDb() != null)
fields.add(f("pklevdb", v(audioStream.getPkLevDb())));
if (audioStream.getRmsLevDb() != null)
fields.add(f("rmslevdb", v(audioStream.getRmsLevDb())));
if (audioStream.getRmsPkDb() != null)
fields.add(f("rmspkdb", v(audioStream.getRmsPkDb())));
if (audioStream.getSamplingRate() != null)
fields.add(f("samplingrate", v(audioStream.getSamplingRate())));
} else if (stream instanceof VideoStream) {
VideoStream videoStream = (VideoStream) stream;
if (videoStream.getBitRate() != null)
fields.add(f("bitrate", v(videoStream.getBitRate())));
if (videoStream.getCaptureDevice() != null)
fields.add(f("capturedevice", v(videoStream.getCaptureDevice())));
if (videoStream.getCaptureDeviceVendor() != null)
fields.add(f("capturedevicevendor", v(videoStream.getCaptureDeviceVendor())));
if (videoStream.getCaptureDeviceVersion() != null)
fields.add(f("capturedeviceversion", v(videoStream.getCaptureDeviceVersion())));
if (videoStream.getEncoderLibraryVendor() != null)
fields.add(f("encoderlibraryvendor", v(videoStream.getEncoderLibraryVendor())));
if (videoStream.getFormat() != null)
fields.add(f("format", v(videoStream.getFormat())));
if (videoStream.getFormatVersion() != null)
fields.add(f("formatversion", v(videoStream.getFormatVersion())));
if (videoStream.getFrameCount() != null)
fields.add(f("framecount", v(videoStream.getFrameCount())));
if (videoStream.getFrameHeight() != null)
fields.add(f("frameheight", v(videoStream.getFrameHeight())));
if (videoStream.getFrameRate() != null)
fields.add(f("framerate", v(videoStream.getFrameRate())));
if (videoStream.getFrameWidth() != null)
fields.add(f("framewidth", v(videoStream.getFrameWidth())));
if (videoStream.getIdentifier() != null)
fields.add(f("identifier", v(videoStream.getIdentifier())));
if (videoStream.getScanOrder() != null)
fields.add(f("scanorder", v(videoStream.getScanOrder().toString())));
if (videoStream.getScanType() != null)
fields.add(f("scantype", v(videoStream.getScanType().toString())));
}
return obj(fields);
}
private String getEventUrl(String eventId) {
return UrlSupport.concat(endpointBaseUrl, eventId);
}
@GET
@Path("{eventId}/scheduling")
@Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0, ApiMediaType.VERSION_1_3_0,
ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0, ApiMediaType.VERSION_1_6_0,
ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0, ApiMediaType.VERSION_1_9_0,
ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
@RestQuery(name = "geteventscheduling", description = "Returns an event's scheduling information.", returnDescription = "", pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, responses = {
@RestResponse(description = "The scheduling information for the specified event is returned.", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "The specified event has no scheduling information.", responseCode = HttpServletResponse.SC_NO_CONTENT),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response getEventScheduling(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id)
throws Exception {
try {
final Opt event = indexService.getEvent(id, elasticsearchIndex);
if (event.isNone()) {
return ApiResponses.notFound(String.format("Unable to find event with id '%s'", id));
}
final JObject scheduling = SchedulingInfo.of(event.get().getIdentifier(), schedulerService).toJson();
if (!scheduling.isEmpty()) {
return ApiResponses.Json.ok(acceptHeader, scheduling);
}
return Response.noContent().build();
} catch (SearchIndexException e) {
logger.error("Unable to get list of publications from event with id '{}'", id, e);
throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
}
}
@PUT
@Path("{eventId}/scheduling")
@Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0, ApiMediaType.VERSION_1_3_0,
ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0, ApiMediaType.VERSION_1_6_0,
ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0, ApiMediaType.VERSION_1_9_0,
ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
@RestQuery(name = "updateeventscheduling", description = "Update an event's scheduling information.", returnDescription = "", pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = Type.STRING) }, restParameters = {
@RestParameter(name = "scheduling", isRequired = true, description = "Scheduling Information", type = Type.STRING),
@RestParameter(name = "allowConflict", description = "Allow conflicts when updating scheduling", isRequired = false, type = Type.BOOLEAN) }, responses = {
@RestResponse(description = "The scheduling information for the specified event is updated.", responseCode = HttpServletResponse.SC_NO_CONTENT),
@RestResponse(description = "The specified event has no scheduling information to update.", responseCode = HttpServletResponse.SC_NOT_ACCEPTABLE),
@RestResponse(description = "The scheduling information could not be updated due to a conflict.", responseCode = HttpServletResponse.SC_CONFLICT),
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response updateEventScheduling(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
@FormParam("scheduling") String scheduling,
@FormParam("allowConflict") @DefaultValue("false") boolean allowConflict) throws Exception {
final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
final Opt event = indexService.getEvent(id, elasticsearchIndex);
if (requestedVersion.isSmallerThan(ApiVersion.VERSION_1_2_0)) {
allowConflict = false;
}
if (event.isNone()) {
return ApiResponses.notFound(String.format("Unable to find event with id '%s'", id));
}
final JSONParser parser = new JSONParser();
JSONObject parsedJson;
try {
parsedJson = (JSONObject) parser.parse(scheduling);
} catch (ParseException e) {
logger.debug("Client sent unparsable scheduling information for event {}: {}", id, scheduling);
return RestUtil.R.badRequest("Unparsable scheduling information");
}
Optional clientError = updateSchedulingInformation(parsedJson, id, requestedVersion, allowConflict);
return clientError.orElse(Response.noContent().build());
}
private Optional updateSchedulingInformation(
JSONObject parsedScheduling,
String id,
ApiVersion requestedVersion,
boolean allowConflict) throws Exception {
SchedulingInfo schedulingInfo;
try {
schedulingInfo = SchedulingInfo.of(parsedScheduling);
} catch (DateTimeParseException e) {
logger.debug("Client sent unparsable start or end date for event {}", id);
return Optional.of(RestUtil.R.badRequest("Unparsable date in scheduling information"));
}
final TechnicalMetadata technicalMetadata = schedulerService.getTechnicalMetadata(id);
// When "inputs" is updated, capture agent configuration needs to be merged
Opt> caConfig = Opt.none();
if (schedulingInfo.getInputs().isSome()) {
final Map configMap = new HashMap<>(technicalMetadata.getCaptureAgentConfiguration());
configMap.put(CaptureParameters.CAPTURE_DEVICE_NAMES, schedulingInfo.getInputs().get());
caConfig = Opt.some(configMap);
}
try {
schedulerService.updateEvent(
id,
schedulingInfo.getStartDate(),
schedulingInfo.getEndDate(),
schedulingInfo.getAgentId(),
Opt.none(),
Opt.none(),
Opt.none(),
caConfig,
allowConflict);
} catch (SchedulerConflictException e) {
final List conflictingEvents = getConflictingEvents(
schedulingInfo.merge(technicalMetadata), agentStateService, schedulerService);
logger.debug("Client tried to change scheduling information causing a conflict for event {}.", id);
return Optional.of(ApiResponses.Json.conflict(requestedVersion,
arr(convertConflictingEvents(Optional.of(id), conflictingEvents, indexService, elasticsearchIndex))));
}
return Optional.empty();
}
@POST
@Path("{eventId}/track")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@RestQuery(name = "updateFlavorWithTrack", description = "Update an events track for a given flavor", returnDescription = "",
pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) },
restParameters = {
@RestParameter(description = "Flavor to add track to, e.g. captions/source",
isRequired = true, name = "flavor", type = RestParameter.Type.STRING),
@RestParameter(description = "Comma separated list of tags for the given track, e.g. archive,publish. "
+ "If a 'lang:LANG-CODE' tag exists and overwriteExisting=true "
+ "only tracks with same lang tag and flavor will be replaced. This behavior is used for captions.",
isRequired = false, name = "tags", type = RestParameter.Type.STRING),
@RestParameter(description = "If true, all other tracks in the specified flavor are REMOVED. "
+ "If tags argument contains a lang:LANG-CODE tag, only elements with same tag would be removed.",
isRequired = true, name = "overwriteExisting", type = RestParameter.Type.BOOLEAN),
@RestParameter(description = "The track file", isRequired = true, name = "track", type = RestParameter.Type.FILE),
},
responses = {
@RestResponse(description = "The specified event does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND),
@RestResponse(description = "The track has been added to the event.", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "The request is invalid or inconsistent.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
})
public Response updateFlavorWithTrack(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id,
@Context HttpServletRequest request) {
logger.debug("updateFlavorWithTrack called");
try {
boolean overwriteExisting = false;
MediaPackageElementFlavor tmpFlavor = MediaPackageElementFlavor.parseFlavor("addTrack/temporary");
MediaPackageElementFlavor newFlavor = null;
Opt event;
List tags = null;
String langTag = null;
try {
event = indexService.getEvent(id, elasticsearchIndex);
} catch (SearchIndexException e) {
return RestUtil.R.badRequest(String.format("Error while searching for event with id %s; %s", id, e.getMessage()));
}
if (event.isNone()) {
return ApiResponses.notFound(String.format("Unable to find event with id '%s'", id));
}
MediaPackage mp = indexService.getEventMediapackage(event.get());
try {
if (workflowService.mediaPackageHasActiveWorkflows(mp.getIdentifier().toString())) {
return RestUtil.R.conflict(String.format("Cannot update while a workflow is running on event '%s'", id));
}
} catch (WorkflowDatabaseException e) {
return RestUtil.R.serverError();
}
if (!ServletFileUpload.isMultipartContent(request)) {
throw new IllegalArgumentException("No multipart content");
}
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());
try {
newFlavor = MediaPackageElementFlavor.parseFlavor(flavorString);
} catch (IllegalArgumentException e) {
return RestUtil.R.badRequest(String.format("Could not parse flavor %s; %s", flavorString, e.getMessage()));
}
} else if ("tags".equals(fieldName)) {
String tagsString = Streams.asString(item.openStream());
if (StringUtils.isNotBlank(tagsString)) {
tags = List.of(StringUtils.split(tagsString, ','));
// find lang tag if exists
for (String tag : tags) {
if (StringUtils.startsWith(StringUtils.trimToEmpty(tag), "lang:")) {
// lang tag is set
langTag = StringUtils.trimToEmpty(tag);
break;
}
}
}
} else if ("overwriteExisting".equals(fieldName)) {
overwriteExisting = Boolean.parseBoolean(Streams.asString(item.openStream()));
}
} else {
// Add track with temporary flavor
if ("track".equals(item.getFieldName())) {
mp = ingestService.addTrack(item.openStream(), item.getName(), tmpFlavor, mp);
}
}
}
if (overwriteExisting) {
// remove existing attachments of the new flavor
Track[] existing = mp.getTracks(newFlavor);
for (int i = 0; i < existing.length; i++) {
// if lang tag is set, remove only matching elements
if (null == langTag || existing[i].containsTag(langTag)) {
mp.remove(existing[i]);
logger.debug("Overwriting existing asset {} {}", tmpFlavor, newFlavor);
}
}
}
// correct the flavor of the new attachment
for (Track track : mp.getTracks(tmpFlavor)) {
track.setFlavor(newFlavor);
if (null != tags) {
for (String tag : tags) {
track.addTag(tag);
}
}
}
logger.debug("Updated asset {} {}", tmpFlavor, newFlavor);
try {
assetManager.takeSnapshot(mp);
} catch (AssetManagerException e) {
logger.error("Error while adding the updated media package ({}) to the archive", mp.getIdentifier(), e);
return RestUtil.R.badRequest(e.getMessage());
}
return Response.status(Status.OK).build();
} catch (IllegalArgumentException | IOException | FileUploadException | IndexServiceException | IngestException
| MediaPackageException e) {
return RestUtil.R.badRequest(String.format("Could not add track: %s", e.getMessage()));
}
}
}