/**
* 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.impl;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.opencastproject.scheduler.impl.Util.getEventIdentifier;
import static org.opencastproject.scheduler.impl.Util.setEventIdentifierImmutable;
import static org.opencastproject.util.data.Tuple.tuple;
import org.opencastproject.capture.admin.api.CaptureAgentStateService;
import org.opencastproject.index.IndexProducer;
import org.opencastproject.mediapackage.Catalog;
import org.opencastproject.mediapackage.EName;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageBuilderFactory;
import org.opencastproject.mediapackage.MediaPackageElement;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageElements;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.message.broker.api.MessageReceiver;
import org.opencastproject.message.broker.api.MessageSender;
import org.opencastproject.message.broker.api.index.AbstractIndexProducer;
import org.opencastproject.message.broker.api.index.IndexRecreateObject;
import org.opencastproject.message.broker.api.index.IndexRecreateObject.Service;
import org.opencastproject.message.broker.api.scheduler.SchedulerItem;
import org.opencastproject.metadata.dublincore.DCMIPeriod;
import org.opencastproject.metadata.dublincore.DublinCore;
import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
import org.opencastproject.metadata.dublincore.DublinCoreCatalogList;
import org.opencastproject.metadata.dublincore.DublinCoreCatalogService;
import org.opencastproject.metadata.dublincore.DublinCoreValue;
import org.opencastproject.metadata.dublincore.DublinCores;
import org.opencastproject.metadata.dublincore.EncodingSchemeUtils;
import org.opencastproject.metadata.dublincore.Precision;
import org.opencastproject.scheduler.api.SchedulerException;
import org.opencastproject.scheduler.api.SchedulerQuery;
import org.opencastproject.scheduler.api.SchedulerQuery.Sort;
import org.opencastproject.scheduler.api.SchedulerService;
import org.opencastproject.security.api.AccessControlList;
import org.opencastproject.security.api.DefaultOrganization;
import org.opencastproject.security.api.Organization;
import org.opencastproject.security.api.OrganizationDirectoryService;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.security.util.SecurityUtil;
import org.opencastproject.series.api.SeriesService;
import org.opencastproject.util.Log;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.OsgiUtil;
import org.opencastproject.util.SolrUtils;
import org.opencastproject.util.data.Effect0;
import org.opencastproject.util.data.Tuple;
import org.opencastproject.util.data.functions.Strings;
import org.opencastproject.workflow.api.WorkflowDefinition;
import org.opencastproject.workflow.api.WorkflowException;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowOperationInstance;
import org.opencastproject.workflow.api.WorkflowParser;
import org.opencastproject.workflow.api.WorkflowService;
import org.opencastproject.workspace.api.Workspace;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import net.fortuna.ical4j.model.Period;
import net.fortuna.ical4j.model.ValidationException;
import net.fortuna.ical4j.model.property.RRule;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.text.WordUtils;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.osgi.framework.ServiceException;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Implementation of {@link SchedulerService}.
*
*/
public class SchedulerServiceImpl extends AbstractIndexProducer implements SchedulerService, ManagedService {
/** The logger */
private static final Logger logger = LoggerFactory.getLogger(SchedulerServiceImpl.class);
/** The metadata key used to store the workflow identifier in an event's metadata */
public static final String WORKFLOW_INSTANCE_ID_KEY = "org.opencastproject.workflow.id";
/** The metadata key used to store the workflow definition in an event's metadata */
public static final String WORKFLOW_DEFINITION_ID_KEY = "org.opencastproject.workflow.definition";
/** The workflow configuration prefix */
public static final String WORKFLOW_CONFIG_PREFIX = "org.opencastproject.workflow.config.";
/** The schedule workflow operation identifier */
public static final String CAPTURE_OPERATION_ID = "capture";
/** The schedule workflow operation identifier */
public static final String SCHEDULE_OPERATION_ID = "schedule";
/** The workflow operation property that stores the event start time, as milliseconds since 1970 */
public static final String WORKFLOW_OPERATION_KEY_SCHEDULE_START = "schedule.start";
/** The workflow operation property that stores the event stop time, as milliseconds since 1970 */
public static final String WORKFLOW_OPERATION_KEY_SCHEDULE_STOP = "schedule.stop";
/** The workflow operation property that stores the event location */
public static final String WORKFLOW_OPERATION_KEY_SCHEDULE_LOCATION = "schedule.location";
/** The immediate workflow creation configuration key */
public static final String IMMEDIATE_WORKFLOW_CREATION = "immediate.workflow.creation";
/** The last modifed cache configuration key */
private static final String CFG_KEY_LAST_MODIFED_CACHE_EXPIRE = "last_modified_cache_expire";
/** The default cache expire time in seconds */
private static final int DEFAULT_CACHE_EXPIRE = 60;
/** The Etag for an empty calendar */
private static final String EMPTY_CALENDAR_ETAG = "mod0";
private ComponentContext cc;
/** The last modified cache */
protected Cache<String, String> lastModifiedCache = CacheBuilder.newBuilder()
.expireAfterWrite(DEFAULT_CACHE_EXPIRE, TimeUnit.SECONDS).build();
/** Whether to immediate create and start a workflow for the event */
protected boolean immediateWorkflowCreation = true;
/** The message broker sender service */
protected MessageSender messageSender;
/** The message broker receiver service */
protected MessageReceiver messageReceiver;
/** The security service used to run the security context with. */
protected OrganizationDirectoryService organizationDirectoryService;
/** The security service used to run the security context with. */
protected SecurityService securityService;
/** The series service */
protected SeriesService seriesService;
/** The workflow service */
protected WorkflowService workflowService;
/** The capture agent state service */
protected CaptureAgentStateService agentService;
/** Persistent storage for events */
protected SchedulerServiceDatabase persistence;
/** Solr index for events */
protected SchedulerServiceIndex index;
/** Workspace */
protected Workspace workspace;
/** The dublin core catalog service */
protected DublinCoreCatalogService dcService;
/** Preprocessing workflow definition for scheduler */
protected WorkflowDefinition preprocessingWorkflowDefinition;
/**
* OSGi callback for setting Series Service.
*
* @param seriesService
*/
public void setSeriesService(SeriesService seriesService) {
this.seriesService = seriesService;
}
/** OSGi callback */
public void setWorkspace(Workspace workspace) {
this.workspace = workspace;
}
/**
* OSGi callback for setting Workflow Service.
*
* @param workflowService
*/
public void setWorkflowService(WorkflowService workflowService) {
this.workflowService = workflowService;
}
/**
* OSGi callback for setting Capture Agent State Service.
*
* @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;
}
/**
* OSGi callback to set Persistence Service.
*
* @param persistence
*/
public void setPersistence(SchedulerServiceDatabase persistence) {
this.persistence = persistence;
}
/**
* OSGi callback to set indexer.
*
* @param index
*/
public void setIndex(SchedulerServiceIndex index) {
this.index = index;
}
/**
* OSGi callback to set message sender.
*
* @param messageSender
*/
public void setMessageSender(MessageSender messageSender) {
this.messageSender = messageSender;
}
/**
* OSGi callback to set message receiver.
*
* @param messageReceiver
*/
public void setMessageReceiver(MessageReceiver messageReceiver) {
this.messageReceiver = messageReceiver;
}
/**
* OSGi callback to set organization directory service.
*
* @param organizationDirectoryService
*/
public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) {
this.organizationDirectoryService = organizationDirectoryService;
}
/**
* OSGi callback to set security service.
*
* @param securityService
*/
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
/**
* OSGi callback to set the dublin core catalog service.
*
* @param dcService
*/
public void setDublinCoreCatalogService(DublinCoreCatalogService dcService) {
this.dcService = dcService;
}
/**
* Activates Scheduler Service. Checks whether we are using synchronous or asynchronous indexing. If asynchronous is
* used, Executor service is set. If index is empty, persistent storage is queried if it contains any series. If that
* is the case, events are retrieved and indexed.
*
* @param cc
* ComponentContext
* @throws Exception
*/
public void activate(ComponentContext cc) throws Exception {
logger.info("Activating Scheduler Service");
long instancesInSolr = 0L;
try {
instancesInSolr = this.index.count();
} catch (Exception e) {
throw new IllegalStateException(e);
}
if (instancesInSolr == 0L) {
try {
DublinCoreCatalog[] events = persistence.getAllEvents();
if (events.length != 0) {
logger.info("The event index is empty. Populating it with {} events", Integer.valueOf(events.length));
for (DublinCoreCatalog event : events) {
final long id = getEventIdentifier(event);
Properties properties = persistence.getEventMetadata(id);
logger.debug("Adding recording event '{}' to the scheduler search index", id);
index.index(event, properties);
index.indexOptOut(id, persistence.isOptOut(id));
index.indexBlacklisted(id, persistence.isBlacklisted(id));
}
logger.info("Finished populating event search index");
}
} catch (Exception e) {
logger.warn("Unable to index event instances: {}", e.getMessage());
throw new ServiceException(e.getMessage());
}
}
this.cc = cc;
super.activate();
}
@Override
public void deactivate() {
super.deactivate();
}
/**
* Returns WorkflowDefinition for executing when event is created.
*
* @return {@link WorkflowDefinition}
* @throws IllegalStateException
* if definition cannot be loaded
*/
protected WorkflowDefinition getPreprocessingWorkflowDefinition() throws IllegalStateException {
if (preprocessingWorkflowDefinition == null) {
InputStream in = null;
try {
in = SchedulerServiceImpl.class.getResourceAsStream("/scheduler-workflow-definition.xml");
preprocessingWorkflowDefinition = WorkflowParser.parseWorkflowDefinition(in);
} catch (Exception e) {
throw new IllegalStateException("Unable to load the preprocessing workflow definition", e);
} finally {
IOUtils.closeQuietly(in);
}
}
return preprocessingWorkflowDefinition;
}
/**
* Starts workflow for new event. Creates {@link MediaPackage} and populates it with values from
* {@link DublinCoreCatalog}.
*
* @param event
* {@link DublinCoreCatalog} associated with event
* @param startDate
* start date of event
* @param endDate
* end date of event
* @param wfProperties
* the workflow configuration properties
* @return {@link WorkflowInstance} of started worflow
* @throws WorkflowException
* if exception occurred while starting worflow
* @throws MediaPackageException
* if media package cannot be created
*/
protected WorkflowInstance startWorkflowInstance(DublinCoreCatalog event, Date startDate, Date endDate,
Map<String, String> wfProperties) throws WorkflowException, MediaPackageException {
// Build a mediapackage using the event metadata
MediaPackage mediapackage = MediaPackageBuilderFactory.newInstance().newMediaPackageBuilder().createNew();
try {
populateMediapackageWithStandardDCFields(mediapackage, event);
} catch (Exception e) {
throw new MediaPackageException(e);
}
mediapackage.setDate(startDate);
mediapackage.setDuration(endDate.getTime() - startDate.getTime());
// Build a properties set for this event
wfProperties.put(WORKFLOW_OPERATION_KEY_SCHEDULE_START, Long.toString(startDate.getTime()));
wfProperties.put(WORKFLOW_OPERATION_KEY_SCHEDULE_STOP, Long.toString(endDate.getTime()));
wfProperties.put(WORKFLOW_OPERATION_KEY_SCHEDULE_LOCATION, event.getFirst(DublinCore.PROPERTY_SPATIAL));
// Start the workflow
return workflowService.start(getPreprocessingWorkflowDefinition(), mediapackage, wfProperties);
}
/**
* Updates workflow for the given event.
* <p>
* This method will only allow updates to workflows that are either in the "schedule" operation or are in instantiated
* or paused state.
*
* @param event
* {@link DublinCoreCatalog} of updated event
* @param startDate
* start date of event
* @param endDate
* end date of event
* @param wfProperties
* the workflow configuration properties
* @throws SchedulerException
* if workflow is not in paused or instantiated state and current operation is not 'schedule'
* @throws NotFoundException
* if workflow with ID from DublinCore cannot be found
* @throws WorkflowException
* if update fails
* @throws UnauthorizedException
* if the current user is not authorized to update the workflow
*/
protected void updateWorkflow(DublinCoreCatalog event, Date startDate, Date endDate, Map<String, String> wfProperties)
throws SchedulerException, NotFoundException, WorkflowException, UnauthorizedException {
WorkflowInstance workflow = workflowService.getWorkflowById(getEventIdentifier(event));
WorkflowOperationInstance operation = workflow.getCurrentOperation();
String operationId = operation.getTemplate();
// if the workflow is not in a hold state or in any of 'schedule' or 'capture' as the current operation, we can't
// update the event
if (!SCHEDULE_OPERATION_ID.equals(operationId) && !CAPTURE_OPERATION_ID.equals(operationId)) {
boolean isPaused = WorkflowInstance.WorkflowState.PAUSED.equals(workflow.getState());
boolean isInstantiated = WorkflowInstance.WorkflowState.INSTANTIATED.equals(workflow.getState());
if (!isPaused && !isInstantiated)
throw new SchedulerException("Workflow " + workflow + " is not in the paused state, so it can not be updated");
}
MediaPackage mediapackage = workflow.getMediaPackage();
// removes old values
for (String creator : mediapackage.getCreators()) {
mediapackage.removeCreator(creator);
}
for (String contributor : mediapackage.getContributors()) {
mediapackage.removeContributor(contributor);
}
for (String subject : mediapackage.getSubjects()) {
mediapackage.removeSubject(subject);
}
// set new values
try {
populateMediapackageWithStandardDCFields(mediapackage, event);
} catch (NotFoundException e) {
throw e;
} catch (Exception e) {
throw new SchedulerException(e);
}
mediapackage.setDate(startDate);
mediapackage.setDuration(endDate.getTime() - startDate.getTime());
// Update the properties
operation.setConfiguration(WORKFLOW_OPERATION_KEY_SCHEDULE_START, Long.toString(startDate.getTime()));
operation.setConfiguration(WORKFLOW_OPERATION_KEY_SCHEDULE_STOP, Long.toString(endDate.getTime()));
operation.setConfiguration(WORKFLOW_OPERATION_KEY_SCHEDULE_LOCATION, event.getFirst(DublinCore.PROPERTY_SPATIAL));
// Set the location in the workflow as well, so that it shows up in the UI properly.
// Update the same workflow global parameters created in the schedule create method
workflow.setConfiguration(WORKFLOW_OPERATION_KEY_SCHEDULE_START, Long.toString(startDate.getTime()));
workflow.setConfiguration(WORKFLOW_OPERATION_KEY_SCHEDULE_STOP, Long.toString(endDate.getTime()));
workflow.setConfiguration(WORKFLOW_OPERATION_KEY_SCHEDULE_LOCATION, event.getFirst(DublinCore.PROPERTY_SPATIAL));
for (Entry<String, String> property : wfProperties.entrySet()) {
workflow.setConfiguration(property.getKey(), property.getValue());
}
// update the workflow
workflowService.update(workflow);
}
/**
* Populates MediaPackage with standard values from DublinCore such as: title, language, license, series id, creators,
* contributors and subjects.
*
* @param mp
* {@link MediaPackage} to be updated
* @param dc
* {@link DublinCoreCatalog} for event
*/
private void populateMediapackageWithStandardDCFields(MediaPackage mp, DublinCoreCatalog dc) throws Exception {
String seriesId = dc.getFirst(DublinCore.PROPERTY_IS_PART_OF);
mp.setTitle(dc.getFirst(DublinCore.PROPERTY_TITLE));
mp.setLanguage(dc.getFirst(DublinCore.PROPERTY_LANGUAGE));
mp.setLicense(dc.getFirst(DublinCore.PROPERTY_LICENSE));
if (isBlank(mp.getSeries()) && isNotBlank(seriesId)) {
// add series dc to mp
// add the episode catalog
DublinCoreCatalog seriesCatalog = seriesService.getSeries(seriesId);
addCatalog(IOUtils.toInputStream(seriesCatalog.toXmlString(), "UTF-8"), "dublincore.xml",
MediaPackageElements.SERIES, mp);
} else if (isNotBlank(mp.getSeries()) && !mp.getSeries().equals(seriesId)) {
// switch to new series dc and remove old
for (Catalog c : mp.getCatalogs(MediaPackageElements.SERIES)) {
logger.debug("Deleting existing series dublin core {} from media package {}", c.getIdentifier(), mp
.getIdentifier().toString());
mp.remove(c);
workspace.delete(c.getURI());
}
seriesId = mp.getSeries();
DublinCoreCatalog seriesCatalog = seriesService.getSeries(seriesId);
addCatalog(IOUtils.toInputStream(seriesCatalog.toXmlString(), "UTF-8"), "dublincore.xml",
MediaPackageElements.SERIES, mp);
} else if (isNotBlank(mp.getSeries()) && isBlank(seriesId)) {
// remove series dc
for (Catalog c : mp.getCatalogs(MediaPackageElements.SERIES)) {
logger.debug("Deleting existing series dublin core {} from media package {}", c.getIdentifier(), mp
.getIdentifier().toString());
mp.remove(c);
workspace.delete(c.getURI());
}
}
// set series id
mp.setSeries(seriesId);
// set series title
if (isNotBlank(seriesId)) {
try {
DublinCoreCatalog sdc = seriesService.getSeries(seriesId);
mp.setSeriesTitle(sdc.getFirst(DublinCore.PROPERTY_TITLE));
} catch (Exception e) {
logger.error("Unable to find series: " + seriesId, e);
throw e;
}
} else {
mp.setSeriesTitle(null);
}
for (DublinCoreValue value : dc.get(DublinCore.PROPERTY_CREATOR)) {
mp.addCreator(value.getValue());
}
for (DublinCoreValue value : dc.get(DublinCore.PROPERTY_CONTRIBUTOR)) {
mp.addContributor(value.getValue());
}
for (DublinCoreValue value : dc.get(DublinCore.PROPERTY_SUBJECT)) {
mp.addSubject(value.getValue());
}
// remove existing episodes
for (Catalog c : mp.getCatalogs(MediaPackageElements.EPISODE)) {
logger.debug("Deleting existing episode dublin core {} from media package {}", c.getIdentifier(), mp
.getIdentifier().toString());
mp.remove(c);
workspace.delete(c.getURI());
}
// add the episode catalog
addCatalog(IOUtils.toInputStream(dc.toXmlString(), "UTF-8"), "dublincore.xml", MediaPackageElements.EPISODE, mp);
}
/**
* Adds a catalog to the working file repository.
*
* @param in
* the catalog's input stream
* @param fileName
* the catalog file name
* @param flavor
* the catalog's flavor
* @param mediaPackage
* the parent mediapackage
* @return the modified mediapackage
* @throws IOException
* if the catalog cannot be stored in the working file repository
*/
private MediaPackage addCatalog(InputStream in, String fileName, MediaPackageElementFlavor flavor,
MediaPackage mediaPackage) throws IOException {
try {
String elementId = UUID.randomUUID().toString();
URI catalogUrl = workspace.put(mediaPackage.getIdentifier().compact(), elementId, fileName, in);
logger.info("Adding catalog with flavor {} to mediapackage {}", flavor, mediaPackage);
MediaPackageElement mpe = mediaPackage.add(catalogUrl, MediaPackageElement.Type.Catalog, flavor);
mpe.setIdentifier(elementId);
return mediaPackage;
} catch (IOException e) {
throw e;
}
}
/**
* Stops workflow with specified ID.
*
* @param eventID
* workflow to be stopped
* @throws NotFoundException
* if there is no workflow with specified ID
* @throws UnauthorizedException
* if the current user is not authorized to stop the workflow
*/
protected void stopWorkflowInstance(long eventID) throws NotFoundException, UnauthorizedException {
try {
WorkflowInstance instance = workflowService.stop(eventID);
for (MediaPackageElement elem : instance.getMediaPackage().getElements()) {
workspace.delete(elem.getURI());
}
} catch (WorkflowException e) {
logger.warn("Can not stop workflow {}: {}", eventID, e.getMessage());
} catch (IOException e) {
logger.warn("Unable to delete mediapackage element {}", e.getMessage());
}
}
@Override
@SuppressWarnings("rawtypes")
public void updated(Dictionary properties) throws ConfigurationException {
if (properties != null) {
for (String immediateWorkflowCreationString : OsgiUtil.getOptCfg(properties, IMMEDIATE_WORKFLOW_CREATION)) {
Boolean immediateBoolean = BooleanUtils.toBooleanObject(immediateWorkflowCreationString);
if (immediateBoolean != null)
immediateWorkflowCreation = immediateBoolean.booleanValue();
}
for (Integer cacheExpireDuration : OsgiUtil.getOptCfg(properties, CFG_KEY_LAST_MODIFED_CACHE_EXPIRE).bind(
Strings.toInt)) {
lastModifiedCache = CacheBuilder.newBuilder().expireAfterWrite(cacheExpireDuration, TimeUnit.SECONDS).build();
}
}
}
/*
* (non-Javadoc)
*
* @see
* org.opencastproject.scheduler.api.SchedulerService#addEvent(org.opencastproject.metadata.dublincore.DublinCoreCatalog
* , java.lang.String)
*/
@Override
public Long addEvent(DublinCoreCatalog eventCatalog, Map<String, String> wfProperties) throws SchedulerException,
UnauthorizedException {
DCMIPeriod period = EncodingSchemeUtils.decodeMandatoryPeriod(eventCatalog.getFirst(DublinCore.PROPERTY_TEMPORAL));
if (!period.hasEnd() || !period.hasStart()) {
throw new IllegalArgumentException(
"Dublin core field dc:temporal does not contain information about start and end of event");
}
final long eventId;
if (immediateWorkflowCreation) {
Date startDate = period.getStart();
Date endDate = period.getEnd();
try {
eventId = startWorkflowInstance(eventCatalog, startDate, endDate, wfProperties).getId();
} catch (WorkflowException e) {
logger.error("Could not start workflow: {}", e.getMessage());
throw new SchedulerException(e);
} catch (MediaPackageException e) {
logger.error("Could not create media package: {}", e.getMessage());
throw new SchedulerException(e);
}
} else {
eventId = RandomUtils.nextLong(0, Long.MAX_VALUE);
}
addEventInternal(setEventIdentifierImmutable(eventId, eventCatalog));
return eventId;
}
/** Add an event. The event DC _must_ have the identfier set. */
private void addEventInternal(final DublinCoreCatalog event) throws SchedulerException, UnauthorizedException {
// store event asociated
final long eventId = getEventIdentifier(event);
try {
persistence.storeEvents(event);
String mediaPackageId = UUID.randomUUID().toString();
persistence.updateEventMediaPackageId(eventId, mediaPackageId);
messageSender.sendObjectMessage(SchedulerItem.SCHEDULER_QUEUE, MessageSender.DestinationType.Queue,
SchedulerItem.updateCatalog(mediaPackageId, event));
} catch (NotFoundException e) {
// If this happens the store operation did not work
throw new IllegalStateException(e);
} catch (SchedulerServiceDatabaseException e) {
logger.error("Could not store event to persistent storage: {}", e);
logger.info("Canceling workflow associated with event");
try {
stopWorkflowInstance(eventId);
} catch (NotFoundException e1) {
// should not happen
}
throw new SchedulerException(e);
}
try {
index.index(event);
} catch (Exception e) {
logger.warn("Unable to index event with ID '{}': {}", eventId, e.getMessage());
throw new SchedulerException(e);
}
// update with default CA properties
try {
updateCaptureAgentMetadata(new Properties(), tuple(eventId, event));
} catch (NotFoundException e) {
// should not happen
throw new IllegalStateException(e);
}
}
/*
* (non-Javadoc)
*
* @see org.opencastproject.scheduler.api.SchedulerService#addReccuringEvent(org.opencastproject.metadata.dublincore.
* DublinCoreCatalog, java.lang.String, java.util.Date, java.util.Date, long)
*/
@Override
public Long[] addReccuringEvent(DublinCoreCatalog templateCatalog, Map<String, String> wfProperties)
throws SchedulerException, UnauthorizedException {
final List<DublinCoreCatalog> eventList;
try {
eventList = createEventCatalogsFromReccurence(templateCatalog);
} catch (Exception e) {
logger.error("Could not create Dublin Cores for events from template: {}", e.getMessage());
throw new SchedulerException(e);
}
List<Long> eventsIDs = new ArrayList<Long>();
for (DublinCoreCatalog event : eventList) {
Long eventId = addEvent(event, wfProperties);
eventsIDs.add(eventId);
}
return eventsIDs.toArray(new Long[eventsIDs.size()]);
}
/*
* (non-Javadoc)
*
* @see org.opencastproject.scheduler.api.SchedulerService#updateCaptureAgentMetadata(java.lang.Long[],
* java.util.Properties)
*/
@Override
public void updateCaptureAgentMetadata(final Properties configuration, Tuple<Long, DublinCoreCatalog>... events)
throws NotFoundException, SchedulerException {
for (Tuple<Long, DublinCoreCatalog> e : events) {
final long eventId = e.getA();
final DublinCoreCatalog event = e.getB();
// create clone and update with matching values from DC
Properties properties = (Properties) configuration.clone();
properties.put("event.title", event.getFirst(DublinCore.PROPERTY_TITLE));
if (StringUtils.isNotBlank(event.getFirst(DublinCore.PROPERTY_IS_PART_OF))) {
properties.put("event.series", event.getFirst(DublinCore.PROPERTY_IS_PART_OF));
}
if (StringUtils.isNotBlank(event.getFirst(DublinCore.PROPERTY_SPATIAL))) {
properties.put("event.location", event.getFirst(DublinCore.PROPERTY_SPATIAL));
}
// store
try {
persistence.updateEventWithMetadata(eventId, properties);
messageSender.sendObjectMessage(SchedulerItem.SCHEDULER_QUEUE, MessageSender.DestinationType.Queue,
SchedulerItem.updateProperties(persistence.getMediaPackageId(eventId),
new HashMap<String, String>((Map) properties)));
} catch (SchedulerServiceDatabaseException ex) {
logger.error("Failed to update capture agent configuration for event '{}': {}", eventId, ex.getMessage());
throw new SchedulerException(ex);
}
try {
index.index(eventId, properties);
} catch (Exception ex) {
logger.warn("Unable to update capture agent properties for event with ID '{}': {}", eventId, ex.getMessage());
throw new SchedulerException(ex);
}
}
}
/*
* (non-Javadoc)
*
* @see org.opencastproject.scheduler.api.SchedulerService#updateEvent(org.opencastproject.metadata.dublincore.
* DublinCoreCatalog)
*/
@Override
public void updateEvent(final long eventId, final DublinCoreCatalog eventCatalog, Map<String, String> wfProperties)
throws NotFoundException, SchedulerException, UnauthorizedException {
if (eventCatalog == null) {
logger.warn("Cannot update <null> event.");
return;
}
updateEvent(setEventIdentifierImmutable(eventId, eventCatalog), wfProperties);
}
/** Internal impl of an event update. The event DC _must_ have the identfier set. */
private void updateEvent(final DublinCoreCatalog event, Map<String, String> wfProperties) throws NotFoundException,
SchedulerException, UnauthorizedException {
final DCMIPeriod period = EncodingSchemeUtils.decodeMandatoryPeriod(event.getFirst(DublinCore.PROPERTY_TEMPORAL));
if (!period.hasEnd() || !period.hasStart()) {
throw new IllegalArgumentException(
"Dublin core field dc:temporal does not contain information about start and end of event");
}
final Date startDate = period.getStart();
final Date endDate = period.getEnd();
final long eventId = getEventIdentifier(event);
try {
verifyActive(eventId);
persistence.updateEvent(event);
messageSender.sendObjectMessage(SchedulerItem.SCHEDULER_QUEUE, MessageSender.DestinationType.Queue,
SchedulerItem.updateCatalog(persistence.getMediaPackageId(eventId), event));
} catch (SchedulerServiceDatabaseException e) {
logger.error("Could not update event {} in persistent storage: {}", eventId, e.getMessage());
throw new SchedulerException(e);
}
try {
final Properties p = persistence.getEventMetadata(eventId);
updateCaptureAgentMetadata(p, tuple(eventId, event));
} catch (SchedulerServiceDatabaseException e) {
logger.error("Could not update event {} in persistent storage: {}", eventId, e.getMessage());
throw new SchedulerException(e);
}
try {
index.index(event);
} catch (Exception e) {
logger.warn("Unable to index event with ID '{}': {}", eventId, e.getMessage());
throw new SchedulerException(e);
}
// update workflow
try {
updateWorkflow(event, startDate, endDate, wfProperties);
} catch (NotFoundException e) {
logger.info("No workflow to update: Event with ID {} does not have a workflow yet", eventId);
} catch (WorkflowException e) {
logger.error("Could not update workflow for event with ID '{}': {}", eventId, e.getMessage());
throw new SchedulerException(e);
}
}
/**
* TODO: Update this function so that it uses a new service function with a single transaction so that it is not slow.
*/
@Override
public void updateEvents(List<Long> eventIds, final DublinCoreCatalog eventCatalog) throws NotFoundException,
SchedulerException, UnauthorizedException {
StringBuffer errors = new StringBuffer();
int errorCount = 0;
SchedulerQuery q = new SchedulerQuery();
q.withIdInList(eventIds);
q.withSort(Sort.EVENT_START);
List<DublinCoreCatalog> catalogs = search(q).getCatalogList();
for (int i = 0; i < catalogs.size(); i++) {
DublinCoreCatalog cat = catalogs.get(i);
for (EName prop : eventCatalog.getProperties()) {
if (DublinCore.PROPERTY_IDENTIFIER.equals(prop)) {
// skip
} else if (!eventCatalog.get(prop).isEmpty()) {
List<DublinCoreValue> vals = eventCatalog.get(prop);
if (DublinCore.PROPERTY_TITLE.equals(prop)) {
List<DublinCoreValue> incrementedVals = new ArrayList<DublinCoreValue>();
for (DublinCoreValue v : vals) {
incrementedVals.add(DublinCoreValue.mk(v.getValue().concat(" " + String.valueOf(i + 1)), v.getLanguage(),
v.getEncodingScheme()));
}
cat.set(prop, incrementedVals);
} else {
cat.set(prop, vals);
}
}
}
try {
updateEvent(getEventIdentifier(cat), cat, new HashMap<String, String>());
} catch (SchedulerException se) {
// TODO: Instead of logging and continuing, should all updates fail if one is in the past?
errors.append((errors.length() > 0 ? " " : "") + se.getMessage());
errorCount++;
logger.error("Could not update catalog for event with ID '{}': {}", getEventIdentifier(cat), se.getMessage());
}
}
// After updating what is possible, throw error on found issues
if (errors.length() > 0) {
// Already logged above, but allow information to be thrown to client
// TODO: convert to an SchedulerEventEndedException
throw new SchedulerException("Could not update " + errorCount + " of " + catalogs.size() + " events: "
+ errors.toString());
}
}
/**
* Given recurrence pattern and template DublinCore, DublinCores for multiple events are generated. Each event will
* have template's title plus sequential number. Spatial property of DublinCore is set to represent time period in
* which event will take place.
*
* @param template
* {@link DublinCoreCatalog} used as template
* @return list of {@link DublinCoreCatalog}s
* @throws ParseException
* if recurrence pattern cannot be parsed
*/
protected List<DublinCoreCatalog> createEventCatalogsFromReccurence(DublinCoreCatalog template)
throws ParseException, IllegalArgumentException {
if (!template.hasValue(DublinCores.OC_PROPERTY_RECURRENCE)) {
throw new IllegalArgumentException("Event has no recurrence pattern.");
}
TimeZone tz = null; // Create timezone based on CA's reported TZ.
if (template.hasValue(DublinCores.OC_PROPERTY_AGENT_TIMEZONE)) {
tz = TimeZone.getTimeZone(template.getFirst(DublinCores.OC_PROPERTY_AGENT_TIMEZONE));
} else { // No timezone was present, assume the serve's local timezone.
tz = TimeZone.getDefault();
logger.warn("The field 'agentTimezone' has not been set in the dubline core catalog. The default server timezone will be used.");
}
DCMIPeriod temporal = EncodingSchemeUtils.decodeMandatoryPeriod(template.getFirst(DublinCore.PROPERTY_TEMPORAL));
if (!temporal.hasEnd() || !temporal.hasStart()) {
throw new IllegalArgumentException(
"Dublin core field dc:temporal does not contain information about start and end of event");
}
Date start = temporal.getStart();
Date end = temporal.getEnd();
Long duration = 0L;
String durationProperty = template.getFirst(DublinCores.OC_PROPERTY_DURATION);
if (StringUtils.isNotBlank(durationProperty))
duration = Long.parseLong(durationProperty);
else
throw new IllegalArgumentException("Dublin core field dc:duration has not been set or is empty");
RRule rrule = new RRule(template.getFirst(DublinCores.OC_PROPERTY_RECURRENCE));
List<Period> periods = Util.calculatePeriods(start, end, duration, rrule, tz);
List<DublinCoreCatalog> events = new LinkedList<DublinCoreCatalog>();
int i = 1;
int length = Integer.toString(periods.size()).length();
for (Period p : periods) {
DublinCoreCatalog event = (DublinCoreCatalog) template.clone();
int numZeros = length - Integer.toString(i).length();
StringBuilder sb = new StringBuilder();
for (int j = 0; j < numZeros; j++) {
sb.append(0);
}
sb.append(i);
event.set(DublinCore.PROPERTY_TITLE, template.getFirst(DublinCore.PROPERTY_TITLE) + " " + sb.toString());
DublinCoreValue eventTime = EncodingSchemeUtils.encodePeriod(new DCMIPeriod(p.getStart(), p.getEnd()),
Precision.Second);
event.set(DublinCore.PROPERTY_TEMPORAL, eventTime);
events.add(event);
i++;
}
return events;
}
/*
* (non-Javadoc)
*
* @see org.opencastproject.scheduler.api.SchedulerService#removeEvent(long)
*/
@Override
public void removeEvent(final long eventId) throws SchedulerException, NotFoundException, UnauthorizedException {
try {
try {
stopWorkflowInstance(eventId);
} catch (NotFoundException e) {
// There is no workflow to stop, continue deleting the event
}
String mediaPackageId = persistence.getMediaPackageId(eventId);
persistence.deleteEvent(eventId);
messageSender.sendObjectMessage(SchedulerItem.SCHEDULER_QUEUE, MessageSender.DestinationType.Queue,
SchedulerItem.delete(mediaPackageId));
} catch (SchedulerServiceDatabaseException e) {
logger.error("Could not remove event '{}' from persistent storage: {}", eventId, e);
throw new SchedulerException(e);
}
if (agentService != null) {
try {
agentService.removeRecording(Long.toString(eventId));
} catch (NotFoundException e) {
logger.info("Agent recording '{}' already removed", eventId);
} catch (Exception e) {
logger.warn("Unable to remove agent recording '{}': {}", eventId, ExceptionUtils.getMessage(e));
}
}
try {
index.delete(eventId);
} catch (Exception e) {
logger.warn("Unable to delete event '{}' from index: {}", eventId, e);
throw new SchedulerException(e);
}
}
/**
* Removes an event but doesn't fail if the event's workflow or persistence record has already been deleted.
*
* @param eventId
* The id of the event to remove.
*/
private void removeEventTolerantOfNotFound(final long eventId) throws SchedulerException, NotFoundException,
UnauthorizedException {
try {
stopWorkflowInstance(eventId);
} catch (NotFoundException e) {
logger.info(
"Skipping removing workflow instance with id {} because it wasn't found, it must have been removed already.",
eventId);
}
try {
String mediaPackageId = persistence.getMediaPackageId(eventId);
persistence.deleteEvent(eventId);
messageSender.sendObjectMessage(SchedulerItem.SCHEDULER_QUEUE, MessageSender.DestinationType.Queue,
SchedulerItem.delete(mediaPackageId));
} catch (NotFoundException e) {
logger.info(
"Skipping removing scheduled event from persistence with id {} because it wasn't found, it must have been removed already.",
eventId);
} catch (SchedulerServiceDatabaseException e) {
logger.error("Could not remove event '{}' from persistent storage: {}", eventId, e);
throw new SchedulerException(e);
}
if (agentService != null) {
try {
agentService.removeRecording(Long.toString(eventId));
} catch (NotFoundException e) {
logger.info("Agent recording '{}' already removed", eventId);
} catch (Exception e) {
logger.warn("Unable to remove agent recording '{}': {}", eventId, ExceptionUtils.getMessage(e));
}
}
try {
index.delete(eventId);
} catch (Exception e) {
logger.warn("Unable to delete event '{}' from index: {}", eventId, e);
throw new SchedulerException(e);
}
}
/*
* (non-Javadoc)
*
* @see org.opencastproject.scheduler.api.SchedulerService#getEventDublinCore(long)
*/
@Override
public DublinCoreCatalog getEventDublinCore(long eventId) throws NotFoundException, SchedulerException {
try {
return index.getDublinCore(eventId);
} catch (SchedulerServiceDatabaseException e) {
logger.error("Could not retrieve Dublin Core for event with ID {}", eventId);
throw new SchedulerException(e);
}
}
/*
* (non-Javadoc)
*
* @see org.opencastproject.scheduler.api.SchedulerService#getEventCaptureAgentConfiguration(long)
*/
@Override
public Properties getEventCaptureAgentConfiguration(long eventId) throws NotFoundException, SchedulerException {
try {
return index.getCaptureAgentProperties(eventId);
} catch (SchedulerServiceDatabaseException e) {
logger.error("Could not retrieve Capture Agent properties for event with ID {}", eventId);
throw new SchedulerException(e);
}
}
/*
* (non-Javadoc)
*
* @see org.opencastproject.scheduler.api.SchedulerService#search(org.opencastproject.scheduler.api.SchedulerQuery)
*/
@Override
public DublinCoreCatalogList search(SchedulerQuery query) throws SchedulerException {
try {
return index.search(query);
} catch (SchedulerServiceDatabaseException e) {
logger.error("Could not execute query: {}", e.getMessage());
throw new SchedulerException(e);
}
}
/*
* (non-Javadoc)
*
* @see org.opencastproject.scheduler.api.SchedulerService#findConflictingEvents(java.lang.String, java.util.Date,
* java.util.Date)
*/
@Override
public DublinCoreCatalogList findConflictingEvents(String captureDeviceID, Date startDate, Date endDate)
throws SchedulerException {
SchedulerQuery q = new SchedulerQuery().setSpatial(captureDeviceID).setEndsFrom(startDate).setStartsTo(endDate);
try {
return index.search(q);
} catch (SchedulerServiceDatabaseException e) {
logger.error("Could not complete search after conflicting events for device '{}': {}", captureDeviceID,
e.getMessage());
throw new SchedulerException(e);
}
}
/*
* (non-Javadoc)
*
* @see org.opencastproject.scheduler.api.SchedulerService#findConflictingEvents(java.lang.String, java.lang.String,
* java.util.Date, java.util.Date, long)
*/
@Override
public DublinCoreCatalogList findConflictingEvents(String captureDeviceID, String rrule, Date startDate,
Date endDate, long duration, String timezone) throws SchedulerException {
RRule rule;
try {
rule = new RRule(rrule);
rule.validate();
} catch (Exception e) {
logger.error("Could not create rule for finding conflicting events: {}", e.getMessage());
throw new SchedulerException(e);
}
TimeZone tz = TimeZone.getTimeZone(timezone);
List<Period> periods = Util.calculatePeriods(startDate, endDate, duration, rule, tz);
List<DublinCoreCatalog> events = new ArrayList<DublinCoreCatalog>();
for (Period p : periods) {
List<DublinCoreCatalog> filterEvents = findConflictingEvents(captureDeviceID, p.getStart(), p.getEnd())
.getCatalogList();
events.addAll(filterEvents);
}
return new DublinCoreCatalogList(events, events.size());
}
/*
* (non-Javadoc)
*
* @see org.opencastproject.scheduler.api.SchedulerService#getCalendarForCaptureAgent(java.lang.String)
*/
@Override
public String getCalendar(SchedulerQuery filter) throws SchedulerException {
List<Tuple<String, String>> eventList;
try {
eventList = index.calendarSearch(filter.setOptOut(false).setBlacklisted(false));
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to retrieve events for capture agent '{}'", filter);
throw new SchedulerException(e);
}
CalendarGenerator cal = new CalendarGenerator(seriesService);
for (Tuple<String, String> event : eventList) {
// If the even properties are empty, skip the event
if (event.getB() == null) {
logger.warn("Properties for event '{}' can't be found, event is not recorded", event.getA());
continue;
}
DublinCoreCatalog catalog;
try {
catalog = dcService.load(new ByteArrayInputStream(event.getA().getBytes("UTF-8")));
} catch (IOException e) {
logger.error("Unable to parse dublinc core of event '{}': {}.", event.getA(), ExceptionUtils.getStackTrace(e));
continue;
}
// Add the entry to the calendar, skip it with a warning if adding fails
try {
cal.addEvent(catalog, event.getA(), event.getB());
} catch (Exception e) {
logger.warn("Error adding event '{}' to calendar: {}. Event is not recorded", getEventIdentifier(catalog),
ExceptionUtils.getStackTrace(e));
continue;
}
}
// Only validate calendars with events. Without any events, the iCalendar won't validate
if (cal.getCalendar().getComponents().size() > 0) {
try {
cal.getCalendar().validate();
} catch (ValidationException e) {
logger.warn("Recording calendar '{}' could not be validated (returning it anyways): {}", filter,
ExceptionUtils.getStackTrace(e));
}
}
return cal.getCalendar().toString(); // CalendarOutputter performance sucks (jmh)
}
@Override
public String getScheduleLastModified(String agentId) throws SchedulerException {
String lastModified = lastModifiedCache.getIfPresent(agentId);
if (lastModified != null)
return lastModified;
populateLastModifiedCache();
lastModified = lastModifiedCache.getIfPresent(agentId);
// If still null set the empty calendar ETag
if (lastModified == null) {
lastModified = EMPTY_CALENDAR_ETAG;
lastModifiedCache.put(agentId, lastModified);
}
return lastModified;
}
private void populateLastModifiedCache() throws SchedulerException {
try {
Map<String, Date> lastModifiedDates = index.getLastModifiedDate(new SchedulerQuery());
for (Entry<String, Date> entry : lastModifiedDates.entrySet()) {
Date lastModifiedDate = entry.getValue() != null ? entry.getValue() : new Date();
lastModifiedCache.put(entry.getKey(), generateLastModifiedHash(lastModifiedDate));
}
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to retrieve last modified for CA: {}", ExceptionUtils.getStackTrace(e));
throw new SchedulerException(e);
}
}
private String generateLastModifiedHash(Date lastModifiedDate) {
return "mod" + Long.toString(lastModifiedDate.getTime());
}
/**
* Determine the cutoff date to remove recordings in GMT
*
* @param buffer
* The number of seconds before now to start the cutoff date
* @return A date that is the number of seconds in buffer before now in GMT.
*/
public static org.joda.time.DateTime getCutoffDate(long buffer) {
return getCutoffDate(buffer, new org.joda.time.DateTime(DateTimeZone.getDefault()));
}
/**
* Determine the cutoff date to remove recordings in GMT
*
* @param buffer
* The number of seconds before now to start the cutoff date
* @param dateTime
* @return A date that is the number of seconds in buffer before now in GMT.
*/
public static org.joda.time.DateTime getCutoffDate(long buffer, org.joda.time.DateTime dateTime) {
org.joda.time.DateTime dt = dateTime.toDateTime(DateTimeZone.UTC).minus(buffer * 1000);
return dt;
}
/*
* (non-Javadoc)
*
* @see org.opencastproject.scheduler.api.SchedulerService#removeScheduledRecordingsBeforeBuffer(long)
*/
@Override
public void removeScheduledRecordingsBeforeBuffer(long buffer) throws SchedulerException {
int recordingsRemoved = 0;
DublinCoreCatalogList finishedEvents;
DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
org.joda.time.DateTime end = getCutoffDate(buffer);
org.joda.time.DateTime start = new org.joda.time.DateTime();
start.withYear(1999);
start.withDayOfYear(1);
logger.info("Starting to look for scheduled recordings that have finished "
+ Log.getHumanReadableTimeString(buffer) + " ago from " + dateTimeFormatter.print(start) + " to "
+ dateTimeFormatter.print(end) + ".");
SchedulerQuery q = new SchedulerQuery();
long id = -1;
try {
q.setEndsFrom(SolrUtils.parseDate(dateTimeFormatter.print(start)));
q.setEndsTo(SolrUtils.parseDate(dateTimeFormatter.print(end)));
} catch (ParseException e) {
logger.error("Unable to parse the date " + end + " as a cut off to remove finished scheduled recordings.");
throw new SchedulerException(e);
}
try {
finishedEvents = search(q);
} catch (SchedulerException e) {
logger.error("Unable to search for finished events: ", e);
throw new SchedulerException(e);
}
logger.debug("Found {} events from search.", finishedEvents.getTotalCount());
for (DublinCoreCatalog catalog : finishedEvents.getCatalogList()) {
try {
String idText = catalog.getFirst(DublinCoreCatalog.PROPERTY_IDENTIFIER);
id = Long.parseLong(StringUtils.trimToNull(idText));
removeEventTolerantOfNotFound(id);
logger.debug("Sucessfully removed scheduled event with id " + id);
recordingsRemoved++;
} catch (NotFoundException e) {
logger.debug("Skipping event with id {} because it is not found", id);
} catch (Exception e) {
logger.warn("Unable to delete event with id '" + id + "':", e);
}
}
logger.info("Found " + recordingsRemoved + " to remove that ended " + Log.getHumanReadableTimeString(buffer)
+ " ago from " + dateTimeFormatter.print(start) + " to " + dateTimeFormatter.print(end) + ".");
}
/**
* Verifies if existing event is found and has not already ended
*
* @param eventId
* @throws SchedulerException
* if event has ended, or NotFoundException if event it not found
*/
private void verifyActive(Long eventId) throws SchedulerException {
DublinCoreCatalog dcc;
try {
dcc = getEventDublinCore(eventId);
final DCMIPeriod period = EncodingSchemeUtils.decodeMandatoryPeriod(dcc.getFirst(DublinCore.PROPERTY_TEMPORAL));
if (!period.hasEnd() || !period.hasStart()) {
// Unlikely to get this error since catalog was already in index
throw new IllegalArgumentException("Dublin core field dc:temporal for event ID " + eventId
+ " does not contain information about start and end of event");
}
final Date endDate = period.getEnd();
// TODO: Assumption of no TimeZone adjustment because catalog temporal is local to server
if (getCurrentDate().after(endDate)) {
String msg = "Event ID " + eventId + " has already ended";
logger.info(msg);
throw new SchedulerException(msg);
}
} catch (NotFoundException e) {
logger.info("Event ID {} is not found", eventId);
throw new SchedulerException(e);
}
}
/**
* Returns current system Date. Enables date to be mocked to simulate the future and the past for testing. Reference:
* http://neovibrant.com/2011/08/05/junit-with-new-date/
*
* @return current system date
*/
public Date getCurrentDate() {
return new Date();
}
@Override
public void updateAccessControlList(long eventId, AccessControlList accessControlList) throws NotFoundException,
SchedulerException {
try {
persistence.updateEventAccessControlList(eventId, accessControlList);
messageSender.sendObjectMessage(SchedulerItem.SCHEDULER_QUEUE, MessageSender.DestinationType.Queue,
SchedulerItem.updateAcl(persistence.getMediaPackageId(eventId), accessControlList));
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to update access control list of event with id '{}': {}", eventId,
ExceptionUtils.getStackTrace(e));
throw new SchedulerException(e);
}
}
@Override
public AccessControlList getAccessControlList(long eventId) throws NotFoundException, SchedulerException {
try {
return persistence.getAccessControlList(eventId);
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to get access control list of event '{}': {}", eventId, ExceptionUtils.getStackTrace(e));
throw new SchedulerException(e);
}
}
@Override
public String getMediaPackageId(long eventId) throws NotFoundException, SchedulerException {
try {
return persistence.getMediaPackageId(eventId);
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to get mediapackage id of event '{}': {}", eventId, ExceptionUtils.getStackTrace(e));
throw new SchedulerException(e);
}
}
@Override
public Long getEventId(String mediaPackageId) throws NotFoundException, SchedulerException {
try {
return persistence.getEventId(mediaPackageId);
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to get event id from mediapackage id '{}': {}", mediaPackageId,
ExceptionUtils.getStackTrace(e));
throw new SchedulerException(e);
}
}
@Override
public void updateOptOutStatus(String mediapackageId, boolean optOut) throws NotFoundException, SchedulerException {
try {
persistence.updateEventOptOutStatus(mediapackageId, optOut);
messageSender.sendObjectMessage(SchedulerItem.SCHEDULER_QUEUE, MessageSender.DestinationType.Queue,
SchedulerItem.updateOptOut(mediapackageId, optOut));
index.indexOptOut(persistence.getEventId(mediapackageId), optOut);
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to update opt out status of event with mediapackage '{}': {}", mediapackageId,
ExceptionUtils.getStackTrace(e));
throw new SchedulerException(e);
}
}
@Override
public void updateReviewStatus(String mediapackageId, ReviewStatus reviewStatus) throws NotFoundException,
SchedulerException {
try {
Date now = new Date();
persistence.updateEventReviewStatus(mediapackageId, reviewStatus, now);
messageSender.sendObjectMessage(SchedulerItem.SCHEDULER_QUEUE, MessageSender.DestinationType.Queue,
SchedulerItem.updateReviewStatus(mediapackageId, reviewStatus, now));
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to update review status of event with mediapackage '{}': {}", mediapackageId,
ExceptionUtils.getStackTrace(e));
throw new SchedulerException(e);
}
}
@Override
public ReviewStatus getReviewStatus(String mediapackageId) throws NotFoundException, SchedulerException {
try {
return persistence.getReviewStatus(mediapackageId);
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to get review status of event with mediapackage '{}': {}", mediapackageId,
ExceptionUtils.getStackTrace(e));
throw new SchedulerException(e);
}
}
@Override
public boolean isOptOut(String mediapackageId) throws NotFoundException, SchedulerException {
try {
return index.isOptOut(persistence.getEventId(mediapackageId));
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to get opt out status of event with mediapackage '{}': {}", mediapackageId,
ExceptionUtils.getStackTrace(e));
throw new SchedulerException(e);
}
}
@Override
public void updateWorkflowConfig(String mediapackageId, Map<String, String> properties) throws NotFoundException,
SchedulerException {
try {
Long eventId = persistence.getEventId(mediapackageId);
Properties eventMetadata = persistence.getEventMetadata(eventId);
for (Entry<String, String> entry : properties.entrySet()) {
eventMetadata.put(WORKFLOW_CONFIG_PREFIX.concat(entry.getKey()), entry.getValue());
}
persistence.updateEventWithMetadata(eventId, eventMetadata);
messageSender.sendObjectMessage(SchedulerItem.SCHEDULER_QUEUE, MessageSender.DestinationType.Queue,
SchedulerItem.updateProperties(mediapackageId, new HashMap<String, String>((Map) eventMetadata)));
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to add workflow configs to the event with mediapackage '{}': {}", mediapackageId,
ExceptionUtils.getStackTrace(e));
throw new SchedulerException(e);
}
}
@Override
public boolean isBlacklisted(String mediapackageId) throws NotFoundException, SchedulerException {
try {
return index.isBlacklisted(persistence.getEventId(mediapackageId));
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to get blacklist status of event with mediapackage '{}': {}", mediapackageId,
ExceptionUtils.getStackTrace(e));
throw new SchedulerException(e);
}
}
@Override
public void updateBlacklistStatus(String mediapackageId, boolean blacklisted) throws NotFoundException,
SchedulerException {
try {
persistence.updateEventBlacklistStatus(mediapackageId, blacklisted);
messageSender.sendObjectMessage(SchedulerItem.SCHEDULER_QUEUE, MessageSender.DestinationType.Queue,
SchedulerItem.updateBlacklist(mediapackageId, blacklisted));
index.indexBlacklisted(persistence.getEventId(mediapackageId), blacklisted);
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to update opt out status of event with mediapackage '{}': {}", mediapackageId,
ExceptionUtils.getStackTrace(e));
throw new SchedulerException(e);
}
}
@Override
public void repopulate(final String indexName) {
final String destinationId = SchedulerItem.SCHEDULER_QUEUE_PREFIX + WordUtils.capitalize(indexName);
Organization organization = new DefaultOrganization();
SecurityUtil.runAs(securityService, organization, SecurityUtil.createSystemUser(cc, organization), new Effect0() {
@Override
protected void run() {
int total = 0;
int current = 1;
try {
total = persistence.countEvents();
logger.info(
"Re-populating '{}' index with scheduled events. There are {} scheduled events to add to the index.",
indexName, total);
DublinCoreCatalog[] catalogs = persistence.getAllEvents();
for (DublinCoreCatalog event : catalogs) {
final long id = getEventIdentifier(event);
try {
Properties properties = persistence.getEventMetadata(id);
String eventId = persistence.getMediaPackageId(id);
ReviewStatus reviewStatus = persistence.getReviewStatus(eventId);
Date reviewDate = persistence.getReviewDate(eventId);
AccessControlList accessControlList = persistence.getAccessControlList(id);
messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue,
SchedulerItem.updateCatalog(eventId, event));
messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue,
SchedulerItem.updateProperties(eventId, new HashMap<String, String>((Map) properties)));
if (accessControlList != null)
messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue,
SchedulerItem.updateAcl(eventId, accessControlList));
messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue,
SchedulerItem.updateOptOut(eventId, persistence.isOptOut(id)));
messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue,
SchedulerItem.updateBlacklist(eventId, persistence.isBlacklisted(id)));
messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue,
SchedulerItem.updateReviewStatus(eventId, reviewStatus, reviewDate));
messageSender.sendObjectMessage(IndexProducer.RESPONSE_QUEUE, MessageSender.DestinationType.Queue,
IndexRecreateObject.update(indexName, IndexRecreateObject.Service.Scheduler, total, current));
current++;
} catch (Exception e) {
logger.warn("Unable to add scheduled event with id {} to the index, skipping", current, id);
}
}
} catch (Exception e) {
logger.warn("Unable to index scheduled instances: {}", e);
throw new ServiceException(e.getMessage());
}
messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue,
IndexRecreateObject.end(indexName, IndexRecreateObject.Service.Scheduler));
}
});
}
@Override
public MessageReceiver getMessageReceiver() {
return messageReceiver;
}
@Override
public Service getService() {
return Service.Scheduler;
}
@Override
public String getClassName() {
return SchedulerServiceImpl.class.getName();
}
@Override
public MessageSender getMessageSender() {
return messageSender;
}
@Override
public SecurityService getSecurityService() {
return securityService;
}
@Override
public String getSystemUserName() {
return SecurityUtil.getSystemUserName(cc);
}
}