/** * 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.adminui.endpoint; import static com.entwinemedia.fn.Stream.$; import static com.entwinemedia.fn.data.Opt.nul; 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 java.lang.String.format; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; import static org.apache.commons.lang3.StringUtils.trimToNull; import static org.opencastproject.index.service.util.RestUtils.conflictJson; import static org.opencastproject.index.service.util.RestUtils.notFound; import static org.opencastproject.index.service.util.RestUtils.okJson; import static org.opencastproject.index.service.util.RestUtils.okJsonList; import static org.opencastproject.util.DateTimeSupport.toUTC; import static org.opencastproject.util.RestUtil.R.badRequest; import static org.opencastproject.util.RestUtil.R.forbidden; import static org.opencastproject.util.RestUtil.R.notFound; import static org.opencastproject.util.RestUtil.R.ok; import static org.opencastproject.util.RestUtil.R.serverError; import static org.opencastproject.util.doc.rest.RestParameter.Type.BOOLEAN; import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING; import static org.opencastproject.util.doc.rest.RestParameter.Type.TEXT; import org.opencastproject.adminui.exception.JobEndpointException; import org.opencastproject.adminui.impl.AdminUIConfiguration; import org.opencastproject.adminui.impl.index.AdminUISearchIndex; import org.opencastproject.adminui.util.QueryPreprocessor; import org.opencastproject.authorization.xacml.manager.api.AclService; import org.opencastproject.authorization.xacml.manager.api.AclServiceException; import org.opencastproject.authorization.xacml.manager.api.EpisodeACLTransition; import org.opencastproject.authorization.xacml.manager.api.ManagedAcl; import org.opencastproject.authorization.xacml.manager.api.TransitionQuery; import org.opencastproject.capture.admin.api.Agent; import org.opencastproject.capture.admin.api.CaptureAgentStateService; import org.opencastproject.capture.admin.api.Recording; import org.opencastproject.event.comment.EventComment; import org.opencastproject.event.comment.EventCommentException; import org.opencastproject.event.comment.EventCommentReply; import org.opencastproject.event.comment.EventCommentService; import org.opencastproject.index.service.api.IndexService; import org.opencastproject.index.service.api.IndexService.Source; import org.opencastproject.index.service.catalog.adapter.MetadataList; import org.opencastproject.index.service.catalog.adapter.MetadataList.Locked; import org.opencastproject.index.service.exception.IndexServiceException; import org.opencastproject.index.service.impl.index.event.Event; 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.resources.list.provider.EventCommentsListProvider; import org.opencastproject.index.service.resources.list.provider.EventsListProvider.Comments; import org.opencastproject.index.service.resources.list.query.EventListQuery; import org.opencastproject.index.service.util.AccessInformationUtil; import org.opencastproject.index.service.util.RestUtils; 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.Publication; import org.opencastproject.mediapackage.Track; import org.opencastproject.mediapackage.VideoStream; import org.opencastproject.mediapackage.track.AudioStreamImpl; import org.opencastproject.mediapackage.track.VideoStreamImpl; import org.opencastproject.metadata.dublincore.DCMIPeriod; import org.opencastproject.metadata.dublincore.DublinCore; import org.opencastproject.metadata.dublincore.DublinCoreCatalog; import org.opencastproject.metadata.dublincore.DublinCoreCatalogList; import org.opencastproject.metadata.dublincore.EncodingSchemeUtils; import org.opencastproject.metadata.dublincore.EventCatalogUIAdapter; import org.opencastproject.metadata.dublincore.MetadataCollection; import org.opencastproject.rest.BulkOperationResult; import org.opencastproject.rest.RestConstants; import org.opencastproject.scheduler.api.SchedulerException; import org.opencastproject.scheduler.api.SchedulerService; import org.opencastproject.security.api.AccessControlList; import org.opencastproject.security.api.AccessControlParser; import org.opencastproject.security.api.AclScope; import org.opencastproject.security.api.AuthorizationService; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.UnauthorizedException; import org.opencastproject.security.api.User; import org.opencastproject.security.urlsigning.exception.UrlSigningException; import org.opencastproject.security.urlsigning.service.UrlSigningService; import org.opencastproject.systems.MatterhornConstants; import org.opencastproject.util.DateTimeSupport; import org.opencastproject.util.Jsons.Val; import org.opencastproject.util.NotFoundException; import org.opencastproject.util.RestUtil; 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.RestQuery; import org.opencastproject.util.doc.rest.RestResponse; import org.opencastproject.util.doc.rest.RestService; import org.opencastproject.workflow.api.ConfiguredWorkflowRef; import org.opencastproject.workflow.api.WorkflowDatabaseException; import org.opencastproject.workflow.api.WorkflowDefinition; import org.opencastproject.workflow.api.WorkflowInstance; import org.opencastproject.workflow.api.WorkflowQuery; import org.opencastproject.workflow.api.WorkflowService; import org.opencastproject.workflow.api.WorkflowUtil; import com.entwinemedia.fn.Fn; import com.entwinemedia.fn.Stream; 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 net.fortuna.ical4j.model.property.RRule; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.codehaus.jettison.json.JSONException; 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.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; import java.text.ParseException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimeZone; 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.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; /** * The event endpoint acts as a facade for WorkflowService and Archive providing a unified query interface and result * set. * <p> * This first implementation uses the {@link org.opencastproject.assetmanager.api.AssetManager}. In a later iteration * the endpoint may abstract over the concrete archive. */ @Path("/") @RestService(name = "eventservice", title = "Event Service", abstractText = "Provides resources and operations related to the events", notes = { "This service offers the event CRUD Operations for the admin UI.", "<strong>Important:</strong> " + "<em>This service is for exclusive use by the module matterhorn-admin-ui-ng. Its API might change " + "anytime without prior notice. Any dependencies other than the admin UI will be strictly ignored. " + "DO NOT use this for integration of third-party applications.<em>"}) public abstract class AbstractEventEndpoint { /** The logging facility */ static final Logger logger = LoggerFactory.getLogger(AbstractEventEndpoint.class); 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; public abstract WorkflowService getWorkflowService(); public abstract AdminUISearchIndex getIndex(); public abstract JobEndpoint getJobService(); public abstract AclService getAclService(); public abstract EventCommentService getEventCommentService(); public abstract SecurityService getSecurityService(); public abstract IndexService getIndexService(); public abstract AuthorizationService getAuthorizationService(); public abstract SchedulerService getSchedulerService(); public abstract CaptureAgentStateService getCaptureAgentStateService(); public abstract AdminUIConfiguration getAdminUIConfiguration(); public abstract long getUrlSigningExpireDuration(); public abstract UrlSigningService getUrlSigningService(); public abstract Boolean signWithClientIP(); /** Default server URL */ protected String serverUrl = "http://localhost:8080"; /** Service url */ protected String serviceUrl = null; /** A parser for handling JSON documents inside the body of a request. **/ private final JSONParser parser = new JSONParser(); /** * Activates REST service. * * @param cc * ComponentContext */ public void activate(ComponentContext cc) { if (cc != null) { String ccServerUrl = cc.getBundleContext().getProperty(MatterhornConstants.SERVER_URL_PROPERTY); if (StringUtils.isNotBlank(ccServerUrl)) this.serverUrl = ccServerUrl; } serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY); } @GET @Path("catalogAdapters") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getcataloguiadapters", description = "Returns the available catalog UI adapters as JSON", returnDescription = "The catalog UI adapters as JSON", reponses = { @RestResponse(description = "Returns the available catalog UI adapters as JSON", responseCode = HttpServletResponse.SC_OK) }) public Response getCatalogAdapters() { List<JValue> adapters = new ArrayList<>(); for (EventCatalogUIAdapter adapter : getIndexService().getEventCatalogUIAdapters()) { List<Field> fields = new ArrayList<>(); fields.add(f("flavor", v(adapter.getFlavor().toString()))); fields.add(f("title", v(adapter.getUITitle()))); adapters.add(obj(fields)); } return okJson(arr(adapters)); } @GET @Path("{eventId}") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getevent", description = "Returns the event by the given id as JSON", returnDescription = "The event as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns the event as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getEventResponse(@PathParam("eventId") String id) throws Exception { for (final Event event : getIndexService().getEvent(id, getIndex())) { event.updatePreview(getAdminUIConfiguration().getPreviewSubtype()); return okJson(eventToJSON(event)); } return notFound("Cannot find an event with id '%s'.", id); } @DELETE @Path("{eventId}") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "deleteevent", description = "Delete a single event.", returnDescription = "Ok if the event has been deleted.", pathParameters = { @RestParameter(name = "eventId", isRequired = true, description = "The id of the event to delete.", type = STRING), }, reponses = { @RestResponse(responseCode = SC_OK, description = "The event has been deleted."), @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "The event could not be found."), @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action") }) public Response deleteEvent(@PathParam("eventId") String id) throws NotFoundException, UnauthorizedException { if (!getIndexService().removeEvent(id)) return Response.serverError().build(); return Response.ok().build(); } @POST @Path("deleteEvents") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "deleteevents", description = "Deletes a json list of events by their given ids e.g. [\"1dbe7255-e17d-4279-811d-a5c7ced689bf\", \"04fae22b-0717-4f59-8b72-5f824f76d529\"]", returnDescription = "Returns a JSON object containing a list of event ids that were deleted, not found or if there was a server error.", reponses = { @RestResponse(description = "Events have been deleted", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "The list of ids could not be parsed into a json list.", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "If the current user is not authorized to perform this action", responseCode = HttpServletResponse.SC_UNAUTHORIZED) }) public Response deleteEvents(String eventIdsContent) throws UnauthorizedException { if (StringUtils.isBlank(eventIdsContent)) { return Response.status(Response.Status.BAD_REQUEST).build(); } JSONArray eventIdsJsonArray; try { eventIdsJsonArray = (JSONArray) parser.parse(eventIdsContent); } catch (org.json.simple.parser.ParseException e) { logger.error("Unable to parse '{}' because: {}", eventIdsContent, ExceptionUtils.getStackTrace(e)); return Response.status(Response.Status.BAD_REQUEST).build(); } catch (ClassCastException e) { logger.error("Unable to cast '{}' because: {}", eventIdsContent, ExceptionUtils.getStackTrace(e)); return Response.status(Response.Status.BAD_REQUEST).build(); } BulkOperationResult result = new BulkOperationResult(); for (Object eventIdObject : eventIdsJsonArray) { String eventId = eventIdObject.toString(); try { if (!getIndexService().removeEvent(eventId)) { result.addServerError(eventId); } else { result.addOk(eventId); } } catch (NotFoundException e) { result.addNotFound(eventId); } } return Response.ok(result.toJson()).build(); } @GET @Path("{eventId}/general.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "geteventgeneral", description = "Returns all the data related to the general tab in the event details modal as JSON", returnDescription = "All the data related to the event general tab as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id (mediapackage id).", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns all the data related to the event general tab as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getEventGeneralTab(@PathParam("eventId") String id) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(id, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", id); // Quick actions have been temporally removed from the general tab // --------------------------------------------------------------- // List<JValue> actions = new ArrayList<JValue>(); // List<WorkflowDefinition> workflowsDefinitions = getWorkflowService().listAvailableWorkflowDefinitions(); // for (WorkflowDefinition wflDef : workflowsDefinitions) { // if (wflDef.containsTag(WORKFLOWDEF_TAG)) { // // actions.add(obj(f("id", v(wflDef.getId())), f("title", v(Opt.nul(wflDef.getTitle()).or(""))), // f("description", v(Opt.nul(wflDef.getDescription()).or(""))), // f("configuration_panel", v(Opt.nul(wflDef.getConfigurationPanel()).or(""))))); // } // } Event event = optEvent.get(); List<JValue> pubJSON = eventPublicationsToJson(event); return okJson(obj(f("publications", arr(pubJSON)), f("optout", v(event.getOptedOut(), Jsons.BLANK)), f("blacklisted", v(event.getBlacklisted(), Jsons.BLANK)), f("review-status", v(event.getReviewStatus(), Jsons.BLANK)))); } private List<JValue> eventPublicationsToJson(Event event) { List<JValue> pubJSON = new ArrayList<>(); for (JObject json : Stream.$(event.getPublications()).filter(EventUtils.internalChannelFilter) .map(publicationToJson)) { pubJSON.add(json); } return pubJSON; } private Event getEventOrThrowNotFoundException(final String eventId) throws NotFoundException, SearchIndexException { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isSome()) { return optEvent.get(); } else { throw new NotFoundException(format("Cannot find an event with id '%s'.", eventId)); } } @GET @Path("{eventId}/comments") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "geteventcomments", description = "Returns all the data related to the comments tab in the event details modal as JSON", returnDescription = "All the data related to the event comments tab as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns all the data related to the event comments tab as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getEventComments(@PathParam("eventId") String eventId) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); try { List<EventComment> comments = getEventCommentService().getComments(eventId); List<Val> commentArr = new ArrayList<>(); for (EventComment c : comments) { commentArr.add(c.toJson()); } return Response.ok(org.opencastproject.util.Jsons.arr(commentArr).toJson(), MediaType.APPLICATION_JSON_TYPE) .build(); } catch (EventCommentException e) { logger.error("Unable to get comments from event {}: {}", eventId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @GET @Path("{eventId}/hasActiveTransaction") @Produces(MediaType.TEXT_PLAIN) @RestQuery(name = "hasactivetransaction", description = "Returns whether there is currently a transaction in progress for the given event", returnDescription = "Whether there is currently a transaction in progress for the given event", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns whether there is currently a transaction in progress for the given event", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response hasActiveTransaction(@PathParam("eventId") String eventId) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); JSONObject json = new JSONObject(); if (WorkflowInstance.WorkflowState.RUNNING.toString().equals(optEvent.get().getWorkflowState())) { json.put("active", true); } else { json.put("active", false); } return Response.ok(json.toJSONString()).build(); } @GET @Produces(MediaType.APPLICATION_JSON) @Path("{eventId}/comment/{commentId}") @RestQuery(name = "geteventcomment", description = "Returns the comment with the given identifier", returnDescription = "Returns the comment as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The comment as JSON."), @RestResponse(responseCode = SC_NOT_FOUND, description = "No event or comment with this identifier was found.") }) public Response getEventComment(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId) throws NotFoundException, Exception { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); try { EventComment comment = getEventCommentService().getComment(commentId); return Response.ok(comment.toJson().toJson()).build(); } catch (NotFoundException e) { throw e; } catch (Exception e) { logger.error("Could not retrieve comment {}: {}", commentId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @PUT @Path("{eventId}/comment/{commentId}") @RestQuery(name = "updateeventcomment", description = "Updates an event comment", returnDescription = "The updated comment as JSON.", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING) }, restParameters = { @RestParameter(name = "text", isRequired = false, description = "The comment text", type = TEXT), @RestParameter(name = "reason", isRequired = false, description = "The comment reason", type = STRING), @RestParameter(name = "resolved", isRequired = false, description = "The comment resolved status", type = RestParameter.Type.BOOLEAN) }, reponses = { @RestResponse(responseCode = SC_NOT_FOUND, description = "The event or comment to update has not been found."), @RestResponse(responseCode = SC_OK, description = "The updated comment as JSON.") }) public Response updateEventComment(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId, @FormParam("text") String text, @FormParam("reason") String reason, @FormParam("resolved") Boolean resolved) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); try { EventComment dto = getEventCommentService().getComment(commentId); if (StringUtils.isNotBlank(text)) { text = text.trim(); } else { text = dto.getText(); } if (StringUtils.isNotBlank(reason)) { reason = reason.trim(); } else { reason = dto.getReason(); } if (resolved == null) resolved = dto.isResolvedStatus(); EventComment updatedComment = EventComment.create(dto.getId(), eventId, getSecurityService().getOrganization().getId(), text, dto.getAuthor(), reason, resolved, dto.getCreationDate(), new Date(), dto.getReplies()); updatedComment = getEventCommentService().updateComment(updatedComment); List<EventComment> comments = getEventCommentService().getComments(eventId); getIndexService().updateCommentCatalog(optEvent.get(), comments); return Response.ok(updatedComment.toJson().toJson()).build(); } catch (NotFoundException e) { throw e; } catch (Exception e) { logger.error("Unable to update the comments catalog on event {}: {}", eventId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @POST @Path("{eventId}/access") @RestQuery(name = "applyAclToEvent", description = "Immediate application of an ACL to an event", returnDescription = "Status code", pathParameters = { @RestParameter(name = "eventId", isRequired = true, description = "The event ID", type = STRING) }, restParameters = { @RestParameter(name = "acl", isRequired = true, description = "The ACL to apply", type = STRING) }, reponses = { @RestResponse(responseCode = SC_OK, description = "The ACL has been successfully applied"), @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the given ACL"), @RestResponse(responseCode = SC_NOT_FOUND, description = "The the event has not been found"), @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action"), @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Internal error") }) public Response applyAclToEvent(@PathParam("eventId") String eventId, @FormParam("acl") String acl) throws NotFoundException, UnauthorizedException, SearchIndexException, IndexServiceException { final AccessControlList accessControlList; try { accessControlList = AccessControlParser.parseAcl(acl); } catch (Exception e) { logger.warn("Unable to parse ACL '{}'", acl); return badRequest(); } try { final Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) { logger.warn("Unable to find the event '{}'", eventId); return notFound(); } Source eventSource = getIndexService().getEventSource(optEvent.get()); if (eventSource == Source.ARCHIVE) { if (getAclService().applyAclToEpisode(eventId, accessControlList, Option.<ConfiguredWorkflowRef> none())) { return ok(); } else { logger.warn("Unable to find the event '{}'", eventId); return notFound(); } } else if (eventSource == Source.WORKFLOW) { logger.warn("An ACL cannot be edited while an event is part of a current workflow because it might" + " lead to inconsistent ACLs i.e. changed after distribution so that the old ACL is still " + "being used by the distribution channel."); return forbidden("Unable to edit an ACL for a current workflow."); } else { // The event doesn't exist as a mediapackage yet so use the scheduler service to update the ACL getSchedulerService().updateAccessControlList(getSchedulerService().getEventId(eventId), accessControlList); return ok(); } } catch (AclServiceException e) { logger.error("Error applying acl '{}' to event '{}' because: {}", new Object[] { accessControlList, eventId, ExceptionUtils.getStackTrace(e) }); return serverError(); } catch (SchedulerException e) { logger.error("Error applying ACL to scheduled event {} because {}", eventId, ExceptionUtils.getStackTrace(e)); return serverError(); } } @POST @Path("{eventId}/comment") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "createeventcomment", description = "Creates a comment related to the event given by the identifier", returnDescription = "The comment related to the event as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, restParameters = { @RestParameter(name = "text", isRequired = true, description = "The comment text", type = TEXT), @RestParameter(name = "resolved", isRequired = false, description = "The comment resolved status", type = RestParameter.Type.BOOLEAN), @RestParameter(name = "reason", isRequired = false, description = "The comment reason", type = STRING) }, reponses = { @RestResponse(description = "The comment has been created.", responseCode = HttpServletResponse.SC_CREATED), @RestResponse(description = "If no text ist set.", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response createEventComment(@PathParam("eventId") String eventId, @FormParam("text") String text, @FormParam("reason") String reason, @FormParam("resolved") Boolean resolved) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); if (StringUtils.isBlank(text)) return Response.status(Status.BAD_REQUEST).build(); User author = getSecurityService().getUser(); try { EventComment createdComment = EventComment.create(Option.<Long> none(), eventId, getSecurityService().getOrganization().getId(), text, author, reason, BooleanUtils.toBoolean(reason)); createdComment = getEventCommentService().updateComment(createdComment); List<EventComment> comments = getEventCommentService().getComments(eventId); getIndexService().updateCommentCatalog(optEvent.get(), comments); return Response.created(getCommentUrl(eventId, createdComment.getId().get())) .entity(createdComment.toJson().toJson()).build(); } catch (Exception e) { logger.error("Unable to create a comment on the event {}: {}", eventId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @POST @Path("{eventId}/comment/{commentId}") @RestQuery(name = "resolveeventcomment", description = "Resolves an event comment", returnDescription = "The resolved comment.", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING) }, reponses = { @RestResponse(responseCode = SC_NOT_FOUND, description = "The event or comment to resolve has not been found."), @RestResponse(responseCode = SC_OK, description = "The resolved comment as JSON.") }) public Response resolveEventComment(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); try { EventComment dto = getEventCommentService().getComment(commentId); EventComment updatedComment = EventComment.create(dto.getId(), dto.getEventId(), dto.getOrganization(), dto.getText(), dto.getAuthor(), dto.getReason(), true, dto.getCreationDate(), new Date(), dto.getReplies()); updatedComment = getEventCommentService().updateComment(updatedComment); List<EventComment> comments = getEventCommentService().getComments(eventId); getIndexService().updateCommentCatalog(optEvent.get(), comments); return Response.ok(updatedComment.toJson().toJson()).build(); } catch (NotFoundException e) { throw e; } catch (Exception e) { logger.error("Could not resolve comment {}: {}", commentId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @DELETE @Path("{eventId}/comment/{commentId}") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "deleteeventcomment", description = "Deletes a event related comment by its identifier", returnDescription = "No content", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "commentId", description = "The comment id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "The event related comment has been deleted.", responseCode = HttpServletResponse.SC_NO_CONTENT), @RestResponse(description = "No event or comment with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response deleteEventComment(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); try { getEventCommentService().deleteComment(commentId); List<EventComment> comments = getEventCommentService().getComments(eventId); getIndexService().updateCommentCatalog(optEvent.get(), comments); return Response.noContent().build(); } catch (NotFoundException e) { throw e; } catch (Exception e) { logger.error("Unable to delete comment {} on event {}: {}", commentId, eventId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @DELETE @Path("{eventId}/comment/{commentId}/{replyId}") @RestQuery(name = "deleteeventreply", description = "Delete an event comment reply", returnDescription = "The updated comment as JSON.", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING), @RestParameter(name = "replyId", isRequired = true, description = "The comment reply identifier", type = STRING) }, reponses = { @RestResponse(responseCode = SC_NOT_FOUND, description = "No event comment or reply with this identifier was found."), @RestResponse(responseCode = SC_OK, description = "The updated comment as JSON.") }) public Response deleteEventCommentReply(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId, @PathParam("replyId") long replyId) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); EventComment comment = null; EventCommentReply reply = null; try { comment = getEventCommentService().getComment(commentId); for (EventCommentReply r : comment.getReplies()) { if (r.getId().isNone() || replyId != r.getId().get().longValue()) continue; reply = r; break; } if (reply == null) throw new NotFoundException("Reply with id " + replyId + " not found!"); comment.removeReply(reply); EventComment updatedComment = getEventCommentService().updateComment(comment); List<EventComment> comments = getEventCommentService().getComments(eventId); getIndexService().updateCommentCatalog(optEvent.get(), comments); return Response.ok(updatedComment.toJson().toJson()).build(); } catch (NotFoundException e) { throw e; } catch (Exception e) { logger.warn("Could not remove event comment reply {} from comment {}: {}", replyId, commentId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @PUT @Path("{eventId}/comment/{commentId}/{replyId}") @RestQuery(name = "updateeventcommentreply", description = "Updates an event comment reply", returnDescription = "The updated comment as JSON.", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING), @RestParameter(name = "replyId", isRequired = true, description = "The comment reply identifier", type = STRING) }, restParameters = { @RestParameter(name = "text", isRequired = true, description = "The comment reply text", type = TEXT) }, reponses = { @RestResponse(responseCode = SC_NOT_FOUND, description = "The event or comment to extend with a reply or the reply has not been found."), @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "If no text is set."), @RestResponse(responseCode = SC_OK, description = "The updated comment as JSON.") }) public Response updateEventCommentReply(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId, @PathParam("replyId") long replyId, @FormParam("text") String text) throws Exception { if (StringUtils.isBlank(text)) return Response.status(Status.BAD_REQUEST).build(); Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); EventComment comment = null; EventCommentReply reply = null; try { comment = getEventCommentService().getComment(commentId); for (EventCommentReply r : comment.getReplies()) { if (r.getId().isNone() || replyId != r.getId().get().longValue()) continue; reply = r; break; } if (reply == null) throw new NotFoundException("Reply with id " + replyId + " not found!"); EventCommentReply updatedReply = EventCommentReply.create(reply.getId(), text.trim(), reply.getAuthor(), reply.getCreationDate(), new Date()); comment.removeReply(reply); comment.addReply(updatedReply); EventComment updatedComment = getEventCommentService().updateComment(comment); List<EventComment> comments = getEventCommentService().getComments(eventId); getIndexService().updateCommentCatalog(optEvent.get(), comments); return Response.ok(updatedComment.toJson().toJson()).build(); } catch (NotFoundException e) { throw e; } catch (Exception e) { logger.warn("Could not update event comment reply {} from comment {}: {}", replyId, commentId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @POST @Path("{eventId}/comment/{commentId}/reply") @RestQuery(name = "createeventcommentreply", description = "Creates an event comment reply", returnDescription = "The updated comment as JSON.", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING) }, restParameters = { @RestParameter(name = "text", isRequired = true, description = "The comment reply text", type = TEXT), @RestParameter(name = "resolved", isRequired = false, description = "Flag defining if this reply solve or not the comment.", type = BOOLEAN) }, reponses = { @RestResponse(responseCode = SC_NOT_FOUND, description = "The event or comment to extend with a reply has not been found."), @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "If no text is set."), @RestResponse(responseCode = SC_OK, description = "The updated comment as JSON.") }) public Response createEventCommentReply(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId, @FormParam("text") String text, @FormParam("resolved") Boolean resolved) throws Exception { if (StringUtils.isBlank(text)) return Response.status(Status.BAD_REQUEST).build(); Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); EventComment comment = null; try { comment = getEventCommentService().getComment(commentId); EventComment updatedComment; if (resolved != null && resolved) { // If the resolve flag is set to true, change to comment to resolved updatedComment = EventComment.create(comment.getId(), comment.getEventId(), comment.getOrganization(), comment.getText(), comment.getAuthor(), comment.getReason(), true, comment.getCreationDate(), new Date(), comment.getReplies()); } else { updatedComment = comment; } User author = getSecurityService().getUser(); EventCommentReply reply = EventCommentReply.create(Option.<Long> none(), text, author); updatedComment.addReply(reply); updatedComment = getEventCommentService().updateComment(updatedComment); List<EventComment> comments = getEventCommentService().getComments(eventId); getIndexService().updateCommentCatalog(optEvent.get(), comments); return Response.ok(updatedComment.toJson().toJson()).build(); } catch (Exception e) { logger.warn("Could not create event comment reply on comment {}: {}", comment, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e); } } @GET @Path("{eventId}/participation.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "geteventparticipationinformation", description = "Get the particition information of a event", returnDescription = "The participation information", pathParameters = { @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING) }, reponses = { @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required form params were missing in the request."), @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event has not been found."), @RestResponse(responseCode = SC_OK, description = "The access information ") }) public Response getEventParticipation(@PathParam("eventId") String eventId) throws Exception { final Event event = getEventOrThrowNotFoundException(eventId); Date startDate = new DateTime(event.getTechnicalStartTime()).toDateTime(DateTimeZone.UTC).toDate(); Date currentDate = new DateTime().toDateTime(DateTimeZone.UTC).toDate(); boolean readOnly = false; if (currentDate.after(startDate)) { readOnly = true; } Boolean optedOut = event.getOptedOut(); return okJson(obj(f("opt_out", v(optedOut != null ? optedOut : false)), f("review_status", v(event.getReviewStatus(), BLANK)), f("read_only", v(readOnly)))); } @GET @Path("{eventId}/metadata.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "geteventmetadata", description = "Returns all the data related to the metadata tab in the event details modal as JSON", returnDescription = "All the data related to the event metadata tab as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns all the data related to the event metadata tab as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getEventMetadata(@PathParam("eventId") String eventId) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); MetadataList metadataList = new MetadataList(); List<EventCatalogUIAdapter> catalogUIAdapters = getIndexService().getEventCatalogUIAdapters(); catalogUIAdapters.remove(getIndexService().getCommonEventCatalogUIAdapter()); Opt<MediaPackage> optMediaPackage = getIndexService().getEventMediapackage(optEvent.get()); if (catalogUIAdapters.size() > 0) { if (optMediaPackage.isSome()) { for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) { metadataList.add(catalogUIAdapter, catalogUIAdapter.getFields(optMediaPackage.get())); } } } metadataList.add(getIndexService().getCommonEventCatalogUIAdapter(), EventUtils.getEventMetadata(optEvent.get(), getIndexService().getCommonEventCatalogUIAdapter())); if (WorkflowInstance.WorkflowState.RUNNING.toString().equals(optEvent.get().getWorkflowState())) metadataList.setLocked(Locked.WORKFLOW_RUNNING); return okJson(metadataList.toJSON()); } @PUT @Path("{eventId}/metadata") @RestQuery(name = "updateeventmetadata", description = "Update the passed metadata for the event with the given Id", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, restParameters = { @RestParameter(name = "metadata", isRequired = true, type = RestParameter.Type.TEXT, description = "The list of metadata to update") }, reponses = { @RestResponse(description = "The metadata have been updated.", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Could not parse metadata.", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "No content is returned.") public Response updateEventMetadata(@PathParam("eventId") String id, @FormParam("metadata") String metadataJSON) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(id, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", id); MetadataList metadataList = getIndexService().updateAllEventMetadata(id, metadataJSON, getIndex()); return okJson(metadataList.toJSON()); } @GET @Path("{eventId}/asset/assets.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getAssetList", description = "Returns the number of assets from each types as JSON", returnDescription = "The number of assets from each types as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns the number of assets from each types as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getAssetList(@PathParam("eventId") String id) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(id, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", id); Opt<MediaPackage> mp = getIndexService().getEventMediapackage(optEvent.get()); int attachments = 0; int catalogs = 0; int media = 0; int publications = 0; if (mp.isSome()) { attachments = mp.get().getAttachments().length; catalogs = mp.get().getCatalogs().length; media = mp.get().getTracks().length; publications = mp.get().getPublications().length; } return okJson(obj(f("attachments", v(attachments)), f("catalogs", v(catalogs)), f("media", v(media)), f("publications", v(publications)))); } @GET @Path("{eventId}/asset/attachment/attachments.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getAttachmentsList", description = "Returns a list of attachments from the given event as JSON", returnDescription = "The list of attachments from the given event as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns a list of attachments from the given event as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getAttachmentsList(@PathParam("eventId") String id) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(id, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", id); Opt<MediaPackage> mp = getIndexService().getEventMediapackage(optEvent.get()); List<JValue> attachments = new ArrayList<JValue>(); if (mp.isSome()) { attachments = getEventMediaPackageElements(mp.get().getAttachments()); } return okJson(arr(attachments)); } @GET @Path("{eventId}/asset/attachment/{id}.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getAttachment", description = "Returns the details of an attachment from the given event and attachment id as JSON", returnDescription = "The details of an attachment from the given event and attachment id as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "id", description = "The attachment id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns the details of an attachment from the given event and attachment id as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event or attachment with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getAttachment(@PathParam("eventId") String eventId, @PathParam("id") String id) throws NotFoundException, SearchIndexException, IndexServiceException { MediaPackage mp = getMediaPackageByEventId(eventId); Attachment attachment = mp.getAttachment(id); if (attachment == null) return notFound("Cannot find an attachment with id '%s'.", id); return okJson(attachmentToJSON(attachment)); } @GET @Path("{eventId}/asset/catalog/catalogs.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getCatalogList", description = "Returns a list of catalogs from the given event as JSON", returnDescription = "The list of catalogs from the given event as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns a list of catalogs from the given event as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getCatalogList(@PathParam("eventId") String id) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(id, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", id); Opt<MediaPackage> mp = getIndexService().getEventMediapackage(optEvent.get()); List<JValue> catalogs = new ArrayList<JValue>(); if (mp.isSome()) { catalogs = getEventMediaPackageElements(mp.get().getCatalogs()); } return okJson(arr(catalogs)); } @GET @Path("{eventId}/asset/catalog/{id}.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getCatalog", description = "Returns the details of a catalog from the given event and catalog id as JSON", returnDescription = "The details of a catalog from the given event and catalog id as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "id", description = "The catalog id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns the details of a catalog from the given event and catalog id as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event or catalog with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getCatalog(@PathParam("eventId") String eventId, @PathParam("id") String id) throws NotFoundException, SearchIndexException, IndexServiceException { MediaPackage mp = getMediaPackageByEventId(eventId); Catalog catalog = mp.getCatalog(id); if (catalog == null) return notFound("Cannot find a catalog with id '%s'.", id); return okJson(catalogToJSON(catalog)); } @GET @Path("{eventId}/asset/media/media.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getMediaList", description = "Returns a list of media from the given event as JSON", returnDescription = "The list of media from the given event as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns a list of media from the given event as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getMediaList(@PathParam("eventId") String id) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(id, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", id); Opt<MediaPackage> mp = getIndexService().getEventMediapackage(optEvent.get()); List<JValue> media = new ArrayList<JValue>(); if (mp.isSome()) { media = getEventMediaPackageElements(mp.get().getTracks()); } return okJson(arr(media)); } @GET @Path("{eventId}/asset/media/{id}.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getMedia", description = "Returns the details of a media from the given event and media id as JSON", returnDescription = "The details of a media from the given event and media id as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "id", description = "The media id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns the media of a catalog from the given event and media id as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event or media with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getMedia(@PathParam("eventId") String eventId, @PathParam("id") String id) throws NotFoundException, SearchIndexException, IndexServiceException { MediaPackage mp = getMediaPackageByEventId(eventId); Track track = mp.getTrack(id); if (track == null) return notFound("Cannot find media with id '%s'.", id); return okJson(trackToJSON(track)); } @GET @Path("{eventId}/asset/publication/publications.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getPublicationList", description = "Returns a list of publications from the given event as JSON", returnDescription = "The list of publications from the given event as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns a list of publications from the given event as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getPublicationList(@PathParam("eventId") String id) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(id, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", id); Opt<MediaPackage> mp = getIndexService().getEventMediapackage(optEvent.get()); List<JValue> publications = new ArrayList<JValue>(); if (mp.isSome()) { publications = getEventPublications(mp.get().getPublications()); } return okJson(arr(publications)); } @GET @Path("{eventId}/asset/publication/{id}.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getPublication", description = "Returns the details of a publication from the given event and publication id as JSON", returnDescription = "The details of a publication from the given event and publication id as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "id", description = "The publication id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns the publication of a catalog from the given event and publication id as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event or publication with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getPublication(@PathParam("eventId") String eventId, @PathParam("id") String id) throws NotFoundException, SearchIndexException, IndexServiceException { MediaPackage mp = getMediaPackageByEventId(eventId); Publication publication = null; for (Publication p : mp.getPublications()) { if (id.equals(p.getIdentifier())) { publication = p; break; } } if (publication == null) return notFound("Cannot find publication with id '%s'.", id); return okJson(publicationToJSON(publication)); } @GET @Path("{eventId}/workflows.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "geteventworkflows", description = "Returns all the data related to the workflows tab in the event details modal as JSON", returnDescription = "All the data related to the event workflows tab as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns all the data related to the event workflows tab as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getEventWorkflows(@PathParam("eventId") String id) throws UnauthorizedException, SearchIndexException, JobEndpointException { Opt<Event> optEvent = getIndexService().getEvent(id, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", id); try { return okJson(getJobService().getTasksAsJSON(new WorkflowQuery().withMediaPackage(id))); } catch (NotFoundException e) { return notFound("Cannot find workflows for event %s", id); } } @GET @Path("{eventId}/workflows/{workflowId}") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "geteventworkflow", description = "Returns all the data related to the single workflow tab in the event details modal as JSON", returnDescription = "All the data related to the event singe workflow tab as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns all the data related to the event single workflow tab as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getEventWorkflow(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId) throws JobEndpointException, SearchIndexException { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); long workflowInstanceId; try { workflowId = StringUtils.remove(workflowId, ".json"); workflowInstanceId = Long.parseLong(workflowId); } catch (Exception e) { logger.warn("Unable to parse workflow id {}", workflowId); return RestUtil.R.badRequest(); } try { return okJson(getJobService().getTasksAsJSON(workflowInstanceId)); } catch (NotFoundException e) { return notFound("Cannot find workflow %s", workflowId); } } @GET @Path("{eventId}/workflows/{workflowId}/operations.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "geteventoperations", description = "Returns all the data related to the workflow/operations tab in the event details modal as JSON", returnDescription = "All the data related to the event workflow/opertations tab as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns all the data related to the event workflow/operations tab as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getEventOperations(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId) throws JobEndpointException, SearchIndexException { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); long workflowInstanceId; try { workflowInstanceId = Long.parseLong(workflowId); } catch (Exception e) { logger.warn("Unable to parse workflow id {}", workflowId); return RestUtil.R.badRequest(); } try { return okJson(getJobService().getOperationsAsJSON(workflowInstanceId)); } catch (NotFoundException e) { return notFound("Cannot find workflow %s", workflowId); } } @GET @Path("{eventId}/workflows/{workflowId}/operations/{operationPosition}") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "geteventoperation", description = "Returns all the data related to the workflow/operation tab in the event details modal as JSON", returnDescription = "All the data related to the event workflow/opertation tab as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "operationPosition", description = "The operation position", isRequired = true, type = RestParameter.Type.INTEGER) }, reponses = { @RestResponse(description = "Returns all the data related to the event workflow/operation tab as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Unable to parse workflowId or operationPosition", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "No operation with these identifiers was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getEventOperation(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId, @PathParam("operationPosition") Integer operationPosition) throws JobEndpointException, SearchIndexException { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); long workflowInstanceId; try { workflowInstanceId = Long.parseLong(workflowId); } catch (Exception e) { logger.warn("Unable to parse workflow id {}", workflowId); return RestUtil.R.badRequest(); } try { return okJson(getJobService().getOperationAsJSON(workflowInstanceId, operationPosition)); } catch (NotFoundException e) { return notFound("Cannot find workflow %s", workflowId); } } @GET @Path("{eventId}/workflows/{workflowId}/errors.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "geteventerrors", description = "Returns all the data related to the workflow/errors tab in the event details modal as JSON", returnDescription = "All the data related to the event workflow/errors tab as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns all the data related to the event workflow/errors tab as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getEventErrors(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId, @Context HttpServletRequest req) throws JobEndpointException, SearchIndexException { // the call to #getEvent should make sure that the calling user has access rights to the workflow // FIXME since there is no dependency between the event and the workflow (the fetched event is // simply ignored) an attacker can get access by using an event he owns and a workflow ID of // someone else. for (final Event ignore : getIndexService().getEvent(eventId, getIndex())) { final long workflowIdLong; try { workflowIdLong = Long.parseLong(workflowId); } catch (Exception e) { logger.warn("Unable to parse workflow id {}", workflowId); return RestUtil.R.badRequest(); } try { return okJson(getJobService().getIncidentsAsJSON(workflowIdLong, req.getLocale(), true)); } catch (NotFoundException e) { return notFound("Cannot find the incident for the workflow %s", workflowId); } } return notFound("Cannot find an event with id '%s'.", eventId); } @GET @Path("{eventId}/workflows/{workflowId}/errors/{errorId}.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "geteventerror", description = "Returns all the data related to the workflow/error tab in the event details modal as JSON", returnDescription = "All the data related to the event workflow/error tab as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "errorId", description = "The error id", isRequired = true, type = RestParameter.Type.STRING) }, reponses = { @RestResponse(description = "Returns all the data related to the event workflow/error tab as JSON", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST), @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }) public Response getEventError(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId, @PathParam("errorId") String errorId, @Context HttpServletRequest req) throws JobEndpointException, SearchIndexException { // the call to #getEvent should make sure that the calling user has access rights to the workflow // FIXME since there is no dependency between the event and the workflow (the fetched event is // simply ignored) an attacker can get access by using an event he owns and a workflow ID of // someone else. for (Event ignore : getIndexService().getEvent(eventId, getIndex())) { final long errorIdLong; try { errorIdLong = Long.parseLong(errorId); } catch (Exception e) { logger.warn("Unable to parse error id {}", errorId); return RestUtil.R.badRequest(); } try { return okJson(getJobService().getIncidentAsJSON(errorIdLong, req.getLocale())); } catch (NotFoundException e) { return notFound("Cannot find the incident %s", errorId); } } return notFound("Cannot find an event with id '%s'.", eventId); } @GET @Path("{eventId}/access.json") @SuppressWarnings("unchecked") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getEventAccessInformation", description = "Get the access information of an event", returnDescription = "The access information", pathParameters = { @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING) }, reponses = { @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required form params were missing in the request."), @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event has not been found."), @RestResponse(responseCode = SC_OK, description = "The access information ") }) public Response getEventAccessInformation(@PathParam("eventId") String eventId) throws Exception { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); // Add all available ACLs to the response JSONArray systemAclsJson = new JSONArray(); List<ManagedAcl> acls = getAclService().getAcls(); for (ManagedAcl acl : acls) { systemAclsJson.add(AccessInformationUtil.serializeManagedAcl(acl)); } // Get the episode ACL final TransitionQuery q = TransitionQuery.query().withId(eventId).withScope(AclScope.Episode); List<EpisodeACLTransition> episodeTransistions; JSONArray transitionsJson = new JSONArray(); try { episodeTransistions = getAclService().getTransitions(q).getEpisodeTransistions(); for (EpisodeACLTransition trans : episodeTransistions) { transitionsJson.add(AccessInformationUtil.serializeEpisodeACLTransition(trans)); } } catch (AclServiceException e) { logger.error( "There was an error while trying to get the ACL transitions for series '{}' from the ACL service: {}", eventId, ExceptionUtils.getStackTrace(e)); return RestUtil.R.serverError(); } AccessControlList activeAcl = new AccessControlList(); try { if (optEvent.get().getAccessPolicy() != null) activeAcl = AccessControlParser.parseAcl(optEvent.get().getAccessPolicy()); } catch (Exception e) { logger.error("Unable to parse access policy because: {}", ExceptionUtils.getStackTrace(e)); } Option<ManagedAcl> currentAcl = AccessInformationUtil.matchAcls(acls, activeAcl); JSONObject episodeAccessJson = new JSONObject(); episodeAccessJson.put("current_acl", currentAcl.isSome() ? currentAcl.get().getId() : 0L); episodeAccessJson.put("acl", AccessControlParser.toJsonSilent(activeAcl)); episodeAccessJson.put("privileges", AccessInformationUtil.serializePrivilegesByRole(activeAcl)); episodeAccessJson.put("transitions", transitionsJson); if (StringUtils.isNotBlank(optEvent.get().getWorkflowState()) && WorkflowUtil.isActive(WorkflowInstance.WorkflowState.valueOf(optEvent.get().getWorkflowState()))) episodeAccessJson.put("locked", true); JSONObject jsonReturnObj = new JSONObject(); jsonReturnObj.put("episode_access", episodeAccessJson); jsonReturnObj.put("system_acls", systemAclsJson); return Response.ok(jsonReturnObj.toString()).build(); } @POST @Path("{eventId}/transitions") @RestQuery(name = "addEventTransition", description = "Adds an ACL transition to an event", returnDescription = "The method doesn't return any content", pathParameters = { @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING) }, restParameters = { @RestParameter(name = "transition", isRequired = true, description = "The transition (JSON object) to add", type = RestParameter.Type.TEXT) }, reponses = { @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required params were missing in the request."), @RestResponse(responseCode = SC_NO_CONTENT, description = "The method doesn't return any content"), @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event has not been found.") }) public Response addEventTransition(@PathParam("eventId") String eventId, @FormParam("transition") String transitionStr) throws SearchIndexException { if (StringUtils.isBlank(eventId) || StringUtils.isBlank(transitionStr)) return RestUtil.R.badRequest("Missing parameters"); Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); try { final org.codehaus.jettison.json.JSONObject t = new org.codehaus.jettison.json.JSONObject(transitionStr); Option<ConfiguredWorkflowRef> workflowRef; if (t.has("workflow_id")) workflowRef = Option.some(ConfiguredWorkflowRef.workflow(t.getString("workflow_id"))); else workflowRef = Option.none(); Option<Long> managedAclId; if (t.has("acl_id")) managedAclId = Option.some(t.getLong("acl_id")); else managedAclId = Option.none(); getAclService().addEpisodeTransition(eventId, managedAclId, new Date(DateTimeSupport.fromUTC(t.getString("application_date"))), workflowRef); return Response.noContent().build(); } catch (AclServiceException e) { logger.error("Error while trying to get ACL transitions for event '{}' from ACL service: {}", eventId, e); throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR); } catch (JSONException e) { return RestUtil.R.badRequest("The transition object is not valid"); } catch (IllegalStateException e) { // That should never happen throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR); } catch (ParseException e) { return RestUtil.R.badRequest("The date could not be parsed"); } } @PUT @Path("{eventId}/transitions/{transitionId}") @RestQuery(name = "updateEventTransition", description = "Updates an ACL transition of an event", returnDescription = "The method doesn't return any content", pathParameters = { @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING), @RestParameter(name = "transitionId", isRequired = true, description = "The transition identifier", type = RestParameter.Type.INTEGER) }, restParameters = { @RestParameter(name = "transition", isRequired = true, description = "The updated transition (JSON object)", type = RestParameter.Type.TEXT) }, reponses = { @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required params were missing in the request."), @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event or transtion has not been found."), @RestResponse(responseCode = SC_NO_CONTENT, description = "The method doesn't return any content") }) public Response updateEventTransition(@PathParam("eventId") String eventId, @PathParam("transitionId") long transitionId, @FormParam("transition") String transitionStr) throws NotFoundException, SearchIndexException { if (StringUtils.isBlank(transitionStr)) return RestUtil.R.badRequest("Missing parameters"); Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); try { final org.codehaus.jettison.json.JSONObject t = new org.codehaus.jettison.json.JSONObject(transitionStr); Option<ConfiguredWorkflowRef> workflowRef; if (t.has("workflow_id")) workflowRef = Option.some(ConfiguredWorkflowRef.workflow(t.getString("workflow_id"))); else workflowRef = Option.none(); Option<Long> managedAclId; if (t.has("acl_id")) managedAclId = Option.some(t.getLong("acl_id")); else managedAclId = Option.none(); getAclService().updateEpisodeTransition(transitionId, managedAclId, new Date(DateTimeSupport.fromUTC(t.getString("application_date"))), workflowRef); return Response.noContent().build(); } catch (JSONException e) { return RestUtil.R.badRequest("The transition object is not valid"); } catch (IllegalStateException e) { // That should never happen throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR); } catch (AclServiceException e) { logger.error("Unable to update transtion {} of event {}: {}", transitionId, eventId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR); } catch (ParseException e) { // That should never happen throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR); } } @PUT @Path("{eventId}/optout/{optout}") @RestQuery(name = "updateEventOptoutStatus", description = "Updates an event's opt out status.", returnDescription = "The method doesn't return any content", pathParameters = { @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING), @RestParameter(name = "optout", isRequired = true, description = "True or false, true to opt out of this recording.", type = RestParameter.Type.BOOLEAN) }, restParameters = {}, reponses = { @RestResponse(responseCode = SC_NOT_FOUND, description = "The event has not been found"), @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action"), @RestResponse(responseCode = SC_NO_CONTENT, description = "The method doesn't return any content") }) public Response updateEventOptOut(@PathParam("eventId") String eventId, @PathParam("optout") boolean optout) throws NotFoundException, UnauthorizedException { try { getIndexService().changeOptOutStatus(eventId, optout, getIndex()); return Response.noContent().build(); } catch (SchedulerException e) { logger.error("Unable to updated opt out status for event with id {}", eventId); throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR); } catch (SearchIndexException e) { logger.error("Unable to get event with id {}", eventId); throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR); } } @POST @Path("optouts") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "changeOptOuts", description = "Change the opt out status of many events", returnDescription = "A JSON array listing which events were or were not opted out.", restParameters = { @RestParameter(name = "eventIds", description = "A JSON array of ids of the events to opt out or in", defaultValue = "[]", isRequired = true, type = RestParameter.Type.STRING), @RestParameter(name = "optout", description = "Whether to opt out or not either true or false.", defaultValue = "false", isRequired = true, type = RestParameter.Type.BOOLEAN), }, reponses = { @RestResponse(description = "Returns a JSON object with the results for the different opted out or in elements such as ok, notFound or error.", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "Unable to parse boolean value to opt out, or parse JSON array of opt out events", responseCode = HttpServletResponse.SC_BAD_REQUEST) }) public Response changeOptOuts(@FormParam("optout") boolean optout, @FormParam("eventIds") String eventIds) { JSONArray eventIdsArray; try { eventIdsArray = (JSONArray) parser.parse(eventIds); } catch (org.json.simple.parser.ParseException e) { logger.warn("Unable to parse event ids {} : {}", eventIds, ExceptionUtils.getStackTrace(e)); return Response.status(Status.BAD_REQUEST).build(); } catch (NullPointerException e) { logger.warn("Unable to parse event ids because it was null {}", eventIds); return Response.status(Status.BAD_REQUEST).build(); } catch (ClassCastException e) { logger.warn("Unable to parse event ids because it was the wrong class {} : {}", eventIds, ExceptionUtils.getStackTrace(e)); return Response.status(Status.BAD_REQUEST).build(); } BulkOperationResult result = new BulkOperationResult(); for (Object idObject : eventIdsArray) { String eventId = idObject.toString(); try { getIndexService().changeOptOutStatus(eventId, optout, getIndex()); result.addOk(eventId); } catch (NotFoundException e) { result.addNotFound(eventId); } catch (Exception e) { logger.error("Could not update opt out status of event {}: {}", eventId, ExceptionUtils.getStackTrace(e)); result.addServerError(eventId); } } return Response.ok(result.toJson()).build(); } @DELETE @Path("{eventId}/transitions/{transitionId}") @RestQuery(name = "deleteEventTransition", description = "Deletes an ACL transition from an event", returnDescription = "The method doesn't return any content", pathParameters = { @RestParameter(name = "eventId", isRequired = true, description = "The series identifier", type = RestParameter.Type.STRING), @RestParameter(name = "transitionId", isRequired = true, description = "The transition identifier", type = RestParameter.Type.INTEGER) }, reponses = { @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event or the transition has not been found."), @RestResponse(responseCode = SC_NO_CONTENT, description = "The method does not return any content") }) public Response deleteEventTransition(@PathParam("eventId") String eventId, @PathParam("transitionId") long transitionId) throws NotFoundException, SearchIndexException { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) return notFound("Cannot find an event with id '%s'.", eventId); try { getAclService().deleteEpisodeTransition(transitionId); return Response.noContent().build(); } catch (AclServiceException e) { logger.error("Error while trying to delete transition '{}' from event '{}': {}", transitionId, eventId, ExceptionUtils.getStackTrace(e)); throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR); } } @GET @Path("new/metadata") @RestQuery(name = "getNewMetadata", description = "Returns all the data related to the metadata tab in the new event modal as JSON", returnDescription = "All the data related to the event metadata tab as JSON", reponses = { @RestResponse(responseCode = SC_OK, description = "Returns all the data related to the event metadata tab as JSON") }) public Response getNewMetadata() { MetadataList metadataList = getIndexService().getMetadataListWithAllEventCatalogUIAdapters(); Opt<MetadataCollection> optMetadataByAdapter = metadataList .getMetadataByAdapter(getIndexService().getCommonEventCatalogUIAdapter()); if (optMetadataByAdapter.isSome()) { MetadataCollection collection = optMetadataByAdapter.get(); collection.removeField(collection.getOutputFields().get(DublinCore.PROPERTY_CREATED.getLocalName())); collection.removeField(collection.getOutputFields().get("duration")); collection.removeField(collection.getOutputFields().get(DublinCore.PROPERTY_IDENTIFIER.getLocalName())); collection.removeField(collection.getOutputFields().get(DublinCore.PROPERTY_SOURCE.getLocalName())); collection.removeField(collection.getOutputFields().get("startDate")); collection.removeField(collection.getOutputFields().get("startTime")); collection.removeField(collection.getOutputFields().get("location")); metadataList.add(getIndexService().getCommonEventCatalogUIAdapter(), collection); } return okJson(metadataList.toJSON()); } @GET @Path("new/processing") @RestQuery(name = "getNewProcessing", description = "Returns all the data related to the processing tab in the new event modal as JSON", returnDescription = "All the data related to the event processing tab as JSON", restParameters = { @RestParameter(name = "tags", isRequired = false, description = "A comma separated list of tags to filter the workflow definitions", type = RestParameter.Type.STRING) }, reponses = { @RestResponse(responseCode = SC_OK, description = "Returns all the data related to the event processing tab as JSON") }) public Response getNewProcessing(@QueryParam("tags") String tagsString) { List<String> tags = RestUtil.splitCommaSeparatedParam(Option.option(tagsString)).value(); // This is the JSON Object which will be returned by this request List<JValue> actions = new ArrayList<>(); try { List<WorkflowDefinition> workflowsDefinitions = getWorkflowService().listAvailableWorkflowDefinitions(); for (WorkflowDefinition wflDef : workflowsDefinitions) { if (wflDef.containsTag(tags)) { actions.add(obj(f("id", v(wflDef.getId())), f("title", v(nul(wflDef.getTitle()).getOr(""))), f("description", v(nul(wflDef.getDescription()).getOr(""))), f("configuration_panel", v(nul(wflDef.getConfigurationPanel()).getOr(""))))); } } } catch (WorkflowDatabaseException e) { logger.error("Unable to get available workflow definitions: {}", ExceptionUtils.getStackTrace(e)); return RestUtil.R.serverError(); } return okJson(arr(actions)); } @POST @Path("new/conflicts") @RestQuery(name = "checkNewConflicts", description = "Checks if the current scheduler parameters are in a conflict with another event", returnDescription = "Returns NO CONTENT if no event are in conflict within specified period or list of conflicting recordings in JSON", restParameters = { @RestParameter(name = "metadata", isRequired = true, description = "The metadata as JSON", type = RestParameter.Type.TEXT) }, reponses = { @RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "No conflicting events found"), @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "There is a conflict"), @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "Missing or invalid parameters") }) public Response getNewConflicts(@FormParam("metadata") String metadata) throws NotFoundException { if (StringUtils.isBlank(metadata)) { logger.warn("Metadata is not specified"); return Response.status(Status.BAD_REQUEST).build(); } JSONObject metadataJson; try { metadataJson = (JSONObject) parser.parse(metadata); } catch (Exception e) { logger.warn("Unable to parse metadata {}", metadata); return RestUtil.R.badRequest("Unable to parse metadata"); } String device; String startDate; String endDate; try { device = (String) metadataJson.get("device"); startDate = (String) metadataJson.get("start"); endDate = (String) metadataJson.get("end"); } catch (Exception e) { logger.warn("Unable to parse metadata {}", metadata); return RestUtil.R.badRequest("Unable to parse metadata"); } if (StringUtils.isBlank(device) || StringUtils.isBlank(startDate) || StringUtils.isBlank(endDate)) { logger.warn("Either device, start date or end date were not specified"); return Response.status(Status.BAD_REQUEST).build(); } Date start; try { start = new Date(DateTimeSupport.fromUTC(startDate)); } catch (Exception e) { logger.warn("Unable to parse start date {}", startDate); return RestUtil.R.badRequest("Unable to parse start date"); } Date end; try { end = new Date(DateTimeSupport.fromUTC(endDate)); } catch (Exception e) { logger.warn("Unable to parse end date {}", endDate); return RestUtil.R.badRequest("Unable to parse end date"); } String rruleString = (String) metadataJson.get("rrule"); RRule rrule = null; TimeZone timeZone = TimeZone.getDefault(); String durationString = null; if (StringUtils.isNotEmpty(rruleString)) { try { rrule = new RRule(rruleString); rrule.validate(); } catch (Exception e) { logger.warn("Unable to parse rrule {}: {}", rruleString, e.getMessage()); return Response.status(Status.BAD_REQUEST).build(); } durationString = (String) metadataJson.get("duration"); if (StringUtils.isBlank(durationString)) { logger.warn("If checking recurrence, must include duration."); return Response.status(Status.BAD_REQUEST).build(); } Agent agent = getCaptureAgentStateService().getAgent(device); String timezone = agent.getConfiguration().getProperty("capture.device.timezone"); if (StringUtils.isBlank(timezone)) { timezone = TimeZone.getDefault().getID(); logger.warn("No 'capture.device.timezone' set on agent {}. The default server timezone {} will be used.", device, timezone); } timeZone = TimeZone.getTimeZone(timezone); } String eventId = (String) metadataJson.get("id"); try { DublinCoreCatalogList events = null; if (StringUtils.isNotEmpty(rruleString)) { events = getSchedulerService().findConflictingEvents(device, rruleString, start, end, Long.parseLong(durationString), timeZone.getID()); } else { events = getSchedulerService().findConflictingEvents(device, start, end); } if (!events.getCatalogList().isEmpty()) { List<JValue> eventsJSON = new ArrayList<>(); for (DublinCoreCatalog event : events.getCatalogList()) { final DCMIPeriod period = EncodingSchemeUtils .decodeMandatoryPeriod(event.getFirst(DublinCore.PROPERTY_TEMPORAL)); eventsJSON.add(obj(f("start", v(DateTimeSupport.toUTC(period.getStart().getTime()))), f("end", v(DateTimeSupport.toUTC(period.getEnd().getTime()))), f("title", v(event.getFirst(DublinCoreCatalog.PROPERTY_TITLE))))); } if (!eventsJSON.isEmpty()) return conflictJson(arr(eventsJSON)); } return Response.noContent().build(); } catch (Exception e) { logger.error("Unable to find conflicting events for {}, {}, {}: {}", device, startDate, endDate, ExceptionUtils.getStackTrace(e)); return RestUtil.R.serverError(); } } @POST @Path("/new") @Consumes(MediaType.MULTIPART_FORM_DATA) @RestQuery(name = "createNewEvent", description = "Creates a new event by the given metadata as JSON and the files in the body", returnDescription = "The workflow identifier", restParameters = { @RestParameter(name = "metadata", isRequired = true, description = "The metadata as JSON", type = RestParameter.Type.TEXT) }, reponses = { @RestResponse(responseCode = HttpServletResponse.SC_CREATED, description = "Event sucessfully added"), @RestResponse(responseCode = SC_BAD_REQUEST, description = "If the metadata is not set or couldn't be parsed") }) public Response createNewEvent(@Context HttpServletRequest request) { try { String result = getIndexService().createEvent(request); return Response.status(Status.CREATED).entity(result).build(); } catch (IllegalArgumentException e) { return RestUtil.R.badRequest(e.getMessage()); } catch (Exception e) { return RestUtil.R.serverError(); } } @GET @Path("events.json") @Produces(MediaType.APPLICATION_JSON) @RestQuery(name = "getevents", description = "Returns all the events as JSON", returnDescription = "All the events as JSON", restParameters = { @RestParameter(name = "filter", isRequired = false, description = "The filter used for the query. They should be formated like that: 'filter1:value1,filter2:value2'", type = STRING), @RestParameter(name = "sort", description = "The order instructions used to sort the query result. Must be in the form '<field name>:(ASC|DESC)'", isRequired = false, type = STRING), @RestParameter(name = "limit", description = "The maximum number of items to return per page.", isRequired = false, type = RestParameter.Type.INTEGER), @RestParameter(name = "offset", description = "The page number.", isRequired = false, type = RestParameter.Type.INTEGER) }, reponses = { @RestResponse(description = "Returns all events as JSON", responseCode = HttpServletResponse.SC_OK) }) public Response getEvents(@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) { Option<Integer> optLimit = Option.option(limit); Option<Integer> optOffset = Option.option(offset); Option<String> optSort = Option.option(trimToNull(sort)); ArrayList<JValue> eventsList = new ArrayList<>(); 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(); } Map<String, String> filters = RestUtils.parseFilter(filter); for (String name : filters.keySet()) { if (EventListQuery.FILTER_PRESENTERS_BIBLIOGRAPHIC_NAME.equals(name)) query.withPresenter(filters.get(name)); if (EventListQuery.FILTER_PRESENTERS_TECHNICAL_NAME.equals(name)) query.withTechnicalPresenters(filters.get(name)); if (EventListQuery.FILTER_CONTRIBUTORS_NAME.equals(name)) query.withContributor(filters.get(name)); if (EventListQuery.FILTER_LOCATION_NAME.equals(name)) query.withLocation(filters.get(name)); if (EventListQuery.FILTER_AGENT_NAME.equals(name)) query.withAgentId(filters.get(name)); if (EventListQuery.FILTER_TEXT_NAME.equals(name)) query.withText(QueryPreprocessor.sanitize(filters.get(name))); if (EventListQuery.FILTER_SERIES_NAME.equals(name)) query.withSeriesId(filters.get(name)); if (EventListQuery.FILTER_STATUS_NAME.equals(name)) query.withEventStatus(filters.get(name)); if (EventListQuery.FILTER_OPTEDOUT_NAME.equals(name)) query.withOptedOut(Boolean.parseBoolean(filters.get(name))); if (EventListQuery.FILTER_REVIEW_STATUS_NAME.equals(name)) query.withReviewStatus(filters.get(name)); if (EventListQuery.FILTER_COMMENTS_NAME.equals(name)) { switch (Comments.valueOf(filters.get(name))) { case NONE: query.withComments(false); break; case OPEN: query.withOpenComments(true); break; case RESOLVED: query.withComments(true); query.withOpenComments(false); break; default: logger.info("Unknown comment {}", filters.get(name)); return Response.status(SC_BAD_REQUEST).build(); } } if (EventListQuery.FILTER_STARTDATE_NAME.equals(name)) { try { Tuple<Date, Date> fromAndToCreationRange = RestUtils.getFromAndToDateRange(filters.get(name)); query.withStartFrom(fromAndToCreationRange.getA()); query.withStartTo(fromAndToCreationRange.getB()); } catch (IllegalArgumentException e) { return RestUtil.R.badRequest(e.getMessage()); } } } 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.PUBLICATION: query.sortByPublicationIgnoringInternal(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.SERIES_NAME: query.sortBySeriesName(criterion.getOrder()); break; case EventIndexSchema.LOCATION: query.sortByLocation(criterion.getOrder()); break; case EventIndexSchema.EVENT_STATUS: query.sortByEventStatus(criterion.getOrder()); break; default: throw new WebApplicationException(Status.BAD_REQUEST); } } } // TODO: Add the comment resolution filter to the query EventCommentsListProvider.RESOLUTION resolution = null; if (StringUtils.isNotBlank(resolutionFilter)) { try { resolution = EventCommentsListProvider.RESOLUTION.valueOf(resolutionFilter); } catch (Exception e) { logger.warn("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 = getIndex().getByQuery(query); } catch (SearchIndexException e) { logger.error("The admin UI Search Index was not able to get the events list: {}", e); return RestUtil.R.serverError(); } // If the results list if empty, we return already a response. if (results.getPageSize() == 0) { logger.debug("No events match the given filters."); return okJsonList(eventsList, nul(offset).getOr(0), nul(limit).getOr(0), 0); } for (SearchResultItem<Event> item : results.getItems()) { Event source = item.getSource(); source.updatePreview(getAdminUIConfiguration().getPreviewSubtype()); eventsList.add(eventToJSON(source)); } return okJsonList(eventsList, nul(offset).getOr(0), nul(limit).getOr(0), results.getHitCount()); } // -- private MediaPackage getMediaPackageByEventId(String eventId) throws SearchIndexException, NotFoundException, IndexServiceException { Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex()); if (optEvent.isNone()) throw new NotFoundException(format("Cannot find an event with id '%s'.", eventId)); Opt<MediaPackage> mp = getIndexService().getEventMediapackage(optEvent.get()); if (mp.isNone()) throw new NotFoundException(format("No mediapackage availalbe on event with id '%s'.", eventId)); return mp.get(); } private URI getCommentUrl(String eventId, long commentId) { return UrlSupport.uri(serverUrl, eventId, "comment", Long.toString(commentId)); } private JValue eventToJSON(Event event) { List<Field> fields = new ArrayList<>(); fields.add(f("id", v(event.getIdentifier()))); fields.add(f("title", v(event.getTitle(), BLANK))); fields.add(f("source", v(event.getSource(), BLANK))); fields.add(f("presenters", arr($(event.getPresenters()).map(Functions.stringToJValue)))); if (StringUtils.isNotBlank(event.getSeriesId())) { String seriesTitle = event.getSeriesName(); String seriesID = event.getSeriesId(); fields.add(f("series", obj(f("id", v(seriesID, BLANK)), f("title", v(seriesTitle, BLANK))))); } fields.add(f("location", v(event.getLocation(), BLANK))); fields.add(f("start_date", v(event.getRecordingStartDate(), BLANK))); fields.add(f("end_date", v(event.getRecordingEndDate(), BLANK))); String schedulingStatus = event.getSchedulingStatus() == null ? null : "EVENTS.EVENTS.SCHEDULING_STATUS." + event.getSchedulingStatus(); fields.add(f("managedAcl", v(event.getManagedAcl(), BLANK))); fields.add(f("scheduling_status", v(schedulingStatus, BLANK))); fields.add(f("workflow_state", v(event.getWorkflowState(), BLANK))); fields.add(f("review_status", v(event.getReviewStatus(), BLANK))); fields.add(f("event_status", v(event.getEventStatus()))); fields.add(f("source", v(getIndexService().getEventSource(event).toString()))); fields.add(f("has_comments", v(event.hasComments()))); fields.add(f("has_open_comments", v(event.hasOpenComments()))); fields.add(f("needs_cutting", v(event.needsCutting()))); fields.add(f("has_preview", v(event.hasPreview()))); fields.add(f("agent_id", v(event.getAgentId(), BLANK))); fields.add(f("technical_start", v(event.getTechnicalStartTime(), BLANK))); fields.add(f("technical_end", v(event.getTechnicalEndTime(), BLANK))); fields.add(f("technical_presenters", arr($(event.getTechnicalPresenters()).map(Functions.stringToJValue)))); fields.add(f("publications", arr(eventPublicationsToJson(event)))); return obj(fields); } private JValue attachmentToJSON(Attachment attachment) { List<Field> fields = new ArrayList<>(); fields.addAll(getEventMediaPackageElementFields(attachment)); fields.addAll(getCommonElementFields(attachment)); return obj(fields); } private JValue catalogToJSON(Catalog catalog) { List<Field> fields = new ArrayList<>(); fields.addAll(getEventMediaPackageElementFields(catalog)); fields.addAll(getCommonElementFields(catalog)); return obj(fields); } private JValue trackToJSON(Track track) { List<Field> fields = new ArrayList<>(); fields.addAll(getEventMediaPackageElementFields(track)); fields.addAll(getCommonElementFields(track)); fields.add(f("duration", v(track.getDuration(), BLANK))); fields.add(f("has_audio", v(track.hasAudio()))); fields.add(f("has_video", v(track.hasVideo()))); fields.add(f("streams", obj(streamsToJSON(track.getStreams())))); return obj(fields); } private List<Field> streamsToJSON(org.opencastproject.mediapackage.Stream[] streams) { List<Field> fields = new ArrayList<>(); List<JValue> audioList = new ArrayList<>(); List<JValue> videoList = new ArrayList<>(); for (org.opencastproject.mediapackage.Stream stream : streams) { // TODO There is a bug with the stream ids, see MH-10325 if (stream instanceof AudioStreamImpl) { List<Field> audio = new ArrayList<>(); AudioStream audioStream = (AudioStream) stream; audio.add(f("id", v(audioStream.getIdentifier(), BLANK))); audio.add(f("type", v(audioStream.getFormat(), BLANK))); audio.add(f("channels", v(audioStream.getChannels(), BLANK))); audio.add(f("bitrate", v(audioStream.getBitRate(), BLANK))); audio.add(f("bitdepth", v(audioStream.getBitDepth(), BLANK))); audio.add(f("samplingrate", v(audioStream.getSamplingRate(), BLANK))); audio.add(f("framecount", v(audioStream.getFrameCount(), BLANK))); audio.add(f("peakleveldb", v(audioStream.getPkLevDb(), BLANK))); audio.add(f("rmsleveldb", v(audioStream.getRmsLevDb(), BLANK))); audio.add(f("rmspeakdb", v(audioStream.getRmsPkDb(), BLANK))); audioList.add(obj(audio)); } else if (stream instanceof VideoStreamImpl) { List<Field> video = new ArrayList<>(); VideoStream videoStream = (VideoStream) stream; video.add(f("id", v(videoStream.getIdentifier(), BLANK))); video.add(f("type", v(videoStream.getFormat(), BLANK))); video.add(f("bitrate", v(videoStream.getBitRate(), BLANK))); video.add(f("framerate", v(videoStream.getFrameRate(), BLANK))); video.add(f("resolution", v(videoStream.getFrameWidth() + "x" + videoStream.getFrameHeight(), BLANK))); video.add(f("framecount", v(videoStream.getFrameCount(), BLANK))); video.add(f("scantype", v(videoStream.getScanType(), BLANK))); video.add(f("scanorder", v(videoStream.getScanOrder(), BLANK))); videoList.add(obj(video)); } else { throw new IllegalArgumentException("Stream must be either audio or video"); } } fields.add(f("audio", arr(audioList))); fields.add(f("video", arr(videoList))); return fields; } private JValue publicationToJSON(Publication publication) { List<Field> fields = new ArrayList<>(); fields.add(f("id", v(publication.getIdentifier(), BLANK))); fields.add(f("channel", v(publication.getChannel(), BLANK))); fields.add(f("mimetype", v(publication.getMimeType(), BLANK))); fields.add(f("tags", arr($(publication.getTags()).map(toStringJValue)))); fields.add(f("url", v(signUrl(publication.getURI()), BLANK))); fields.addAll(getCommonElementFields(publication)); return obj(fields); } private List<Field> getCommonElementFields(MediaPackageElement element) { List<Field> fields = new ArrayList<>(); fields.add(f("size", v(element.getSize(), BLANK))); fields.add(f("checksum", v(element.getChecksum() != null ? element.getChecksum().getValue() : null, BLANK))); fields.add(f("reference", v(element.getReference() != null ? element.getReference().getIdentifier() : null, BLANK))); return fields; } /** * Render an array of {@link Publication}s into a list of JSON values. * * @param publications * The elements to pull the data from to create the list of {@link JValue}s * @return {@link List} of {@link JValue}s that represent the {@link Publication} */ private List<JValue> getEventPublications(Publication[] publications) { List<JValue> publicationJSON = new ArrayList<>(); for (Publication publication : publications) { publicationJSON.add(obj(f("id", v(publication.getIdentifier(), BLANK)), f("channel", v(publication.getChannel(), BLANK)), f("mimetype", v(publication.getMimeType(), BLANK)), f("tags", arr($(publication.getTags()).map(toStringJValue))), f("url", v(signUrl(publication.getURI()), BLANK)))); } return publicationJSON; } private URI signUrl(URI url) { if (getUrlSigningService().accepts(url.toString())) { try { String clientIP = null; if (signWithClientIP()) { clientIP = getSecurityService().getUserIP(); } return URI.create(getUrlSigningService().sign(url.toString(), getUrlSigningExpireDuration(), null, clientIP)); } catch (UrlSigningException e) { logger.warn("Unable to sign url '{}': {}", url, ExceptionUtils.getStackTrace(e)); } } return url; } /** * Render an array of {@link MediaPackageElement}s into a list of JSON values. * * @param elements * The elements to pull the data from to create the list of {@link JValue}s * @return {@link List} of {@link JValue}s that represent the {@link MediaPackageElement} */ private List<JValue> getEventMediaPackageElements(MediaPackageElement[] elements) { List<JValue> elementJSON = new ArrayList<>(); for (MediaPackageElement element : elements) { elementJSON.add(obj(getEventMediaPackageElementFields(element))); } return elementJSON; } private List<Field> getEventMediaPackageElementFields(MediaPackageElement element) { List<Field> fields = new ArrayList<>(); fields.add(f("id", v(element.getIdentifier(), BLANK))); fields.add(f("type", v(element.getFlavor().toString(), BLANK))); fields.add(f("mimetype", v(element.getMimeType(), BLANK))); List<JValue> tags = Stream.$(element.getTags()).map(toStringJValue).toList(); fields.add(f("tags", arr(tags))); fields.add(f("url", v(signUrl(element.getURI()), BLANK))); return fields; } private static final Fn<String, JValue> toStringJValue = new Fn<String, JValue>() { @Override public JValue apply(String stringValue) { return v(stringValue, BLANK); } }; private final Fn<Publication, JObject> publicationToJson = new Fn<Publication, JObject>() { @Override public JObject apply(Publication publication) { final Opt<String> channel = Opt.nul(EventUtils.PUBLICATION_CHANNELS.get(publication.getChannel())); String url = publication.getURI() == null ? "" : signUrl(publication.getURI()).toString(); return obj(f("id", v(publication.getChannel())), f("name", v(channel.getOr("EVENTS.EVENTS.DETAILS.GENERAL.CUSTOM"))), f("url", v(url, NULL))); } }; protected static final Fn<Opt<Recording>, JObject> recordingToJson = new Fn<Opt<Recording>, JObject>() { @Override public JObject apply(Opt<Recording> recording) { if (recording.isNone()) { return obj(); } return obj(f("id", v(recording.get().getID(), BLANK)), f("lastCheckInTime", v(recording.get().getLastCheckinTime(), BLANK)), f("lastCheckInTimeUTC", v(toUTC(recording.get().getLastCheckinTime()), BLANK)), f("state", v(recording.get().getState(), BLANK))); } }; }