All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opencastproject.scheduler.impl.CaptureNowProlongingService Maven / Gradle / Ivy

There is a newer version: 16.7
Show newest version
/*
 * 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_TEMPORAL;

import org.opencastproject.mediapackage.Catalog;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElements;
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.EncodingSchemeUtils;
import org.opencastproject.metadata.dublincore.Precision;
import org.opencastproject.scheduler.api.SchedulerException;
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.workspace.api.Workspace;

import com.entwinemedia.fn.data.Opt;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
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.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
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.io.IOException;
import java.net.URI;
import java.util.Date;
import java.util.Dictionary;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/** Prolong immediate recordings before reaching the end, as long as there are no conflicts */
@Component(
    immediate = true,
    service = { ManagedService.class,CaptureNowProlongingService.class },
    property = {
        "service.description=Capture Prolonging Service"
    }
)
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 workspace */
  private Workspace workspace;

  /** The bundle context for this osgi component */
  private ComponentContext componentContext;

  /** Sets the scheduler service */
  @Reference
  public void setSchedulerService(SchedulerService schedulerService) {
    this.schedulerService = schedulerService;
  }

  /** Sets the security service */
  @Reference
  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  /** Sets the service registry */
  @Reference
  public void setServiceRegistry(ServiceRegistry serviceRegistry) {
    this.serviceRegistry = serviceRegistry;
  }

  /** Sets the organization directory service */
  @Reference
  public void setOrgDirectoryService(OrganizationDirectoryService orgDirectoryService) {
    this.orgDirectoryService = orgDirectoryService;
  }

  /** Sets the workspace */
  @Reference
  public void setWorkspace(Workspace workspace) {
    this.workspace = workspace;
  }

  /**
   * Activates the component
   *
   * @param cc
   *          the component's context
   */
  @Activate
  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
   */
  @Deactivate
  public void deactivate(ComponentContext cc) {
    componentContext = null;
    shutdown();
  }

  @Override
  public void updated(Dictionary properties) throws ConfigurationException {
    // Read configuration for the default initial duration
    try {
      initialTime = Integer.parseInt(StringUtils.defaultIfBlank((String) properties.get(CFG_KEY_INITIAL_TIME), "300"));
    } catch (NumberFormatException e) {
      throw new ConfigurationException(CFG_KEY_INITIAL_TIME, "Not an integer", e);
    }
    initialTime = Math.max(initialTime, 90) * 1000;

    // Read configuration for the prolonging time
    try {
      prolongingTime = Integer.parseInt(
              StringUtils.defaultIfBlank((String) properties.get(CFG_KEY_PROLONGING_TIME), "300"));
    } catch (NumberFormatException e) {
      throw new ConfigurationException(CFG_KEY_PROLONGING_TIME, "Not an integer", e);
    }
    prolongingTime = Math.max(prolongingTime, 90) * 1000;
  }

  /**
   * 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;
  }

  public Workspace getWorkspace() {
    return workspace;
  }

  // --

  /** Quartz work horse. */
  public static class Runner implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
      logger.debug("Starting ad-hoc 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 ad-hoc recordings", e);
      }
      logger.debug("Finished ad-hoc prolonging job for agent '{}'", jobExecutionContext.getTrigger().getName());
    }

    private void execute(final CaptureNowProlongingService prolongingService, final String agentId) {
      for (Organization organization : prolongingService.getOrgDirectoryService().getOrganizations()) {
        User user = SecurityUtil.createSystemUser(prolongingService.getComponentContext(), organization);
        SecurityUtil.runAs(prolongingService.getSecurityService(), organization, user, () -> {
          try {
            MediaPackage mp = prolongingService.getCurrentRecording(agentId);
            Optional dublinCore = DublinCoreUtil.loadEpisodeDublinCore(
                prolongingService.getWorkspace(),
                mp);
            if (dublinCore.isPresent()
                    && EncodingSchemeUtils.decodeMandatoryPeriod(dublinCore.get().getFirst(PROPERTY_TEMPORAL))
                            .getEnd().before(DateTime.now().plusSeconds(90).toDate())) {
              prolong(prolongingService, mp, dublinCore.get(), agentId);
            } else {
              logger.debug("Wait another minute before extending the ad-hoc recording for agent '{}'", agentId);
            }
          } catch (NotFoundException e) {
            logger.info("Unable to extend the ad-hoc recording for agent '{}': No ad-hoc recording found", agentId);
          } catch (Exception e) {
            logger.error("Error extending the ad-hoc recording for agent '{}': {}", agentId, e);
          }
        });
      }
    }

    private void prolong(final CaptureNowProlongingService prolongingService, final MediaPackage event,
            final DublinCoreCatalog dublinCore, final String agentId)
            throws NotFoundException, ServiceRegistryException {
      try {
        logger.info("Extending ad-hoc recording for agent '{}'", agentId);
        prolongingService.prolongEvent(event, dublinCore, agentId);
      } catch (UnauthorizedException e) {
        logger.error("Error extending the ad-hoc recording for agent '{}': Permission denied", agentId);
      } catch (NotFoundException e) {
        logger.warn("Error extending the ad-hoc recording for agent '{}': No ad-hoc recording found", agentId);
      } catch (Exception e) {
        logger.error("Error extending the ad-hoc recording for agent '{}': {}", agentId, e);
      }
    }

  }

  /**
   * Returns the current event for the given capture agent.
   *
   * @param agentId
   *          the capture agent
   * @return the recording
   * @throws NotFoundException
   *           if the there is no current recording
   * @throws UnauthorizedException
   *           if the event cannot be read due to a lack of access rights
   * @throws SchedulerException
   *           if accessing the scheduling database fails
   */
  public MediaPackage getCurrentRecording(String agentId)
          throws NotFoundException, UnauthorizedException, SchedulerException {
    Opt current = schedulerService.getCurrentRecording(agentId);
    if (current.isNone()) {
      logger.warn("Unable to load the current recording for agent '{}': no recording found", agentId);
      throw new NotFoundException("No current recording found for agent '" + agentId + "'");
    }
    return current.get();
  }

  /**
   * Extends the current recording.
   *
   * @param event
   *          the recording's media package
   * @param dublinCore
   *          the recording's dublin core catalog
   * @param agentId
   *          the agent
   * @throws UnauthorizedException
   *           if the event cannot be updated due to a lack of access rights
   * @throws NotFoundException
   *           if the event cannot be found
   * @throws SchedulerException
   *           if updating the scheduling data fails
   * @throws IOException
   *           if updating the calendar to the worksapce fails
   * @throws IllegalArgumentException
   *           if a URI cannot be created using the arguments provided
   */
  public void prolongEvent(MediaPackage event, DublinCoreCatalog dublinCore, String agentId)
          throws UnauthorizedException, NotFoundException, SchedulerException, IllegalArgumentException, IOException {
    String eventId = event.getIdentifier().toString();

    DCMIPeriod period = EncodingSchemeUtils.decodeMandatoryPeriod(dublinCore.getFirst(DublinCore.PROPERTY_TEMPORAL));

    Date prolongedEndDate = new DateTime(period.getEnd()).plus(getProlongingTime()).toDate();

    dublinCore.set(PROPERTY_TEMPORAL,
            EncodingSchemeUtils.encodePeriod(new DCMIPeriod(period.getStart(), prolongedEndDate), Precision.Second));

    List events = schedulerService.findConflictingEvents(agentId, period.getStart(), prolongedEndDate);
    for (MediaPackage conflictMediaPackage : events) {
      if (eventId.equals(conflictMediaPackage.getIdentifier().toString()))
        continue;

      Optional conflictingDc = DublinCoreUtil.loadEpisodeDublinCore(workspace, conflictMediaPackage);
      if (conflictingDc.isEmpty()) {
        continue;
      }

      Date conflictingStartDate = EncodingSchemeUtils
              .decodeMandatoryPeriod(conflictingDc.get().getFirst(DublinCore.PROPERTY_TEMPORAL)).getStart();

      prolongedEndDate = new DateTime(conflictingStartDate).minusMinutes(1).toDate();

      dublinCore.set(PROPERTY_TEMPORAL,
              EncodingSchemeUtils.encodePeriod(new DCMIPeriod(period.getStart(), prolongedEndDate), Precision.Second));

      logger.info(
              "A scheduled event is preventing the current recording on agent '{}' to be further extended. Extending to one minute before the conflicting event",
              agentId);
      stop(agentId);
      break;
    }

    // Update the episode dublin core
    Catalog[] episodeCatalogs = event.getCatalogs(MediaPackageElements.EPISODE);
    if (episodeCatalogs.length > 0) {
      Catalog c = episodeCatalogs[0];
      String filename = FilenameUtils.getName(c.getURI().toString());
      URI uri = workspace.put(event.getIdentifier().toString(), c.getIdentifier(), filename,
              IOUtils.toInputStream(dublinCore.toXmlString(), "UTF-8"));
      c.setURI(uri);
      // setting the URI to a new source so the checksum will most like be invalid
      c.setChecksum(null);
    }

    schedulerService.updateEvent(eventId, Opt. none(), Opt.some(prolongedEndDate), Opt. none(),
            Opt.> none(), Opt.some(event), Opt.> none(),
            Opt.> none());
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy