/** * 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.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_0_0; import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING; import org.opencastproject.external.common.ApiResponses; import org.opencastproject.external.common.ApiVersion; import org.opencastproject.external.impl.index.ExternalIndex; 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.MetadataList; import org.opencastproject.index.service.catalog.adapter.MetadataList.Locked; import org.opencastproject.index.service.catalog.adapter.events.CommonEventCatalogUIAdapter; import org.opencastproject.index.service.exception.IndexServiceException; import org.opencastproject.index.service.impl.index.IndexObject; import org.opencastproject.index.service.impl.index.event.Event; import org.opencastproject.index.service.impl.index.event.Event.SchedulingStatus; import org.opencastproject.index.service.impl.index.event.EventHttpServletRequest; import org.opencastproject.index.service.impl.index.event.EventIndexSchema; import org.opencastproject.index.service.impl.index.event.EventSearchQuery; import org.opencastproject.index.service.impl.index.event.EventUtils; import org.opencastproject.index.service.impl.index.series.Series; 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.matterhorn.search.SearchIndexException; import org.opencastproject.matterhorn.search.SearchResult; import org.opencastproject.matterhorn.search.SearchResultItem; import org.opencastproject.matterhorn.search.SortCriterion; import org.opencastproject.mediapackage.Attachment; import org.opencastproject.mediapackage.AudioStream; import org.opencastproject.mediapackage.Catalog; import org.opencastproject.mediapackage.MediaPackage; import org.opencastproject.mediapackage.MediaPackageElement; 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.EventCatalogUIAdapter; import org.opencastproject.metadata.dublincore.MetadataCollection; import org.opencastproject.metadata.dublincore.MetadataField; import org.opencastproject.rest.RestConstants; import org.opencastproject.scheduler.api.SchedulerException; import org.opencastproject.security.api.AccessControlEntry; import org.opencastproject.security.api.AccessControlList; import org.opencastproject.security.api.AccessControlParser; 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.MatterhornConstants; import org.opencastproject.util.Log; import org.opencastproject.util.NotFoundException; import org.opencastproject.util.OsgiUtil; 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.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.workflow.api.WorkflowInstance; import com.entwinemedia.fn.Fn; import com.entwinemedia.fn.Fn2; 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.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.joda.time.DateTime; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.Dictionary; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; 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("/") @RestService(name = "externalapievents", title = "External API Events Service", notes = "", abstractText = "Provides resources and operations related to the events") public class EventsEndpoint implements ManagedService { private static final String METADATA_JSON_KEY = "metadata"; 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 * 60; /** 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"; /** The logging facility */ private static final Logger logger = LoggerFactory.getLogger(EventsEndpoint.class); /** A parser for handling JSON documents inside the body of a request. **/ public static final JSONParser parser = new JSONParser(); /** Default server URL */ protected String serverUrl = "http://localhost:8080"; /** Service url */ protected String serviceUrl = null; private static long expireSeconds = DEFAULT_URL_SIGNING_EXPIRE_DURATION; private String previewSubtype = DEFAULT_PREVIEW_SUBTYPE; /** The resolutions */ private enum CommentResolution { ALL, UNRESOLVED, RESOLVED; }; /* OSGi service references */ private ExternalIndex externalIndex; private IndexService indexService; private IngestService ingestService; private SecurityService securityService; private CommonEventCatalogUIAdapter eventCatalogUIAdapter; private final List<EventCatalogUIAdapter> catalogUIAdapters = new ArrayList<>(); private UrlSigningService urlSigningService; /** OSGi DI */ void setExternalIndex(ExternalIndex externalIndex) { this.externalIndex = externalIndex; } /** OSGi DI */ public void setIndexService(IndexService indexService) { this.indexService = indexService; } /** OSGi DI */ public void setIngestService(IngestService ingestService) { this.ingestService = ingestService; } /** OSGi DI */ void setSecurityService(SecurityService securityService) { this.securityService = securityService; } /** OSGi DI */ public void setUrlSigningService(UrlSigningService urlSigningService) { this.urlSigningService = urlSigningService; } public SecurityService getSecurityService() { return securityService; } /** OSGi DI. */ public void setCommonEventCatalogUIAdapter(CommonEventCatalogUIAdapter eventCatalogUIAdapter) { this.eventCatalogUIAdapter = eventCatalogUIAdapter; } /** OSGi DI. */ public void addCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) { catalogUIAdapters.add(catalogUIAdapter); } /** OSGi DI. */ public void removeCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) { catalogUIAdapters.remove(catalogUIAdapter); } private List<EventCatalogUIAdapter> getEventCatalogUIAdapters() { return new ArrayList<>(getEventCatalogUIAdapters(getSecurityService().getOrganization().getId())); } public List<EventCatalogUIAdapter> getEventCatalogUIAdapters(String organization) { List<EventCatalogUIAdapter> adapters = new ArrayList<>(); for (EventCatalogUIAdapter adapter : catalogUIAdapters) { if (organization.equals(adapter.getOrganization())) { adapters.add(adapter); } } return adapters; } /** OSGi activation method */ void activate(ComponentContext cc) { this.serverUrl = "http://localhost:8080"; if (cc != null) { String ccServerUrl = cc.getBundleContext().getProperty(MatterhornConstants.EXTERNAL_API_URL_ORG_PROPERTY); if (ccServerUrl != null) { logger.debug("Configured server url is {}", ccServerUrl); this.serverUrl = ccServerUrl; } } serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY); logger.info("Activated External API - Events Endpoint"); } /** OSGi callback if properties file is present */ @Override public void updated(Dictionary<String, ?> properties) throws ConfigurationException { if (properties == null) { logger.info("No configuration available, using defaults"); return; } Opt<Long> expiration = OsgiUtil.getOptCfg(properties, URL_SIGNING_EXPIRES_DURATION_SECONDS_KEY).toOpt() .map(com.entwinemedia.fn.fns.Strings.toLongF); if (expiration.isSome()) { expireSeconds = expiration.get(); logger.info("The property {} has been configured to expire signed URLs in {}.", URL_SIGNING_EXPIRES_DURATION_SECONDS_KEY, Log.getHumanReadableTimeString(expireSeconds)); } else { expireSeconds = DEFAULT_URL_SIGNING_EXPIRE_DURATION; logger.info("The property {} has not been configured, so the default is being used to expire signed URLs in {}.", URL_SIGNING_EXPIRES_DURATION_SECONDS_KEY, Log.getHumanReadableTimeString(expireSeconds)); } if ((properties != null) && (properties.get(PREVIEW_SUBTYPE) != null)) { previewSubtype = StringUtils.trimToNull((String) properties.get(PREVIEW_SUBTYPE)); logger.info("Preview subtype is '{}'", previewSubtype); } else { previewSubtype = DEFAULT_PREVIEW_SUBTYPE; logger.warn("No preview subtype configured, using '{}'", previewSubtype); } } @GET @Path("{eventId}") @Produces({ "application/json", "application/v1.0.0+json" }) @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 = "withpublications", isRequired = false, description = "Whether the publication ids and urls should be included in the response.", type = Type.BOOLEAN), }, reponses = { @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("withpublications") Boolean withPublications) throws Exception { for (final Event event : indexService.getEvent(id, externalIndex)) { event.updatePreview(previewSubtype); return ApiResponses.Json.ok(ApiVersion.VERSION_1_0_0, eventToJSON(event, withAcl, withMetadata, withPublications, sign)); } return ApiResponses.notFound("Cannot find an event with id '%s'.", id); } @GET @Path("{eventId}/media") @Produces({ "application/json", "application/v1.0.0+json" }) public Response getEventMedia(@HeaderParam("Accept") String acceptHeader, @PathParam("eventId") String id) throws Exception { ArrayList<TrackImpl> tracks = new ArrayList<>(); for (final Event event : indexService.getEvent(id, externalIndex)) { for (final MediaPackage mp : indexService.getEventMediapackage(event)) { for (Track track : mp.getTracks()) { if (track instanceof TrackImpl) { tracks.add((TrackImpl) track); } } List<JValue> tracksJson = new ArrayList<>(); for (Track track : tracks) { List<Field> 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("identifier", v(track.getMimeType().toString()))); fields.add(f("size", v(track.getSize()))); if (track.getStreams() != null) { List<Field> 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<JValue> 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(ApiVersion.VERSION_1_0_0, arr(tracksJson)); } } return ApiResponses.notFound("Cannot find an event with id '%s'.", id); } @DELETE @Path("{eventId}") @Produces({ "application/json", "application/v1.0.0+json" }) @RestQuery(name = "deleteevent", description = "Deletes an event.", returnDescription = "", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, reponses = { @RestResponse(description = "The event has been deleted.", responseCode = HttpServletResponse.SC_NO_CONTENT), @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 NotFoundException, UnauthorizedException { if (!indexService.removeEvent(id)) return Response.serverError().build(); return ApiResponses.Json.noContent(ApiVersion.VERSION_1_0_0); } @POST @Path("{eventId}") @Produces({ "application/json", "application/v1.0.0+json" }) @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 = "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), }, reponses = { @RestResponse(description = "The event has been updated.", responseCode = HttpServletResponse.SC_NO_CONTENT), @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) { return updateEvent(eventId, request); } private Response updateEvent(String eventId, HttpServletRequest request) { try { for (final Event event : indexService.getEvent(eventId, externalIndex)) { EventHttpServletRequest eventHttpServletRequest = EventHttpServletRequest.updateFromHttpServletRequest(event, request, getEventCatalogUIAdapters()); if (eventHttpServletRequest.getMetadataList().isSome()) { indexService.updateEventMetadata(eventId, eventHttpServletRequest.getMetadataList().get(), externalIndex); } if (eventHttpServletRequest.getAcl().isSome()) { indexService.updateEventAcl(eventId, eventHttpServletRequest.getAcl().get(), externalIndex); } return ApiResponses.Json.noContent(ApiVersion.VERSION_1_0_0); } 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 '{}' because: {}", eventId, ExceptionUtils.getStackTrace(e)); return RestUtil.R.badRequest(e.getMessage()); } catch (IndexServiceException e) { logger.error("Unable to get multi part fields or file for event '{}' because: {}", eventId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); } catch (SearchIndexException e) { logger.error("Unable to update event '{}' because: {}", eventId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR); } } @POST @Path("/") @Produces({ "application/json", "application/v1.0.0+json" }) @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 = "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) }, reponses = { @RestResponse(description = "A new event is created and its identifier is returned in the Location header.", responseCode = HttpServletResponse.SC_CREATED), @RestResponse(description = "The request is invalid or inconsistent..", responseCode = HttpServletResponse.SC_BAD_REQUEST) }) public Response createNewEvent(@HeaderParam("Accept") String acceptHeader, @Context HttpServletRequest request) { try { return createNewEvent(request); } catch (IllegalArgumentException e) { logger.debug("Unable to create event because: {}", ExceptionUtils.getStackTrace(e)); return RestUtil.R.badRequest(e.getMessage()); } catch (IndexServiceException e) { if (e.getCause() != null && e.getCause() instanceof NotFoundException || e.getCause() instanceof IllegalArgumentException) { logger.debug("Unable to create event because: {}", ExceptionUtils.getStackTrace(e)); return RestUtil.R.badRequest(e.getCause().getMessage()); } else { logger.error("Unable to create event because: {}", ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); } } catch (Exception e) { logger.error("Unable to create event because: {}", ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); } } @SuppressWarnings("unchecked") private Response createNewEvent(HttpServletRequest request) throws ParseException, IOException, IndexServiceException, java.text.ParseException, MediaPackageException, IngestException, NotFoundException, SchedulerException, UnauthorizedException { JSONObject source = new JSONObject(); source.put("type", "UPLOAD"); EventHttpServletRequest eventHttpServletRequest = EventHttpServletRequest.createFromHttpServletRequest(request, ingestService, getEventCatalogUIAdapters(), source); String eventId = indexService.createEvent(eventHttpServletRequest); return ApiResponses.Json.created(VERSION_1_0_0, URI.create(getEventUrl(eventId)), obj(f("identifier", v(eventId)))); } @GET @Path("/") @Produces({ "application/json", "application/v1.0.0+json" }) @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 = "withpublications", isRequired = false, description = "Whether the publication ids and urls should be included in the response.", type = Type.BOOLEAN), @RestParameter(name = "filter", isRequired = false, description = "A comma seperated list of filters to limit the results with. A filter is the filter's name followed by a colon \":\" and then the value to filter with so it is the form <Filter Name>:<Value to Filter With>.", 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: <Sort Name>:ASC or <Sort Name>: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 = "Number of results to skip based on the limit. 0 is the first set of results up to the limit, 1 is the second set of results after the first limit, 2 is third set of results after skipping the first two sets of results etc.", isRequired = false, type = RestParameter.Type.INTEGER) }, reponses = { @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") String 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("withpublications") Boolean withPublications) { Option<Integer> optLimit = Option.option(limit); Option<Integer> optOffset = Option.option(offset); Option<String> 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(); } // Parse the filters if (StringUtils.isNotBlank(filter)) { for (String f : filter.split(",")) { String[] filterTuple = f.split(":"); if (filterTuple.length < 2) { logger.info("No value for filter {} in filters list: {}", filterTuple[0], filter); continue; } String name = filterTuple[0]; String value = filterTuple[1]; if ("presenters".equals(name)) query.withPresenter(value); if ("contributors".equals(name)) query.withContributor(value); if ("location".equals(name)) query.withLocation(value); if ("textFilter".equals(name)) query.withText("*" + value + "*"); if ("series".equals(name)) query.withSeriesId(value); if ("subject".equals(name)) query.withSubject(value); } } if (optSort.isSome()) { Set<SortCriterion> 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.REVIEW_STATUS: query.sortByReviewStatus(criterion.getOrder()); break; case EventIndexSchema.WORKFLOW_STATE: query.sortByWorkflowState(criterion.getOrder()); break; case EventIndexSchema.SCHEDULING_STATUS: query.sortBySchedulingStatus(criterion.getOrder()); break; case EventIndexSchema.SERIES_NAME: query.sortBySeriesName(criterion.getOrder()); break; case EventIndexSchema.LOCATION: query.sortByLocation(criterion.getOrder()); break; default: return RestUtil.R .badRequest(String.format("Unknown search criterion in request: %s", criterion.getFieldName())); } } } // TODO: Add the comment resolution filter to the query CommentResolution resolution = null; if (StringUtils.isNotBlank(resolutionFilter)) { try { resolution = 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<Event> results = null; try { results = externalIndex.getByQuery(query); } catch (SearchIndexException e) { logger.error("The External Search Index was not able to get the events list: {}", ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); } SearchResultItem<Event>[] items = results.getItems(); List<IndexObject> events = new ArrayList<>(); for (SearchResultItem<Event> item : items) { Event source = item.getSource(); source.updatePreview(previewSubtype); events.add(source); } try { return getJsonEvents(acceptHeader, events, withAcl, withMetadata, withPublications, sign); } catch (Exception e) { logger.error("Unable to get events because: {}", ExceptionUtils.getStackTrace(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. * @return A {@link Response} with the accept header and body as the Json array of {@link Event}s. * @throws NotFoundException * @throws IndexServiceException */ protected Response getJsonEvents(String acceptHeader, List<IndexObject> events, Boolean withAcl, Boolean withMetadata, Boolean withPublications, Boolean withSignedUrls) throws IndexServiceException, SearchIndexException, NotFoundException { List<JValue> eventsList = new ArrayList<>(); for (IndexObject item : events) { eventsList.add(eventToJSON((Event) item, withAcl, withMetadata, withPublications, withSignedUrls)); } return ApiResponses.Json.ok(ApiVersion.VERSION_1_0_0, arr(eventsList)); } private void extendEventsStatusOverview(List<Field> fields, Series series) throws SearchIndexException { EventSearchQuery query = new EventSearchQuery(getSecurityService().getOrganization().getId(), getSecurityService().getUser()).withoutActions().withSeriesId(series.getIdentifier()); SearchResult<Event> result = externalIndex.getByQuery(query); // collect recording statuses int blacklisted = 0; int optOut = 0; int ready = 0; for (SearchResultItem<Event> item : result.getItems()) { Event event = item.getSource(); if (event.getSchedulingStatus() == null) continue; SchedulingStatus schedulingStatus = SchedulingStatus.valueOf(event.getSchedulingStatus()); if (SchedulingStatus.BLACKLISTED.equals(schedulingStatus)) { blacklisted++; } else if (series.isOptedOut() || SchedulingStatus.OPTED_OUT.equals(schedulingStatus)) { optOut++; } else { ready++; } } fields.add(f("events", obj(f("BLACKLISTED", v(blacklisted)), f("OPTED_OUT", v(optOut)), f("READY", v(ready))))); } /** * 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 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 SearchIndexException * Thrown if unable to get event publications from search service * @throws NotFoundException * Thrown if unable to find all of the metadata */ protected JValue eventToJSON(Event event, Boolean withAcl, Boolean withMetadata, Boolean withPublications, Boolean withSignedUrls) throws IndexServiceException, SearchIndexException, NotFoundException { List<Field> 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)))); List<JValue> 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))); fields.add(f("start", v(event.getTechnicalStartTime(), BLANK))); if (event.getTechnicalEndTime() != null) { long duration = new DateTime(event.getTechnicalStartTime()).getMillis() - new DateTime(event.getTechnicalEndTime()).getMillis(); fields.add(f("duration", v(duration))); } 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<MetadataList> metadata = getEventMetadata(event); if (metadata.isSome()) { fields.add(f("metadata", metadata.get().toJSON())); } } catch (Exception e) { logger.error("Unable to get metadata for event '{}' because: {}", event.getIdentifier(), ExceptionUtils.getStackTrace(e)); throw new IndexServiceException("Unable to add metadata to event", e); } } if (withPublications != null && withPublications) { List<JValue> publications = getPublications(event.getIdentifier(), withSignedUrls); fields.add(f("publications", arr(publications))); } return obj(fields); } private List<JValue> splitSubjectIntoArray(final String subject) { return com.entwinemedia.fn.Stream.$(subject.split(",")).map(new Fn<String, JValue>() { @Override public JValue apply(String a) { return v(a.trim()); } }).toList(); } @GET @Path("{eventId}/acl") @Produces({ "application/json", "application/v1.0.0+json" }) @RestQuery(name = "geteventacl", description = "Returns an event's access policy.", returnDescription = "", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, reponses = { @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, externalIndex)) { AccessControlList acl = getAclFromEvent(event); return ApiResponses.Json.ok(ApiVersion.VERSION_1_0_0, arr(AclUtils.serializeAclToJson(acl))); } return ApiResponses.notFound("Cannot find an event with id '%s'.", id); } @PUT @Path("{eventId}/acl") @Produces({ "application/json", "application/v1.0.0+json" }) @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) }, reponses = { @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, externalIndex).isSome()) { AccessControlList accessControlList; try { accessControlList = AclUtils.deserializeJsonToAcl(acl, false); } catch (ParseException e) { logger.debug("Unable to update event acl to '{}' because: {}", acl, ExceptionUtils.getStackTrace(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 '{}' because: {}", acl, ExceptionUtils.getStackTrace(e)); return R.badRequest(e.getMessage()); } try { accessControlList = indexService.updateEventAcl(id, accessControlList, externalIndex); } catch (IllegalArgumentException e) { logger.error("Unable to update event '{}' acl with '{}' because: {}", new Object[] { id, acl, ExceptionUtils.getStackTrace(e) }); return Response.status(Status.FORBIDDEN).build(); } return ApiResponses.Json.noContent(ApiVersion.VERSION_1_0_0); } else { return ApiResponses.notFound("Cannot find an event with id '%s'.", id); } } @POST @Path("{eventId}/acl/{action}") @Produces({ "application/json", "application/v1.0.0+json" }) @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) }, reponses = { @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<AccessControlEntry> entries = new ArrayList<>(); for (final Event event : indexService.getEvent(id, externalIndex)) { 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, externalIndex); } catch (IllegalArgumentException e) { logger.error("Unable to update event '{}' acl entry with action '{}' and role '{}' because: {}", new Object[] { id, action, role, ExceptionUtils.getStackTrace(e) }); return Response.status(Status.FORBIDDEN).build(); } return ApiResponses.Json.noContent(ApiVersion.VERSION_1_0_0); } return ApiResponses.notFound("Cannot find an event with id '%s'.", id); } @DELETE @Path("{eventId}/acl/{action}/{role}") @Produces({ "application/json", "application/v1.0.0+json" }) @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) }, reponses = { @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<AccessControlEntry> entries = new ArrayList<>(); for (final Event event : indexService.getEvent(id, externalIndex)) { 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, externalIndex); } catch (IllegalArgumentException e) { logger.error("Unable to delete event's '{}' acl entry with action '{}' and role '{}' because: {}", new Object[] { id, action, role, ExceptionUtils.getStackTrace(e) }); return Response.status(Status.FORBIDDEN).build(); } return ApiResponses.Json.noContent(ApiVersion.VERSION_1_0_0); } return ApiResponses.notFound("Cannot find an event with id '%s'.", id); } @GET @Path("{eventId}/metadata") @Produces({ "application/json", "application/v1.0.0+json" }) @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) }, reponses = { @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 { if (StringUtils.trimToNull(type) == null) { Opt<MetadataList> metadataList = getEventMetadataById(id); if (metadataList.isSome()) return ApiResponses.Json.ok(ApiVersion.VERSION_1_0_0, metadataList.get().toJSON()); else return ApiResponses.notFound("Cannot find an event with id '%s'.", id); } else { return getEventMetadataByType(id, type); } } protected Opt<MetadataList> getEventMetadataById(String id) throws IndexServiceException, Exception { for (final Event event : indexService.getEvent(id, externalIndex)) { return getEventMetadata(event); } return Opt.<MetadataList> none(); } protected Opt<MetadataList> getEventMetadata(Event event) throws IndexServiceException, Exception { MetadataList metadataList = new MetadataList(); List<EventCatalogUIAdapter> catalogUIAdapters = getEventCatalogUIAdapters(); catalogUIAdapters.remove(this.eventCatalogUIAdapter); Opt<MediaPackage> optMediaPackage = indexService.getEventMediapackage(event); if (catalogUIAdapters.size() > 0 && optMediaPackage.isSome()) { for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) { // TODO: This is very slow: metadataList.add(catalogUIAdapter, catalogUIAdapter.getFields(optMediaPackage.get())); } } // TODO: This is slow: MetadataCollection collection = EventUtils.getEventMetadata(event, eventCatalogUIAdapter); 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<MediaPackageElementFlavor> 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) throws IndexServiceException, Exception { for (final Event event : indexService.getEvent(id, externalIndex)) { Opt<MediaPackageElementFlavor> 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. if (flavor.get().equals(eventCatalogUIAdapter.getFlavor())) { MetadataCollection collection = EventUtils.getEventMetadata(event, eventCatalogUIAdapter); ExternalMetadataUtils.changeSubjectToSubjects(collection); ExternalMetadataUtils.removeCollectionList(collection); return ApiResponses.Json.ok(ApiVersion.VERSION_1_0_0, collection.toJSON()); } // Try the other catalogs List<EventCatalogUIAdapter> catalogUIAdapters = getEventCatalogUIAdapters(); catalogUIAdapters.remove(eventCatalogUIAdapter); Opt<MediaPackage> optMediaPackage = indexService.getEventMediapackage(event); if (catalogUIAdapters.size() > 0 && optMediaPackage.isSome()) { for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) { if (flavor.get().equals(catalogUIAdapter.getFlavor())) { MetadataCollection fields = catalogUIAdapter.getFields(optMediaPackage.get()); ExternalMetadataUtils.removeCollectionList(fields); return ApiResponses.Json.ok(ApiVersion.VERSION_1_0_0, fields.toJSON()); } } } 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") @Produces({ "application/json", "application/v1.0.0+json" }) @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) }, reponses = { @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<String, String> updatedFields; try { updatedFields = RequestUtils.getKeyValueMap(metadataJSON); } catch (ParseException e) { logger.debug("Unable to update event '{}' with metadata type '{}' and content '{}' because: {}", new Object[] { id, type, metadataJSON, ExceptionUtils.getStackTrace(e) }); return RestUtil.R.badRequest(String.format("Unable to parse metadata fields as json from '%s' because '%s'", metadataJSON, ExceptionUtils.getStackTrace(e))); } catch (IllegalArgumentException e) { logger.debug("Unable to update event '{}' with metadata type '{}' and content '{}' because: {}", new Object[] { id, type, metadataJSON, ExceptionUtils.getStackTrace(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<MediaPackageElementFlavor> 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)); } MetadataCollection collection = null; EventCatalogUIAdapter adapter = null; for (final Event event : indexService.getEvent(id, externalIndex)) { MetadataList metadataList = new MetadataList(); // Try the main catalog first as we load it from the index. 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<EventCatalogUIAdapter> catalogUIAdapters = getEventCatalogUIAdapters(); catalogUIAdapters.remove(eventCatalogUIAdapter); Opt<MediaPackage> optMediaPackage = indexService.getEventMediapackage(event); if (catalogUIAdapters.size() > 0 && optMediaPackage.isSome()) { for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) { if (flavor.get().equals(catalogUIAdapter.getFlavor())) { collection = catalogUIAdapter.getFields(optMediaPackage.get()); adapter = eventCatalogUIAdapter; } else { metadataList.add(catalogUIAdapter, catalogUIAdapter.getFields(optMediaPackage.get())); } } } 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()); if (field == null) { return 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 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)); } collection.removeField(field); JSONArray subjectArray = (JSONArray) parser.parse(updatedFields.get(key)); collection.addField( MetadataField.copyMetadataFieldWithValue(field, StringUtils.join(subjectArray.iterator(), ","))); } else { MetadataField<?> field = collection.getOutputFields().get(key); if (field == null) { return 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 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)); } collection.removeField(field); collection.addField(MetadataField.copyMetadataFieldWithValue(field, updatedFields.get(key))); } } metadataList.add(adapter, collection); indexService.updateEventMetadata(id, metadataList, externalIndex); return ApiResponses.Json.noContent(ApiVersion.VERSION_1_0_0); } return ApiResponses.notFound("Cannot find an event with id '%s'.", id); } @DELETE @Path("{eventId}/metadata") @Produces({ "application/json", "application/v1.0.0+json" }) @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) }, reponses = { @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, externalIndex)) { Opt<MediaPackageElementFlavor> 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)); } 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 '{}' because {}", new Object[] { type, id, ExceptionUtils.getStackTrace(e) }); throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); } catch (IllegalStateException e) { logger.debug("Unable to remove metadata catalog with type '{}' from event '{}' because {}", new Object[] { type, id, ExceptionUtils.getStackTrace(e) }); throw new WebApplicationException(e, Status.BAD_REQUEST); } catch (UnauthorizedException e) { return Response.status(Status.UNAUTHORIZED).build(); } return ApiResponses.Json.noContent(ApiVersion.VERSION_1_0_0); } return ApiResponses.notFound("Cannot find an event with id '%s'.", id); } @GET @Path("{eventId}/publications") @Produces({ "application/json", "application/v1.0.0+json" }) @RestQuery(name = "geteventpublications", description = "Returns an event's list of publications.", returnDescription = "", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = STRING) }, reponses = { @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) throws Exception { try { return ApiResponses.Json.ok(ApiVersion.VERSION_1_0_0, arr(getPublications(id, sign))); } catch (NotFoundException e) { return ApiResponses.notFound(e.getMessage()); } catch (SearchIndexException e) { logger.error("Unable to get list of publications from event with id '{}' because {}", id, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); } } private final Fn2<Publication, Boolean, JObject> publicationToJson = new Fn2<Publication, Boolean, JObject>() { @Override public JObject apply(Publication publication, Boolean sign) { String url = publication.getURI() == null ? "" : publication.getURI().toString(); return obj(f("id", v(publication.getIdentifier())), f("channel", v(publication.getChannel())), f("mediatype", v(publication.getMimeType(), BLANK)), f("url", v(url)), f("media", arr(getPublicationTracksJson(publication, sign))), f("attachments", arr(getPublicationAttachmentsJson(publication, sign))), f("metadata", arr(getPublicationCatalogsJson(publication, sign)))); } private String getMediaPackageElementUri(MediaPackageElement element, Boolean sign) { String elementUri; if (sign) { elementUri = getSignedUri(element.getURI()); } else { elementUri = element.getURI() == null ? null : element.getURI().toString(); } return elementUri; } private String getSignedUri(URI uri) { if (uri == null) { return null; } String location = uri.toString(); if (urlSigningService.accepts(location)) { try { location = urlSigningService.sign(location, expireSeconds, null, null); } catch (UrlSigningException e) { logger.error("Unable to sign URI {} because: {}", uri, ExceptionUtils.getStackTrace(e)); return uri.toString(); } } return location; } private List<JValue> getPublicationTracksJson(Publication publication, Boolean sign) { List<JValue> tracks = new ArrayList<>(); for (Track track : publication.getTracks()) { VideoStream[] videoStreams = TrackSupport.byType(track.getStreams(), VideoStream.class); List<Field> trackInfo = new ArrayList<>(); if (videoStreams.length > 0) { // Only supporting one stream, like in many other places... final Stream stream = videoStreams[0]; if (stream instanceof VideoStream) { final VideoStream videoStream = (VideoStream) stream; 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()))); } } tracks.add(obj(f("id", v(track.getIdentifier(), BLANK)), f("mediatype", v(track.getMimeType(), BLANK)), f("url", v(getMediaPackageElementUri(track, 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())), f("description", v(track.getDescription(), BLANK))).merge(trackInfo)); } return tracks; } private List<JValue> getPublicationAttachmentsJson(Publication publication, Boolean sign) { List<JValue> 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(getMediaPackageElementUri(attachment, 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<JValue> getPublicationCatalogsJson(Publication publication, Boolean sign) { List<JValue> 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(getMediaPackageElementUri(catalog, 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; } }; private List<JValue> getPublications(String id, Boolean withSignedUrls) throws NotFoundException, SearchIndexException { for (final Event event : indexService.getEvent(id, externalIndex)) { List<JValue> pubJSON = new ArrayList<>(); pubJSON = new ArrayList<JValue>($(event.getPublications()).filter(EventUtils.internalChannelFilter) .map(publicationToJson._2(withSignedUrls)).toList()); return pubJSON; } throw new NotFoundException(String.format("Unable to find event with id '%s'", id)); } @GET @Path("{eventId}/publications/{publicationId}") @Produces({ "application/json", "application/v1.0.0+json" }) @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) }, reponses = { @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 { return ApiResponses.Json.ok(ApiVersion.VERSION_1_0_0, getPublication(eventId, publicationId, sign)); } catch (NotFoundException e) { return ApiResponses.notFound(e.getMessage()); } catch (SearchIndexException e) { logger.error("Unable to get list of publications from event with id '{}' because {}", eventId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); } } private JObject getPublication(String eventId, String publicationId, Boolean withSignedUrls) throws SearchIndexException, NotFoundException { for (final Event event : indexService.getEvent(eventId, externalIndex)) { List<Publication> publications = $(event.getPublications()).filter(EventUtils.internalChannelFilter).toList(); for (Publication publication : publications) { if (publicationId.equals(publication.getIdentifier())) { return $(publication).map(publicationToJson._2(withSignedUrls)).head2(); } } 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)); } /** * Change the simplified fields of key values provided to the external api into a {@link MetadataList}. * * @param json * The json string that contains an array of metadata field lists for the different catalogs. * @return A {@link MetadataList} with the fields populated with the values provided. * @throws ParseException * Thrown if unable to parse the json string. * @throws NotFoundException * Thrown if unable to find the catalog or field that the json refers to. */ protected MetadataList deserializeMetadataList(String json) throws ParseException, NotFoundException { MetadataList metadataList = new MetadataList(); JSONArray jsonCatalogs = (JSONArray) parser.parse(json); for (int i = 0; i < jsonCatalogs.size(); i++) { JSONObject catalog = (JSONObject) jsonCatalogs.get(i); String flavorString = catalog.get("flavor").toString(); if (StringUtils.trimToNull(flavorString) == null) { throw new IllegalArgumentException( "Unable to create new event as no flavor was given for one of the metadata collections"); } MediaPackageElementFlavor flavor = MediaPackageElementFlavor.parseFlavor(flavorString); MetadataCollection collection = null; EventCatalogUIAdapter adapter = null; for (EventCatalogUIAdapter eventCatalogUIAdapter : getEventCatalogUIAdapters()) { if (eventCatalogUIAdapter.getFlavor().equals(flavor)) { adapter = eventCatalogUIAdapter; collection = eventCatalogUIAdapter.getRawFields(); } } if (collection == null) { throw new IllegalArgumentException( String.format("Unable to find an EventCatalogUIAdapter with Flavor '%s'", flavorString)); } String fieldsJson = catalog.get("fields").toString(); if (StringUtils.trimToNull(fieldsJson) != null) { Map<String, String> fields = RequestUtils.getKeyValueMap(fieldsJson); for (String key : fields.keySet()) { MetadataField<?> field = collection.getOutputFields().get(key); if (field == null) { throw new NotFoundException(String.format( "Cannot find a metadata field with id '%s' from Catalog with Flavor '%s'.", key, flavorString)); } collection.removeField(field); collection.addField(MetadataField.copyMetadataFieldWithValue(field, fields.get(key))); } } metadataList.add(adapter, collection); } return metadataList; } /** * 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 because: {}", ExceptionUtils.getStackTrace(e)); } return activeAcl; } private JValue getJsonStream(Stream stream) { List<Field> 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(serverUrl, serviceUrl, eventId); } }