org.opencastproject.scheduler.endpoint.SchedulerRestService Maven / Gradle / Ivy
/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License
* at:
*
* http://opensource.org/licenses/ecl2.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*/
package org.opencastproject.scheduler.endpoint;
import static com.entwinemedia.fn.Prelude.chuck;
import static com.entwinemedia.fn.Stream.$;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.apache.commons.lang3.exception.ExceptionUtils.getMessage;
import static org.opencastproject.capture.CaptureParameters.AGENT_REGISTRATION_TYPE;
import static org.opencastproject.capture.CaptureParameters.AGENT_REGISTRATION_TYPE_ADHOC;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_CREATED;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_SPATIAL;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TEMPORAL;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TITLE;
import static org.opencastproject.util.Jsons.arr;
import static org.opencastproject.util.Jsons.obj;
import static org.opencastproject.util.Jsons.p;
import static org.opencastproject.util.Jsons.v;
import static org.opencastproject.util.RestUtil.generateErrorResponse;
import static org.opencastproject.util.data.Monadics.mlist;
import org.opencastproject.capture.admin.api.Agent;
import org.opencastproject.capture.admin.api.AgentState;
import org.opencastproject.capture.admin.api.CaptureAgentStateService;
import org.opencastproject.mediapackage.Catalog;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageBuilderFactory;
import org.opencastproject.mediapackage.MediaPackageElement;
import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageElements;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.mediapackage.MediaPackageParser;
import org.opencastproject.mediapackage.MediaPackageSupport;
import org.opencastproject.metadata.dublincore.DCMIPeriod;
import org.opencastproject.metadata.dublincore.DublinCore;
import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
import org.opencastproject.metadata.dublincore.DublinCoreUtil;
import org.opencastproject.metadata.dublincore.DublinCores;
import org.opencastproject.metadata.dublincore.EncodingSchemeUtils;
import org.opencastproject.metadata.dublincore.Precision;
import org.opencastproject.rest.RestConstants;
import org.opencastproject.scheduler.api.Recording;
import org.opencastproject.scheduler.api.SchedulerConflictException;
import org.opencastproject.scheduler.api.SchedulerException;
import org.opencastproject.scheduler.api.SchedulerService;
import org.opencastproject.scheduler.api.TechnicalMetadata;
import org.opencastproject.scheduler.impl.CaptureNowProlongingService;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.systems.OpencastConstants;
import org.opencastproject.util.DateTimeSupport;
import org.opencastproject.util.Jsons;
import org.opencastproject.util.Jsons.Arr;
import org.opencastproject.util.Jsons.Prop;
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.doc.rest.RestParameter;
import org.opencastproject.util.doc.rest.RestParameter.Type;
import org.opencastproject.util.doc.rest.RestQuery;
import org.opencastproject.util.doc.rest.RestResponse;
import org.opencastproject.util.doc.rest.RestService;
import org.opencastproject.workspace.api.Workspace;
import com.entwinemedia.fn.data.Opt;
import net.fortuna.ical4j.model.property.RRule;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.URI;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
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.Properties;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
/**
* REST Endpoint for Scheduler Service
*/
@Path("/")
@RestService(name = "schedulerservice", title = "Scheduler Service", abstractText = "This service creates, edits and retrieves and helps managing scheduled capture events.", notes = {
"All paths above are relative to the REST endpoint base (something like http://your.server/files)",
"If the service is down or not working it will return a status 503, this means the the underlying service is "
+ "not working and is either restarting or has failed",
"A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
+ "other words, there is a bug! You should file an error report with your server logs from the time when the "
+ "error occurred: Opencast Issue Tracker" })
public class SchedulerRestService {
private static final Logger logger = LoggerFactory.getLogger(SchedulerRestService.class);
/** Key for the default workflow definition in config.properties */
private static final String DEFAULT_WORKFLOW_DEFINITION = "org.opencastproject.workflow.default.definition";
private SchedulerService service;
private CaptureAgentStateService agentService;
private CaptureNowProlongingService prolongingService;
private Workspace workspace;
private String defaultWorkflowDefinitionId;
protected String serverUrl = UrlSupport.DEFAULT_BASE_URL;
protected String serviceUrl = null;
/**
* Method to set the service this REST endpoint uses
*
* @param service
*/
public void setService(SchedulerService service) {
this.service = service;
}
/**
* Method to unset the service this REST endpoint uses
*
* @param service
*/
public void unsetService(SchedulerService service) {
this.service = null;
}
/**
* Method to set the prolonging service this REST endpoint uses
*
* @param prolongingService
*/
public void setProlongingService(CaptureNowProlongingService prolongingService) {
this.prolongingService = prolongingService;
}
/**
* Method to unset the prolonging service this REST endpoint uses
*
* @param prolongingService
*/
public void unsetProlongingService(CaptureNowProlongingService prolongingService) {
this.prolongingService = null;
}
/**
* Method to set the capture agent state service this REST endpoint uses
*
* @param agentService
*/
public void setCaptureAgentStateService(CaptureAgentStateService agentService) {
this.agentService = agentService;
}
/**
* Method to unset the capture agent state service this REST endpoint uses
*
* @param agentService
*/
public void unsetCaptureAgentStateService(CaptureAgentStateService agentService) {
this.agentService = null;
}
/**
* Method to set the workspace this REST endpoint uses
*
* @param workspace
*/
public void setWorkspace(Workspace workspace) {
this.workspace = workspace;
}
/**
* The method that will be called, if the service will be activated
*
* @param cc
* The ComponentContext of this service
*/
public void activate(ComponentContext cc) {
// Get the configured server URL
if (cc != null) {
String ccServerUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
logger.debug("configured server url is {}", ccServerUrl);
if (ccServerUrl == null) {
serverUrl = UrlSupport.DEFAULT_BASE_URL;
} else {
serverUrl = ccServerUrl;
}
serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
defaultWorkflowDefinitionId = StringUtils
.trimToNull(cc.getBundleContext().getProperty(DEFAULT_WORKFLOW_DEFINITION));
if (defaultWorkflowDefinitionId == null)
defaultWorkflowDefinitionId = "schedule-and-upload";
}
}
/**
* Gets a XML with the media package for the specified event.
*
* @param eventId
* The unique ID of the event.
* @return media package XML for the event
*/
@GET
@Produces(MediaType.TEXT_XML)
@Path("{id:.+}/mediapackage.xml")
@RestQuery(name = "getmediapackagexml", description = "Retrieves media package for specified event", returnDescription = "media package in XML", pathParameters = {
@RestParameter(name = "id", isRequired = true, description = "ID of event for which media package will be retrieved", type = Type.STRING) }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "DublinCore of event is in the body of response"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does not exist"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission to remove the event. Maybe you need to authenticate.") })
public Response getMediaPackageXml(@PathParam("id") String eventId) throws UnauthorizedException {
try {
MediaPackage result = service.getMediaPackage(eventId);
return Response.ok(MediaPackageParser.getAsXml(result)).build();
} catch (NotFoundException e) {
logger.info("Event with id '{}' does not exist.", eventId);
return Response.status(Status.NOT_FOUND).build();
} catch (SchedulerException e) {
logger.error("Unable to retrieve event with id '{}': {}", eventId, getMessage(e));
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
/**
* Gets a XML with the Dublin Core metadata for the specified event.
*
* @param eventId
* The unique ID of the event.
* @return Dublin Core XML for the event
*/
@GET
@Produces(MediaType.TEXT_XML)
@Path("{id:.+}/dublincore.xml")
@RestQuery(name = "recordingsasxml", description = "Retrieves DublinCore for specified event", returnDescription = "DublinCore in XML", pathParameters = {
@RestParameter(name = "id", isRequired = true, description = "ID of event for which DublinCore will be retrieved", type = Type.STRING) }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "DublinCore of event is in the body of response"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does not exist"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission to remove the event. Maybe you need to authenticate.") })
public Response getDublinCoreMetadataXml(@PathParam("id") String eventId) throws UnauthorizedException {
try {
DublinCoreCatalog result = service.getDublinCore(eventId);
return Response.ok(result.toXmlString()).build();
} catch (NotFoundException e) {
logger.info("Event with id '{}' does not exist.", eventId);
return Response.status(Status.NOT_FOUND).build();
} catch (UnauthorizedException e) {
throw e;
} catch (Exception e) {
logger.error("Unable to retrieve event with id '{}': {}", eventId, getMessage(e));
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
/**
* Gets a Dublin Core metadata for the specified event as JSON.
*
* @param eventId
* The unique ID of the event.
* @return Dublin Core JSON for the event
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("{id:.+}/dublincore.json")
@RestQuery(name = "recordingsasjson", description = "Retrieves DublinCore for specified event", returnDescription = "DublinCore in JSON", pathParameters = {
@RestParameter(name = "id", isRequired = true, description = "ID of event for which DublinCore will be retrieved", type = Type.STRING) }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "DublinCore of event is in the body of response"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does not exist"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission to remove the event. Maybe you need to authenticate.") })
public Response getDublinCoreMetadataJSON(@PathParam("id") String eventId) throws UnauthorizedException {
try {
DublinCoreCatalog result = service.getDublinCore(eventId);
return Response.ok(result.toJson()).build();
} catch (NotFoundException e) {
logger.info("Event with id '{}' does not exist.", eventId);
return Response.status(Status.NOT_FOUND).build();
} catch (UnauthorizedException e) {
throw e;
} catch (Exception e) {
logger.error("Unable to retrieve event with id '{}': {}", eventId, getMessage(e));
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
/**
* Gets a XML with the media package for the specified event.
*
* @param eventId
* The unique ID of the event.
* @return media package XML for the event
*/
@GET
@Produces(MediaType.TEXT_XML)
@Path("{id:.+}/technical.json")
@RestQuery(name = "gettechnicalmetadatajson", description = "Retrieves the technical metadata for specified event", returnDescription = "technical metadata as JSON", pathParameters = {
@RestParameter(name = "id", isRequired = true, description = "ID of event for which the technical metadata will be retrieved", type = Type.STRING) }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "technical metadata of event is in the body of response"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does not exist"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission to remove the event. Maybe you need to authenticate.") })
public Response getTechnicalMetadataJSON(@PathParam("id") String eventId) throws UnauthorizedException {
try {
TechnicalMetadata metadata = service.getTechnicalMetadata(eventId);
Val state = v("");
Val lastHeard = v("");
if (metadata.getRecording().isSome()) {
state = v(metadata.getRecording().get().getState());
lastHeard = v(DateTimeSupport.toUTC(metadata.getRecording().get().getLastCheckinTime()));
}
Arr presenters = arr(mlist(metadata.getPresenters()).map(Jsons.stringVal));
List wfProperties = new ArrayList<>();
for (Entry entry : metadata.getWorkflowProperties().entrySet()) {
wfProperties.add(p(entry.getKey(), entry.getValue()));
}
List agentConfig = new ArrayList<>();
for (Entry entry : metadata.getCaptureAgentConfiguration().entrySet()) {
agentConfig.add(p(entry.getKey(), entry.getValue()));
}
return RestUtil.R.ok(obj(p("id", metadata.getEventId()), p("location", metadata.getAgentId()),
p("start", DateTimeSupport.toUTC(metadata.getStartDate().getTime())),
p("end", DateTimeSupport.toUTC(metadata.getEndDate().getTime())),
p("presenters", presenters), p("wfProperties", obj(wfProperties.toArray(new Prop[wfProperties.size()]))),
p("agentConfig", obj(agentConfig.toArray(new Prop[agentConfig.size()]))), p("state", state),
p("lastHeardFrom", lastHeard)));
} catch (NotFoundException e) {
logger.info("Event with id '{}' does not exist.", eventId);
return Response.status(Status.NOT_FOUND).build();
} catch (SchedulerException e) {
logger.error("Unable to retrieve event with id '{}': {}", eventId, getMessage(e));
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
/**
* Gets the workflow configuration for the specified event.
*
* @param eventId
* The unique ID of the event.
* @return the workflow configuration
*/
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("{id:.+}/workflow.properties")
@RestQuery(name = "recordingsagentproperties", description = "Retrieves workflow configuration for specified event", returnDescription = "workflow configuration in the form of key, value pairs", pathParameters = {
@RestParameter(name = "id", isRequired = true, description = "ID of event for which workflow configuration will be retrieved", type = Type.STRING) }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "workflow configuration of event is in the body of response"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does not exist"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission to remove the event. Maybe you need to authenticate.") })
public Response getWorkflowConfiguration(@PathParam("id") String eventId) throws UnauthorizedException {
try {
Map result = service.getWorkflowConfig(eventId);
String serializedProperties = serializeProperties(result);
return Response.ok(serializedProperties).build();
} catch (NotFoundException e) {
logger.info("Event with id '{}' does not exist.", eventId);
return Response.status(Status.NOT_FOUND).build();
} catch (SchedulerException e) {
logger.error("Unable to retrieve workflow configuration for event with id '{}': {}", eventId, getMessage(e));
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
/**
* Gets java Properties file with technical metadata for the specified event.
*
* @param eventId
* The unique ID of the event.
* @return Java Properties File with the metadata for the event
*/
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("{id:.+}/agent.properties")
@RestQuery(name = "recordingsagentproperties", description = "Retrieves Capture Agent properties for specified event", returnDescription = "Capture Agent properties in the form of key, value pairs", pathParameters = {
@RestParameter(name = "id", isRequired = true, description = "ID of event for which agent properties will be retrieved", type = Type.STRING) }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Capture Agent properties of event is in the body of response"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does not exist"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission to remove the event. Maybe you need to authenticate.") })
public Response getCaptureAgentMetadata(@PathParam("id") String eventId) throws UnauthorizedException {
try {
Map result = service.getCaptureAgentConfiguration(eventId);
String serializedProperties = serializeProperties(result);
return Response.ok(serializedProperties).build();
} catch (NotFoundException e) {
logger.info("Event with id '{}' does not exist.", eventId);
return Response.status(Status.NOT_FOUND).build();
} catch (SchedulerException e) {
logger.error("Unable to retrieve event with id '{}': {}", eventId, getMessage(e));
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
/**
*
* Removes the specified event. Returns true if the event was found and could be removed.
*
* @param eventId
* The unique ID of the event.
* @return true if the event was found and could be deleted.
*/
@DELETE
@Path("{id:.+}")
@Produces(MediaType.TEXT_PLAIN)
@RestQuery(name = "deleterecordings", description = "Removes scheduled event with specified ID.", returnDescription = "OK if event were successfully removed or NOT FOUND if event with specified ID does not exist", pathParameters = {
@RestParameter(name = "id", isRequired = true, description = "Event ID", type = Type.STRING) }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Event was successfully removed"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does not exist"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission to remove the event. Maybe you need to authenticate."),
@RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "Event with specified ID is locked by a transaction, unable to delete event.") })
public Response deleteEvent(@PathParam("id") String eventId) throws UnauthorizedException {
try {
service.removeEvent(eventId);
return Response.status(Response.Status.OK).build();
} catch (NotFoundException e) {
logger.info("Event with id '{}' does not exist.", eventId);
return Response.status(Status.NOT_FOUND).build();
} catch (UnauthorizedException e) {
throw e;
} catch (Exception e) {
logger.error("Unable to delete event with id '{}': {}", eventId, getMessage(e));
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
/**
* Gets the iCalendar with all (even old) events for the specified filter.
*
* @param captureAgentId
* The ID that specifies the capture agent.
* @param seriesId
* The ID that specifies series.
*
* @return an iCalendar
*/
@GET
@Produces("text/calendar")
// NOTE: charset not supported by current jaxrs impl (is ignored), set explicitly in response
@Path("calendars")
@RestQuery(name = "getcalendar", description = "Returns iCalendar for specified set of events", returnDescription = "ICalendar for events", restParameters = {
@RestParameter(name = "agentid", description = "Filter events by capture agent", isRequired = false, type = Type.STRING),
@RestParameter(name = "seriesid", description = "Filter events by series", isRequired = false, type = Type.STRING),
@RestParameter(name = "cutoff", description = "A cutoff date in UNIX milliseconds to limit the number of events returned in the calendar.", isRequired = false, type = Type.INTEGER) }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_NOT_MODIFIED, description = "Events were not modified since last request"),
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Events were modified, new calendar is in the body") })
public Response getCalendar(@QueryParam("agentid") String captureAgentId, @QueryParam("seriesid") String seriesId,
@QueryParam("cutoff") Long cutoff, @Context HttpServletRequest request) {
Date endDate = null;
if (cutoff != null) {
try {
endDate = new Date(cutoff);
} catch (NumberFormatException e) {
return Response.status(Status.BAD_REQUEST).build();
}
}
try {
String lastModified = null;
// If the etag matches the if-not-modified header,return a 304
if (StringUtils.isNotBlank(captureAgentId)) {
lastModified = service.getScheduleLastModified(captureAgentId);
String ifNoneMatch = request.getHeader(HttpHeaders.IF_NONE_MATCH);
if (StringUtils.isNotBlank(ifNoneMatch) && ifNoneMatch.equals(lastModified)) {
return Response.notModified(lastModified).expires(null).build();
}
}
String result = service.getCalendar(Opt.nul(StringUtils.trimToNull(captureAgentId)),
Opt.nul(StringUtils.trimToNull(seriesId)), Opt.nul(endDate));
ResponseBuilder response = Response.ok(result).header(HttpHeaders.CONTENT_TYPE, "text/calendar; charset=UTF-8");
if (StringUtils.isNotBlank(lastModified))
response.header(HttpHeaders.ETAG, lastModified);
return response.build();
} catch (Exception e) {
logger.error("Unable to get calendar for capture agent '{}':", captureAgentId, e);
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("{id}/lastmodified")
@RestQuery(name = "agentlastmodified", description = "Retrieves the last modified hash for specified agent", returnDescription = "The last modified hash", pathParameters = {
@RestParameter(name = "id", isRequired = true, description = "ID of capture agent for which the last modified hash will be retrieved", type = Type.STRING) }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "The last modified hash of agent is in the body of response") })
public Response getLastModified(@PathParam("id") String agentId) {
try {
String lastModified = service.getScheduleLastModified(agentId);
return Response.ok(lastModified).build();
} catch (Exception e) {
logger.error("Unable to retrieve agent last modified hash of agent id '{}': {}", agentId, getMessage(e));
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
@POST
@Path("/removeOldScheduledRecordings")
@RestQuery(name = "removeOldScheduledRecordings", description = "This will find and remove any scheduled events before the buffer time to keep performance in the scheduler optimum.", returnDescription = "No return value", responses = {
@RestResponse(responseCode = SC_OK, description = "Removed old scheduled recordings."),
@RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse buffer."),
@RestResponse(responseCode = SC_UNAUTHORIZED, description = "You do not have permission to remove old schedulings. Maybe you need to authenticate.") }, restParameters = {
@RestParameter(name = "buffer", type = RestParameter.Type.INTEGER, defaultValue = "604800", isRequired = true, description = "The amount of seconds before now that a capture has to have stopped capturing. It must be 0 or greater.") })
public Response removeOldScheduledRecordings(@FormParam("buffer") long buffer) throws UnauthorizedException {
if (buffer < 0) {
return Response.status(SC_BAD_REQUEST).build();
}
try {
service.removeScheduledRecordingsBeforeBuffer(buffer);
} catch (SchedulerException e) {
logger.error("Error while trying to remove old scheduled recordings", e);
throw new WebApplicationException(e);
}
return Response.ok().build();
}
/**
* Creates new event based on parameters. All times and dates are in milliseconds.
*/
@POST
@Path("/")
@RestQuery(name = "newrecording", description = "Creates new event with specified parameters",
returnDescription = "If an event was successfully created",
restParameters = {
@RestParameter(name = "start", isRequired = true, type = Type.INTEGER, description = "The start date of the event in milliseconds from 1970-01-01T00:00:00Z"),
@RestParameter(name = "end", isRequired = true, type = Type.INTEGER, description = "The end date of the event in milliseconds from 1970-01-01T00:00:00Z"),
@RestParameter(name = "agent", isRequired = true, type = Type.STRING, description = "The agent of the event"),
@RestParameter(name = "users", isRequired = false, type = Type.STRING, description = "Comma separated list of user ids (speakers/lecturers) for the event"),
@RestParameter(name = "mediaPackage", isRequired = true, type = Type.TEXT, description = "The media package of the event"),
@RestParameter(name = "wfproperties", isRequired = false, type = Type.TEXT, description = "Workflow "
+ "configuration keys for the event. Each key will be prefixed by 'org.opencastproject.workflow"
+ ".config.' and added to the capture agent parameters."),
@RestParameter(name = "agentparameters", isRequired = false, type = Type.TEXT, description = "The capture agent properties for the event"),
@RestParameter(name = "source", isRequired = false, type = Type.STRING, description = "The scheduling source of the event"),
}, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_CREATED, description = "Event is successfully created"),
@RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "Unable to create event, conflicting events found (ConflicsFound)"),
@RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "Unable to create event, event locked by a transaction (TransactionLock)"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission to create the event. Maybe you need to authenticate."),
@RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "Missing or invalid information for this request") })
public Response addEvent(@FormParam("start") long startTime, @FormParam("end") long endTime,
@FormParam("agent") String agentId, @FormParam("users") String users,
@FormParam("mediaPackage") String mediaPackageXml, @FormParam("wfproperties") String workflowProperties,
@FormParam("agentparameters") String agentParameters,
@FormParam("source") String schedulingSource)
throws UnauthorizedException {
if (endTime <= startTime || startTime < 0) {
logger.debug("Cannot add event without proper start and end time");
return RestUtil.R.badRequest("Cannot add event without proper start and end time");
}
if (StringUtils.isBlank(agentId)) {
logger.debug("Cannot add event without agent identifier");
return RestUtil.R.badRequest("Cannot add event without agent identifier");
}
if (StringUtils.isBlank(mediaPackageXml)) {
logger.debug("Cannot add event without media package");
return RestUtil.R.badRequest("Cannot add event without media package");
}
MediaPackage mediaPackage;
try {
mediaPackage = MediaPackageParser.getFromXml(mediaPackageXml);
} catch (MediaPackageException e) {
logger.debug("Could not parse media package", e);
return RestUtil.R.badRequest("Could not parse media package");
}
String eventId = mediaPackage.getIdentifier().toString();
Map caProperties = new HashMap<>();
if (StringUtils.isNotBlank(agentParameters)) {
try {
Properties prop = parseProperties(agentParameters);
caProperties.putAll((Map) prop);
} catch (Exception e) {
logger.info("Could not parse capture agent properties: {}", agentParameters);
return RestUtil.R.badRequest("Could not parse capture agent properties");
}
}
Map wfProperties = new HashMap<>();
if (StringUtils.isNotBlank(workflowProperties)) {
try {
Properties prop = parseProperties(workflowProperties);
wfProperties.putAll((Map) prop);
} catch (IOException e) {
logger.info("Could not parse workflow configuration properties: {}", workflowProperties);
return RestUtil.R.badRequest("Could not parse workflow configuration properties");
}
}
Set userIds = new HashSet<>();
String[] ids = StringUtils.split(users, ",");
if (ids != null)
userIds.addAll(Arrays.asList(ids));
DateTime startDate = new DateTime(startTime).toDateTime(DateTimeZone.UTC);
DateTime endDate = new DateTime(endTime).toDateTime(DateTimeZone.UTC);
try {
service.addEvent(startDate.toDate(), endDate.toDate(), agentId, userIds, mediaPackage, wfProperties, caProperties,
Opt.nul(schedulingSource));
return Response.status(Status.CREATED)
.header("Location", serverUrl + serviceUrl + '/' + eventId + "/mediapackage.xml").build();
} catch (UnauthorizedException e) {
throw e;
} catch (SchedulerConflictException e) {
return Response.status(Status.CONFLICT).entity(generateErrorResponse(e)).type(MediaType.APPLICATION_JSON).build();
} catch (Exception e) {
logger.error("Unable to create new event with id '{}'", eventId, e);
return Response.serverError().build();
}
}
/**
* Creates new event based on parameters. All times and dates are in milliseconds.
*/
@POST
@Path("/multiple")
@RestQuery(name = "newrecordings", description = "Creates new event with specified parameters",
returnDescription = "If an event was successfully created",
restParameters = {
@RestParameter(name = "rrule", isRequired = true, type = Type.STRING, description = "The recurrence rule for the events"),
@RestParameter(name = "start", isRequired = true, type = Type.INTEGER, description = "The start date of the event in milliseconds from 1970-01-01T00:00:00Z"),
@RestParameter(name = "end", isRequired = true, type = Type.INTEGER, description = "The end date of the event in milliseconds from 1970-01-01T00:00:00Z"),
@RestParameter(name = "duration", isRequired = true, type = Type.INTEGER, description = "The duration of the events in milliseconds"),
@RestParameter(name = "tz", isRequired = true, type = Type.INTEGER, description = "The timezone of the events"),
@RestParameter(name = "agent", isRequired = true, type = Type.STRING, description = "The agent of the event"),
@RestParameter(name = "users", isRequired = false, type = Type.STRING, description = "Comma separated list of user ids (speakers/lecturers) for the event"),
@RestParameter(name = "templateMp", isRequired = true, type = Type.TEXT, description = "The template mediapackage for the events"),
@RestParameter(name = "wfproperties", isRequired = false, type = Type.TEXT, description = "Workflow "
+ "configuration keys for the event. Each key will be prefixed by 'org.opencastproject.workflow"
+ ".config.' and added to the capture agent parameters."),
@RestParameter(name = "agentparameters", isRequired = false, type = Type.TEXT, description = "The capture agent properties for the event"),
@RestParameter(name = "source", isRequired = false, type = Type.STRING, description = "The scheduling source of the event"),
}, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_CREATED, description = "Event is successfully created"),
@RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "Unable to create event, conflicting events found (ConflicsFound)"),
@RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "Unable to create event, event locked by a transaction (TransactionLock)"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission to create the event. Maybe you need to authenticate."),
@RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "Missing or invalid information for this request") })
public Response addMultipleEvents(@FormParam("rrule") String rruleString, @FormParam("start") long startTime,
@FormParam("end") long endTime, @FormParam("duration") long duration, @FormParam("tz") String tzString,
@FormParam("agent") String agentId, @FormParam("users") String users,
@FormParam("templateMp") MediaPackage templateMp, @FormParam("wfproperties") String workflowProperties,
@FormParam("agentparameters") String agentParameters,
@FormParam("source") String schedulingSource)
throws UnauthorizedException {
if (endTime <= startTime || startTime < 0) {
logger.debug("Cannot add event without proper start and end time");
return RestUtil.R.badRequest("Cannot add event without proper start and end time");
}
RRule rrule;
try {
rrule = new RRule(rruleString);
} catch (ParseException e) {
logger.debug("Could not parse recurrence rule");
return RestUtil.R.badRequest("Could not parse recurrence rule");
}
if (duration < 1) {
logger.debug("Cannot schedule events with durations less than 1");
return RestUtil.R.badRequest("Cannot schedule events with durations less than 1");
}
if (StringUtils.isBlank(tzString)) {
logger.debug("Cannot schedule events with blank timezone");
return RestUtil.R.badRequest("Cannot schedule events with blank timezone");
}
TimeZone tz = TimeZone.getTimeZone(tzString);
if (StringUtils.isBlank(agentId)) {
logger.debug("Cannot add event without agent identifier");
return RestUtil.R.badRequest("Cannot add event without agent identifier");
}
Map caProperties = new HashMap<>();
if (StringUtils.isNotBlank(agentParameters)) {
try {
Properties prop = parseProperties(agentParameters);
caProperties.putAll((Map) prop);
} catch (Exception e) {
logger.info("Could not parse capture agent properties: {}", agentParameters);
return RestUtil.R.badRequest("Could not parse capture agent properties");
}
}
Map wfProperties = new HashMap<>();
if (StringUtils.isNotBlank(workflowProperties)) {
try {
Properties prop = parseProperties(workflowProperties);
wfProperties.putAll((Map) prop);
} catch (IOException e) {
logger.info("Could not parse workflow configuration properties: {}", workflowProperties);
return RestUtil.R.badRequest("Could not parse workflow configuration properties");
}
}
Set userIds = new HashSet<>();
String[] ids = StringUtils.split(users, ",");
if (ids != null)
userIds.addAll(Arrays.asList(ids));
// ical4j expects start and end dates to be in TimeZone to be schedule to (not UTC)
DateTime startDate = new DateTime(startTime).toDateTime(DateTimeZone.forTimeZone(tz));
DateTime endDate = new DateTime(endTime).toDateTime(DateTimeZone.forTimeZone(tz));
try {
service.addMultipleEvents(rrule, startDate.toDate(), endDate.toDate(), duration, tz, agentId, userIds, templateMp, wfProperties, caProperties,
Opt.nul(schedulingSource));
return Response.status(Status.CREATED).build();
} catch (UnauthorizedException e) {
throw e;
} catch (SchedulerConflictException e) {
return Response.status(Status.CONFLICT).entity(generateErrorResponse(e)).type(MediaType.APPLICATION_JSON).build();
} catch (Exception e) {
logger.error("Unable to create new events", e);
return Response.serverError().build();
}
}
@PUT
@Path("{id}")
@RestQuery(name = "updaterecordings", description = "Updates specified event", returnDescription = "Status OK is returned if event was successfully updated, NOT FOUND if specified event does not exist or BAD REQUEST if data is missing or invalid", pathParameters = {
@RestParameter(name = "id", description = "ID of event to be updated", isRequired = true, type = Type.STRING) }, restParameters = {
@RestParameter(name = "start", isRequired = false, description = "Updated start date for event", type = Type.INTEGER),
@RestParameter(name = "end", isRequired = false, description = "Updated end date for event", type = Type.INTEGER),
@RestParameter(name = "agent", isRequired = false, description = "Updated agent for event", type = Type.STRING),
@RestParameter(name = "users", isRequired = false, type = Type.STRING, description = "Updated comma separated list of user ids (speakers/lecturers) for the event"),
@RestParameter(name = "mediaPackage", isRequired = false, description = "Updated media package for event", type = Type.TEXT),
@RestParameter(name = "wfproperties", isRequired = false, description = "Workflow configuration properties", type = Type.TEXT),
@RestParameter(name = "agentparameters", isRequired = false, description = "Updated Capture Agent properties", type = Type.TEXT)
}, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Event was successfully updated"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does not exist"),
@RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "Unable to update event, conflicting events found (ConflicsFound)"),
@RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "Unable to update event, event locked by a transaction (TransactionLock)"),
@RestResponse(responseCode = HttpServletResponse.SC_FORBIDDEN, description = "Event with specified ID cannot be updated"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission to update the event. Maybe you need to authenticate."),
@RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "Data is missing or invalid") })
public Response updateEvent(@PathParam("id") String eventID, @FormParam("start") Long startTime,
@FormParam("end") Long endTime, @FormParam("agent") String agentId, @FormParam("users") String users,
@FormParam("mediaPackage") String mediaPackageXml, @FormParam("wfproperties") String workflowProperties,
@FormParam("agentparameters") String agentParameters) throws UnauthorizedException {
if (startTime != null) {
if (startTime < 0) {
logger.debug("Cannot add event with negative start time ({} < 0)", startTime);
return RestUtil.R.badRequest("Cannot add event with negative start time");
}
if (endTime != null && endTime <= startTime) {
logger.debug("Cannot add event without proper end time ({} <= {})", startTime, endTime);
return RestUtil.R.badRequest("Cannot add event without proper end time");
}
}
MediaPackage mediaPackage = null;
if (StringUtils.isNotBlank(mediaPackageXml)) {
try {
mediaPackage = MediaPackageParser.getFromXml(mediaPackageXml);
} catch (Exception e) {
logger.debug("Could not parse media packagey", e);
return Response.status(Status.BAD_REQUEST).build();
}
}
Map caProperties = null;
if (StringUtils.isNotBlank(agentParameters)) {
try {
Properties prop = parseProperties(agentParameters);
caProperties = new HashMap<>();
caProperties.putAll((Map) prop);
} catch (Exception e) {
logger.debug("Could not parse capture agent properties: {}", agentParameters, e);
return Response.status(Status.BAD_REQUEST).build();
}
}
Map wfProperties = null;
if (StringUtils.isNotBlank(workflowProperties)) {
try {
Properties prop = parseProperties(workflowProperties);
wfProperties = new HashMap<>();
wfProperties.putAll((Map) prop);
} catch (IOException e) {
logger.debug("Could not parse workflow configuration properties: {}", workflowProperties, e);
return Response.status(Status.BAD_REQUEST).build();
}
}
Set userIds = null;
String[] ids = StringUtils.split(StringUtils.trimToNull(users), ",");
if (ids != null) {
userIds = new HashSet<>(Arrays.asList(ids));
}
Date startDate = null;
if (startTime != null) {
startDate = new DateTime(startTime).toDateTime(DateTimeZone.UTC).toDate();
}
Date endDate = null;
if (endTime != null) {
endDate = new DateTime(endTime).toDateTime(DateTimeZone.UTC).toDate();
}
try {
service.updateEvent(eventID, Opt.nul(startDate), Opt.nul(endDate), Opt.nul(StringUtils.trimToNull(agentId)),
Opt.nul(userIds), Opt.nul(mediaPackage), Opt.nul(wfProperties), Opt.nul(caProperties));
return Response.ok().build();
} catch (SchedulerConflictException e) {
return Response.status(Status.CONFLICT).entity(generateErrorResponse(e)).type(MediaType.APPLICATION_JSON).build();
} catch (SchedulerException e) {
logger.warn("Error updating event with id '{}'", eventID, e);
return Response.status(Status.FORBIDDEN).build();
} catch (NotFoundException e) {
logger.info("Event with id '{}' does not exist.", eventID);
return Response.status(Status.NOT_FOUND).build();
} catch (UnauthorizedException e) {
throw e;
} catch (Exception e) {
logger.error("Unable to update event with id '{}'", eventID, e);
return Response.serverError().build();
}
}
@GET
@Path("currentRecording/{agent}")
@Produces(MediaType.TEXT_XML)
@RestQuery(name = "currentrecording", description = "Get the current capture event as XML", returnDescription = "The current capture event as XML", pathParameters = {
@RestParameter(name = "agent", isRequired = true, type = Type.STRING, description = "The agent identifier") }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "current event is in the body of response"),
@RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "There is no current recording") })
public Response currentRecording(@PathParam("agent") String agentId) throws UnauthorizedException {
try {
Opt current = service.getCurrentRecording(agentId);
if (current.isNone()) {
return Response.noContent().build();
} else {
return Response.ok(MediaPackageParser.getAsXml(current.get())).build();
}
} catch (UnauthorizedException e) {
throw e;
} catch (Exception e) {
logger.error("Unable to get the current recording for agent '{}'", agentId, e);
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
@GET
@Path("upcomingRecording/{agent}")
@Produces(MediaType.TEXT_XML)
@RestQuery(name = "upcomingrecording", description = "Get the upcoming capture event as XML", returnDescription = "The upcoming capture event as XML", pathParameters = {
@RestParameter(name = "agent", isRequired = true, type = Type.STRING, description = "The agent identifier") }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "upcoming event is in the body of response"),
@RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "There is no upcoming recording") })
public Response upcomingRecording(@PathParam("agent") String agentId) throws UnauthorizedException {
try {
Opt upcoming = service.getUpcomingRecording(agentId);
if (upcoming.isNone()) {
return Response.noContent().build();
} else {
return Response.ok(MediaPackageParser.getAsXml(upcoming.get())).build();
}
} catch (UnauthorizedException e) {
throw e;
} catch (Exception e) {
logger.error("Unable to get the upcoming recording for agent '{}'", agentId, e);
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
@GET
@Path("eventCount")
@Produces(MediaType.TEXT_PLAIN)
@RestQuery(name = "eventcount", description = "Get the number of scheduled events", returnDescription = "The number of scheduled events", responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "The event count") })
public Response eventCount() throws UnauthorizedException {
try {
return Response.ok("" + service.getEventCount()).build();
} catch (UnauthorizedException e) {
throw e;
} catch (Exception e) {
logger.error("Unable to get the event count", e);
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
@GET
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Path("recordings.{type:xml|json}")
@RestQuery(name = "recordingsaslist", description = "Searches recordings and returns result as XML or JSON", returnDescription = "XML or JSON formated results",
pathParameters = {
@RestParameter(name = "type", isRequired = true, description = "The media type of the response [xml|json]", type = Type.STRING) },
restParameters = {
@RestParameter(name = "agent", description = "Search by device", isRequired = false, type = Type.STRING),
@RestParameter(name = "startsfrom", description = "Search by when does event start", isRequired = false, type = Type.INTEGER),
@RestParameter(name = "startsto", description = "Search by when does event start", isRequired = false, type = Type.INTEGER),
@RestParameter(name = "endsfrom", description = "Search by when does event finish", isRequired = false, type = Type.INTEGER),
@RestParameter(name = "endsto", description = "Search by when does event finish", isRequired = false, type = Type.INTEGER) },
responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Search completed, results returned in body") })
public Response getEventsAsList(@PathParam("type") final String type, @QueryParam("agent") String device,
@QueryParam("startsfrom") Long startsFromTime,
@QueryParam("startsto") Long startsToTime, @QueryParam("endsfrom") Long endsFromTime,
@QueryParam("endsto") Long endsToTime) throws UnauthorizedException {
Date startsfrom = null;
Date startsTo = null;
Date endsFrom = null;
Date endsTo = null;
if (startsFromTime != null)
startsfrom = new DateTime(startsFromTime).toDateTime(DateTimeZone.UTC).toDate();
if (startsToTime != null)
startsTo = new DateTime(startsToTime).toDateTime(DateTimeZone.UTC).toDate();
if (endsFromTime != null)
endsFrom = new DateTime(endsFromTime).toDateTime(DateTimeZone.UTC).toDate();
if (endsToTime != null)
endsTo = new DateTime(endsToTime).toDateTime(DateTimeZone.UTC).toDate();
try {
List events = service.search(Opt.nul(StringUtils.trimToNull(device)), Opt.nul(startsfrom),
Opt.nul(startsTo), Opt.nul(endsFrom), Opt.nul(endsTo));
if ("json".equalsIgnoreCase(type)) {
return Response.ok(getEventListAsJsonString(events)).build();
} else {
return Response.ok(MediaPackageParser.getArrayAsXml(events)).build();
}
} catch (UnauthorizedException e) {
throw e;
} catch (Exception e) {
logger.error("Unable to perform search: {}", getMessage(e));
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("conflicts.json")
@RestQuery(name = "conflictingrecordingsasjson", description = "Searches for conflicting recordings based on parameters", returnDescription = "Returns NO CONTENT if no recordings are in conflict within specified period or list of conflicting recordings in JSON", restParameters = {
@RestParameter(name = "agent", description = "Device identifier for which conflicts will be searched", isRequired = true, type = Type.STRING),
@RestParameter(name = "start", description = "Start time of conflicting period, in milliseconds", isRequired = true, type = Type.INTEGER),
@RestParameter(name = "end", description = "End time of conflicting period, in milliseconds", isRequired = true, type = Type.INTEGER),
@RestParameter(name = "rrule", description = "Rule for recurrent conflicting, specified as: \"FREQ=WEEKLY;BYDAY=day(s);BYHOUR=hour;BYMINUTE=minute\". FREQ is required. BYDAY may include one or more (separated by commas) of the following: SU,MO,TU,WE,TH,FR,SA.", isRequired = false, type = Type.STRING),
@RestParameter(name = "duration", description = "If recurrence rule is specified duration of each conflicting period, in milliseconds", isRequired = false, type = Type.INTEGER),
@RestParameter(name = "timezone", description = "The timezone of the capture device", isRequired = false, type = Type.STRING) }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "No conflicting events found"),
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Found conflicting events, returned in body of response"),
@RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "Missing or invalid parameters"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "Not authorized to make this request"),
@RestResponse(responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR, description = "A detailed stack track of the internal issue.")})
public Response getConflictingEventsJson(@QueryParam("agent") String device, @QueryParam("rrule") String rrule,
@QueryParam("start") Long startDate, @QueryParam("end") Long endDate, @QueryParam("duration") Long duration,
@QueryParam("timezone") String timezone) throws UnauthorizedException {
try {
List events = getConflictingEvents(device, rrule, startDate, endDate, duration, timezone);
if (!events.isEmpty()) {
String eventsJsonString = getEventListAsJsonString(events);
return Response.ok(eventsJsonString).build();
} else {
return Response.noContent().build();
}
} catch (IllegalArgumentException e) {
return Response.status(Status.BAD_REQUEST).build();
} catch (UnauthorizedException e) {
throw e;
} catch (Exception e) {
logger.error("Unable to find conflicting events for {}, {}, {}, {}, {}:",
device, rrule, startDate, endDate, duration, e);
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
@GET
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Path("conflicts.{type:xml|json}")
@RestQuery(name = "conflictingrecordings", description = "Searches for conflicting recordings based on parameters and returns result as XML or JSON", returnDescription = "Returns NO CONTENT if no recordings are in conflict within specified period or list of conflicting recordings in XML or JSON",
pathParameters = {
@RestParameter(name = "type", isRequired = true, description = "The media type of the response [xml|json]", type = Type.STRING) },
restParameters = {
@RestParameter(name = "agent", description = "Device identifier for which conflicts will be searched", isRequired = true, type = Type.STRING),
@RestParameter(name = "start", description = "Start time of conflicting period, in milliseconds", isRequired = true, type = Type.INTEGER),
@RestParameter(name = "end", description = "End time of conflicting period, in milliseconds", isRequired = true, type = Type.INTEGER),
@RestParameter(name = "rrule", description = "Rule for recurrent conflicting, specified as: \"FREQ=WEEKLY;BYDAY=day(s);BYHOUR=hour;BYMINUTE=minute\". FREQ is required. BYDAY may include one or more (separated by commas) of the following: SU,MO,TU,WE,TH,FR,SA.", isRequired = false, type = Type.STRING),
@RestParameter(name = "duration", description = "If recurrence rule is specified duration of each conflicting period, in milliseconds", isRequired = false, type = Type.INTEGER),
@RestParameter(name = "timezone", description = "The timezone of the capture device", isRequired = false, type = Type.STRING) }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "No conflicting events found"),
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Found conflicting events, returned in body of response"),
@RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "Missing or invalid parameters"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "Not authorized to make this request"),
@RestResponse(responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR, description = "A detailed stack track of the internal issue.")})
public Response getConflicts(@PathParam("type") final String type, @QueryParam("agent") String device, @QueryParam("rrule") String rrule,
@QueryParam("start") Long startDate, @QueryParam("end") Long endDate, @QueryParam("duration") Long duration,
@QueryParam("timezone") String timezone) throws UnauthorizedException {
// Pass dates in the TZ to be schedule to (not UTC)
// If no timezone passed, use the local timezone of the system
if (StringUtils.isBlank(timezone)) {
timezone = DateTimeZone.getDefault().toString();
}
try {
List events = getConflictingEvents(device, rrule, startDate, endDate, duration, timezone);
if (!events.isEmpty()) {
if ("json".equalsIgnoreCase(type)) {
return Response.ok(getEventListAsJsonString(events)).build();
} else {
return Response.ok(MediaPackageParser.getArrayAsXml(events)).build();
}
} else {
return Response.noContent().build();
}
} catch (IllegalArgumentException e) {
return Response.status(Status.BAD_REQUEST).build();
} catch (UnauthorizedException e) {
throw e;
} catch (Exception e) {
logger.error("Unable to find conflicting events for {}, {}, {}, {}, {}",
device, rrule, startDate, endDate, duration, e);
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
@PUT
@Path("{id}/recordingStatus")
@RestQuery(name = "updateRecordingState", description = "Set the status of a given recording, registering it if it is new", pathParameters = {
@RestParameter(description = "The ID of a given recording", isRequired = true, name = "id", type = Type.STRING) }, restParameters = {
@RestParameter(description = "The state of the recording. Must be one of the following: unknown, capturing, capture_finished, capture_error, manifest, manifest_error, manifest_finished, compressing, compressing_error, uploading, upload_finished, upload_error.", isRequired = true, name = "state", type = Type.STRING) }, responses = {
@RestResponse(description = "{id} set to {state}", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "{id} or state {state} is empty or the {state} is not known", responseCode = HttpServletResponse.SC_BAD_REQUEST),
@RestResponse(description = "Recording with {id} could not be found", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "")
public Response updateRecordingState(@PathParam("id") String id, @FormParam("state") String state)
throws NotFoundException {
if (StringUtils.isEmpty(id) || StringUtils.isEmpty(state))
return Response.serverError().status(Response.Status.BAD_REQUEST).build();
try {
if (service.updateRecordingState(id, state)) {
return Response.ok(id + " set to " + state).build();
} else {
return Response.status(Response.Status.BAD_REQUEST).build();
}
} catch (SchedulerException e) {
logger.debug("Unable to set recording state of {}:", id, e);
return Response.serverError().build();
}
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("{id}/recordingStatus")
@RestQuery(name = "getRecordingState", description = "Return the state of a given recording", pathParameters = {
@RestParameter(description = "The ID of a given recording", isRequired = true, name = "id", type = Type.STRING) }, restParameters = {}, responses = {
@RestResponse(description = "Returns the state of the recording with the correct id", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "The recording with the specified ID does not exist", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "")
public Response getRecordingState(@PathParam("id") String id) throws NotFoundException {
try {
Recording rec = service.getRecordingState(id);
return RestUtil.R
.ok(obj(p("id", rec.getID()), p("state", rec.getState()), p("lastHeardFrom", rec.getLastCheckinTime())));
} catch (SchedulerException e) {
logger.debug("Unable to get recording state of {}:", id, e);
return Response.serverError().build();
}
}
@DELETE
@Path("{id}/recordingStatus")
@RestQuery(name = "removeRecording", description = "Remove record of a given recording", pathParameters = {
@RestParameter(description = "The ID of a given recording", isRequired = true, name = "id", type = Type.STRING) }, restParameters = {}, responses = {
@RestResponse(description = "{id} removed", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "{id} is empty", responseCode = HttpServletResponse.SC_BAD_REQUEST),
@RestResponse(description = "Recording with {id} could not be found", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "")
public Response removeRecording(@PathParam("id") String id) throws NotFoundException {
if (StringUtils.isEmpty(id))
return Response.serverError().status(Response.Status.BAD_REQUEST).build();
try {
service.removeRecording(id);
return Response.ok(id + " removed").build();
} catch (SchedulerException e) {
logger.debug("Unable to remove recording with id '{}':", id, e);
return Response.serverError().build();
}
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("recordingStatus")
@RestQuery(name = "getAllRecordings", description = "Return all registered recordings and their state", pathParameters = {}, restParameters = {}, responses = {
@RestResponse(description = "Returns all known recordings.", responseCode = HttpServletResponse.SC_OK) }, returnDescription = "")
public Response getAllRecordings() {
try {
List update = new ArrayList<>();
for (Entry e : service.getKnownRecordings().entrySet()) {
update.add(obj(p("id", e.getValue().getID()), p("state", e.getValue().getState()),
p("lastHeardFrom", e.getValue().getLastCheckinTime())));
}
return RestUtil.R.ok(arr(update).toJson());
} catch (SchedulerException e) {
logger.debug("Unable to get all recordings:", e);
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
/**
*
*
*
* Prolonging service
*
*
*
*
*/
@GET
@Path("capture/{agent}")
@Produces(MediaType.APPLICATION_JSON)
@RestQuery(name = "currentcapture", description = "Get the current capture event catalog as JSON", returnDescription = "The current capture event catalog as JSON", pathParameters = {
@RestParameter(name = "agent", isRequired = true, type = Type.STRING, description = "The agent identifier") }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "DublinCore of current capture event is in the body of response"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "There is no ongoing recording"),
@RestResponse(responseCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE, description = "The agent is not ready to communicate") })
public Response currentCapture(@PathParam("agent") String agentId) throws NotFoundException {
if (service == null || agentService == null)
return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE)
.entity("Scheduler service is unavailable, please wait...").build();
try {
Opt current = service.getCurrentRecording(agentId);
if (current.isNone()) {
logger.info("No recording to stop found for agent '{}'!", agentId);
throw new NotFoundException("No recording to stop found for agent: " + agentId);
} else {
DublinCoreCatalog catalog = DublinCoreUtil.loadEpisodeDublinCore(workspace, current.get()).get();
return Response.ok(catalog.toJson()).build();
}
} catch (NotFoundException e) {
throw e;
} catch (Exception e) {
logger.error("Unable to get the immediate recording for agent '{}': {}", agentId, e);
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
@GET
@Path("capture/{agent}/upcoming")
@Produces(MediaType.APPLICATION_JSON)
@RestQuery(name = "upcomingcapture", description = "Get the upcoming capture event catalog as JSON", returnDescription = "The upcoming capture event catalog as JSON", pathParameters = {
@RestParameter(name = "agent", isRequired = true, type = Type.STRING, description = "The agent identifier") }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "DublinCore of the upcomfing capture event is in the body of response"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "There is no upcoming recording"),
@RestResponse(responseCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE, description = "The agent is not ready to communicate") })
public Response upcomingCapture(@PathParam("agent") String agentId) throws NotFoundException {
if (service == null || agentService == null)
return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE)
.entity("Scheduler service is unavailable, please wait...").build();
try {
Opt upcoming = service.getUpcomingRecording(agentId);
if (upcoming.isNone()) {
logger.info("No recording to stop found for agent '{}'!", agentId);
throw new NotFoundException("No recording to stop found for agent: " + agentId);
} else {
DublinCoreCatalog catalog = DublinCoreUtil.loadEpisodeDublinCore(workspace, upcoming.get()).get();
return Response.ok(catalog.toJson()).build();
}
} catch (NotFoundException e) {
throw e;
} catch (Exception e) {
logger.error("Unable to get the immediate recording for agent '{}': {}", agentId, e);
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
}
@POST
@Path("capture/{agent}")
@RestQuery(name = "startcapture", description = "Create an immediate event", returnDescription = "If events were successfully generated, status CREATED is returned", pathParameters = {
@RestParameter(name = "agent", isRequired = true, type = Type.STRING, description = "The agent identifier") }, restParameters = {
@RestParameter(name = "workflowDefinitionId", isRequired = false, type = Type.STRING, description = "The workflow definition id to use") }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_CREATED, description = "Recording started"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "There is no such agent"),
@RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "The agent is already recording"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission to start this immediate capture. Maybe you need to authenticate."),
@RestResponse(responseCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE, description = "The agent is not ready to communicate") })
public Response startCapture(@PathParam("agent") String agentId, @FormParam("workflowDefinitionId") String wfId)
throws NotFoundException, UnauthorizedException {
if (service == null || agentService == null || prolongingService == null)
return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE)
.entity("Scheduler service is unavailable, please wait...").build();
// Lookup the agent. If it doesn't exist, add a temporary registration
boolean adHocRegistration = false;
try {
agentService.getAgent(agentId);
} catch (NotFoundException e) {
Properties adHocProperties = new Properties();
adHocProperties.put(AGENT_REGISTRATION_TYPE, AGENT_REGISTRATION_TYPE_ADHOC);
agentService.setAgentConfiguration(agentId, adHocProperties);
agentService.setAgentState(agentId, AgentState.CAPTURING);
adHocRegistration = true;
logger.info("Temporarily registered agent '{}' for ad-hoc recording", agentId);
}
try {
Date now = new Date();
Date temporaryEndDate = DateTime.now().plus(prolongingService.getInitialTime()).toDate();
try {
List events = service.findConflictingEvents(agentId, now, temporaryEndDate);
if (!events.isEmpty()) {
logger.info("An already existing event is in a conflict with the the one to be created on the agent {}!",
agentId);
return Response.status(Status.CONFLICT).build();
}
} catch (SchedulerException e) {
logger.error("Unable to create immediate event on agent {}: {}", agentId, e);
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
String workflowId = defaultWorkflowDefinitionId;
if (StringUtils.isNotBlank(wfId))
workflowId = wfId;
Map caProperties = new HashMap<>();
caProperties.put("org.opencastproject.workflow.definition", workflowId);
caProperties.put("event.location", agentId);
caProperties.put("event.title", "Capture now event");
// caProperties.put("org.opencastproject.workflow.config.captionHold", "false");
// caProperties.put("org.opencastproject.workflow.config.archiveOp", "true");
// caProperties.put("org.opencastproject.workflow.config.trimHold", "false");
// TODO default metadata? configurable?
// A temporal with start and end period is needed! As well PROPERTY_SPATIAL is needed
DublinCoreCatalog eventCatalog = DublinCores.mkOpencastEpisode().getCatalog();
eventCatalog.set(PROPERTY_TITLE, "Capture now event");
eventCatalog.set(PROPERTY_TEMPORAL,
EncodingSchemeUtils.encodePeriod(new DCMIPeriod(now, temporaryEndDate), Precision.Second));
eventCatalog.set(PROPERTY_SPATIAL, agentId);
eventCatalog.set(PROPERTY_CREATED, EncodingSchemeUtils.encodeDate(new Date(), Precision.Minute));
// eventCatalog.set(PROPERTY_CREATOR, "demo");
// eventCatalog.set(PROPERTY_SUBJECT, "demo");
// eventCatalog.set(PROPERTY_LANGUAGE, "demo");
// eventCatalog.set(PROPERTY_CONTRIBUTOR, "demo");
// eventCatalog.set(PROPERTY_DESCRIPTION, "demo");
// TODO workflow properties
Map wfProperties = new HashMap<>();
MediaPackage mediaPackage = null;
try {
mediaPackage = MediaPackageBuilderFactory.newInstance().newMediaPackageBuilder().createNew();
mediaPackage = addCatalog(workspace, IOUtils.toInputStream(eventCatalog.toXmlString(), "UTF-8"),
"dublincore.xml", MediaPackageElements.EPISODE, mediaPackage);
prolongingService.schedule(agentId);
service.addEvent(now, temporaryEndDate, agentId, Collections. emptySet(), mediaPackage, wfProperties,
caProperties, Opt. none());
return Response.status(Status.CREATED)
.header("Location", serverUrl + serviceUrl + '/' + mediaPackage.getIdentifier().toString() + ".xml")
.build();
} catch (Exception e) {
prolongingService.stop(agentId);
if (e instanceof UnauthorizedException)
throw (UnauthorizedException) e;
logger.error("Unable to create immediate event on agent {}: {}", agentId, e);
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
} finally {
if (mediaPackage != null) {
for (MediaPackageElement elem : $(mediaPackage.getElements())
.bind(MediaPackageSupport.Filters.byFlavor(MediaPackageElements.EPISODE).toFn())) {
try {
workspace.delete(elem.getURI());
} catch (NotFoundException e) {
logger.warn("Unable to find (and hence, delete), this mediapackage '{}' element '{}'",
mediaPackage.getIdentifier(), elem.getIdentifier());
} catch (IOException e) {
chuck(e);
}
}
}
}
} catch (Throwable t) {
throw t;
} finally {
if (adHocRegistration) {
agentService.removeAgent(agentId);
logger.info("Removed temporary registration for agent '{}'", agentId);
}
}
}
@DELETE
@Path("capture/{agent}")
@Produces(MediaType.TEXT_PLAIN)
@RestQuery(name = "stopcapture", description = "Stops an immediate capture.", returnDescription = "OK if event were successfully stopped", pathParameters = {
@RestParameter(name = "agent", isRequired = true, description = "The agent identifier", type = Type.STRING) }, responses = {
@RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Recording stopped"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_MODIFIED, description = "The recording was already stopped"),
@RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "There is no such agent"),
@RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission to stop this immediate capture. Maybe you need to authenticate."),
@RestResponse(responseCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE, description = "The agent is not ready to communicate") })
public Response stopCapture(@PathParam("agent") String agentId) throws NotFoundException, UnauthorizedException {
if (service == null || agentService == null || prolongingService == null)
return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE)
.entity("Scheduler service is unavailable, please wait...").build();
boolean isAdHoc = false;
try {
Agent agent = agentService.getAgent(agentId);
String registrationType = (String) agent.getConfiguration().get(AGENT_REGISTRATION_TYPE);
isAdHoc = AGENT_REGISTRATION_TYPE_ADHOC.equals(registrationType);
} catch (NotFoundException e) {
logger.debug("Temporarily registered agent '{}' for ad-hoc recording already removed", agentId);
}
try {
String eventId;
MediaPackage mp;
DublinCoreCatalog eventCatalog;
try {
Opt current = service.getCurrentRecording(agentId);
if (current.isNone()) {
logger.info("No recording to stop found for agent '{}'!", agentId);
return Response.notModified().build();
} else {
mp = current.get();
eventCatalog = DublinCoreUtil.loadEpisodeDublinCore(workspace, mp).get();
eventId = mp.getIdentifier().toString();
}
} catch (Exception e) {
logger.error("Unable to get the immediate recording for agent '{}': {}", agentId, e);
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
try {
DCMIPeriod period = EncodingSchemeUtils
.decodeMandatoryPeriod(eventCatalog.getFirst(DublinCore.PROPERTY_TEMPORAL));
eventCatalog.set(PROPERTY_TEMPORAL,
EncodingSchemeUtils.encodePeriod(new DCMIPeriod(period.getStart(), new Date()), Precision.Second));
mp = addCatalog(workspace, IOUtils.toInputStream(eventCatalog.toXmlString(), "UTF-8"), "dublincore.xml",
MediaPackageElements.EPISODE, mp);
service.updateEvent(eventId, Opt. none(), Opt. none(), Opt. none(),
Opt.> none(), Opt.some(mp), Opt.
© 2015 - 2025 Weber Informatics LLC | Privacy Policy