Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.opencastproject.scheduler.impl.SchedulerServiceImpl 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.impl;
import static com.entwinemedia.fn.Stream.$;
import static com.entwinemedia.fn.data.Opt.some;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.opencastproject.scheduler.impl.SchedulerUtil.calculateChecksum;
import static org.opencastproject.scheduler.impl.SchedulerUtil.episodeToMp;
import static org.opencastproject.scheduler.impl.SchedulerUtil.eventOrganizationFilter;
import static org.opencastproject.scheduler.impl.SchedulerUtil.isNotEpisodeDublinCore;
import static org.opencastproject.scheduler.impl.SchedulerUtil.recordToMp;
import static org.opencastproject.scheduler.impl.SchedulerUtil.uiAdapterToFlavor;
import static org.opencastproject.security.api.SecurityConstants.GLOBAL_ADMIN_ROLE;
import static org.opencastproject.util.EqualsUtil.ne;
import static org.opencastproject.util.Log.getHumanReadableTimeString;
import static org.opencastproject.util.RequireUtil.notEmpty;
import static org.opencastproject.util.RequireUtil.notNull;
import static org.opencastproject.util.RequireUtil.requireTrue;
import static org.opencastproject.util.data.Monadics.mlist;
import org.opencastproject.assetmanager.api.Asset;
import org.opencastproject.assetmanager.api.AssetManager;
import org.opencastproject.assetmanager.api.Availability;
import org.opencastproject.assetmanager.api.Snapshot;
import org.opencastproject.assetmanager.api.query.AQueryBuilder;
import org.opencastproject.assetmanager.api.query.ARecord;
import org.opencastproject.assetmanager.api.query.AResult;
import org.opencastproject.assetmanager.api.query.ASelectQuery;
import org.opencastproject.assetmanager.api.query.Predicate;
import org.opencastproject.elasticsearch.api.SearchIndexException;
import org.opencastproject.elasticsearch.index.AbstractSearchIndex;
import org.opencastproject.elasticsearch.index.event.Event;
import org.opencastproject.elasticsearch.index.event.EventIndexUtils;
import org.opencastproject.index.rebuild.AbstractIndexProducer;
import org.opencastproject.index.rebuild.IndexRebuildException;
import org.opencastproject.index.rebuild.IndexRebuildService;
import org.opencastproject.mediapackage.Catalog;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElement;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageElements;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.mediapackage.MediaPackageSupport;
import org.opencastproject.mediapackage.identifier.Id;
import org.opencastproject.mediapackage.identifier.IdImpl;
import org.opencastproject.message.broker.api.MessageSender;
import org.opencastproject.message.broker.api.scheduler.SchedulerItem;
import org.opencastproject.message.broker.api.scheduler.SchedulerItemList;
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.DublinCoreValue;
import org.opencastproject.metadata.dublincore.DublinCores;
import org.opencastproject.metadata.dublincore.EncodingSchemeUtils;
import org.opencastproject.metadata.dublincore.EventCatalogUIAdapter;
import org.opencastproject.metadata.dublincore.Precision;
import org.opencastproject.scheduler.api.Recording;
import org.opencastproject.scheduler.api.RecordingImpl;
import org.opencastproject.scheduler.api.RecordingState;
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.api.TechnicalMetadataImpl;
import org.opencastproject.scheduler.api.Util;
import org.opencastproject.scheduler.impl.persistence.ExtendedEventDto;
import org.opencastproject.security.api.AccessControlList;
import org.opencastproject.security.api.AccessControlParser;
import org.opencastproject.security.api.AccessControlUtil;
import org.opencastproject.security.api.AuthorizationService;
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.api.User;
import org.opencastproject.security.util.SecurityUtil;
import org.opencastproject.series.api.SeriesService;
import org.opencastproject.util.DateTimeSupport;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.OsgiUtil;
import org.opencastproject.util.XmlNamespaceBinding;
import org.opencastproject.util.XmlNamespaceContext;
import org.opencastproject.util.data.Option;
import org.opencastproject.util.data.functions.Misc;
import org.opencastproject.util.data.functions.Strings;
import org.opencastproject.workspace.api.Workspace;
import com.entwinemedia.fn.Fn;
import com.entwinemedia.fn.Stream;
import com.entwinemedia.fn.data.Opt;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import net.fortuna.ical4j.model.Period;
import net.fortuna.ical4j.model.TimeZoneRegistry;
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
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.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Implementation of {@link SchedulerService}.
*/
public class SchedulerServiceImpl extends AbstractIndexProducer implements SchedulerService, ManagedService {
/** The logger */
private static final Logger logger = LoggerFactory.getLogger(SchedulerServiceImpl.class);
/** The last modifed cache configuration key */
private static final String CFG_KEY_LAST_MODIFED_CACHE_EXPIRE = "last_modified_cache_expire";
/** The maintenance configuration key */
private static final String CFG_KEY_MAINTENANCE = "maintenance";
/** 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";
/** The workflow configuration prefix */
public static final String WORKFLOW_CONFIG_PREFIX = "org.opencastproject.workflow.config.";
private static final String SNAPSHOT_OWNER = SchedulerService.JOB_TYPE;
private static final Gson gson = new Gson();
/**
* Deserializes properties stored in string columns of the extended event table
* @param props Properties as retrieved from the DB
* @return deserialized key-value pairs
*/
private static Map deserializeExtendedEventProperties(String props) {
if (props == null || props.trim().isEmpty()) {
return new HashMap<>();
}
Type type = new TypeToken>() { }.getType();
return gson.fromJson(props, type);
}
/** The last modified cache */
protected Cache lastModifiedCache = CacheBuilder.newBuilder()
.expireAfterWrite(DEFAULT_CACHE_EXPIRE, TimeUnit.SECONDS).build();
/** The message broker sender service */
private MessageSender messageSender;
/** Persistent storage for events */
private SchedulerServiceDatabase persistence;
/** The series service */
private SeriesService seriesService;
/** The security service used to run the security context with. */
private SecurityService securityService;
/** The asset manager */
private AssetManager assetManager;
/** The workspace */
private Workspace workspace;
/** The authorization service */
private AuthorizationService authorizationService;
/** The organization directory service */
private OrganizationDirectoryService orgDirectoryService;
/** The Elasticsearch indices */
private AbstractSearchIndex adminUiIndex;
private AbstractSearchIndex externalApiIndex;
/** The list of registered event catalog UI adapters */
private List eventCatalogUIAdapters = new ArrayList<>();
/** The system user name */
private String systemUserName;
private ComponentContext componentContext;
/**
* OSGi callback to set message sender.
*
* @param messageSender
*/
public void setMessageSender(MessageSender messageSender) {
this.messageSender = messageSender;
}
/**
* OSGi callback to set Persistence Service.
*
* @param persistence
*/
public void setPersistence(SchedulerServiceDatabase persistence) {
this.persistence = persistence;
}
/**
* OSGi callback for setting Series Service.
*
* @param seriesService
*/
public void setSeriesService(SeriesService seriesService) {
this.seriesService = seriesService;
}
/**
* OSGi callback to set security service.
*
* @param securityService
*/
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
/**
* OSGi callback to set the asset manager.
*
* @param assetManager
*/
public void setAssetManager(AssetManager assetManager) {
this.assetManager = assetManager;
}
/**
* OSGi callback to set the workspace.
*
* @param workspace
*/
public void setWorkspace(Workspace workspace) {
this.workspace = workspace;
}
/**
* OSGi callback to set the authorization service.
*
* @param authorizationService
*/
public void setAuthorizationService(AuthorizationService authorizationService) {
this.authorizationService = authorizationService;
}
/**
* Send a message on the scheduler queue
*
* This is just a little shorthand because we send a lot of messages.
*
* @param message message to send
*/
private void sendSchedulerMessage(Serializable message) {
messageSender.sendObjectMessage(SchedulerItem.SCHEDULER_QUEUE, MessageSender.DestinationType.Queue, message);
}
/**
* OSGi callback to set the organization directory service.
*
* @param orgDirectoryService
*/
public void setOrgDirectoryService(OrganizationDirectoryService orgDirectoryService) {
this.orgDirectoryService = orgDirectoryService;
}
/**
* OSgi callback to set the Admin UI index.
*
* @param index
* the admin UI index.
*/
public void setAdminUiIndex(AbstractSearchIndex index) {
this.adminUiIndex = index;
}
/**
* OSGi callback to set the External API index
*
* @param index
* the external API index.
*/
public void setExternalApiIndex(AbstractSearchIndex index) {
this.externalApiIndex = index;
}
/** OSGi callback to add {@link EventCatalogUIAdapter} instance. */
public void addCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) {
eventCatalogUIAdapters.add(catalogUIAdapter);
}
/** OSGi callback to remove {@link EventCatalogUIAdapter} instance. */
public void removeCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) {
eventCatalogUIAdapters.remove(catalogUIAdapter);
}
/**
* Activates Scheduler Service.
*
* @param cc
* ComponentContext
* @throws Exception
*/
public void activate(ComponentContext cc) throws Exception {
this.componentContext = cc;
systemUserName = SecurityUtil.getSystemUserName(cc);
logger.info("Activating Scheduler Service");
}
@Override
public void updated(Dictionary properties) throws ConfigurationException {
if (properties != null) {
final Option cacheExpireDuration = OsgiUtil.getOptCfg(properties, CFG_KEY_LAST_MODIFED_CACHE_EXPIRE)
.bind(Strings.toInt);
if (cacheExpireDuration.isSome()) {
lastModifiedCache = CacheBuilder.newBuilder().expireAfterWrite(cacheExpireDuration.get(), TimeUnit.SECONDS)
.build();
logger.info("Set last modified cache to {}", getHumanReadableTimeString(cacheExpireDuration.get()));
} else {
logger.info("Set last modified cache to default {}", getHumanReadableTimeString(DEFAULT_CACHE_EXPIRE));
}
final Option maintenance = OsgiUtil.getOptCfgAsBoolean(properties, CFG_KEY_MAINTENANCE);
if (maintenance.getOrElse(false)) {
final String name = SchedulerServiceImpl.class.getName();
logger.warn("Putting scheduler into maintenance mode. This only makes sense when migrating data. If this is not"
+ " intended, edit the config file '{}.cfg' accordingly and restart opencast.", name);
componentContext.disableComponent(name);
}
}
}
@Override
public void addEvent(Date startDateTime, Date endDateTime, String captureAgentId, Set userIds,
MediaPackage mediaPackage, Map wfProperties, Map caMetadata,
Opt schedulingSource)
throws UnauthorizedException, SchedulerException {
addEventInternal(startDateTime, endDateTime, captureAgentId, userIds, mediaPackage, wfProperties, caMetadata,
schedulingSource);
}
private void addEventInternal(Date startDateTime, Date endDateTime, String captureAgentId, Set userIds,
MediaPackage mediaPackage, Map wfProperties, Map caMetadata,
Opt schedulingSource)
throws SchedulerException {
notNull(startDateTime, "startDateTime");
notNull(endDateTime, "endDateTime");
notEmpty(captureAgentId, "captureAgentId");
notNull(userIds, "userIds");
notNull(mediaPackage, "mediaPackage");
notNull(wfProperties, "wfProperties");
notNull(caMetadata, "caMetadata");
notNull(schedulingSource, "schedulingSource");
if (endDateTime.before(startDateTime))
throw new IllegalArgumentException("The end date is before the start date");
final String mediaPackageId = mediaPackage.getIdentifier().toString();
try {
AQueryBuilder query = assetManager.createQuery();
AResult result = query.select(query.nothing())
.where(withOrganization(query).and(query.mediaPackageId(mediaPackageId).and(query.version().isLatest())))
.run();
Opt record = result.getRecords().head();
if (record.isSome()) {
logger.warn("Mediapackage with id '{}' already exists!", mediaPackageId);
throw new SchedulerConflictException("Mediapackage with id '" + mediaPackageId + "' already exists!");
}
Opt seriesId = Opt.nul(StringUtils.trimToNull(mediaPackage.getSeries()));
List conflictingEvents = findConflictingEvents(captureAgentId, startDateTime, endDateTime);
if (conflictingEvents.size() > 0) {
logger.info("Unable to add event {}, conflicting events found: {}", mediaPackageId, conflictingEvents);
throw new SchedulerConflictException(
"Unable to add event, conflicting events found for event " + mediaPackageId);
}
// Load dublincore and acl for update
Opt dublinCore = DublinCoreUtil.loadEpisodeDublinCore(workspace, mediaPackage);
AccessControlList acl = authorizationService.getActiveAcl(mediaPackage).getA();
// Get updated agent properties
Map finalCaProperties = getFinalAgentProperties(caMetadata, wfProperties, captureAgentId,
seriesId, dublinCore);
// Persist asset
String checksum = calculateChecksum(workspace, getEventCatalogUIAdapterFlavors(), startDateTime, endDateTime,
captureAgentId, userIds, mediaPackage, dublinCore, wfProperties,
finalCaProperties, acl);
persistEvent(mediaPackageId, checksum, Opt.some(startDateTime), Opt.some(endDateTime),
Opt.some(captureAgentId), Opt.some(userIds), Opt.some(mediaPackage), Opt.some(wfProperties),
Opt.some(finalCaProperties), schedulingSource);
// Update live event
updateLiveEvent(mediaPackageId, Opt.some(acl), dublinCore, Opt.some(startDateTime),
Opt.some(endDateTime), Opt.some(captureAgentId), Opt.some(finalCaProperties));
// Update Elasticsearch indices
updateEventInIndex(mediaPackageId, adminUiIndex, Opt.some(acl), dublinCore, Opt.some(startDateTime),
Opt.some(endDateTime), Opt.some(userIds), Opt.some(captureAgentId), Opt.some(finalCaProperties),
Opt.none());
updateEventInIndex(mediaPackageId, externalApiIndex, Opt.some(acl), dublinCore, Opt.some(startDateTime),
Opt.some(endDateTime), Opt.some(userIds), Opt.some(captureAgentId), Opt.some(finalCaProperties),
Opt.none());
// Update last modified
touchLastEntry(captureAgentId);
} catch (SchedulerException e) {
throw e;
} catch (Exception e) {
logger.error("Failed to create event with id '{}':", mediaPackageId, e);
throw new SchedulerException(e);
}
}
@Override
public Map addMultipleEvents(RRule rRule, Date start, Date end, Long duration, TimeZone tz,
String captureAgentId, Set userIds, MediaPackage templateMp, Map wfProperties,
Map caMetadata, Opt schedulingSource)
throws UnauthorizedException, SchedulerConflictException, SchedulerException {
// input Rrule is UTC. Needs to be adjusted to tz
Util.adjustRrule(rRule, start, tz);
List periods = Util.calculatePeriods(start, end, duration, rRule, tz);
if (periods.isEmpty()) {
return Collections.emptyMap();
}
return addMultipleEventInternal(periods, captureAgentId, userIds, templateMp, wfProperties, caMetadata,
schedulingSource);
}
private Map addMultipleEventInternal(List periods, String captureAgentId,
Set userIds, MediaPackage templateMp, Map wfProperties,
Map caMetadata, Opt schedulingSource) throws SchedulerException {
notNull(periods, "periods");
requireTrue(periods.size() > 0, "periods");
notEmpty(captureAgentId, "captureAgentId");
notNull(userIds, "userIds");
notNull(templateMp, "mediaPackages");
notNull(wfProperties, "wfProperties");
notNull(caMetadata, "caMetadata");
notNull(schedulingSource, "schedulingSource");
Map scheduledEvents = new ConcurrentHashMap<>();
try {
LinkedList ids = new LinkedList<>();
AQueryBuilder qb = assetManager.createQuery();
Predicate p = null;
//While we don't have a list of IDs equal to the number of periods
while (ids.size() <= periods.size()) {
//Create a list of IDs equal to the number of periods, along with a set of AM predicates
while (ids.size() <= periods.size()) {
Id id = new IdImpl(UUID.randomUUID().toString());
ids.add(id);
Predicate np = qb.mediaPackageId(id.toString());
//Haha, p = np jokes with the AM query language. Ha. Haha. Ha. (Sob...)
if (null == p) {
p = np;
} else {
p = p.or(np);
}
}
//Select the list of ids which alread exist. Hint: this needs to be zero
AResult result = qb.select(qb.nothing()).where(withOrganization(qb).and(p).and(qb.version().isLatest())).run();
//If there is conflict, clear the list and start over
if (result.getTotalSize() > 0) {
ids.clear();
}
}
Opt seriesId = Opt.nul(StringUtils.trimToNull(templateMp.getSeries()));
List conflictingEvents = findConflictingEvents(periods, captureAgentId, TimeZone.getDefault());
if (conflictingEvents.size() > 0) {
logger.info("Unable to add events, conflicting events found: {}", conflictingEvents);
throw new SchedulerConflictException("Unable to add event, conflicting events found");
}
final Organization org = securityService.getOrganization();
final User user = securityService.getUser();
periods.parallelStream().forEach(event -> SecurityUtil.runAs(securityService, org, user, () -> {
final int currentCounter = periods.indexOf(event);
MediaPackage mediaPackage = (MediaPackage) templateMp.clone();
Date startDate = new Date(event.getStart().getTime());
Date endDate = new Date(event.getEnd().getTime());
Id id = ids.get(currentCounter);
//Get, or make, the DC catalog
DublinCoreCatalog dc;
Opt dcOpt = DublinCoreUtil.loadEpisodeDublinCore(workspace,
templateMp);
if (dcOpt.isSome()) {
dc = dcOpt.get();
dc = (DublinCoreCatalog) dc.clone();
// make sure to bind the OC_PROPERTY namespace
dc.addBindings(XmlNamespaceContext
.mk(XmlNamespaceBinding.mk(DublinCores.OC_PROPERTY_NS_PREFIX, DublinCores.OC_PROPERTY_NS_URI)));
} else {
dc = DublinCores.mkOpencastEpisode().getCatalog();
}
// Set the new media package identifier
mediaPackage.setIdentifier(id);
// Update dublincore title and temporal
String newTitle = dc.getFirst(DublinCore.PROPERTY_TITLE) + String.format(" %0" + Integer.toString(periods.size()).length() + "d", currentCounter + 1);
dc.set(DublinCore.PROPERTY_TITLE, newTitle);
DublinCoreValue eventTime = EncodingSchemeUtils.encodePeriod(new DCMIPeriod(startDate, endDate),
Precision.Second);
dc.set(DublinCore.PROPERTY_TEMPORAL, eventTime);
try {
mediaPackage = updateDublincCoreCatalog(mediaPackage, dc);
} catch (Exception e) {
Misc.chuck(e);
}
mediaPackage.setTitle(newTitle);
String mediaPackageId = mediaPackage.getIdentifier().toString();
//Converting from iCal4j DateTime objects to plain Date objects to prevent AMQ issues below
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
cal.setTime(event.getStart());
Date startDateTime = cal.getTime();
cal.setTime(event.getEnd());
Date endDateTime = cal.getTime();
// Load dublincore and acl for update
Opt dublinCore = DublinCoreUtil.loadEpisodeDublinCore(workspace, mediaPackage);
AccessControlList acl = authorizationService.getActiveAcl(mediaPackage).getA();
// Get updated agent properties
Map finalCaProperties = getFinalAgentProperties(caMetadata, wfProperties, captureAgentId,
seriesId, dublinCore);
// Persist asset
String checksum = calculateChecksum(workspace, getEventCatalogUIAdapterFlavors(), startDateTime, endDateTime,
captureAgentId, userIds, mediaPackage, dublinCore, wfProperties, finalCaProperties, acl);
try {
persistEvent(mediaPackageId, checksum, Opt.some(startDateTime), Opt.some(endDateTime),
Opt.some(captureAgentId), Opt.some(userIds), Opt.some(mediaPackage), Opt.some(wfProperties),
Opt.some(finalCaProperties), schedulingSource);
} catch (Exception e) {
Misc.chuck(e);
}
// Update live event
updateLiveEvent(mediaPackageId, some(acl), dublinCore, Opt.some(startDateTime), Opt.some(endDateTime),
Opt.some(captureAgentId), Opt.some(finalCaProperties));
// Update Elasticsearch indices
updateEventInIndex(mediaPackageId, adminUiIndex, some(acl), dublinCore, Opt.some(startDateTime), Opt.some(endDateTime),
Opt.some(userIds), Opt.some(captureAgentId), Opt.some(finalCaProperties), Opt.none());
updateEventInIndex(mediaPackageId, externalApiIndex, some(acl), dublinCore, Opt.some(startDateTime), Opt.some(endDateTime),
Opt.some(userIds), Opt.some(captureAgentId), Opt.some(finalCaProperties), Opt.none());
scheduledEvents.put(mediaPackageId, event);
for (MediaPackageElement mediaPackageElement : mediaPackage.getElements()) {
try {
workspace.delete(mediaPackage.getIdentifier().toString(), mediaPackageElement.getIdentifier());
} catch (NotFoundException | IOException e) {
logger.warn("Failed to delete media package element", e);
}
}
}));
return scheduledEvents;
} catch (SchedulerException e) {
throw e;
} catch (Exception e) {
throw new SchedulerException(e);
} finally {
// Update last modified
if (!scheduledEvents.isEmpty()) {
touchLastEntry(captureAgentId);
}
}
}
@Override
public void updateEvent(final String mpId, Opt startDateTime, Opt endDateTime, Opt captureAgentId,
Opt> userIds, Opt mediaPackage, Opt> wfProperties,
Opt> caMetadata)
throws NotFoundException, UnauthorizedException, SchedulerException {
updateEventInternal(mpId, startDateTime, endDateTime, captureAgentId, userIds, mediaPackage,
wfProperties, caMetadata, false);
}
@Override
public void updateEvent(final String mpId, Opt startDateTime, Opt endDateTime, Opt captureAgentId,
Opt> userIds, Opt mediaPackage, Opt> wfProperties,
Opt> caMetadata, boolean allowConflict)
throws NotFoundException, UnauthorizedException, SchedulerException {
updateEventInternal(mpId, startDateTime, endDateTime, captureAgentId, userIds, mediaPackage,
wfProperties, caMetadata, allowConflict);
}
private void updateEventInternal(final String mpId, Opt startDateTime,
Opt endDateTime, Opt captureAgentId, Opt> userIds, Opt mediaPackage,
Opt> wfProperties, Opt> caMetadata, boolean allowConflict)
throws NotFoundException, SchedulerException {
notEmpty(mpId, "mpId");
notNull(startDateTime, "startDateTime");
notNull(endDateTime, "endDateTime");
notNull(captureAgentId, "captureAgentId");
notNull(userIds, "userIds");
notNull(mediaPackage, "mediaPackage");
notNull(wfProperties, "wfProperties");
notNull(caMetadata, "caMetadata");
try {
AQueryBuilder query = assetManager.createQuery();
ASelectQuery select = query
.select(query.snapshot())
.where(withOrganization(query).and(query.mediaPackageId(mpId).and(query.version().isLatest())
.and(withOwner(query))));
Opt optEvent = select.run().getRecords().head();
Opt optExtEvent = persistence.getEvent(mpId);
if (optEvent.isNone() || optExtEvent.isNone())
throw new NotFoundException("No event found while updating event " + mpId);
ARecord record = optEvent.get();
if (record.getSnapshot().isNone())
throw new NotFoundException("No mediapackage found while updating event " + mpId);
Opt dublinCoreOpt = loadEpisodeDublinCoreFromAsset(record.getSnapshot().get());
if (dublinCoreOpt.isNone())
throw new NotFoundException("No dublincore found while updating event " + mpId);
final ExtendedEventDto extendedEventDto = optExtEvent.get();
Date start = extendedEventDto.getStartDate();
Date end = extendedEventDto.getEndDate();
verifyActive(mpId, end);
if ((startDateTime.isSome() || endDateTime.isSome()) && endDateTime.getOr(end).before(startDateTime.getOr(start)))
throw new SchedulerException("The end date is before the start date");
String agentId = extendedEventDto.getCaptureAgentId();
Opt seriesId = Opt.nul(record.getSnapshot().get().getMediaPackage().getSeries());
// Check for conflicting events
// Check scheduling conficts in case a property relevant for conflicts has changed
if ((captureAgentId.isSome() || startDateTime.isSome() || endDateTime.isSome())
&& (!allowConflict || !isAdmin())) {
List conflictingEvents = $(findConflictingEvents(captureAgentId.getOr(agentId),
startDateTime.getOr(start), endDateTime.getOr(end))).filter(new Fn() {
@Override
public Boolean apply(MediaPackage mp) {
return !mpId.equals(mp.getIdentifier().toString());
}
}).toList();
if (conflictingEvents.size() > 0) {
logger.info("Unable to update event {}, conflicting events found: {}", mpId, conflictingEvents);
throw new SchedulerConflictException("Unable to update event, conflicting events found for event " + mpId);
}
}
Set presenters = getPresenters(Opt.nul(extendedEventDto.getPresenters()).getOr(""));
Map wfProps = deserializeExtendedEventProperties(extendedEventDto.getWorkflowProperties());
Map caProperties = deserializeExtendedEventProperties(extendedEventDto.getCaptureAgentProperties());
boolean propertiesChanged = false;
boolean dublinCoreChanged = false;
// Get workflow properties
for (Map wfPropsToUpdate : wfProperties) {
propertiesChanged = true;
wfProps = wfPropsToUpdate;
}
// Get capture agent properties
for (Map caMetadataToUpdate : caMetadata) {
propertiesChanged = true;
caProperties = caMetadataToUpdate;
}
if (captureAgentId.isSome())
propertiesChanged = true;
Opt acl = Opt.none();
Opt dublinCore = Opt.none();
Opt aclOld =
some(authorizationService.getActiveAcl(record.getSnapshot().get().getMediaPackage()).getA());
//update metadata for dublincore
if (startDateTime.isSome() && endDateTime.isSome()) {
DublinCoreValue eventTime = EncodingSchemeUtils
.encodePeriod(new DCMIPeriod(startDateTime.get(), endDateTime.get()), Precision.Second);
dublinCoreOpt.get().set(DublinCore.PROPERTY_TEMPORAL, eventTime);
if (captureAgentId.isSome()) {
dublinCoreOpt.get().set(DublinCore.PROPERTY_SPATIAL, captureAgentId.get());
}
dublinCore = dublinCoreOpt;
dublinCoreChanged = true;
}
for (MediaPackage mpToUpdate : mediaPackage) {
// Check for series change
if (ne(record.getSnapshot().get().getMediaPackage().getSeries(), mpToUpdate.getSeries())) {
propertiesChanged = true;
seriesId = Opt.nul(mpToUpdate.getSeries());
}
// Check for ACL change and send update
AccessControlList aclNew = authorizationService.getActiveAcl(mpToUpdate).getA();
if (aclOld.isNone() || !AccessControlUtil.equals(aclNew, aclOld.get())) {
acl = some(aclNew);
}
// Check for dublin core change and send update
Opt dublinCoreNew = DublinCoreUtil.loadEpisodeDublinCore(workspace, mpToUpdate);
if (dublinCoreNew.isSome() && !DublinCoreUtil.equals(dublinCoreOpt.get(), dublinCoreNew.get())) {
dublinCoreChanged = true;
propertiesChanged = true;
dublinCore = dublinCoreNew;
}
}
Opt> finalCaProperties = Opt.none();
if (propertiesChanged) {
finalCaProperties = Opt.some(getFinalAgentProperties(caProperties, wfProps, captureAgentId.getOr(agentId),
seriesId, some(dublinCore.getOr(dublinCoreOpt.get()))));
}
String checksum = calculateChecksum(workspace, getEventCatalogUIAdapterFlavors(), startDateTime.getOr(start),
endDateTime.getOr(end), captureAgentId.getOr(agentId), userIds.getOr(presenters),
mediaPackage.getOr(record.getSnapshot().get().getMediaPackage()),
some(dublinCore.getOr(dublinCoreOpt.get())), wfProperties.getOr(wfProps),
finalCaProperties.getOr(caProperties), acl.getOr(new AccessControlList()));
String oldChecksum = extendedEventDto.getChecksum();
if (checksum.equals(oldChecksum)) {
logger.debug("Updated event {} has same checksum, ignore update", mpId);
return;
}
// Update asset
persistEvent(mpId, checksum, startDateTime, endDateTime, captureAgentId, userIds,
mediaPackage, wfProperties, finalCaProperties, Opt. none());
// Update live event
updateLiveEvent(mpId, acl, dublinCore, startDateTime, endDateTime, Opt.some(agentId),
finalCaProperties);
// Update Elasticsearch indices
updateEventInIndex(mpId, adminUiIndex, acl, dublinCore, startDateTime, endDateTime, userIds, Opt.some(agentId),
finalCaProperties, Opt.none());
updateEventInIndex(mpId, externalApiIndex, acl, dublinCore, startDateTime, endDateTime, userIds, Opt.some(agentId),
finalCaProperties, Opt.none());
// Update last modified
if (propertiesChanged || dublinCoreChanged || startDateTime.isSome() || endDateTime.isSome()) {
touchLastEntry(agentId);
for (String agent : captureAgentId) {
touchLastEntry(agent);
}
}
} catch (NotFoundException e) {
throw e;
} catch (SchedulerException e) {
throw e;
} catch (Exception e) {
throw new SchedulerException(e);
}
}
private boolean isAdmin() {
return (securityService.getUser().hasRole(GLOBAL_ADMIN_ROLE)
|| securityService.getUser().hasRole(securityService.getOrganization().getAdminRole()));
}
private Opt loadEpisodeDublinCoreFromAsset(Snapshot snapshot) {
Option dcCatalog = mlist(snapshot.getMediaPackage().getElements())
.filter(MediaPackageSupport.Filters.isEpisodeDublinCore).headOpt();
if (dcCatalog.isNone())
return Opt.none();
Opt asset = assetManager.getAsset(snapshot.getVersion(),
snapshot.getMediaPackage().getIdentifier().toString(), dcCatalog.get().getIdentifier());
if (asset.isNone())
return Opt.none();
if (Availability.OFFLINE.equals(asset.get().getAvailability()))
return Opt.none();
InputStream inputStream = null;
try {
inputStream = asset.get().getInputStream();
return Opt.some(DublinCores.read(inputStream));
} finally {
IOUtils.closeQuietly(inputStream);
}
}
private void verifyActive(String eventId, Date end) throws SchedulerException {
if (end == null) {
throw new IllegalArgumentException("Start and/or end date for event ID " + eventId + " is not set");
}
// TODO: Assumption of no TimeZone adjustment because catalog temporal is local to server
if (new Date().after(end)) {
logger.info("Event ID {} has already ended as its end time was {} and current time is {}", eventId,
DateTimeSupport.toUTC(end.getTime()), DateTimeSupport.toUTC(new Date().getTime()));
throw new SchedulerException("Event ID " + eventId + " has already ended at "
+ DateTimeSupport.toUTC(end.getTime()) + " and now is " + DateTimeSupport.toUTC(new Date().getTime()));
}
}
@Override
public synchronized void removeEvent(String mediaPackageId)
throws NotFoundException, SchedulerException {
notEmpty(mediaPackageId, "mediaPackageId");
try {
// Check if there are properties only from scheduler
AQueryBuilder query = assetManager.createQuery();
long deletedProperties = 0;
Opt extEvtOpt = persistence.getEvent(mediaPackageId);
if (extEvtOpt.isSome()) {
String agentId = extEvtOpt.get().getCaptureAgentId();
persistence.deleteEvent(mediaPackageId);
if (StringUtils.isNotEmpty(agentId))
touchLastEntry(agentId);
}
// Delete scheduler snapshot
long deletedSnapshots = query.delete(SNAPSHOT_OWNER, query.snapshot())
.where(withOrganization(query).and(query.mediaPackageId(mediaPackageId)))
.name("delete episode").run();
if (deletedProperties + deletedSnapshots == 0)
throw new NotFoundException();
// Update live event
sendSchedulerMessage(new SchedulerItemList(mediaPackageId, SchedulerItem.delete()));
// Update Elasticsearch indices
removeSchedulingFromIndex(mediaPackageId, adminUiIndex);
removeSchedulingFromIndex(mediaPackageId, externalApiIndex);
} catch (NotFoundException | SchedulerException e) {
throw e;
} catch (Exception e) {
logger.error("Could not remove event '{}' from persistent storage: {}", mediaPackageId, e);
throw new SchedulerException(e);
}
}
@Override
public MediaPackage getMediaPackage(String mediaPackageId) throws NotFoundException, SchedulerException {
notEmpty(mediaPackageId, "mediaPackageId");
try {
return getEventMediaPackage(mediaPackageId);
} catch (RuntimeNotFoundException e) {
throw e.getWrappedException();
} catch (Exception e) {
logger.error("Failed to get mediapackage of event '{}':", mediaPackageId, e);
throw new SchedulerException(e);
}
}
@Override
public DublinCoreCatalog getDublinCore(String mediaPackageId) throws NotFoundException, SchedulerException {
notEmpty(mediaPackageId, "mediaPackageId");
try {
AQueryBuilder query = assetManager.createQuery();
AResult result = query.select(query.snapshot())
.where(withOrganization(query).and(query.mediaPackageId(mediaPackageId)).and(withOwner(query))
.and(query.version().isLatest()))
.run();
Opt record = result.getRecords().head();
if (record.isNone())
throw new NotFoundException();
Opt dublinCore = loadEpisodeDublinCoreFromAsset(record.get().getSnapshot().get());
if (dublinCore.isNone())
throw new NotFoundException("No dublincore catalog found " + mediaPackageId);
return dublinCore.get();
} catch (NotFoundException e) {
throw e;
} catch (Exception e) {
logger.error("Failed to get dublin core catalog of event '{}':", mediaPackageId, e);
throw new SchedulerException(e);
}
}
@Override
public TechnicalMetadata getTechnicalMetadata(String mediaPackageId)
throws NotFoundException, UnauthorizedException, SchedulerException {
notEmpty(mediaPackageId, "mediaPackageId");
try {
final Opt extEvt = persistence.getEvent(mediaPackageId);
if (extEvt.isNone())
throw new NotFoundException();
return getTechnicalMetadata(extEvt.get());
} catch (NotFoundException e) {
throw e;
} catch (Exception e) {
logger.error("Failed to get technical metadata of event '{}':", mediaPackageId, e);
throw new SchedulerException(e);
}
}
@Override
public Map getWorkflowConfig(String mediaPackageId) throws NotFoundException, SchedulerException {
notEmpty(mediaPackageId, "mediaPackageId");
try {
Opt record = persistence.getEvent(mediaPackageId);
if (record.isNone())
throw new NotFoundException();
return deserializeExtendedEventProperties(record.get().getWorkflowProperties());
} catch (NotFoundException e) {
throw e;
} catch (Exception e) {
logger.error("Failed to get workflow configuration of event '{}':", mediaPackageId, e);
throw new SchedulerException(e);
}
}
@Override
public Map getCaptureAgentConfiguration(String mediaPackageId)
throws NotFoundException, SchedulerException {
notEmpty(mediaPackageId, "mediaPackageId");
try {
Opt record = persistence.getEvent(mediaPackageId);
if (record.isNone())
throw new NotFoundException();
return deserializeExtendedEventProperties(record.get().getCaptureAgentProperties());
} catch (NotFoundException e) {
throw e;
} catch (Exception e) {
logger.error("Failed to get capture agent contiguration of event '{}':", mediaPackageId, e);
throw new SchedulerException(e);
}
}
@Override
public int getEventCount() throws SchedulerException {
try {
return persistence.countEvents();
} catch (Exception e) {
throw new SchedulerException(e);
}
}
@Override
public List search(Opt captureAgentId, Opt startsFrom, Opt startsTo,
Opt endFrom, Opt endTo) throws SchedulerException {
try {
return persistence.search(captureAgentId, startsFrom, startsTo, endFrom, endTo, Opt.none()).parallelStream()
.map(ExtendedEventDto::getMediaPackageId)
.map(this::getEventMediaPackage).collect(Collectors.toList());
} catch (Exception e) {
throw new SchedulerException(e);
}
}
@Override
public Opt getCurrentRecording(String captureAgentId) throws SchedulerException {
try {
final Date now = new Date();
List result = persistence.search(Opt.some(captureAgentId), Opt.none(), Opt.some(now), Opt.some(now), Opt.none(), Opt.some(1));
if (result.isEmpty()) {
return Opt.none();
}
return Opt.some(getEventMediaPackage(result.get(0).getMediaPackageId()));
} catch (Exception e) {
throw new SchedulerException(e);
}
}
@Override
public Opt getUpcomingRecording(String captureAgentId) throws SchedulerException {
try {
final Date now = new Date();
List result = persistence.search(Opt.some(captureAgentId), Opt.some(now), Opt.none(), Opt.none(), Opt.none(), Opt.some(1));
if (result.isEmpty()) {
return Opt.none();
}
return Opt.some(getEventMediaPackage(result.get(0).getMediaPackageId()));
} catch (Exception e) {
throw new SchedulerException(e);
}
}
@Override
public List findConflictingEvents(String captureDeviceID, Date startDate, Date endDate)
throws SchedulerException {
try {
final Organization organization = securityService.getOrganization();
final User user = SecurityUtil.createSystemUser(systemUserName, organization);
List conflictingEvents = new ArrayList();
SecurityUtil.runAs(securityService, organization, user, () -> {
try {
conflictingEvents.addAll(persistence.getEvents(captureDeviceID, startDate, endDate, Util.EVENT_MINIMUM_SEPARATION_MILLISECONDS)
.stream().map(this::getEventMediaPackage).collect(Collectors.toList()));
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to get conflicting events", e);
}
});
return conflictingEvents;
} catch (Exception e) {
throw new SchedulerException(e);
}
}
@Override
public List findConflictingEvents(String captureAgentId, RRule rrule, Date start, Date end,
long duration, TimeZone tz) throws SchedulerException {
notEmpty(captureAgentId, "captureAgentId");
notNull(rrule, "rrule");
notNull(start, "start");
notNull(end, "end");
notNull(tz, "timeZone");
Util.adjustRrule(rrule, start, tz);
final List periods = Util.calculatePeriods(start, end, duration, rrule, tz);
if (periods.isEmpty()) {
return Collections.emptyList();
}
return findConflictingEvents(periods, captureAgentId, tz);
}
private boolean checkPeriodOverlap(final List periods) {
final List sortedPeriods = new ArrayList<>(periods);
sortedPeriods.sort(Comparator.comparing(Period::getStart));
Period prior = periods.get(0);
for (Period current : periods.subList(1, periods.size())) {
if (current.getStart().compareTo(prior.getEnd()) < 0) {
return true;
}
prior = current;
}
return false;
}
private List findConflictingEvents(List periods, String captureAgentId, TimeZone tz)
throws SchedulerException {
notEmpty(captureAgentId, "captureAgentId");
notNull(periods, "periods");
requireTrue(periods.size() > 0, "periods");
// First, check if there are overlaps inside the periods to be added (this is possible if you specify an RRULE via
// the external API, for example; the admin ui should prevent this from happening). Then check for conflicts with
// existing events.
if (checkPeriodOverlap(periods)) {
throw new IllegalArgumentException("RRULE periods overlap");
}
try {
TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry();
Set events = new HashSet<>();
for (Period event : periods) {
event.setTimeZone(registry.getTimeZone(tz.getID()));
final Date startDate = event.getStart();
final Date endDate = event.getEnd();
events.addAll(findConflictingEvents(captureAgentId, startDate, endDate));
}
return new ArrayList<>(events);
} catch (Exception e) {
throw new SchedulerException(e);
}
}
@Override
public String getCalendar(Opt captureAgentId, Opt seriesId, Opt cutoff)
throws SchedulerException {
try {
final Map searchResult = persistence.search(captureAgentId, Opt.none(), cutoff,
Opt.some(DateTime.now().minusHours(1).toDate()), Opt.none(), Opt.none()).stream()
.collect(Collectors.toMap(ExtendedEventDto::getMediaPackageId, Function.identity()));
final AQueryBuilder query = assetManager.createQuery();
final AResult result = query.select(query.snapshot())
.where(withOrganization(query).and(query.mediaPackageIds(searchResult.keySet().toArray(new String[0])))
.and(withOwner(query)).and(query.version().isLatest()))
.run();
final CalendarGenerator cal = new CalendarGenerator(seriesService);
for (final ARecord record : result.getRecords()) {
final Opt optMp = record.getSnapshot().map(episodeToMp);
// If the event media package is empty, skip the event
if (optMp.isNone()) {
logger.warn("Mediapackage for event '{}' can't be found, event is not recorded", record.getMediaPackageId());
continue;
}
if (seriesId.isSome() && !seriesId.get().equals(optMp.get().getSeries())) {
continue;
}
Opt catalogOpt = loadEpisodeDublinCoreFromAsset(record.getSnapshot().get());
if (catalogOpt.isNone()) {
logger.warn("No episode catalog available, skipping!");
continue;
}
final Map caMetadata = deserializeExtendedEventProperties(searchResult.get(record.getMediaPackageId()).getCaptureAgentProperties());
// If the even properties are empty, skip the event
if (caMetadata.isEmpty()) {
logger.warn("Properties for event '{}' can't be found, event is not recorded", record.getMediaPackageId());
continue;
}
final String agentId = searchResult.get(record.getMediaPackageId()).getCaptureAgentId();
final Date start = searchResult.get(record.getMediaPackageId()).getStartDate();
final Date end = searchResult.get(record.getMediaPackageId()).getEndDate();
final Date lastModified = record.getSnapshot().get().getArchivalDate();
// Add the entry to the calendar, skip it with a warning if adding fails
try {
cal.addEvent(optMp.get(), catalogOpt.get(), agentId, start, end, lastModified, toPropertyString(caMetadata));
} catch (Exception e) {
logger.warn("Error adding event '{}' to calendar, event is not recorded", record.getMediaPackageId(), e);
}
}
// Only validate calendars with events. Without any events, the iCalendar won't validate
if (cal.getCalendar().getComponents().size() > 0) {
cal.getCalendar().validate();
}
return cal.getCalendar().toString();
} catch (Exception e) {
throw new SchedulerException(e);
}
}
@Override
public String getScheduleLastModified(String captureAgentId) throws SchedulerException {
notEmpty(captureAgentId, "captureAgentId");
try {
String lastModified = lastModifiedCache.getIfPresent(captureAgentId);
if (lastModified != null)
return lastModified;
populateLastModifiedCache();
lastModified = lastModifiedCache.getIfPresent(captureAgentId);
// If still null set the empty calendar ETag
if (lastModified == null) {
lastModified = EMPTY_CALENDAR_ETAG;
lastModifiedCache.put(captureAgentId, lastModified);
}
return lastModified;
} catch (Exception e) {
throw new SchedulerException(e);
}
}
@Override
public void removeScheduledRecordingsBeforeBuffer(long buffer) throws SchedulerException {
DateTime end = new DateTime(DateTimeZone.UTC).minus(buffer * 1000);
logger.info("Starting to look for scheduled recordings that have finished before {}.",
DateTimeSupport.toUTC(end.getMillis()));
List finishedEvents;
try {
finishedEvents = persistence.search(Opt. none(), Opt. none(), Opt. none(), Opt. none(),
Opt.some(end.toDate()), Opt.none());
logger.debug("Found {} events from search.", finishedEvents.size());
} catch (Exception e) {
throw new SchedulerException(e);
}
int recordingsRemoved = 0;
for (ExtendedEventDto extEvt : finishedEvents) {
final String eventId = extEvt.getMediaPackageId();
try {
removeEvent(eventId);
logger.debug("Sucessfully removed scheduled event with id " + eventId);
recordingsRemoved++;
} catch (NotFoundException e) {
logger.debug("Skipping event with id {} because it is not found", eventId);
} catch (Exception e) {
logger.warn("Unable to delete event with id '{}':", eventId, e);
}
}
logger.info("Found {} to remove that ended before {}.", recordingsRemoved, DateTimeSupport.toUTC(end.getMillis()));
}
@Override
public boolean updateRecordingState(String id, String state) throws NotFoundException, SchedulerException {
notEmpty(id, "id");
notEmpty(state, "state");
if (!RecordingState.KNOWN_STATES.contains(state)) {
logger.warn("Invalid recording state: {}.", state);
return false;
}
try {
final Opt optExtEvt = persistence.getEvent(id);
if (optExtEvt.isNone())
throw new NotFoundException();
final String prevRecordingState = optExtEvt.get().getRecordingState();
final Recording r = new RecordingImpl(id, state);
if (!state.equals(prevRecordingState)) {
logger.debug("Setting Recording {} to state {}.", id, state);
// Update live event
sendSchedulerMessage(new SchedulerItemList(r.getID(), Collections.singletonList(SchedulerItem
.updateRecordingStatus(r.getState(), r.getLastCheckinTime()))));
// Update Elasticsearch indices
updateEventInIndex(r.getID(), adminUiIndex, Opt.none(), Opt.none(), Opt.none(), Opt.none(), Opt.none(),
Opt.none(), Opt.none(), Opt.some(r.getState()));
updateEventInIndex(r.getID(), externalApiIndex, Opt.none(), Opt.none(), Opt.none(), Opt.none(), Opt.none(),
Opt.none(), Opt.none(), Opt.some(r.getState()));
} else {
logger.debug("Recording state not changed");
}
persistence.storeEvent(
id,
securityService.getOrganization().getId(),
Opt.none(),
Opt.none(),
Opt.none(),
Opt.none(),
Opt.some(r.getState()),
Opt.some(r.getLastCheckinTime()),
Opt.none(),
Opt.none(),
Opt.none(),
Opt.none(),
Opt.none()
);
return true;
} catch (NotFoundException e) {
throw e;
} catch (Exception e) {
throw new SchedulerException(e);
}
}
@Override
public Recording getRecordingState(String id) throws NotFoundException, SchedulerException {
notEmpty(id, "id");
try {
Opt extEvt = persistence.getEvent(id);
if (extEvt.isNone() || extEvt.get().getRecordingState() == null) {
throw new NotFoundException();
}
return new RecordingImpl(id, extEvt.get().getRecordingState(), extEvt.get().getRecordingLastHeard());
} catch (NotFoundException e) {
throw e;
} catch (Exception e) {
throw new SchedulerException(e);
}
}
@Override
public void removeRecording(String id) throws NotFoundException, SchedulerException {
notEmpty(id, "id");
try {
persistence.resetRecordingState(id);
// Update live event
sendSchedulerMessage(new SchedulerItemList(id, SchedulerItem.deleteRecordingState()));
// Update Elasticsearch indices
removeRecordingStatusFromIndex(id, adminUiIndex);
removeRecordingStatusFromIndex(id, externalApiIndex);
} catch (NotFoundException e) {
throw e;
} catch (Exception e) {
throw new SchedulerException(e);
}
}
@Override
public Map getKnownRecordings() throws SchedulerException {
try {
return persistence.getKnownRecordings().parallelStream()
.collect(
Collectors.toMap(ExtendedEventDto::getMediaPackageId,
dto -> new RecordingImpl(dto.getMediaPackageId(), dto.getRecordingState(), dto.getRecordingLastHeard()))
);
} catch (Exception e) {
throw new SchedulerException(e);
}
}
private synchronized void persistEvent(final String mpId, final String checksum,
final Opt startDateTime, final Opt endDateTime, final Opt captureAgentId,
final Opt> userIds, final Opt mediaPackage,
final Opt> wfProperties, final Opt> caProperties,
final Opt schedulingSource) throws SchedulerServiceDatabaseException {
// Store scheduled mediapackage
for (MediaPackage mpToUpdate : mediaPackage) {
assetManager.takeSnapshot(SNAPSHOT_OWNER, mpToUpdate);
}
// Store extended event
persistence.storeEvent(
mpId,
securityService.getOrganization().getId(),
captureAgentId,
startDateTime,
endDateTime,
schedulingSource,
Opt.none(),
Opt.none(),
userIds.isSome() ? Opt.some(String.join(",", userIds.get())) : Opt.none(),
Opt.some(new Date()),
Opt.some(checksum),
wfProperties,
caProperties
);
}
/**
* Update the event in the Elasticsearch index. Fields will only be updated of the corresponding Opt is not none.
*
* @param mediaPackageId
* @param index
* @param acl
* @param dublinCore
* @param startTime
* @param endTime
* @param presenters
* @param agentId
* @param properties
* @param recordingStatus
*/
private void updateEventInIndex(String mediaPackageId, AbstractSearchIndex index, Opt acl,
Opt dublinCore, Opt startTime, Opt endTime, Opt> presenters,
Opt agentId, Opt> properties, Opt recordingStatus) {
String organization = getSecurityService().getOrganization().getId();
User user = getSecurityService().getUser();
Function, Optional> updateFunction = (Optional eventOpt) -> {
Event event = eventOpt.orElse(new Event(mediaPackageId, organization));
if (acl.isSome()) {
event.setAccessPolicy(AccessControlParser.toJsonSilent(acl.get()));
}
if (dublinCore.isSome()) {
EventIndexUtils.updateEvent(event, dublinCore.get());
if (isBlank(event.getCreator()))
event.setCreator(getSecurityService().getUser().getName());
// Update series name if not already done
try {
EventIndexUtils.updateSeriesName(event, organization, user, index);
} catch (SearchIndexException e) {
logger.error("Error updating the series name of the event {} in the {} index.", mediaPackageId,
index.getIndexName(), e);
}
}
if (presenters.isSome()) {
event.setTechnicalPresenters(new ArrayList<>(presenters.get()));
}
if (agentId.isSome()) {
event.setAgentId(agentId.get());
}
if (recordingStatus.isSome() && !recordingStatus.get().equals(RecordingState.UNKNOWN)) {
event.setRecordingStatus(recordingStatus.get());
}
if (properties.isSome()) {
event.setAgentConfiguration(properties.get());
}
if (startTime.isSome()) {
String startTimeStr = startTime == null ? null : DateTimeSupport.toUTC(startTime.get().getTime());
event.setTechnicalStartTime(startTimeStr);
}
if (endTime.isSome()) {
String endTimeStr = endTime == null ? null : DateTimeSupport.toUTC(endTime.get().getTime());
event.setTechnicalEndTime(endTimeStr);
}
return Optional.of(event);
};
try {
index.addOrUpdateEvent(mediaPackageId, updateFunction, organization, user);
logger.debug("Scheduled event {} updated in the {} index.", mediaPackageId, index.getIndexName());
} catch (SearchIndexException e) {
logger.error("Error updating the scheduled event {} in the {} index.", mediaPackageId, index.getIndexName(), e);
}
}
/**
* Set recording status to null for this event in the Elasticsearch index.
*
* @param mediaPackageId
* @param index
*/
private void removeRecordingStatusFromIndex(String mediaPackageId, AbstractSearchIndex index) {
String organization = getSecurityService().getOrganization().getId();
User user = getSecurityService().getUser();
Function, Optional> updateFunction = (Optional eventOpt) -> {
Event event = eventOpt.orElse(new Event(mediaPackageId, organization));
event.setRecordingStatus(null);
return Optional.of(event);
};
try {
index.addOrUpdateEvent(mediaPackageId, updateFunction, organization, user);
logger.debug("Recording state of event {} removed from the {} index.", mediaPackageId, index.getIndexName());
} catch (SearchIndexException e) {
logger.error("Failed to remove the recording state of event {} from the {} index.", mediaPackageId,
index.getIndexName(), e);
}
}
/**
* Remove scheduling information for this event from the Elasticsearch index.
*
* @param mediaPackageId
* @param index
*/
private void removeSchedulingFromIndex(String mediaPackageId, AbstractSearchIndex index) {
String organization = getSecurityService().getOrganization().getId();
User user = getSecurityService().getUser();
try {
index.deleteScheduling(organization, user, mediaPackageId);
logger.debug("Scheduling information of event {} removed from the {} index.", mediaPackageId, index.getIndexName());
} catch (NotFoundException e) {
logger.warn("Scheduled recording {} not found for deletion from the {} index.", mediaPackageId,
index.getIndexName());
} catch (SearchIndexException e) {
logger.error("Failed to delete the scheduling information of event {} from the {} index.", mediaPackageId,
index.getIndexName(), e);
}
}
/**
* Send messages to trigger an update in the LiveScheduleService.
*
* @param mpId
* @param acl
* @param dublinCore
* @param startTime
* @param endTime
* @param agentId
* @param properties
*/
private void updateLiveEvent(String mpId, Opt acl, Opt dublinCore,
Opt startTime, Opt endTime, Opt agentId, Opt> properties) {
List items = new ArrayList<>();
if (acl.isSome()) {
items.add(SchedulerItem.updateAcl(acl.get()));
}
if (dublinCore.isSome()) {
items.add(SchedulerItem.updateCatalog(dublinCore.get()));
}
if (startTime.isSome()) {
items.add(SchedulerItem.updateStart(startTime.get()));
}
if (endTime.isSome()) {
items.add(SchedulerItem.updateEnd(endTime.get()));
}
if (agentId.isSome()) {
items.add(SchedulerItem.updateAgent(agentId.get()));
}
if (properties.isSome()) {
items.add(SchedulerItem.updateProperties(properties.get()));
}
if (!items.isEmpty()) {
sendSchedulerMessage(new SchedulerItemList(mpId, items));
}
}
private Map getFinalAgentProperties(Map caMetadata, Map wfProperties,
String captureAgentId, Opt seriesId, Opt dublinCore) {
Map properties = new HashMap<>();
for (Entry entry : caMetadata.entrySet()) {
if (entry.getKey().startsWith(WORKFLOW_CONFIG_PREFIX))
continue;
properties.put(entry.getKey(), entry.getValue());
}
for (Entry entry : wfProperties.entrySet()) {
properties.put(WORKFLOW_CONFIG_PREFIX.concat(entry.getKey()), entry.getValue());
}
if (dublinCore.isSome()) {
properties.put("event.title", dublinCore.get().getFirst(DublinCore.PROPERTY_TITLE));
}
if (seriesId.isSome()) {
properties.put("event.series", seriesId.get());
}
properties.put("event.location", captureAgentId);
return properties;
}
private void touchLastEntry(String captureAgentId) throws SchedulerException {
// touch last entry
try {
logger.debug("Marking calendar feed for {} as modified", captureAgentId);
persistence.touchLastEntry(captureAgentId);
populateLastModifiedCache();
} catch (SchedulerServiceDatabaseException e) {
logger.error("Failed to update last modified entry of agent '{}':", captureAgentId, e);
}
}
private void populateLastModifiedCache() throws SchedulerException {
try {
Map lastModifiedDates = persistence.getLastModifiedDates();
for (Entry entry : lastModifiedDates.entrySet()) {
Date lastModifiedDate = entry.getValue() != null ? entry.getValue() : new Date();
lastModifiedCache.put(entry.getKey(), generateLastModifiedHash(lastModifiedDate));
}
} catch (Exception e) {
throw new SchedulerException(e);
}
}
private String generateLastModifiedHash(Date lastModifiedDate) {
return "mod" + Long.toString(lastModifiedDate.getTime());
}
private String toPropertyString(Map properties) {
StringBuilder wfPropertiesString = new StringBuilder();
for (Map.Entry entry : properties.entrySet())
wfPropertiesString.append(entry.getKey() + "=" + entry.getValue() + "\n");
return wfPropertiesString.toString();
}
private MediaPackage getEventMediaPackage(String mediaPackageId) {
AQueryBuilder query = assetManager.createQuery();
AResult result = query.select(query.snapshot())
.where(withOrganization(query).and(query.mediaPackageId(mediaPackageId)).and(withOwner(query))
.and(query.version().isLatest()))
.run();
Opt record = result.getRecords().head();
if (record.isNone())
throw new RuntimeNotFoundException(new NotFoundException());
return record.bind(recordToMp).get();
}
/**
*
* @param mp
* the mediapackage to update
* @param dc
* the dublincore metadata to use to update the mediapackage
* @return the updated mediapackage
* @throws IOException
* Thrown if an IO error occurred adding the dc catalog file
* @throws MediaPackageException
* Thrown if an error occurred updating the mediapackage or the mediapackage does not contain a catalog
*/
private MediaPackage updateDublincCoreCatalog(MediaPackage mp, DublinCoreCatalog dc)
throws IOException, MediaPackageException {
try (InputStream inputStream = IOUtils.toInputStream(dc.toXmlString(), "UTF-8")) {
// Update dublincore catalog
Catalog[] catalogs = mp.getCatalogs(MediaPackageElements.EPISODE);
if (catalogs.length > 0) {
Catalog catalog = catalogs[0];
URI uri = workspace.put(mp.getIdentifier().toString(), catalog.getIdentifier(), "dublincore.xml", inputStream);
catalog.setURI(uri);
// setting the URI to a new source so the checksum will most like be invalid
catalog.setChecksum(null);
} else {
throw new MediaPackageException("Unable to find catalog");
}
}
return mp;
}
private TechnicalMetadata getTechnicalMetadata(ExtendedEventDto extEvt) {
final String agentId = extEvt.getCaptureAgentId();
final Date start = extEvt.getStartDate();
final Date end = extEvt.getEndDate();
final Set presenters = getPresenters(Opt.nul(extEvt.getPresenters()).getOr(""));
final Opt recordingStatus = Opt.nul(extEvt.getRecordingState());
final Opt lastHeard = Opt.nul(extEvt.getRecordingLastHeard());
final Map caMetadata = deserializeExtendedEventProperties(extEvt.getCaptureAgentProperties());
final Map wfProperties = deserializeExtendedEventProperties(extEvt.getWorkflowProperties());
Recording recording = null;
if (recordingStatus.isSome() && lastHeard.isSome())
recording = new RecordingImpl(extEvt.getMediaPackageId(), recordingStatus.get(), lastHeard.get());
return new TechnicalMetadataImpl(extEvt.getMediaPackageId(), agentId, start, end, presenters, wfProperties,
caMetadata, Opt.nul(recording));
}
private Predicate withOrganization(AQueryBuilder query) {
return query.organizationId().eq(securityService.getOrganization().getId());
}
private Predicate withOwner(AQueryBuilder query) {
return query.owner().eq(SNAPSHOT_OWNER);
}
private Set getPresenters(String presentersString) {
return new HashSet<>(Arrays.asList(StringUtils.split(presentersString, ",")));
}
/**
* @return A {@link List} of {@link MediaPackageElementFlavor} that provide the extended metadata to the front end.
*/
private List getEventCatalogUIAdapterFlavors() {
String organization = securityService.getOrganization().getId();
return Stream.$(eventCatalogUIAdapters).filter(eventOrganizationFilter._2(organization)).map(uiAdapterToFlavor)
.filter(isNotEpisodeDublinCore).toList();
}
@Override
public void repopulate(final AbstractSearchIndex index) throws IndexRebuildException {
final int[] current = {0};
final int total;
try {
total = persistence.countEvents();
} catch (SchedulerServiceDatabaseException e) {
logIndexRebuildError(logger, index.getIndexName(), e);
throw new IndexRebuildException(index.getIndexName(), getService(), e);
}
logIndexRebuildBegin(logger, index.getIndexName(), total, "scheduled events");
for (Organization organization: orgDirectoryService.getOrganizations()) {
final User user = SecurityUtil.createSystemUser(systemUserName, organization);
SecurityUtil.runAs(securityService, organization, user, () -> {
final List events;
try {
events = persistence.getEvents();
} catch (SchedulerServiceDatabaseException e) {
logIndexRebuildError(logger, index.getIndexName(), e, organization);
return;
}
for (ExtendedEventDto event : events) {
String mpId = event.getMediaPackageId();
current[0] = current[0] + 1;
try {
final Set presenters = getPresenters(Opt.nul(event.getPresenters()).getOr(""));
final Map caMetadata = deserializeExtendedEventProperties(event.getCaptureAgentProperties());
AQueryBuilder query = assetManager.createQuery();
final AResult result = query.select(query.snapshot())
.where(query.mediaPackageId(mpId).and(query.version().isLatest())).run();
final Snapshot snapshot = result.getRecords().head().get().getSnapshot().get();
final Opt acl = Opt.some(authorizationService.getActiveAcl(snapshot.getMediaPackage()).getA());
final Opt dublinCore = loadEpisodeDublinCoreFromAsset(snapshot);
updateEventInIndex(mpId, index, acl, dublinCore, Opt.some(event.getStartDate()),
Opt.some(event.getEndDate()), Opt.some(presenters), Opt.some(event.getCaptureAgentId()),
Opt.some(caMetadata), Opt.nul(event.getRecordingState()));
logIndexRebuildProgress(logger, index.getIndexName(), total, current[0]);
} catch (Exception e) {
logSkippingElement(logger, "scheduled event", mpId, e);
}
}
});
}
}
@Override
public IndexRebuildService.Service getService() {
return IndexRebuildService.Service.Scheduler;
}
public SecurityService getSecurityService() {
return securityService;
}
}