/** * 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.opencastproject.metadata.dublincore.DublinCore.PROPERTY_IDENTIFIER; import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TEMPORAL; import org.opencastproject.metadata.dublincore.DCMIPeriod; import org.opencastproject.metadata.dublincore.DublinCore; import org.opencastproject.metadata.dublincore.DublinCoreCatalog; import org.opencastproject.metadata.dublincore.DublinCoreCatalogList; import org.opencastproject.metadata.dublincore.EncodingSchemeUtils; import org.opencastproject.metadata.dublincore.Precision; import org.opencastproject.scheduler.api.SchedulerException; import org.opencastproject.scheduler.api.SchedulerQuery; import org.opencastproject.scheduler.api.SchedulerService; 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.serviceregistry.api.ServiceRegistry; import org.opencastproject.serviceregistry.api.ServiceRegistryException; import org.opencastproject.util.NotFoundException; import org.opencastproject.util.OsgiUtil; import org.opencastproject.util.data.Effect0; import org.joda.time.DateTime; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.osgi.service.component.ComponentContext; import org.quartz.Job; import org.quartz.JobDetail; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.Trigger; import org.quartz.TriggerUtils; import org.quartz.impl.StdSchedulerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; import java.util.Dictionary; import java.util.HashMap; /** Prolong immediate recordings before reaching the end, as long as there are no conflicts */ public class CaptureNowProlongingService implements ManagedService { /** Log facility */ private static final Logger logger = LoggerFactory.getLogger(CaptureNowProlongingService.class); private static final String CFG_KEY_INITIAL_TIME = "initial-time"; private static final String CFG_KEY_PROLONGING_TIME = "prolonging-time"; private static final String JOB_NAME = "mh-capture-prolonging-job"; private static final String JOB_GROUP = "mh-capture-prolonging-job-group"; private static final String TRIGGER_GROUP = "mh-capture-prolonging-trigger-group"; private static final String JOB_PARAM_PARENT = "parent"; /** The initial time in millis */ private int initialTime = -1; /** The prolonging time in millis */ private int prolongingTime = -1; /** The quartz scheduler */ private org.quartz.Scheduler quartz; /** The scheduler service */ private SchedulerService schedulerService; /** The security service */ private SecurityService securityService; /** The service registry */ private ServiceRegistry serviceRegistry; /** The organization directory service */ private OrganizationDirectoryService orgDirectoryService; /** The bundle context for this osgi component */ private ComponentContext componentContext; /** Sets the scheduler service */ public void setSchedulerService(SchedulerService schedulerService) { this.schedulerService = schedulerService; } /** Sets the security service */ public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } /** Sets the service registry */ public void setServiceRegistry(ServiceRegistry serviceRegistry) { this.serviceRegistry = serviceRegistry; } /** Sets the organization directory service */ public void setOrgDirectoryService(OrganizationDirectoryService orgDirectoryService) { this.orgDirectoryService = orgDirectoryService; } /** * Activates the component * * @param cc * the component's context */ public void activate(ComponentContext cc) { componentContext = cc; try { quartz = new StdSchedulerFactory().getScheduler(); quartz.start(); // create and set the job. To actually run it call schedule(..) final JobDetail job = new JobDetail(JOB_NAME, JOB_GROUP, Runner.class); job.setDurability(true); job.setVolatility(true); job.getJobDataMap().put(JOB_PARAM_PARENT, this); quartz.addJob(job, true); } catch (org.quartz.SchedulerException e) { throw new RuntimeException(e); } } /** * Deactivates the component */ public void deactivate(ComponentContext cc) { componentContext = null; shutdown(); } @Override public void updated(@SuppressWarnings("rawtypes") Dictionary properties) throws ConfigurationException { initialTime = OsgiUtil.getCfgAsInt(properties, CFG_KEY_INITIAL_TIME); if (initialTime <= 90) { initialTime = 90000; } else { initialTime *= 1000; // Configuration holds initial time in seconds, value must be in milliseconds } prolongingTime = OsgiUtil.getCfgAsInt(properties, CFG_KEY_PROLONGING_TIME); if (prolongingTime <= 90) { prolongingTime = 90000; } else { prolongingTime *= 1000; // Configuration holds prolonging time in seconds, value must be in milliseconds } } /** * Set the schedule and start or restart the scheduler. */ public void schedule(String agentId) throws org.quartz.SchedulerException { logger.debug("Capture prolonging job for agent '{}' is run every minute.", agentId); final Trigger trigger = TriggerUtils.makeMinutelyTrigger(); trigger.setStartTime(DateTime.now().plusMinutes(1).toDate()); trigger.setName(agentId); trigger.setGroup(TRIGGER_GROUP); trigger.setJobName(JOB_NAME); trigger.setJobGroup(JOB_GROUP); if (quartz.getTrigger(agentId, TRIGGER_GROUP) == null) { quartz.scheduleJob(trigger); } else { quartz.rescheduleJob(agentId, TRIGGER_GROUP, trigger); } } public void stop(String agentId) { try { quartz.unscheduleJob(agentId, TRIGGER_GROUP); logger.info("Stopped prolonging capture for agent '{}'", agentId); } catch (Exception e) { logger.error("Error stopping Quartz job for agent '{}': {}", agentId, e); } } /** Shutdown the scheduler. */ public void shutdown() { try { quartz.shutdown(); } catch (org.quartz.SchedulerException ignore) { } } // just to make sure Quartz is being shut down... @Override protected void finalize() throws Throwable { super.finalize(); shutdown(); } /** * Returns the initial time duration (in milliseconds) of a recording started by the CaptureNow service * * @return the initial time */ public int getInitialTime() { return initialTime; } /** * Returns the time duration (in milliseconds) a recording is prolonged by the prolonging job. * * @return the prolonging time */ public int getProlongingTime() { return prolongingTime; } public SecurityService getSecurityService() { return securityService; } public ComponentContext getComponentContext() { return componentContext; } public ServiceRegistry getServiceRegistry() { return serviceRegistry; } public OrganizationDirectoryService getOrgDirectoryService() { return orgDirectoryService; } // -- /** Quartz work horse. */ public static class Runner implements Job { @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { logger.debug("Start capture prolonging job for agent '{}'", jobExecutionContext.getTrigger().getName()); try { execute((CaptureNowProlongingService) jobExecutionContext.getJobDetail().getJobDataMap().get(JOB_PARAM_PARENT), jobExecutionContext.getTrigger().getName()); } catch (Exception e) { throw new JobExecutionException("An error occurred while prolonging captures", e); } logger.debug("Finished capture prolonging job for agent '{}'", jobExecutionContext.getTrigger().getName()); } private void execute(final CaptureNowProlongingService prolongingService, final String agentId) { try { DublinCoreCatalog eventCatalog = prolongingService.getImmediateEvent(agentId); Date endDate = EncodingSchemeUtils.decodeMandatoryPeriod(eventCatalog.getFirst(DublinCore.PROPERTY_TEMPORAL)) .getEnd(); if (endDate.before(DateTime.now().plusSeconds(90).toDate())) { prolong(prolongingService, eventCatalog, agentId); } else { logger.debug("Wait another minute to check for prolinging the immediate recording for agent '{}'", agentId); } } catch (Exception e) { logger.error("Unable to get the immediate recording for agent '{}': {}", agentId, e); } } private void prolong(final CaptureNowProlongingService prolongingService, final DublinCoreCatalog eventCatalog, final String agentId) throws NotFoundException, ServiceRegistryException { long eventId = Long.parseLong(eventCatalog.getFirst(PROPERTY_IDENTIFIER)); org.opencastproject.job.api.Job job = prolongingService.getServiceRegistry().getJob(eventId); Organization organization = prolongingService.getOrgDirectoryService().getOrganization(job.getOrganization()); User user = SecurityUtil.createSystemUser(prolongingService.getComponentContext(), organization); SecurityUtil.runAs(prolongingService.getSecurityService(), organization, user, new Effect0() { @Override protected void run() { try { prolongingService.prolongEvent(eventCatalog, agentId); logger.info("Prolonged immediate recording for agent '{}'", agentId); } catch (UnauthorizedException e) { logger.error( "Unable to update the prolonged recording for agent '{}': You don't have the right permissions!", agentId); } catch (NotFoundException e) { logger.warn( "Unable to update the prolonged recording for agent '{}': No immedia capture found for updating!", agentId); } catch (Exception e) { logger.error("Unable to update the prolonged recording for agent '{}': {}", agentId, e); } } }); } } public DublinCoreCatalog getImmediateEvent(String agentId) throws NotFoundException, SchedulerException { SchedulerQuery q = new SchedulerQuery().setSpatial(agentId).setStartsTo(new Date()).setEndsFrom(new Date()); DublinCoreCatalogList search = schedulerService.search(q); if (search.getCatalogList().isEmpty()) { logger.warn("Unable to prolong capture, no recording found for agent '{}'!", agentId); throw new NotFoundException("No immediate recording found for agent: " + agentId); } return search.getCatalogList().get(0); } public void prolongEvent(DublinCoreCatalog eventCatalog, String agentId) throws UnauthorizedException, NotFoundException, SchedulerException { long eventId = Long.parseLong(eventCatalog.getFirst(PROPERTY_IDENTIFIER)); DCMIPeriod period = EncodingSchemeUtils.decodeMandatoryPeriod(eventCatalog.getFirst(DublinCore.PROPERTY_TEMPORAL)); Date prolongedEndDate = new DateTime(period.getEnd()).plus(getProlongingTime()).toDate(); eventCatalog.set(PROPERTY_TEMPORAL, EncodingSchemeUtils.encodePeriod(new DCMIPeriod(period.getStart(), prolongedEndDate), Precision.Second)); DublinCoreCatalogList events = schedulerService.findConflictingEvents(agentId, period.getStart(), prolongedEndDate); for (DublinCoreCatalog conflictCatalog : events.getCatalogList()) { if (eventId == Long.parseLong(conflictCatalog.getFirst(PROPERTY_IDENTIFIER))) continue; Date conflictingStartDate = EncodingSchemeUtils .decodeMandatoryPeriod(conflictCatalog.getFirst(DublinCore.PROPERTY_TEMPORAL)).getStart(); prolongedEndDate = new DateTime(conflictingStartDate).minusMinutes(1).toDate(); eventCatalog.set(PROPERTY_TEMPORAL, EncodingSchemeUtils.encodePeriod(new DCMIPeriod(period.getStart(), prolongedEndDate), Precision.Second)); logger.info( "An existing event is in a conflict with the one to be prolong on the agent '{}'. Prolong to a minute before the conflicting event.", agentId); stop(agentId); break; } schedulerService.updateEvent(eventId, eventCatalog, new HashMap<String, String>()); } }