AbstractEventEndpoint.java
/*
* 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 java.lang.String.format;
import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED;
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_SERVICE_UNAVAILABLE;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
import static org.apache.commons.lang3.StringUtils.trimToNull;
import static org.opencastproject.adminui.endpoint.EndpointUtil.transformAccessControList;
import static org.opencastproject.index.service.impl.util.EventUtils.internalChannelFilter;
import static org.opencastproject.index.service.util.JSONUtils.arrayToJsonArray;
import static org.opencastproject.index.service.util.JSONUtils.collectionToJsonArray;
import static org.opencastproject.index.service.util.JSONUtils.mapToJsonObject;
import static org.opencastproject.index.service.util.JSONUtils.safeString;
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.notFoundJson;
import static org.opencastproject.index.service.util.RestUtils.okJson;
import static org.opencastproject.index.service.util.RestUtils.okJsonList;
import static org.opencastproject.index.service.util.RestUtils.serverErrorJson;
import static org.opencastproject.util.DateTimeSupport.toUTC;
import static org.opencastproject.util.RestUtil.R.badRequest;
import static org.opencastproject.util.RestUtil.R.conflict;
import static org.opencastproject.util.RestUtil.R.forbidden;
import static org.opencastproject.util.RestUtil.R.noContent;
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.tobira.TobiraException;
import org.opencastproject.adminui.tobira.TobiraService;
import org.opencastproject.adminui.util.BulkUpdateUtil;
import org.opencastproject.assetmanager.api.AssetManager;
import org.opencastproject.authorization.xacml.manager.api.AclService;
import org.opencastproject.authorization.xacml.manager.api.ManagedAcl;
import org.opencastproject.authorization.xacml.manager.util.AccessInformationUtil;
import org.opencastproject.capture.CaptureParameters;
import org.opencastproject.capture.admin.api.Agent;
import org.opencastproject.capture.admin.api.CaptureAgentStateService;
import org.opencastproject.elasticsearch.api.SearchIndexException;
import org.opencastproject.elasticsearch.api.SearchResult;
import org.opencastproject.elasticsearch.api.SearchResultItem;
import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
import org.opencastproject.elasticsearch.index.objects.event.Event;
import org.opencastproject.elasticsearch.index.objects.event.EventIndexSchema;
import org.opencastproject.elasticsearch.index.objects.event.EventSearchQuery;
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.exception.IndexServiceException;
import org.opencastproject.index.service.exception.UnsupportedAssetException;
import org.opencastproject.index.service.impl.util.EventUtils;
import org.opencastproject.index.service.resources.list.provider.EventsListProvider.Comments;
import org.opencastproject.index.service.resources.list.provider.EventsListProvider.IsPublished;
import org.opencastproject.index.service.resources.list.query.EventListQuery;
import org.opencastproject.index.service.resources.list.query.SeriesListQuery;
import org.opencastproject.index.service.util.JSONUtils;
import org.opencastproject.index.service.util.RestUtils;
import org.opencastproject.list.api.ListProviderException;
import org.opencastproject.list.api.ListProvidersService;
import org.opencastproject.list.api.ResourceListQuery;
import org.opencastproject.list.impl.ResourceListQueryImpl;
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.MediaPackageException;
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.SubtitleStreamImpl;
import org.opencastproject.mediapackage.track.VideoStreamImpl;
import org.opencastproject.metadata.dublincore.DublinCore;
import org.opencastproject.metadata.dublincore.DublinCoreMetadataCollection;
import org.opencastproject.metadata.dublincore.EventCatalogUIAdapter;
import org.opencastproject.metadata.dublincore.MetadataField;
import org.opencastproject.metadata.dublincore.MetadataJson;
import org.opencastproject.metadata.dublincore.MetadataList;
import org.opencastproject.metadata.dublincore.MetadataList.Locked;
import org.opencastproject.rest.BulkOperationResult;
import org.opencastproject.rest.RestConstants;
import org.opencastproject.scheduler.api.Recording;
import org.opencastproject.scheduler.api.SchedulerException;
import org.opencastproject.scheduler.api.SchedulerService;
import org.opencastproject.scheduler.api.TechnicalMetadata;
import org.opencastproject.scheduler.api.Util;
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.Organization;
import org.opencastproject.security.api.Permissions;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.security.api.User;
import org.opencastproject.security.api.UserDirectoryService;
import org.opencastproject.security.urlsigning.exception.UrlSigningException;
import org.opencastproject.security.urlsigning.service.UrlSigningService;
import org.opencastproject.security.util.SecurityUtil;
import org.opencastproject.systems.OpencastConstants;
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.Tuple;
import org.opencastproject.util.data.Tuple3;
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.requests.SortCriterion;
import org.opencastproject.workflow.api.RetryStrategy;
import org.opencastproject.workflow.api.WorkflowDatabaseException;
import org.opencastproject.workflow.api.WorkflowDefinition;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowOperationInstance;
import org.opencastproject.workflow.api.WorkflowService;
import org.opencastproject.workflow.api.WorkflowStateException;
import org.opencastproject.workflow.api.WorkflowUtil;
import com.google.gson.JsonArray;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import net.fortuna.ical4j.model.property.RRule;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jettison.json.JSONException;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.text.ParseException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.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("/admin-ng/event")
public abstract class AbstractEventEndpoint {
/**
* Scheduling JSON keys
*/
public static final String SCHEDULING_AGENT_ID_KEY = "agentId";
public static final String SCHEDULING_START_KEY = "start";
public static final String SCHEDULING_END_KEY = "end";
private static final String SCHEDULING_AGENT_CONFIGURATION_KEY = "agentConfiguration";
public static final String SCHEDULING_PREVIOUS_AGENTID = "previousAgentId";
public static final String SCHEDULING_PREVIOUS_PREVIOUSENTRIES = "previousEntries";
private static final String WORKFLOW_ACTION_STOP = "STOP";
/** The logging facility */
static final Logger logger = LoggerFactory.getLogger(AbstractEventEndpoint.class);
/** The configuration key that defines the default workflow definition */
//TODO Move to a constants file instead of declaring it at the top of multiple files?
protected static final String WORKFLOW_DEFINITION_DEFAULT = "org.opencastproject.workflow.default.definition";
private static final String WORKFLOW_STATUS_TRANSLATION_PREFIX = "EVENTS.EVENTS.DETAILS.WORKFLOWS.OPERATION_STATUS.";
/** 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 AssetManager getAssetManager();
public abstract WorkflowService getWorkflowService();
public abstract ElasticsearchIndex getIndex();
public abstract JobEndpoint getJobService();
public abstract SeriesEndpoint getSeriesEndpoint();
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();
public abstract Boolean getOnlySeriesWithWriteAccessEventModal();
public abstract Boolean getOnlyEventsWithWriteAccessEventsTab();
public abstract UserDirectoryService getUserDirectoryService();
public abstract ListProvidersService getListProvidersService();
/** Default server URL */
protected String serverUrl = "http://localhost:8080";
/** Service url */
protected String serviceUrl = null;
/** The default workflow identifier, if one is configured */
protected String defaultWorkflowDefinionId = null;
/** The system user name (default set here for unit tests) */
private String systemUserName = "opencast_system_account";
/**
* Activates REST service.
*
* @param cc
* ComponentContext
*/
@Activate
public void activate(ComponentContext cc) {
if (cc != null) {
String ccServerUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
if (StringUtils.isNotBlank(ccServerUrl)) {
this.serverUrl = ccServerUrl;
}
this.serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
String ccDefaultWorkflowDefinionId = StringUtils.trimToNull(cc.getBundleContext()
.getProperty(WORKFLOW_DEFINITION_DEFAULT));
if (StringUtils.isNotBlank(ccDefaultWorkflowDefinionId)) {
this.defaultWorkflowDefinionId = ccDefaultWorkflowDefinionId;
}
systemUserName = SecurityUtil.getSystemUserName(cc);
}
}
/* As the list of event ids can grow large, we use a POST request to avoid problems with too large query strings */
@POST
@Path("workflowProperties")
@Produces(MediaType.APPLICATION_JSON)
@RestQuery(
name = "workflowProperties",
description = "Returns workflow properties for the specified events",
returnDescription = "The workflow properties for every event as JSON",
restParameters = {
@RestParameter(name = "eventIds", description = "A JSON array of ids of the events", isRequired = true,
type = RestParameter.Type.STRING)},
responses = {
@RestResponse(description = "Returns the workflow properties for the events as JSON",
responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "The list of ids could not be parsed into a json list.",
responseCode = HttpServletResponse.SC_BAD_REQUEST)
})
public Response getEventWorkflowProperties(@FormParam("eventIds") String eventIds) throws UnauthorizedException {
if (StringUtils.isBlank(eventIds)) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
JSONParser parser = new JSONParser();
List<String> ids;
try {
ids = (List<String>) parser.parse(eventIds);
} catch (org.json.simple.parser.ParseException e) {
logger.error("Unable to parse '{}'", eventIds, e);
return Response.status(Response.Status.BAD_REQUEST).build();
} catch (ClassCastException e) {
logger.error("Unable to cast '{}'", eventIds, e);
return Response.status(Response.Status.BAD_REQUEST).build();
}
final Map<String, Map<String, String>> eventWithProperties = getIndexService().getEventWorkflowProperties(ids);
JsonObject jsonEvents = new JsonObject();
for (Entry<String, Map<String, String>> event : eventWithProperties.entrySet()) {
JsonObject jsonProperties = new JsonObject();
for (Entry<String, String> property : event.getValue().entrySet()) {
jsonProperties.add(property.getKey(), new JsonPrimitive(property.getValue()));
}
jsonEvents.add(event.getKey(), jsonProperties);
}
return okJson(jsonEvents);
}
@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",
responses = {
@RestResponse(description = "Returns the available catalog UI adapters as JSON",
responseCode = HttpServletResponse.SC_OK)
})
public Response getCatalogAdapters() {
JsonArray jsonAdapters = new JsonArray();
for (EventCatalogUIAdapter adapter : getIndexService().getEventCatalogUIAdapters()) {
JsonObject obj = new JsonObject();
obj.addProperty("flavor", adapter.getFlavor().toString());
obj.addProperty("title", adapter.getUITitle());
jsonAdapters.add(obj);
}
return okJson(jsonAdapters);
}
@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)
},
responses = {
@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 {
Optional<Event> eventOpt = getIndexService().getEvent(id, getIndex());
if (eventOpt.isPresent()) {
Event event = eventOpt.get();
event.updatePreview(getAdminUIConfiguration().getPreviewSubtype());
JsonObject json = eventToJSON(event, Optional.empty(), Optional.empty());
return okJson(json);
}
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),
},
responses = {
@RestResponse(responseCode = SC_OK, description = "The event has been deleted."),
@RestResponse(responseCode = SC_ACCEPTED, description = "The event will be retracted and deleted "
+ "afterwards."),
@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 UnauthorizedException, SearchIndexException {
final Optional<Event> event = checkAgentAccessForEvent(id);
if (event.isEmpty()) {
return RestUtil.R.notFound(id);
}
final IndexService.EventRemovalResult result;
try {
result = getIndexService().removeEvent(event.get(), getAdminUIConfiguration().getRetractWorkflowId());
} catch (WorkflowDatabaseException e) {
logger.error("Workflow database is not reachable. This may be a temporary problem.");
return RestUtil.R.serverError();
} catch (NotFoundException e) {
logger.error("Configured retract workflow not found. Check your configuration.");
return RestUtil.R.serverError();
}
switch (result) {
case SUCCESS:
return Response.ok().build();
case RETRACTING:
return Response.accepted().build();
case GENERAL_FAILURE:
return Response.serverError().build();
case NOT_FOUND:
return RestUtil.R.notFound(id);
default:
throw new RuntimeException("Unknown EventRemovalResult type: " + result.name());
}
}
@POST
@Path("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.",
responses = {
@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, SearchIndexException {
if (StringUtils.isBlank(eventIdsContent)) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
JSONParser parser = new JSONParser();
JSONArray eventIdsJsonArray;
try {
eventIdsJsonArray = (JSONArray) parser.parse(eventIdsContent);
} catch (org.json.simple.parser.ParseException e) {
logger.error("Unable to parse '{}'", eventIdsContent, e);
return Response.status(Response.Status.BAD_REQUEST).build();
} catch (ClassCastException e) {
logger.error("Unable to cast '{}'", eventIdsContent, e);
return Response.status(Response.Status.BAD_REQUEST).build();
}
BulkOperationResult result = new BulkOperationResult();
for (Object eventIdObject : eventIdsJsonArray) {
final String eventId = eventIdObject.toString();
try {
final Optional<Event> event = checkAgentAccessForEvent(eventId);
if (event.isPresent()) {
final IndexService.EventRemovalResult currentResult = getIndexService().removeEvent(event.get(),
getAdminUIConfiguration().getRetractWorkflowId());
switch (currentResult) {
case SUCCESS:
result.addOk(eventId);
break;
case RETRACTING:
result.addAccepted(eventId);
break;
case GENERAL_FAILURE:
result.addServerError(eventId);
break;
case NOT_FOUND:
result.addNotFound(eventId);
break;
default:
throw new RuntimeException("Unknown EventRemovalResult type: " + currentResult.name());
}
} else {
result.addNotFound(eventId);
}
} catch (UnauthorizedException e) {
result.addUnauthorized(eventId);
} catch (WorkflowDatabaseException e) {
logger.error("Workflow database is not reachable. This may be a temporary problem.");
return RestUtil.R.serverError();
} catch (NotFoundException e) {
logger.error("Configured retract workflow not found. Check your configuration.");
return RestUtil.R.serverError();
}
}
return Response.ok(result.toJson()).build();
}
@GET
@Path("{eventId}/publications.json")
@Produces(MediaType.APPLICATION_JSON)
@RestQuery(
name = "geteventpublications",
description = "Returns all the data related to the publications tab in the event details modal as JSON",
returnDescription = "All the data related to the event publications tab as JSON",
pathParameters = {
@RestParameter(name = "eventId", description = "The event id (mediapackage id).", isRequired = true,
type = RestParameter.Type.STRING)
},
responses = {
@RestResponse(description = "Returns all the data related to the event publications tab as JSON",
responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "No event with this identifier was found.",
responseCode = HttpServletResponse.SC_NOT_FOUND)
})
public Response getEventPublicationsTab(@PathParam("eventId") String id) throws Exception {
Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", id);
}
// Quick actions have been temporally removed from the publications 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();
// Convert event publications to JSON array
List<JsonObject> pubJSONList = eventPublicationsToJson(event);
JsonArray publicationsJsonArray = new JsonArray();
for (JsonObject pubJson : pubJSONList) {
publicationsJsonArray.add(pubJson);
}
JsonObject result = new JsonObject();
result.add("publications", publicationsJsonArray);
// Add start-date and end-date as strings, or blank if null
String startDate = event.getRecordingStartDate() != null
? event.getRecordingStartDate().toString()
: "";
String endDate = event.getRecordingEndDate() != null
? event.getRecordingEndDate().toString()
: "";
result.addProperty("start-date", startDate);
result.addProperty("end-date", endDate);
return okJson(result);
}
private List<JsonObject> eventPublicationsToJson(Event event) {
List<JsonObject> pubJSON = new ArrayList<>();
for (Publication publication : event.getPublications()) {
if (internalChannelFilter.test(publication)) {
pubJSON.add(publicationToJson.apply(publication));
}
}
return pubJSON;
}
private List<JsonObject> eventCommentsToJson(List<EventComment> comments) {
List<JsonObject> commentArr = new ArrayList<>();
for (EventComment c : comments) {
JsonObject author = new JsonObject();
author.addProperty("name", c.getAuthor().getName());
if (c.getAuthor().getEmail() != null) {
author.addProperty("email", c.getAuthor().getEmail());
} else {
author.add("email", null);
}
author.addProperty("username", c.getAuthor().getUsername());
JsonArray replies = new JsonArray();
List<JsonObject> replyJsonList = eventCommentRepliesToJson(c.getReplies());
for (JsonObject replyJson : replyJsonList) {
replies.add(replyJson);
}
JsonObject commentJson = new JsonObject();
commentJson.addProperty("reason", c.getReason());
commentJson.addProperty("resolvedStatus", c.isResolvedStatus());
commentJson.addProperty("modificationDate", c.getModificationDate().toInstant().toString());
commentJson.add("replies", replies);
commentJson.add("author", author);
commentJson.addProperty("id", c.getId().get());
commentJson.addProperty("text", c.getText());
commentJson.addProperty("creationDate", c.getCreationDate().toInstant().toString());
commentArr.add(commentJson);
}
return commentArr;
}
private List<JsonObject> eventCommentRepliesToJson(List<EventCommentReply> replies) {
List<JsonObject> repliesArr = new ArrayList<>();
for (EventCommentReply r : replies) {
JsonObject author = new JsonObject();
author.addProperty("name", r.getAuthor().getName());
if (r.getAuthor().getEmail() != null) {
author.addProperty("email", r.getAuthor().getEmail());
} else {
author.add("email", null);
}
author.addProperty("username", r.getAuthor().getUsername());
JsonObject replyJson = new JsonObject();
replyJson.addProperty("id", r.getId().get());
replyJson.addProperty("text", r.getText());
replyJson.addProperty("creationDate", r.getCreationDate().toInstant().toString());
replyJson.addProperty("modificationDate", r.getModificationDate().toInstant().toString());
replyJson.add("author", author);
repliesArr.add(replyJson);
}
return repliesArr;
}
@GET
@Path("{eventId}/scheduling.json")
@Produces(MediaType.APPLICATION_JSON)
@RestQuery(
name = "getEventSchedulingMetadata",
description = "Returns all of the scheduling metadata for an event",
returnDescription = "All the technical metadata related to scheduling as JSON",
pathParameters = {
@RestParameter(name = "eventId", description = "The event id (mediapackage id).", isRequired = true,
type = RestParameter.Type.STRING)
},
responses = {
@RestResponse(description = "Returns all the data related to the event scheduling tab as JSON",
responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "No event with this identifier was found.",
responseCode = HttpServletResponse.SC_NOT_FOUND)
})
public Response getEventScheduling(@PathParam("eventId") String eventId)
throws NotFoundException, UnauthorizedException, SearchIndexException {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", eventId);
}
try {
TechnicalMetadata technicalMetadata = getSchedulerService().getTechnicalMetadata(eventId);
return okJson(technicalMetadataToJson(technicalMetadata));
} catch (SchedulerException e) {
logger.error("Unable to get technical metadata for event with id {}", eventId);
throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
}
}
@POST
@Path("scheduling.json")
@Produces(MediaType.APPLICATION_JSON)
@RestQuery(
name = "getEventsScheduling",
description = "Returns all of the scheduling metadata for a list of events",
returnDescription = "All the technical metadata related to scheduling as JSON",
restParameters = {
@RestParameter(name = "eventIds", description = "An array of event IDs (mediapackage id)", isRequired = true,
type = RestParameter.Type.STRING),
@RestParameter(name = "ignoreNonScheduled", description = "Whether events that are not really scheduled "
+ "events should be ignored or produce an error", isRequired = true, type = RestParameter.Type.BOOLEAN)
},
responses = {
@RestResponse(description = "Returns all the data related to the event scheduling tab as JSON",
responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "No event with this identifier was found.",
responseCode = HttpServletResponse.SC_NOT_FOUND)
})
public Response getEventsScheduling(@FormParam("eventIds") final List<String> eventIds,
@FormParam("ignoreNonScheduled") final boolean ignoreNonScheduled) {
JsonArray fields = new JsonArray();
for (final String eventId : eventIds) {
try {
fields.add(technicalMetadataToJson(getSchedulerService().getTechnicalMetadata(eventId)));
} catch (final NotFoundException e) {
if (!ignoreNonScheduled) {
logger.warn("Unable to find id {}", eventId, e);
return notFound("Cannot find an event with id '%s'.", eventId);
}
} catch (final UnauthorizedException e) {
logger.warn("Unauthorized access to event ID {}", eventId, e);
return Response.status(Status.BAD_REQUEST).build();
} catch (final SchedulerException e) {
logger.warn("Scheduler exception accessing event ID {}", eventId, e);
return Response.status(Status.BAD_REQUEST).build();
}
}
return okJson(fields);
}
@PUT
@Path("{eventId}/scheduling")
@RestQuery(
name = "updateEventScheduling",
description = "Updates the scheduling information 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)
},
restParameters = {
@RestParameter(name = "scheduling", isRequired = true, description = "The updated scheduling (JSON object)",
type = RestParameter.Type.TEXT)
},
responses = {
@RestResponse(responseCode = SC_BAD_REQUEST, description = "The required params were missing in the "
+ "request."),
@RestResponse(responseCode = SC_NOT_FOUND, description = "If the event has not been found."),
@RestResponse(responseCode = SC_NO_CONTENT, description = "The method doesn't return any content")
})
public Response updateEventScheduling(@PathParam("eventId") String eventId,
@FormParam("scheduling") String scheduling)
throws NotFoundException, UnauthorizedException, SearchIndexException, IndexServiceException {
if (StringUtils.isBlank(scheduling)) {
return RestUtil.R.badRequest("Missing parameters");
}
try {
final Event event = getEventOrThrowNotFoundException(eventId);
updateEventScheduling(scheduling, event);
return Response.noContent().build();
} catch (JSONException e) {
return RestUtil.R.badRequest("The scheduling object is not valid");
} catch (ParseException e) {
return RestUtil.R.badRequest("The UTC dates in the scheduling object is not valid");
} catch (SchedulerException e) {
logger.error("Unable to update scheduling technical metadata of event {}", eventId, e);
throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
} catch (IllegalStateException e) {
return RestUtil.R.badRequest(e.getMessage());
}
}
private void updateEventScheduling(String scheduling, Event event) throws NotFoundException, UnauthorizedException,
SchedulerException, JSONException, ParseException, SearchIndexException, IndexServiceException {
final TechnicalMetadata technicalMetadata = getSchedulerService().getTechnicalMetadata(event.getIdentifier());
final org.codehaus.jettison.json.JSONObject schedulingJson = new org.codehaus.jettison.json.JSONObject(
scheduling);
Optional<String> agentId = Optional.empty();
if (schedulingJson.has(SCHEDULING_AGENT_ID_KEY)) {
agentId = Optional.of(schedulingJson.getString(SCHEDULING_AGENT_ID_KEY));
logger.trace("Updating agent id of event '{}' from '{}' to '{}'",
event.getIdentifier(), technicalMetadata.getAgentId(), agentId);
}
Optional<String> previousAgentId = Optional.empty();
if (schedulingJson.has(SCHEDULING_PREVIOUS_AGENTID)) {
previousAgentId = Optional.of(schedulingJson.getString(SCHEDULING_PREVIOUS_AGENTID));
}
Optional<String> previousAgentInputs = Optional.empty();
Optional<String> agentInputs = Optional.empty();
if (agentId.isPresent() && previousAgentId.isPresent()) {
Agent previousAgent = getCaptureAgentStateService().getAgent(previousAgentId.get());
Agent agent = getCaptureAgentStateService().getAgent(agentId.get());
previousAgentInputs = Optional.ofNullable(previousAgent.getCapabilities().getProperty(
CaptureParameters.CAPTURE_DEVICE_NAMES));
agentInputs = Optional.ofNullable(agent.getCapabilities().getProperty(CaptureParameters.CAPTURE_DEVICE_NAMES));
}
// Check if we are allowed to re-schedule on this agent
checkAgentAccessForAgent(technicalMetadata.getAgentId());
if (agentId.isPresent()) {
checkAgentAccessForAgent(agentId.get());
}
Optional<Date> start = Optional.empty();
if (schedulingJson.has(SCHEDULING_START_KEY)) {
start = Optional.of(new Date(DateTimeSupport.fromUTC(schedulingJson.getString(SCHEDULING_START_KEY))));
logger.trace("Updating start time of event '{}' id from '{}' to '{}'",
event.getIdentifier(), DateTimeSupport.toUTC(technicalMetadata.getStartDate().getTime()),
DateTimeSupport.toUTC(start.get().getTime()));
}
Optional<Date> end = Optional.empty();
if (schedulingJson.has(SCHEDULING_END_KEY)) {
end = Optional.of(new Date(DateTimeSupport.fromUTC(schedulingJson.getString(SCHEDULING_END_KEY))));
logger.trace("Updating end time of event '{}' id from '{}' to '{}'",
event.getIdentifier(), DateTimeSupport.toUTC(technicalMetadata.getEndDate().getTime()),
DateTimeSupport.toUTC(end.get().getTime()));
}
Optional<Map<String, String>> agentConfiguration = Optional.empty();
if (schedulingJson.has(SCHEDULING_AGENT_CONFIGURATION_KEY)) {
agentConfiguration = Optional.of(JSONUtils.toMap(schedulingJson.getJSONObject(
SCHEDULING_AGENT_CONFIGURATION_KEY)));
logger.trace("Updating agent configuration of event '{}' id from '{}' to '{}'",
event.getIdentifier(), technicalMetadata.getCaptureAgentConfiguration(), agentConfiguration);
}
Optional<Map<String, String>> previousAgentInputMethods = Optional.empty();
if (schedulingJson.has(SCHEDULING_PREVIOUS_PREVIOUSENTRIES)) {
previousAgentInputMethods = Optional.of(
JSONUtils.toMap(schedulingJson.getJSONObject(SCHEDULING_PREVIOUS_PREVIOUSENTRIES)));
}
// If we had previously selected an agent, and both the old and new agent have the same set of input channels,
// copy which input channels are active to the new agent
if (previousAgentInputs.isPresent() && previousAgentInputs.isPresent() && agentInputs.isPresent()) {
Map<String, String> map = previousAgentInputMethods.get();
String mapAsString = map.keySet().stream()
.collect(Collectors.joining(","));
String previousInputs = mapAsString;
if (previousAgentInputs.equals(agentInputs)) {
final Map<String, String> configMap = new HashMap<>(agentConfiguration.get());
configMap.put(CaptureParameters.CAPTURE_DEVICE_NAMES, previousInputs);
agentConfiguration = Optional.of(configMap);
}
}
if ((start.isPresent() || end.isPresent())
&& end.orElse(technicalMetadata.getEndDate()).before(start.orElse(technicalMetadata.getStartDate()))) {
throw new IllegalStateException("The end date is before the start date");
}
if (!start.isEmpty() || !end.isEmpty() || !agentId.isEmpty() || !agentConfiguration.isEmpty()) {
getSchedulerService().updateEvent(event.getIdentifier(), start, end, agentId, Optional.empty(), Optional.empty(),
Optional.empty(), agentConfiguration);
}
}
private Event getEventOrThrowNotFoundException(final String eventId) throws NotFoundException, SearchIndexException {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isPresent()) {
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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
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, 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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", eventId);
}
JSONObject json = new JSONObject();
if (WorkflowUtil.isActive(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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
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, 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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
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, 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)
},
responses = {
@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 Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
logger.warn("Unable to find the event '{}'", eventId);
return notFound();
}
Source eventSource = getIndexService().getEventSource(optEvent.get());
if (eventSource == Source.ARCHIVE) {
Optional<MediaPackage> mediaPackage = getAssetManager().getMediaPackage(eventId);
Optional<AccessControlList> aclOpt = Optional.ofNullable(accessControlList);
// the episode service is the source of authority for the retrieval of media packages
if (mediaPackage.isPresent()) {
MediaPackage episodeSvcMp = mediaPackage.get();
aclOpt.ifPresentOrElse(
aclPresent -> {
try {
MediaPackage mp = getAuthorizationService()
.setAcl(episodeSvcMp, AclScope.Episode, aclPresent)
.getA();
getAssetManager().takeSnapshot(mp);
} catch (MediaPackageException e) {
logger.error("Error getting ACL from media package", e);
}
},
() -> {
MediaPackage mp = getAuthorizationService().removeAcl(episodeSvcMp, AclScope.Episode);
getAssetManager().takeSnapshot(mp);
}
);
return ok();
}
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.");
JSONObject json = new JSONObject();
json.put("Error", "Unable to edit an ACL for a current workflow.");
return conflict(json.toJSONString());
} else {
MediaPackage mediaPackage = getIndexService().getEventMediapackage(optEvent.get());
mediaPackage = getAuthorizationService().setAcl(mediaPackage, AclScope.Episode, accessControlList).getA();
// We could check agent access here if we want to forbid updating ACLs for users without access.
getSchedulerService().updateEvent(eventId, Optional.empty(), Optional.empty(), Optional.empty(),
Optional.empty(), Optional.of(mediaPackage), Optional.empty(), Optional.empty());
return ok();
}
} catch (MediaPackageException e) {
if (e.getCause() instanceof UnauthorizedException) {
return forbidden();
}
logger.error("Error applying acl '{}' to event '{}'", accessControlList, eventId, e);
return serverError();
} catch (SchedulerException e) {
logger.error("Error applying ACL to scheduled event {}", eventId, 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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
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(Optional.<Long> empty(), 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, 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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
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, 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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
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, 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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
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().isEmpty() || 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, 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)
},
responses = {
@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();
}
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
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().isEmpty() || 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, 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)
},
responses = {
@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();
}
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
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(Optional.<Long> empty(), 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, e);
throw new WebApplicationException(e);
}
}
/**
* Removes emtpy series titles from the collection of the isPartOf Field
* @param ml the list to modify
*/
private void removeSeriesWithNullTitlesFromFieldCollection(MetadataList ml) {
// get Series MetadataField from MetadataList
MetadataField seriesField = Optional.ofNullable(ml.getMetadataList().get("dublincore/episode"))
.flatMap(titledMetadataCollection -> Optional.ofNullable(titledMetadataCollection.getCollection()))
.flatMap(dcMetadataCollection -> Optional.ofNullable(dcMetadataCollection.getOutputFields()))
.flatMap(metadataFields -> Optional.ofNullable(metadataFields.get("isPartOf")))
.orElse(null);
if (seriesField == null || seriesField.getCollection() == null) {
return;
}
// Remove null keys
Map<String, String> seriesCollection = seriesField.getCollection();
seriesCollection.remove(null);
seriesField.setCollection(seriesCollection);
return;
}
@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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", eventId);
}
Event event = optEvent.get();
MetadataList metadataList = new MetadataList();
// Load extended metadata
List<EventCatalogUIAdapter> extendedCatalogUIAdapters = getIndexService().getExtendedEventCatalogUIAdapters();
if (!extendedCatalogUIAdapters.isEmpty()) {
MediaPackage mediaPackage;
try {
mediaPackage = getIndexService().getEventMediapackage(event);
} catch (IndexServiceException e) {
if (e.getCause() instanceof NotFoundException) {
return notFound("Cannot find data for event %s", eventId);
} else if (e.getCause() instanceof UnauthorizedException) {
return Response.status(Status.FORBIDDEN).entity("Not authorized to access " + eventId).build();
}
logger.error("Internal error when trying to access metadata for " + eventId, e);
return serverError();
}
for (EventCatalogUIAdapter extendedCatalogUIAdapter : extendedCatalogUIAdapters) {
metadataList.add(extendedCatalogUIAdapter, extendedCatalogUIAdapter.getFields(mediaPackage));
}
}
// Load common metadata
// We do this after extended metadata because we want to overwrite any extended metadata adapters with the same
// flavor instead of the other way around.
EventCatalogUIAdapter eventCatalogUiAdapter = getIndexService().getCommonEventCatalogUIAdapter();
DublinCoreMetadataCollection metadataCollection = eventCatalogUiAdapter.getRawFields(getCollectionQueryDisable());
EventUtils.setEventMetadataValues(event, metadataCollection);
metadataList.add(eventCatalogUiAdapter, metadataCollection);
// remove series with empty titles from the collection of the isPartOf field as these can't be converted to json
removeSeriesWithNullTitlesFromFieldCollection(metadataList);
// lock metadata?
final String wfState = event.getWorkflowState();
if (wfState != null && WorkflowUtil.isActive(WorkflowInstance.WorkflowState.valueOf(wfState))) {
metadataList.setLocked(Locked.WORKFLOW_RUNNING);
}
return okJson(MetadataJson.listToJson(metadataList, true));
}
/**
* Create a special query that disables filling the collection of a series, for performance reasons.
* The collection can still be fetched via the listprovider endpoint.
*
* @return a map with resource list queries belonging to metadata fields
*/
private Map getCollectionQueryDisable() {
HashMap<String, ResourceListQuery> collectionQueryOverrides = new HashMap();
SeriesListQuery seriesListQuery = new SeriesListQuery();
seriesListQuery.setLimit(0);
collectionQueryOverrides.put(DublinCore.PROPERTY_IS_PART_OF.getLocalName(), seriesListQuery);
return collectionQueryOverrides;
}
@POST // use POST instead of GET because of a possibly long list of ids
@Path("events/metadata.json")
@Produces(MediaType.APPLICATION_JSON)
@RestQuery(
name = "geteventsmetadata",
description = "Returns all the data related to the edit events metadata modal as JSON",
returnDescription = "All the data related to the edit events metadata modal as JSON",
restParameters = {
@RestParameter(name = "eventIds", description = "The event ids", isRequired = true,
type = RestParameter.Type.STRING)
}, responses = {
@RestResponse(description = "Returns all the data related to the edit events metadata modal as JSON",
responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "No events to update, either not found or with running workflow, details in "
+ "response body.", responseCode = HttpServletResponse.SC_NOT_FOUND)
})
public Response getEventsMetadata(@FormParam("eventIds") String eventIds) throws Exception {
if (StringUtils.isBlank(eventIds)) {
return badRequest("Event ids can't be empty");
}
JSONParser parser = new JSONParser();
List<String> ids;
try {
ids = (List<String>) parser.parse(eventIds);
} catch (org.json.simple.parser.ParseException e) {
logger.error("Unable to parse '{}'", eventIds, e);
return badRequest("Unable to parse event ids");
} catch (ClassCastException e) {
logger.error("Unable to cast '{}'", eventIds, e);
return badRequest("Unable to parse event ids");
}
Set<String> eventsNotFound = new HashSet();
Set<String> eventsWithRunningWorkflow = new HashSet();
Set<String> eventsMerged = new HashSet();
// collect the metadata of all events
List<DublinCoreMetadataCollection> collectedMetadata = new ArrayList();
for (String eventId: ids) {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
// not found?
if (optEvent.isEmpty()) {
eventsNotFound.add(eventId);
continue;
}
Event event = optEvent.get();
// check if there's a running workflow
final String wfState = event.getWorkflowState();
if (wfState != null && WorkflowUtil.isActive(WorkflowInstance.WorkflowState.valueOf(wfState))) {
eventsWithRunningWorkflow.add(eventId);
continue;
}
// collect metadata
EventCatalogUIAdapter eventCatalogUiAdapter = getIndexService().getCommonEventCatalogUIAdapter();
DublinCoreMetadataCollection metadataCollection = eventCatalogUiAdapter.getRawFields(
getCollectionQueryDisable());
EventUtils.setEventMetadataValues(event, metadataCollection);
collectedMetadata.add(metadataCollection);
eventsMerged.add(eventId);
}
// no events found?
if (collectedMetadata.isEmpty()) {
JsonObject response = new JsonObject();
response.add("notFound", collectionToJsonArray(eventsNotFound));
response.add("runningWorkflow", collectionToJsonArray(eventsWithRunningWorkflow));
return Response.status(Status.NOT_FOUND).type(MediaType.APPLICATION_JSON_TYPE).build();
}
// merge metadata of events
DublinCoreMetadataCollection mergedMetadata;
if (collectedMetadata.size() == 1) {
mergedMetadata = collectedMetadata.get(0);
}
else {
//use first metadata collection as base
mergedMetadata = new DublinCoreMetadataCollection(collectedMetadata.get(0));
collectedMetadata.remove(0);
for (MetadataField field : mergedMetadata.getFields()) {
for (DublinCoreMetadataCollection otherMetadataCollection : collectedMetadata) {
MetadataField matchingField = otherMetadataCollection.getOutputFields().get(field.getOutputID());
// check if fields have the same value
if (!Objects.equals(field.getValue(), matchingField.getValue())) {
field.setDifferentValues();
break;
}
}
}
}
JsonObject result = new JsonObject();
result.add("metadata", MetadataJson.collectionToJson(mergedMetadata, true));
result.add("notFound", collectionToJsonArray(eventsNotFound));
result.add("runningWorkflow", collectionToJsonArray(eventsWithRunningWorkflow));
result.add("merged", collectionToJsonArray(eventsMerged));
return okJson(result);
}
@PUT
@Path("bulk/update")
@RestQuery(
name = "bulkupdate",
description = "Update all of the given events at once",
restParameters = {
@RestParameter(name = "update", isRequired = true, type = RestParameter.Type.TEXT,
description = "The list of groups with events and fields to update.")
}, responses = {
@RestResponse(description = "All events have been updated successfully.",
responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "Could not parse update instructions.",
responseCode = HttpServletResponse.SC_BAD_REQUEST),
@RestResponse(description = "Field updating metadata or scheduling information. Some events may have been "
+ "updated. Details are available in the response body.",
responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR),
@RestResponse(description = "The events in the response body were not found. No events were updated.",
responseCode = HttpServletResponse.SC_NOT_FOUND)
},
returnDescription = "In case of success, no content is returned. In case of errors while updating the metadata "
+ "or scheduling information, the errors are returned. In case events were not found, their ids are returned")
public Response bulkUpdate(@FormParam("update") String updateJson) {
final BulkUpdateUtil.BulkUpdateInstructions instructions;
try {
instructions = new BulkUpdateUtil.BulkUpdateInstructions(updateJson);
} catch (IllegalArgumentException e) {
return badRequest("Cannot parse bulk update instructions");
}
final Map<String, String> metadataUpdateFailures = new HashMap<>();
final Map<String, String> schedulingUpdateFailures = new HashMap<>();
for (final BulkUpdateUtil.BulkUpdateInstructionGroup groupInstructions : instructions.getGroups()) {
// Get all the events to edit
final Map<String, Optional<Event>> events = groupInstructions.getEventIds().stream()
.collect(Collectors.toMap(id -> id, id -> BulkUpdateUtil.getEvent(getIndexService(), getIndex(), id)));
// Check for invalid (non-existing) event ids
final Set<String> notFoundIds = events.entrySet().stream()
.filter(e -> !e.getValue().isPresent())
.map(Entry::getKey)
.collect(Collectors.toSet());
if (!notFoundIds.isEmpty()) {
return notFoundJson(collectionToJsonArray(notFoundIds));
}
events.values().forEach(e -> e.ifPresent(event -> {
JSONObject metadata = null;
// Update the scheduling information
try {
if (groupInstructions.getScheduling() != null) {
// Since we only have the start/end time, we have to add the correct date(s) for this event.
final JSONObject scheduling = BulkUpdateUtil.addSchedulingDates(event, groupInstructions.getScheduling());
updateEventScheduling(scheduling.toJSONString(), event);
// We have to update the non-technical metadata as well to keep them in sync with the technical ones.
metadata = BulkUpdateUtil.toNonTechnicalMetadataJson(scheduling);
}
} catch (Exception exception) {
schedulingUpdateFailures.put(event.getIdentifier(), exception.getMessage());
}
// Update the event metadata
try {
if (groupInstructions.getMetadata() != null || metadata != null) {
metadata = BulkUpdateUtil.mergeMetadataFields(metadata, groupInstructions.getMetadata());
getIndexService().updateAllEventMetadata(event.getIdentifier(),
JSONArray.toJSONString(Collections.singletonList(metadata)), getIndex());
}
} catch (Exception exception) {
metadataUpdateFailures.put(event.getIdentifier(), exception.getMessage());
}
}));
}
// Check if there were any errors updating the metadata or scheduling information
if (!metadataUpdateFailures.isEmpty() || !schedulingUpdateFailures.isEmpty()) {
JsonObject json = new JsonObject();
json.add("metadataFailures", mapToJsonObject(metadataUpdateFailures));
json.add("schedulingFailures", mapToJsonObject(schedulingUpdateFailures));
return serverErrorJson(json);
}
return ok();
}
@POST
@Path("bulk/conflicts")
@RestQuery(
name = "getBulkConflicts",
description = "Checks if the current bulk update scheduling settings 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 = "update", isRequired = true, type = RestParameter.Type.TEXT, description = "The list "
+ "of events and fields to update.")
},
responses = {
@RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "No conflicting events found"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "The events in the response "
+ "body were not found. No events were updated."),
@RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "There is a conflict"),
@RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "Missing or invalid "
+ "parameters")
})
public Response getBulkConflicts(@FormParam("update") final String updateJson) throws NotFoundException {
final BulkUpdateUtil.BulkUpdateInstructions instructions;
try {
instructions = new BulkUpdateUtil.BulkUpdateInstructions(updateJson);
} catch (IllegalArgumentException e) {
return badRequest("Cannot parse bulk update instructions");
}
final Map<String, List<JsonObject>> conflicts = new HashMap<>();
final List<Tuple3<String, Optional<Event>, JSONObject>> eventsWithSchedulingOpt = instructions.getGroups().stream()
.flatMap(group -> group.getEventIds().stream().map(eventId -> Tuple3
.tuple3(eventId, BulkUpdateUtil.getEvent(getIndexService(), getIndex(), eventId), group.getScheduling())))
.collect(Collectors.toList());
// Check for invalid (non-existing) event ids
final Set<String> notFoundIds = eventsWithSchedulingOpt.stream().filter(e -> !e.getB().isPresent())
.map(Tuple3::getA).collect(Collectors.toSet());
if (!notFoundIds.isEmpty()) {
return notFoundJson(collectionToJsonArray(notFoundIds));
}
final List<Tuple<Event, JSONObject>> eventsWithScheduling = eventsWithSchedulingOpt.stream()
.map(e -> Tuple.tuple(e.getB().get(), e.getC())).collect(Collectors.toList());
final Set<String> changedIds = eventsWithScheduling.stream().map(e -> e.getA().getIdentifier())
.collect(Collectors.toSet());
for (final Tuple<Event, JSONObject> eventWithGroup : eventsWithScheduling) {
final Event event = eventWithGroup.getA();
final JSONObject groupScheduling = eventWithGroup.getB();
try {
if (groupScheduling != null) {
// Since we only have the start/end time, we have to add the correct date(s) for this event.
final JSONObject scheduling = BulkUpdateUtil.addSchedulingDates(event, groupScheduling);
final Date start = Date.from(Instant.parse((String) scheduling.get(SCHEDULING_START_KEY)));
final Date end = Date.from(Instant.parse((String) scheduling.get(SCHEDULING_END_KEY)));
final String agentId = Optional.ofNullable((String) scheduling.get(SCHEDULING_AGENT_ID_KEY))
.orElse(event.getAgentId());
final List<JsonObject> currentConflicts = new ArrayList<>();
// Check for conflicts between the events themselves
eventsWithScheduling.stream()
.filter(otherEvent -> !otherEvent.getA().getIdentifier().equals(event.getIdentifier()))
.forEach(otherEvent -> {
final JSONObject otherScheduling = BulkUpdateUtil.addSchedulingDates(otherEvent.getA(),
otherEvent.getB());
final Date otherStart = Date.from(Instant.parse((String) otherScheduling.get(SCHEDULING_START_KEY)));
final Date otherEnd = Date.from(Instant.parse((String) otherScheduling.get(SCHEDULING_END_KEY)));
final String otherAgentId = Optional.ofNullable((String) otherScheduling.get(SCHEDULING_AGENT_ID_KEY))
.orElse(otherEvent.getA().getAgentId());
if (!otherAgentId.equals(agentId)) {
// different agent -> no conflict
return;
}
if (Util.schedulingIntervalsOverlap(start, end, otherStart, otherEnd)) {
// conflict
currentConflicts.add(convertEventToConflictingObject(
DateTimeSupport.toUTC(otherStart.getTime()),
DateTimeSupport.toUTC(otherEnd.getTime()),
otherEvent.getA().getTitle()));
}
});
// Check for conflicts with other events from the database
final List<MediaPackage> conflicting = getSchedulerService().findConflictingEvents(agentId, start, end)
.stream()
.filter(mp -> !changedIds.contains(mp.getIdentifier().toString()))
.collect(Collectors.toList());
if (!conflicting.isEmpty()) {
currentConflicts.addAll(convertToConflictObjects(event.getIdentifier(), conflicting));
}
conflicts.put(event.getIdentifier(), currentConflicts);
}
} catch (final SchedulerException | UnauthorizedException | SearchIndexException exception) {
throw new RuntimeException(exception);
}
}
if (!conflicts.isEmpty()) {
JsonArray responseJson = new JsonArray();
conflicts.forEach((eventId, conflictingEvents) -> {
if (!conflictingEvents.isEmpty()) {
JsonObject obj = new JsonObject();
obj.addProperty("eventId", eventId);
JsonArray conflictsArray = new JsonArray();
for (JsonObject conflict : conflictingEvents) {
conflictsArray.add(conflict);
}
obj.add("conflicts", conflictsArray);
responseJson.add(obj);
}
});
if (responseJson.size() > 0) {
return conflictJson(responseJson);
}
}
return noContent();
}
@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")
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", id);
}
try {
MetadataList metadataList = getIndexService().updateAllEventMetadata(id, metadataJSON, getIndex());
return okJson(MetadataJson.listToJson(metadataList, true));
} catch (IllegalArgumentException e) {
return badRequest(String.format("Event %s metadata can't be updated.: %s", id, e.getMessage()));
}
}
@PUT
@Path("events/metadata")
@RestQuery(
name = "updateeventsmetadata",
description = "Update the passed metadata for the events with the given ids",
restParameters = {
@RestParameter(name = "eventIds", isRequired = true, type = RestParameter.Type.STRING,
description = "The ids of the events to update"),
@RestParameter(name = "metadata", isRequired = true, type = RestParameter.Type.TEXT,
description = "The metadata fields to update"),
},
responses = {
@RestResponse(description = "All events have been updated successfully.",
responseCode = HttpServletResponse.SC_NO_CONTENT),
@RestResponse(description = "One or multiple errors occured while updating event metadata. "
+ "Some events may have been updated successfully. "
+ "Details are available in the response body.",
responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
},
returnDescription = "In case of complete success, no content is returned. Otherwise, the response content "
+ "contains the ids of events that couldn't be found and the ids and errors of events where the update "
+ "failed as well as the ids of the events that were updated successfully.")
public Response updateEventsMetadata(@FormParam("eventIds") String eventIds, @FormParam("metadata") String metadata)
throws Exception {
if (StringUtils.isBlank(eventIds)) {
return badRequest("Event ids can't be empty");
}
JSONParser parser = new JSONParser();
List<String> ids;
try {
ids = (List<String>) parser.parse(eventIds);
} catch (org.json.simple.parser.ParseException e) {
logger.error("Unable to parse '{}'", eventIds, e);
return badRequest("Unable to parse event ids");
} catch (ClassCastException e) {
logger.error("Unable to cast '{}'", eventIds, e);
return badRequest("Unable to parse event ids");
}
// try to update each event
Set<String> eventsNotFound = new HashSet<>();
Set<String> eventsUpdated = new HashSet<>();
Set<String> eventsUpdateFailure = new HashSet();
for (String eventId : ids) {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
// not found?
if (optEvent.isEmpty()) {
eventsNotFound.add(eventId);
continue;
}
// update
try {
getIndexService().updateAllEventMetadata(eventId, metadata, getIndex());
eventsUpdated.add(eventId);
} catch (IllegalArgumentException e) {
eventsUpdateFailure.add(eventId);
}
}
// errors occurred?
if (!eventsNotFound.isEmpty() || !eventsUpdateFailure.isEmpty()) {
JsonObject errorJson = new JsonObject();
errorJson.add("updateFailures", collectionToJsonArray(eventsUpdateFailure));
errorJson.add("notFound", collectionToJsonArray(eventsNotFound));
errorJson.add("updated", collectionToJsonArray(eventsUpdated));
return serverErrorJson(errorJson);
}
return noContent();
}
@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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", id);
}
MediaPackage mp;
try {
mp = getIndexService().getEventMediapackage(optEvent.get());
} catch (IndexServiceException e) {
if (e.getCause() instanceof NotFoundException) {
return notFound("Cannot find data for event %s", id);
} else if (e.getCause() instanceof UnauthorizedException) {
return Response.status(Status.FORBIDDEN).entity("Not authorized to access " + id).build();
}
logger.error("Internal error when trying to access metadata for " + id, e);
return serverError();
}
int attachments = mp.getAttachments().length;
int catalogs = mp.getCatalogs().length;
int media = mp.getTracks().length;
int publications = mp.getPublications().length;
JsonObject result = new JsonObject();
result.addProperty("attachments", attachments);
result.addProperty("catalogs", catalogs);
result.addProperty("media", media);
result.addProperty("publications", publications);
return okJson(result);
}
@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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", id);
}
MediaPackage mp = getIndexService().getEventMediapackage(optEvent.get());
return okJson(getEventMediaPackageElements(mp.getAttachments()));
}
@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)
},
responses = {
@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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", id);
}
MediaPackage mp = getIndexService().getEventMediapackage(optEvent.get());
return okJson(getEventMediaPackageElements(mp.getCatalogs()));
}
@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)
},
responses = {
@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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", id);
}
MediaPackage mp = getIndexService().getEventMediapackage(optEvent.get());
return okJson(getEventMediaPackageElements(mp.getTracks()));
}
@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)
},
responses = {
@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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", id);
}
MediaPackage mp = getIndexService().getEventMediapackage(optEvent.get());
return okJson(getEventPublications(mp.getPublications()));
}
@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)
},
responses = {
@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}/tobira/pages")
@RestQuery(
name = "getEventHostPages",
description = "Returns the pages of a connected Tobira instance that contain the given event",
returnDescription = "The Tobira pages that contain the given event",
pathParameters = {
@RestParameter(
name = "eventId",
isRequired = true,
description = "The event identifier",
type = STRING
),
},
responses = {
@RestResponse(
responseCode = SC_OK,
description = "The Tobira pages containing the given event"
),
@RestResponse(
responseCode = SC_NOT_FOUND,
description = "Tobira doesn't know about the given event"
),
@RestResponse(
responseCode = SC_SERVICE_UNAVAILABLE,
description = "Tobira is not configured (correctly)"
),
}
)
public Response getEventHostPages(@PathParam("eventId") String eventId) {
var tobira = TobiraService.getTobira(getSecurityService().getOrganization().getId());
if (!tobira.ready()) {
return Response.status(Status.SERVICE_UNAVAILABLE)
.entity("Tobira is not configured (correctly)")
.build();
}
try {
var eventData = tobira.getEventHostPages(eventId);
if (eventData == null) {
throw new WebApplicationException(NOT_FOUND);
}
eventData.put("baseURL", tobira.getOrigin());
return Response.ok(eventData.toJSONString()).build();
} catch (TobiraException e) {
throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
}
}
@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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", id);
}
try {
if (optEvent.get().getEventStatus().equals("EVENTS.EVENTS.STATUS.SCHEDULED")) {
Map<String, String> workflowConfig = getSchedulerService().getWorkflowConfig(id);
JsonObject configJson = new JsonObject();
for (Map.Entry<String, String> entry : workflowConfig.entrySet()) {
configJson.addProperty(entry.getKey(), safeString(entry.getValue()));
}
Map<String, String> agentConfiguration = getSchedulerService().getCaptureAgentConfiguration(id);
JsonObject responseJson = new JsonObject();
responseJson.addProperty("workflowId", agentConfiguration.getOrDefault(
CaptureParameters.INGEST_WORKFLOW_DEFINITION, ""));
responseJson.add("configuration", configJson);
return okJson(responseJson);
} else {
List<WorkflowInstance> workflowInstances = getWorkflowService().getWorkflowInstancesByMediaPackage(id);
JsonArray jsonArray = new JsonArray();
for (WorkflowInstance instance : workflowInstances) {
JsonObject instanceJson = new JsonObject();
instanceJson.addProperty("id", instance.getId());
instanceJson.addProperty("title", safeString(instance.getTitle()));
instanceJson.addProperty("status", WORKFLOW_STATUS_TRANSLATION_PREFIX + instance.getState().toString());
Date created = instance.getDateCreated();
instanceJson.addProperty("submitted", created != null ? DateTimeSupport.toUTC(created.getTime()) : "");
String submitter = instance.getCreatorName();
instanceJson.addProperty("submitter", safeString(submitter));
User user = submitter == null ? null : getUserDirectoryService().loadUser(submitter);
String submitterName = null;
String submitterEmail = null;
if (user != null) {
submitterName = user.getName();
submitterEmail = user.getEmail();
}
instanceJson.addProperty("submitterName", safeString(submitterName));
instanceJson.addProperty("submitterEmail", safeString(submitterEmail));
jsonArray.add(instanceJson);
}
JsonObject result = new JsonObject();
result.add("results", jsonArray);
result.addProperty("count", workflowInstances.size());
return okJson(result);
}
} catch (NotFoundException e) {
return notFound("Cannot find workflows for event %s", id);
} catch (SchedulerException e) {
logger.error("Unable to get workflow data for event with id {}", id);
throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
} catch (WorkflowDatabaseException e) {
throw new JobEndpointException(String.format("Not able to get the list of job from the database: %s", e),
e.getCause());
}
}
@PUT
@Path("{eventId}/workflows")
@RestQuery(
name = "updateEventWorkflow",
description = "Update the workflow configuration for the scheduled event with the given id",
pathParameters = {
@RestParameter(name = "eventId", description = "The event id", isRequired = true,
type = RestParameter.Type.STRING)
},
restParameters = {
@RestParameter(name = "configuration", isRequired = true, description = "The workflow configuration as JSON",
type = RestParameter.Type.TEXT)
},
responses = {
@RestResponse(description = "Request executed succesfully", responseCode = HttpServletResponse.SC_NO_CONTENT),
@RestResponse(description = "No event with this identifier was found.",
responseCode = HttpServletResponse.SC_NOT_FOUND)
},
returnDescription = "The method does not retrun any content.")
public Response updateEventWorkflow(@PathParam("eventId") String id, @FormParam("configuration") String configuration)
throws SearchIndexException, UnauthorizedException {
Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", id);
}
if (optEvent.get().isScheduledEvent() && !optEvent.get().hasRecordingStarted()) {
try {
JSONObject configJSON;
try {
configJSON = (JSONObject) new JSONParser().parse(configuration);
} catch (Exception e) {
logger.warn("Unable to parse the workflow configuration {}", configuration);
return badRequest();
}
Optional<Map<String, String>> caMetadataOpt = Optional.empty();
Optional<Map<String, String>> workflowConfigOpt = Optional.empty();
String workflowId = (String) configJSON.get("id");
Map<String, String> caMetadata = new HashMap<>(getSchedulerService().getCaptureAgentConfiguration(id));
if (!workflowId.equals(caMetadata.get(CaptureParameters.INGEST_WORKFLOW_DEFINITION))) {
caMetadata.put(CaptureParameters.INGEST_WORKFLOW_DEFINITION, workflowId);
caMetadataOpt = Optional.of(caMetadata);
}
Map<String, String> workflowConfig = new HashMap<>((JSONObject) configJSON.get("configuration"));
Map<String, String> oldWorkflowConfig = new HashMap<>(getSchedulerService().getWorkflowConfig(id));
if (!oldWorkflowConfig.equals(workflowConfig)) {
workflowConfigOpt = Optional.of(workflowConfig);
}
if (caMetadataOpt.isEmpty() && workflowConfigOpt.isEmpty()) {
return Response.noContent().build();
}
checkAgentAccessForAgent(optEvent.get().getAgentId());
getSchedulerService().updateEvent(id, Optional.empty(), Optional.empty(), Optional.empty(),
Optional.empty(), Optional.empty(), workflowConfigOpt, caMetadataOpt);
return Response.noContent().build();
} catch (NotFoundException e) {
return notFound("Cannot find event %s in scheduler service", id);
} catch (SchedulerException e) {
logger.error("Unable to update scheduling workflow data for event with id {}", id);
throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
}
} else {
return badRequest(String.format("Event %s workflow can not be updated as the recording already started.", 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)
},
responses = {
@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 SearchIndexException {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
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 {
WorkflowInstance instance = getWorkflowService().getWorkflowById(workflowInstanceId);
// Retrieve submission date with the workflow instance main job
Date created = instance.getDateCreated();
Date completed = instance.getDateCompleted();
if (completed == null) {
completed = new Date();
}
long executionTime = completed.getTime() - created.getTime();
JsonObject configurationObj = new JsonObject();
for (Entry<String, String> entry : instance.getConfigurations().entrySet()) {
configurationObj.addProperty(entry.getKey(), safeString(entry.getValue()));
}
JsonObject json = new JsonObject();
json.addProperty("status", WORKFLOW_STATUS_TRANSLATION_PREFIX + instance.getState());
json.addProperty("description", safeString(instance.getDescription()));
json.addProperty("executionTime", executionTime);
json.addProperty("wiid", instance.getId());
json.addProperty("title", safeString(instance.getTitle()));
json.addProperty("wdid", safeString(instance.getTemplate()));
if (!configurationObj.isEmpty()) {
json.add("configuration", configurationObj);
}
json.addProperty("submittedAt", DateTimeSupport.toUTC(created.getTime()));
json.addProperty("creator", safeString(instance.getCreatorName()));
return okJson(json);
} catch (NotFoundException e) {
return notFound("Cannot find workflow %s", workflowId);
} catch (WorkflowDatabaseException e) {
logger.error("Unable to get workflow {} of event {}", workflowId, eventId, e);
return serverError();
} catch (UnauthorizedException e) {
return forbidden();
}
}
@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)
},
responses = {
@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 SearchIndexException {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
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 {
WorkflowInstance instance = getWorkflowService().getWorkflowById(workflowInstanceId);
List<WorkflowOperationInstance> operations = instance.getOperations();
JsonArray operationsJsonArray = new JsonArray();
for (WorkflowOperationInstance wflOp : operations) {
JsonObject operationJson = new JsonObject();
operationJson.addProperty("status", WORKFLOW_STATUS_TRANSLATION_PREFIX + wflOp.getState());
operationJson.addProperty("title", safeString(wflOp.getTemplate()));
operationJson.addProperty("description", safeString(wflOp.getDescription()));
operationJson.addProperty("id", wflOp.getId());
if (!wflOp.getConfigurationKeys().isEmpty()) {
operationJson.add("configuration", collectionToJsonArray(wflOp.getConfigurationKeys()));
}
operationsJsonArray.add(operationJson);
}
return okJson(operationsJsonArray);
} catch (NotFoundException e) {
return notFound("Cannot find workflow %s", workflowId);
} catch (WorkflowDatabaseException e) {
logger.error("Unable to get workflow operations of event {} and workflow {}", eventId, workflowId, e);
return serverError();
} catch (UnauthorizedException e) {
return forbidden();
}
}
@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)
},
responses = {
@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 SearchIndexException {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
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();
}
WorkflowInstance instance;
try {
instance = getWorkflowService().getWorkflowById(workflowInstanceId);
} catch (NotFoundException e) {
return notFound("Cannot find workflow %s", workflowId);
} catch (WorkflowDatabaseException e) {
logger.error("Unable to get workflow operation of event {} and workflow {} at position {}", eventId, workflowId,
operationPosition, e);
return serverError();
} catch (UnauthorizedException e) {
return forbidden();
}
List<WorkflowOperationInstance> operations = instance.getOperations();
if (operationPosition < operations.size()) {
WorkflowOperationInstance wflOp = operations.get(operationPosition);
JsonObject json = new JsonObject();
json.addProperty("retry_strategy", wflOp.getRetryStrategy() != null ? wflOp.getRetryStrategy().toString() : "");
json.addProperty("execution_host", safeString(wflOp.getExecutionHost()));
json.addProperty("failed_attempts", wflOp.getFailedAttempts());
json.addProperty("max_attempts", wflOp.getMaxAttempts());
json.addProperty("exception_handler_workflow", safeString(wflOp.getExceptionHandlingWorkflow()));
json.addProperty("fail_on_error", wflOp.isFailOnError());
json.addProperty("description", safeString(wflOp.getDescription()));
json.addProperty("state", WORKFLOW_STATUS_TRANSLATION_PREFIX + wflOp.getState());
json.addProperty("job", wflOp.getId());
json.addProperty("name", safeString(wflOp.getTemplate()));
json.addProperty("time_in_queue", wflOp.getTimeInQueue() != null ? wflOp.getTimeInQueue() : 0);
json.addProperty("started", wflOp.getDateStarted() != null ? toUTC(wflOp.getDateStarted().getTime()) : "");
json.addProperty("completed", wflOp.getDateCompleted() != null ? toUTC(wflOp.getDateCompleted().getTime()) : "");
return okJson(json);
}
return notFound("Cannot find workflow operation of workflow %s at position %s", workflowId, operationPosition);
}
@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)
},
responses = {
@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.
Optional<Event> eventOpt = getIndexService().getEvent(eventId, getIndex());
if (eventOpt.isPresent()) {
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)
},
responses = {
@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.
Optional<Event> eventOpt = getIndexService().getEvent(eventId, getIndex());
if (eventOpt.isPresent()) {
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)
},
responses = {
@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 {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
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));
}
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", e);
}
Optional<ManagedAcl> currentAcl = AccessInformationUtil.matchAclsLenient(acls, activeAcl,
getAdminUIConfiguration().getMatchManagedAclRolePrefixes());
JSONObject episodeAccessJson = new JSONObject();
episodeAccessJson.put("current_acl", currentAcl.isPresent() ? currentAcl.get().getId() : 0L);
episodeAccessJson.put("acl", transformAccessControList(activeAcl, getUserDirectoryService()));
episodeAccessJson.put("privileges", AccessInformationUtil.serializePrivilegesByRole(activeAcl));
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();
}
// MH-12085 Add manually uploaded assets, multipart file upload has to be a POST
@POST
@Path("{eventId}/assets")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@RestQuery(
name = "updateAssets",
description = "Update or create an asset for the eventId by the given metadata as JSON and files in the body",
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 asset metadata")
},
responses = {
@RestResponse(description = "The asset has been added.", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "Could not add asset, problem with the metadata or files.",
responseCode = HttpServletResponse.SC_BAD_REQUEST),
@RestResponse(description = "No event with this identifier was found.",
responseCode = HttpServletResponse.SC_NOT_FOUND)
},
returnDescription = "The workflow identifier")
public Response updateAssets(@PathParam("eventId") final String eventId,
@Context HttpServletRequest request) throws Exception {
try {
MediaPackage mp = getMediaPackageByEventId(eventId);
String result = getIndexService().updateEventAssets(mp, request);
return Response.status(Status.CREATED).entity(result).build();
} catch (NotFoundException e) {
return notFound("Cannot find an event with id '%s'.", eventId);
} catch (IllegalArgumentException | UnsupportedAssetException e) {
return RestUtil.R.badRequest(e.getMessage());
} catch (Exception e) {
return RestUtil.R.serverError();
}
}
@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",
responses = {
@RestResponse(responseCode = SC_OK, description = "Returns all the data related to the event metadata tab as "
+ "JSON")
})
public Response getNewMetadata() {
MetadataList metadataList = new MetadataList();
// Extended metadata
List<EventCatalogUIAdapter> extendedCatalogUIAdapters = getIndexService().getExtendedEventCatalogUIAdapters();
for (EventCatalogUIAdapter extendedCatalogUIAdapter : extendedCatalogUIAdapters) {
metadataList.add(extendedCatalogUIAdapter, extendedCatalogUIAdapter.getRawFields());
}
// Common metadata
// We do this after extended metadata because we want to overwrite any extended metadata adapters with the same
// flavor instead of the other way around.
EventCatalogUIAdapter commonCatalogUiAdapter = getIndexService().getCommonEventCatalogUIAdapter();
DublinCoreMetadataCollection commonMetadata = commonCatalogUiAdapter.getRawFields(getCollectionQueryDisable());
if (commonMetadata.getOutputFields().containsKey(DublinCore.PROPERTY_CREATED.getLocalName())) {
commonMetadata.removeField(commonMetadata.getOutputFields().get(DublinCore.PROPERTY_CREATED.getLocalName()));
}
if (commonMetadata.getOutputFields().containsKey("duration")) {
commonMetadata.removeField(commonMetadata.getOutputFields().get("duration"));
}
if (commonMetadata.getOutputFields().containsKey(DublinCore.PROPERTY_IDENTIFIER.getLocalName())) {
commonMetadata.removeField(commonMetadata.getOutputFields().get(DublinCore.PROPERTY_IDENTIFIER.getLocalName()));
}
if (commonMetadata.getOutputFields().containsKey(DublinCore.PROPERTY_SOURCE.getLocalName())) {
commonMetadata.removeField(commonMetadata.getOutputFields().get(DublinCore.PROPERTY_SOURCE.getLocalName()));
}
if (commonMetadata.getOutputFields().containsKey("startDate")) {
commonMetadata.removeField(commonMetadata.getOutputFields().get("startDate"));
}
if (commonMetadata.getOutputFields().containsKey("startTime")) {
commonMetadata.removeField(commonMetadata.getOutputFields().get("startTime"));
}
if (commonMetadata.getOutputFields().containsKey("location")) {
commonMetadata.removeField(commonMetadata.getOutputFields().get("location"));
}
// Set publisher to user
if (commonMetadata.getOutputFields().containsKey(DublinCore.PROPERTY_PUBLISHER.getLocalName())) {
MetadataField publisher = commonMetadata.getOutputFields().get(DublinCore.PROPERTY_PUBLISHER.getLocalName());
Map<String, String> users = new HashMap<>();
if (publisher.getCollection() != null) {
users = publisher.getCollection();
}
String loggedInUser = getSecurityService().getUser().getName();
if (!users.containsKey(loggedInUser)) {
users.put(loggedInUser, loggedInUser);
}
publisher.setValue(loggedInUser);
}
metadataList.add(commonCatalogUiAdapter, commonMetadata);
// remove series with empty titles from the collection of the isPartOf field as these can't be converted to json
removeSeriesWithNullTitlesFromFieldCollection(metadataList);
return okJson(MetadataJson.listToJson(metadataList, true));
}
@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)
},
responses = {
@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(Optional.ofNullable(tagsString));
JsonArray workflowsArray = new JsonArray();
try {
List<WorkflowDefinition> workflowsDefinitions = getWorkflowService().listAvailableWorkflowDefinitions();
for (WorkflowDefinition wflDef : workflowsDefinitions) {
if (wflDef.containsTag(tags)) {
JsonObject wfJson = new JsonObject();
wfJson.addProperty("id", wflDef.getId());
wfJson.add("tags", arrayToJsonArray(wflDef.getTags()));
wfJson.addProperty("title", safeString(wflDef.getTitle()));
wfJson.addProperty("description", safeString(wflDef.getDescription()));
wfJson.addProperty("displayOrder", wflDef.getDisplayOrder());
wfJson.addProperty("configuration_panel", safeString(wflDef.getConfigurationPanel()));
wfJson.addProperty("configuration_panel_json", safeString(wflDef.getConfigurationPanelJson()));
workflowsArray.add(wfJson);
}
}
} catch (WorkflowDatabaseException e) {
logger.error("Unable to get available workflow definitions", e);
return RestUtil.R.serverError();
}
JsonObject data = new JsonObject();
data.add("workflows", workflowsArray);
data.addProperty("default_workflow_id", defaultWorkflowDefinionId);
return okJson(data);
}
@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)
},
responses = {
@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();
}
JSONParser parser = new JSONParser();
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 {
List<MediaPackage> events = null;
if (StringUtils.isNotEmpty(rruleString)) {
events = getSchedulerService().findConflictingEvents(device, rrule, start, end, Long.parseLong(durationString),
timeZone);
} else {
events = getSchedulerService().findConflictingEvents(device, start, end);
}
if (!events.isEmpty()) {
final List<JsonObject> eventsJSON = convertToConflictObjects(eventId, events);
if (!eventsJSON.isEmpty()) {
JsonArray jsonArray = new JsonArray();
for (JsonObject jsonObj : eventsJSON) {
jsonArray.add(jsonObj);
}
return conflictJson(jsonArray);
}
}
return Response.noContent().build();
} catch (Exception e) {
logger.error("Unable to find conflicting events for {}, {}, {}",
device, startDate, endDate, e);
return RestUtil.R.serverError();
}
}
private List<JsonObject> convertToConflictObjects(final String eventId, final List<MediaPackage> events)
throws SearchIndexException {
final List<JsonObject> eventsJSON = new ArrayList<>();
final Organization organization = getSecurityService().getOrganization();
final User user = SecurityUtil.createSystemUser(systemUserName, organization);
SecurityUtil.runAs(getSecurityService(), organization, user, () -> {
try {
for (final MediaPackage event : events) {
final Optional<Event> eventOpt = getIndexService().getEvent(event.getIdentifier().toString(), getIndex());
if (eventOpt.isPresent()) {
final Event e = eventOpt.get();
if (StringUtils.isNotEmpty(eventId) && eventId.equals(e.getIdentifier())) {
continue;
}
eventsJSON.add(convertEventToConflictingObject(e.getTechnicalStartTime(), e.getTechnicalEndTime(),
e.getTitle()));
} else {
logger.warn("Index out of sync! Conflicting event catalog {} not found on event index!",
event.getIdentifier().toString());
}
}
} catch (Exception e) {
logger.error("Failed to get conflicting events", e);
}
});
return eventsJSON;
}
private JsonObject convertEventToConflictingObject(final String start, final String end, final String title) {
JsonObject json = new JsonObject();
json.addProperty("start", start);
json.addProperty("end", end);
json.addProperty("title", title);
return json;
}
@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)
},
responses = {
@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);
if (StringUtils.isEmpty(result)) {
return RestUtil.R.badRequest("The date range provided did not include any events");
}
return Response.status(Status.CREATED).entity(result).build();
} catch (IllegalArgumentException | UnsupportedAssetException 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),
@RestParameter(name = "getComments", description = "If comments should be fetched", isRequired = false,
type = RestParameter.Type.BOOLEAN)
},
responses = {
@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,
@QueryParam("getComments") Boolean getComments) {
Optional<Integer> optLimit = Optional.ofNullable(limit);
Optional<Integer> optOffset = Optional.ofNullable(offset);
Optional<String> optSort = Optional.ofNullable(trimToNull(sort));
Optional<Boolean> optGetComments = Optional.ofNullable(getComments);
List<JsonObject> eventsList = new ArrayList<>();
final Organization organization = getSecurityService().getOrganization();
final User user = getSecurityService().getUser();
if (organization == null || user == null) {
return Response.status(SC_SERVICE_UNAVAILABLE).build();
}
EventSearchQuery query = new EventSearchQuery(organization.getId(), user);
// If the limit is set to 0, this is not taken into account
if (optLimit.isPresent() && limit == 0) {
optLimit = Optional.empty();
}
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_LANGUAGE_NAME.equals(name)) {
query.withLanguage(filters.get(name));
}
if (EventListQuery.FILTER_AGENT_NAME.equals(name)) {
query.withAgentId(filters.get(name));
}
if (EventListQuery.FILTER_TEXT_NAME.equals(name)) {
query.withText(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_PUBLISHER_NAME.equals(name)) {
query.withPublisher(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_IS_PUBLISHED_NAME.equals(name)) {
if (filters.containsKey(name)) {
switch (IsPublished.valueOf(filters.get(name))) {
case YES:
query.withIsPublished(true);
break;
case NO:
query.withIsPublished(false);
break;
default:
break;
}
} else {
logger.info("Query for invalid published status: {}", 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.isPresent()) {
ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
for (SortCriterion criterion : sortCriteria) {
switch (criterion.getFieldName()) {
case EventIndexSchema.UID:
query.sortByUID(criterion.getOrder());
break;
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:
final String msg = String.format("Unknown sort criteria field %s", criterion.getFieldName());
logger.debug(msg);
return RestUtil.R.badRequest(msg);
}
}
}
// We search for write actions
if (getOnlyEventsWithWriteAccessEventsTab()) {
query.withoutActions();
query.withAction(Permissions.Action.WRITE);
query.withAction(Permissions.Action.READ);
}
if (optLimit.isPresent()) {
query.withLimit(optLimit.get());
}
if (optOffset.isPresent()) {
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, Optional.ofNullable(offset).orElse(0), Optional.ofNullable(limit).orElse(0), 0);
}
Map<String, String> languages;
try {
languages = getListProvidersService().getList("LANGUAGES", new ResourceListQueryImpl(), false);
} catch (ListProviderException e) {
logger.info("Could not get languages from listprovider");
throw new WebApplicationException(e);
}
for (SearchResultItem<Event> item : results.getItems()) {
Event source = item.getSource();
source.updatePreview(getAdminUIConfiguration().getPreviewSubtype());
List<EventComment> comments = null;
if (optGetComments.isPresent() && optGetComments.get()) {
try {
comments = getEventCommentService().getComments(source.getIdentifier());
} catch (EventCommentException e) {
logger.error("Unable to get comments from event {}", source.getIdentifier(), e);
throw new WebApplicationException(e);
}
}
eventsList.add(eventToJSON(source, Optional.ofNullable(comments), Optional.ofNullable(languages)));
}
return okJsonList(eventsList, Optional.ofNullable(offset).orElse(0), Optional.ofNullable(limit).orElse(0),
results.getHitCount());
}
// --
private MediaPackage getMediaPackageByEventId(String eventId)
throws SearchIndexException, NotFoundException, IndexServiceException {
Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
if (optEvent.isEmpty()) {
throw new NotFoundException(format("Cannot find an event with id '%s'.", eventId));
}
return getIndexService().getEventMediapackage(optEvent.get());
}
private URI getCommentUrl(String eventId, long commentId) {
return UrlSupport.uri(serverUrl, eventId, "comment", Long.toString(commentId));
}
private JsonObject eventToJSON(Event event, Optional<List<EventComment>> comments,
Optional<Map<String, String>> languages) {
JsonObject json = new JsonObject();
json.addProperty("id", event.getIdentifier());
json.addProperty("title", event.getTitle() != null ? event.getTitle() : "");
json.addProperty("source", event.getSource() != null ? event.getSource() : "");
json.add("presenters", collectionToJsonArray(event.getPresenters()));
if (StringUtils.isNotBlank(event.getSeriesId())) {
JsonObject seriesObj = new JsonObject();
seriesObj.addProperty("id", event.getSeriesId() != null ? event.getSeriesId() : "");
seriesObj.addProperty("title", event.getSeriesName() != null ? event.getSeriesName() : "");
json.add("series", seriesObj);
}
json.addProperty("location", safeString(event.getLocation()));
json.addProperty("start_date", safeString(event.getRecordingStartDate()));
json.addProperty("end_date", safeString(event.getRecordingEndDate()));
json.addProperty("managedAcl", safeString(event.getManagedAcl()));
json.addProperty("workflow_state", safeString(event.getWorkflowState()));
json.addProperty("event_status", event.getEventStatus());
json.addProperty("displayable_status", event.getDisplayableStatus(getWorkflowService().getWorkflowStateMappings()));
json.addProperty("source", getIndexService().getEventSource(event).toString());
json.addProperty("has_comments", event.hasComments());
json.addProperty("has_open_comments", event.hasOpenComments());
json.addProperty("needs_cutting", event.needsCutting());
json.addProperty("has_preview", event.hasPreview());
json.addProperty("agent_id", safeString(event.getAgentId()));
json.addProperty("technical_start", safeString(event.getTechnicalStartTime()));
json.addProperty("technical_end", safeString(event.getTechnicalEndTime()));
json.addProperty("language", safeString(event.getLanguage()));
json.addProperty("language_translation_key", languages.isPresent()
? safeString(languages.get().get(event.getLanguage())) : "");
json.add("technical_presenters", collectionToJsonArray(event.getTechnicalPresenters()));
json.add("publications", collectionToJsonArray(eventPublicationsToJson(event)));
if (comments.isPresent()) {
json.add("comments", collectionToJsonArray(eventCommentsToJson(comments.get())));
}
return json;
}
private void mergeJsonObjects(JsonObject target, JsonObject source) {
for (String key : source.keySet()) {
target.add(key, source.get(key));
}
}
private JsonObject attachmentToJSON(Attachment attachment) {
JsonObject json = new JsonObject();
mergeJsonObjects(json, getEventMediaPackageElementFields(attachment));
mergeJsonObjects(json, getCommonElementFields(attachment));
return json;
}
private JsonObject catalogToJSON(Catalog catalog) {
JsonObject json = new JsonObject();
mergeJsonObjects(json, getEventMediaPackageElementFields(catalog));
mergeJsonObjects(json, getCommonElementFields(catalog));
return json;
}
private JsonObject trackToJSON(Track track) {
JsonObject json = new JsonObject();
mergeJsonObjects(json, getEventMediaPackageElementFields(track));
mergeJsonObjects(json, getCommonElementFields(track));
json.addProperty("duration", track.getDuration());
json.addProperty("has_audio", track.hasAudio());
json.addProperty("has_video", track.hasVideo());
json.addProperty("has_subtitle", track.hasSubtitle());
json.add("streams", streamsToJSON(track.getStreams()));
return json;
}
private JsonObject streamsToJSON(org.opencastproject.mediapackage.Stream[] streams) {
JsonArray audioArray = new JsonArray();
JsonArray videoArray = new JsonArray();
JsonArray subtitleArray = new JsonArray();
for (org.opencastproject.mediapackage.Stream stream : streams) {
if (stream instanceof AudioStreamImpl) {
AudioStream audioStream = (AudioStream) stream;
JsonObject audioJson = new JsonObject();
audioJson.addProperty("id", safeString(audioStream.getIdentifier()));
audioJson.addProperty("type", safeString(audioStream.getFormat()));
audioJson.addProperty("channels", safeString(audioStream.getChannels()));
audioJson.addProperty("bitrate", audioStream.getBitRate());
audioJson.addProperty("bitdepth", safeString(audioStream.getBitDepth()));
audioJson.addProperty("samplingrate", safeString(audioStream.getSamplingRate()));
audioJson.addProperty("framecount", safeString(audioStream.getFrameCount()));
audioJson.addProperty("peakleveldb", safeString(audioStream.getPkLevDb()));
audioJson.addProperty("rmsleveldb", safeString(audioStream.getRmsLevDb()));
audioJson.addProperty("rmspeakdb", safeString(audioStream.getRmsPkDb()));
audioArray.add(audioJson);
} else if (stream instanceof VideoStreamImpl) {
VideoStream videoStream = (VideoStream) stream;
JsonObject videoJson = new JsonObject();
videoJson.addProperty("id", safeString(videoStream.getIdentifier()));
videoJson.addProperty("type", safeString(videoStream.getFormat()));
videoJson.addProperty("bitrate", videoStream.getBitRate());
videoJson.addProperty("framerate", safeString(videoStream.getFrameRate()));
videoJson.addProperty("resolution", safeString(videoStream.getFrameWidth() + "x"
+ videoStream.getFrameHeight()));
videoJson.addProperty("framecount", safeString(videoStream.getFrameCount()));
videoJson.addProperty("scantype", safeString(videoStream.getScanType()));
videoJson.addProperty("scanorder", safeString(videoStream.getScanOrder()));
videoArray.add(videoJson);
} else if (stream instanceof SubtitleStreamImpl) {
SubtitleStreamImpl subtitleStream = (SubtitleStreamImpl) stream;
JsonObject subtitleJson = new JsonObject();
subtitleJson.addProperty("id", safeString(subtitleStream.getIdentifier()));
subtitleJson.addProperty("type", safeString(subtitleStream.getFormat()));
subtitleArray.add(subtitleJson);
} else {
throw new IllegalArgumentException("Stream must be either audio, video, or subtitle");
}
}
JsonObject result = new JsonObject();
result.add("audio", audioArray);
result.add("video", videoArray);
result.add("subtitle", subtitleArray);
return result;
}
private JsonObject publicationToJSON(Publication publication) {
JsonObject json = new JsonObject();
json.addProperty("id", safeString(publication.getIdentifier()));
json.addProperty("channel", safeString(publication.getChannel()));
json.addProperty("mimetype", safeString(publication.getMimeType()));
json.add("tags", arrayToJsonArray(publication.getTags()));
URI uri = signUrl(publication.getURI());
json.addProperty("url", safeString(uri));
JsonObject commonFields = getCommonElementFields(publication);
for (String key : commonFields.keySet()) {
json.add(key, commonFields.get(key));
}
return json;
}
private JsonObject getCommonElementFields(MediaPackageElement element) {
JsonObject fields = new JsonObject();
fields.addProperty("size", element.getSize());
fields.addProperty("checksum", element.getChecksum() != null ? element.getChecksum().getValue() : "");
fields.addProperty("reference", element.getReference() != null ? element.getReference().getIdentifier() : "");
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 {@link JsonArray}
* @return {@link JsonArray} that represent the {@link Publication}
*/
private JsonArray getEventPublications(Publication[] publications) {
JsonArray publicationJsonArray = new JsonArray();
for (Publication publication : publications) {
JsonObject pubJson = new JsonObject();
pubJson.addProperty("id", safeString(publication.getIdentifier()));
pubJson.addProperty("channel", safeString(publication.getChannel()));
pubJson.addProperty("mimetype", safeString(publication.getMimeType()));
pubJson.add("tags", arrayToJsonArray(publication.getTags()));
pubJson.addProperty("url", safeString(signUrl(publication.getURI())));
publicationJsonArray.add(pubJson);
}
return publicationJsonArray;
}
private URI signUrl(URI url) {
if (url == null) {
return null;
}
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, 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 {@link JsonArray}
* @return {@link JsonArray} that represent the {@link MediaPackageElement}
*/
private JsonArray getEventMediaPackageElements(MediaPackageElement[] elements) {
JsonArray elementJsonArray = new JsonArray();
for (MediaPackageElement element : elements) {
JsonObject elementJson = getEventMediaPackageElementFields(element);
elementJsonArray.add(elementJson);
}
return elementJsonArray;
}
private JsonObject getEventMediaPackageElementFields(MediaPackageElement element) {
JsonObject json = new JsonObject();
json.addProperty("id", safeString(element.getIdentifier()));
json.addProperty("type", safeString(element.getFlavor()));
json.addProperty("mimetype", safeString(element.getMimeType()));
json.add("tags", arrayToJsonArray(element.getTags()));
json.addProperty("url", safeString(signUrl(element.getURI())));
return json;
}
private final Function<Publication, JsonObject> publicationToJson = publication -> {
String channelName = EventUtils.PUBLICATION_CHANNELS.get(publication.getChannel());
if (channelName == null) {
channelName = "EVENTS.EVENTS.DETAILS.PUBLICATIONS.CUSTOM";
}
String url = publication.getURI() == null ? "" : signUrl(publication.getURI()).toString();
JsonObject json = new JsonObject();
json.addProperty("id", publication.getChannel());
json.addProperty("name", channelName);
json.addProperty("url", url);
return json;
};
private JsonObject technicalMetadataToJson(TechnicalMetadata technicalMetadata) {
JsonObject json = new JsonObject();
json.addProperty("agentId", technicalMetadata.getAgentId() != null ? technicalMetadata.getAgentId() : "");
if (technicalMetadata.getCaptureAgentConfiguration() != null) {
json.add("agentConfiguration", mapToJsonObject(technicalMetadata.getCaptureAgentConfiguration()));
} else {
json.add("agentConfiguration", JsonNull.INSTANCE);
}
if (technicalMetadata.getStartDate() != null) {
String startUtc = DateTimeSupport.toUTC(technicalMetadata.getStartDate().getTime());
json.addProperty("start", startUtc);
} else {
json.addProperty("start", "");
}
if (technicalMetadata.getEndDate() != null) {
String endUtc = DateTimeSupport.toUTC(technicalMetadata.getEndDate().getTime());
json.addProperty("end", endUtc);
} else {
json.addProperty("end", "");
}
String eventId = technicalMetadata.getEventId();
json.addProperty("eventId", safeString(eventId));
json.add("presenters", collectionToJsonArray(technicalMetadata.getPresenters()));
Optional<Recording> optRecording = technicalMetadata.getRecording();
if (optRecording.isPresent()) {
json.add("recording", recordingToJson(optRecording.get()));
}
return json;
}
public static JsonObject recordingToJson(Recording recording) {
JsonObject json = new JsonObject();
json.addProperty("id", safeString(recording.getID()));
json.addProperty("lastCheckInTime", recording.getLastCheckinTime() != null ? recording.getLastCheckinTime() : 0L);
json.addProperty("lastCheckInTimeUTC", recording.getLastCheckinTime() != null
? toUTC(recording.getLastCheckinTime()) : "");
json.addProperty("state", safeString(recording.getState()));
return json;
}
@PUT
@Path("{eventId}/workflows/{workflowId}/action/{action}")
@RestQuery(
name = "workflowAction",
description = "Performs the given action for the given workflow.",
returnDescription = "",
pathParameters = {
@RestParameter(name = "eventId", description = "The id of the media package", isRequired = true,
type = RestParameter.Type.STRING),
@RestParameter(name = "workflowId", description = "The id of the workflow", isRequired = true,
type = RestParameter.Type.STRING),
@RestParameter(name = "action", description = "The action to take: STOP, RETRY or NONE (abort processing)",
isRequired = true, type = RestParameter.Type.STRING)
},
responses = {
@RestResponse(responseCode = SC_OK, description = "Workflow resumed."),
@RestResponse(responseCode = SC_NOT_FOUND, description = "Event or workflow instance not found."),
@RestResponse(responseCode = SC_BAD_REQUEST, description = "Invalid action entered."),
@RestResponse(responseCode = SC_UNAUTHORIZED, description = "You do not have permission to perform the "
+ "action. Maybe you need to authenticate."),
@RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "An exception occurred.")
})
public Response workflowAction(@PathParam("eventId") String id, @PathParam("workflowId") long wfId,
@PathParam("action") String action) {
if (StringUtils.isEmpty(id) || StringUtils.isEmpty(action)) {
return badRequest();
}
try {
final Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", id);
}
final WorkflowInstance wfInstance = getWorkflowService().getWorkflowById(wfId);
if (!wfInstance.getMediaPackage().getIdentifier().toString().equals(id)) {
return badRequest(String.format("Workflow %s is not associated to event %s", wfId, id));
}
if (RetryStrategy.NONE.toString().equalsIgnoreCase(action)
|| RetryStrategy.RETRY.toString().equalsIgnoreCase(action)) {
getWorkflowService().resume(wfId, Collections.singletonMap("retryStrategy", action));
return ok();
}
if (WORKFLOW_ACTION_STOP.equalsIgnoreCase(action)) {
getWorkflowService().stop(wfId);
return ok();
}
return badRequest("Action not supported: " + action);
} catch (NotFoundException e) {
return notFound("Workflow not found: '%d'.", wfId);
} catch (IllegalStateException e) {
return badRequest(String.format("Action %s not allowed for current workflow state. EventId: %s", action, id));
} catch (UnauthorizedException e) {
return forbidden();
} catch (Exception e) {
return serverError();
}
}
@DELETE
@Path("{eventId}/workflows/{workflowId}")
@RestQuery(
name = "deleteWorkflow",
description = "Deletes a workflow",
returnDescription = "The method doesn't return any content",
pathParameters = {
@RestParameter(name = "eventId", isRequired = true, description = "The event identifier",
type = RestParameter.Type.STRING),
@RestParameter(name = "workflowId", isRequired = true, description = "The workflow identifier",
type = RestParameter.Type.INTEGER)
},
responses = {
@RestResponse(responseCode = SC_BAD_REQUEST, description = "When trying to delete the latest workflow of the "
+ "event."),
@RestResponse(responseCode = SC_NOT_FOUND, description = "If the event or the workflow has not been found."),
@RestResponse(responseCode = SC_NO_CONTENT, description = "The method does not return any content")
})
public Response deleteWorkflow(@PathParam("eventId") String id, @PathParam("workflowId") long wfId)
throws SearchIndexException {
final Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
try {
if (optEvent.isEmpty()) {
return notFound("Cannot find an event with id '%s'.", id);
}
final WorkflowInstance wfInstance = getWorkflowService().getWorkflowById(wfId);
if (!wfInstance.getMediaPackage().getIdentifier().toString().equals(id)) {
return badRequest(String.format("Workflow %s is not associated to event %s", wfId, id));
}
if (wfId == optEvent.get().getWorkflowId()) {
return badRequest(String.format("Cannot delete current workflow %s from event %s."
+ " Only older workflows can be deleted.", wfId, id));
}
getWorkflowService().remove(wfId);
return Response.noContent().build();
} catch (WorkflowStateException e) {
return badRequest("Deleting is not allowed for current workflow state. EventId: " + id);
} catch (NotFoundException e) {
return notFound("Workflow not found: '%d'.", wfId);
} catch (UnauthorizedException e) {
return forbidden();
} catch (Exception e) {
return serverError();
}
}
private Optional<Event> checkAgentAccessForEvent(final String eventId)
throws UnauthorizedException, SearchIndexException {
final Optional<Event> event = getIndexService().getEvent(eventId, getIndex());
if (event.isEmpty() || !event.get().getEventStatus().contains("SCHEDULE")) {
return event;
}
SecurityUtil.checkAgentAccess(getSecurityService(), event.get().getAgentId());
return event;
}
private void checkAgentAccessForAgent(final String agentId) throws UnauthorizedException {
SecurityUtil.checkAgentAccess(getSecurityService(), agentId);
}
}