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

org.opencastproject.external.util.SchedulingUtils 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.external.util;

import static com.entwinemedia.fn.data.json.Jsons.arr;
import static com.entwinemedia.fn.data.json.Jsons.f;
import static com.entwinemedia.fn.data.json.Jsons.obj;
import static com.entwinemedia.fn.data.json.Jsons.v;
import static java.time.ZoneOffset.UTC;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import org.opencastproject.capture.CaptureParameters;
import org.opencastproject.capture.admin.api.Agent;
import org.opencastproject.capture.admin.api.CaptureAgentStateService;
import org.opencastproject.elasticsearch.api.SearchIndexException;
import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
import org.opencastproject.elasticsearch.index.objects.event.Event;
import org.opencastproject.index.service.api.IndexService;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.scheduler.api.SchedulerException;
import org.opencastproject.scheduler.api.SchedulerService;
import org.opencastproject.scheduler.api.TechnicalMetadata;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.util.NotFoundException;

import com.entwinemedia.fn.data.Opt;
import com.entwinemedia.fn.data.json.Field;
import com.entwinemedia.fn.data.json.JObject;
import com.entwinemedia.fn.data.json.JValue;

import net.fortuna.ical4j.model.property.RRule;

import org.apache.commons.lang3.StringUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.TimeZone;

public final class SchedulingUtils {

  /** The logging facility */
  private static final Logger logger = LoggerFactory.getLogger(SchedulingUtils.class);

  private static final String JSON_KEY_AGENT_ID = "agent_id";
  private static final String JSON_KEY_START_DATE = "start";
  private static final String JSON_KEY_END_DATE = "end";
  private static final String JSON_KEY_DURATION = "duration";
  private static final String JSON_KEY_INPUTS = "inputs";
  private static final String JSON_KEY_RRULE = "rrule";


  private SchedulingUtils() {
  }

  public static class SchedulingInfo {
    private Opt startDate = Opt.none();
    private Opt endDate = Opt.none();
    private Opt duration = Opt.none();
    private Opt agentId = Opt.none();
    private Opt inputs = Opt.none();
    private Opt rrule = Opt.none();

    public SchedulingInfo() {
    }

    /**
     * Copy the given SchedulingInfo object.
     *
     * @param other
     *          The SchedulingInfo object to copy.
     */
    public SchedulingInfo(SchedulingInfo other) {
      this.startDate = other.startDate;
      this.endDate = other.endDate;
      this.duration = other.duration;
      this.agentId = other.agentId;
      this.inputs = other.inputs;
      this.rrule = other.rrule;
    }

    public Opt getStartDate() {
      return startDate;
    }

    public void setStartDate(Opt startDate) {
      this.startDate = startDate;
    }

    public Opt getEndDate() {
      if (endDate.isSome()) {
        return endDate;
      } else if (startDate.isSome() && duration.isSome()) {
        return Opt.some(Date.from(startDate.get().toInstant().plusMillis(duration.get())));
      } else {
        return Opt.none();
      }
    }

    public void setEndDate(Opt endDate) {
      this.endDate = endDate;
    }

    public Opt getDuration() {
      if (duration.isSome()) {
        return duration;
      } else if (startDate.isSome() && endDate.isSome()) {
        return Opt.some(endDate.get().getTime() - startDate.get().getTime());
      } else {
        return Opt.none();
      }
    }

    public void setDuration(Opt duration) {
      this.duration = duration;
    }

    public Opt getAgentId() {
      return agentId;
    }

    public void setAgentId(Opt agentId) {
      this.agentId = agentId;
    }

    public Opt getInputs() {
      return inputs;
    }

    public void setInputs(Opt inputs) {
      this.inputs = inputs;
    }

    public Opt getRrule() {
      return rrule;
    }

    public void setRrule(Opt rrule) {
      this.rrule = rrule;
    }

    /**
     * @return A JSON representation of this ScheudlingInfo object.
     */
    public JObject toJson() {
      final List fields = new ArrayList<>();
      final DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_DATE_TIME;
      if (startDate.isSome()) {
        fields.add(f(JSON_KEY_START_DATE, dateFormatter.format(startDate.get().toInstant().atZone(UTC))));
      }
      if (endDate.isSome()) {
        fields.add(f(JSON_KEY_END_DATE, dateFormatter.format(endDate.get().toInstant().atZone(UTC))));
      }
      if (agentId.isSome()) {
        fields.add(f(JSON_KEY_AGENT_ID, agentId.get()));
      }
      if (inputs.isSome()) {
        fields.add(f(JSON_KEY_INPUTS, arr(inputs.get().split(","))));
      }
      return obj(fields);
    }

    /**
     * @return A JSON source representation of this SchedulingInfo as needed by the IndexService to create an event.
     */
    @SuppressWarnings("unchecked")
    public JSONObject toSource() {
      final JSONObject source = new JSONObject();
      if (rrule.isSome()) {
        source.put("type", "SCHEDULE_MULTIPLE");
      } else {
        source.put("type", "SCHEDULE_SINGLE");
      }
      final JSONObject sourceMetadata = new JSONObject();
      final DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_DATE_TIME;
      if (startDate.isSome()) {
        sourceMetadata.put("start", dateFormatter.format(startDate.get().toInstant().atZone(UTC)));
      }
      if (endDate.isSome()) {
        sourceMetadata.put("end", dateFormatter.format(endDate.get().toInstant().atZone(UTC)));
      }
      if (agentId.isSome()) {
        sourceMetadata.put("device", agentId.get());
      }
      if (getDuration().isSome()) {
        sourceMetadata.put("duration", String.valueOf(getDuration().get()));
      }
      if (rrule.isSome()) {
        sourceMetadata.put("rrule", rrule.get().getValue());
      }
      sourceMetadata.put("inputs", inputs.getOr(""));

      source.put("metadata", sourceMetadata);
      return source;
    }

    /**
     * Creates a new SchedulingInfo of this instance which uses start date, end date, and agent id form the given
     * {@link TechnicalMetadata} if they are not present in this instance.
     *
     * @param metadata
     *          The {@link TechnicalMetadata} of which to use start date, end date, and agent id in case they are missing.
     *
     * @return The new SchedulingInfo with start date, end date, and agent id set.
     */
    public SchedulingInfo merge(TechnicalMetadata metadata) {
      SchedulingInfo result = new SchedulingInfo(this);
      if (result.startDate.isNone()) {
        result.startDate = Opt.some(metadata.getStartDate());
      }
      if (result.endDate.isNone()) {
        result.endDate = Opt.some(metadata.getEndDate());
      }
      if (result.agentId.isNone()) {
        result.agentId = Opt.some(metadata.getAgentId());
      }
      return result;
    }

    /**
     * Parse the given json and create a new SchedulingInfo.
     *
     * @param json
     *          The JSONObject to parse.
     *
     * @return The SchedulingInfo instance represented by the given JSON.
     */
    public static SchedulingInfo of(JSONObject json) {
      final DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_DATE_TIME;
      final SchedulingInfo schedulingInfo = new SchedulingInfo();
      final String startDate = (String) json.get(JSON_KEY_START_DATE);
      final String endDate = (String) json.get(JSON_KEY_END_DATE);
      final String agentId = (String) json.get(JSON_KEY_AGENT_ID);
      final JSONArray inputs = (JSONArray) json.get(JSON_KEY_INPUTS);
      final String rrule = (String) json.get(JSON_KEY_RRULE);

      // Special handling because the original implementation required String but now we require long
      final String durationString = Objects.toString(json.get(JSON_KEY_DURATION), null);

      if (isNotBlank(startDate)) {
        schedulingInfo.startDate = Opt.some(Date.from(Instant.from(dateFormatter.parse(startDate))));
      }
      if (isNotBlank(endDate)) {
        schedulingInfo.endDate = Opt.some(Date.from(Instant.from(dateFormatter.parse(endDate))));
      }
      if (isNotBlank(agentId)) {
        schedulingInfo.agentId = Opt.some(agentId);
      }
      if (isNotBlank(durationString)) {
        try {
          schedulingInfo.duration = Opt.some(Long.parseLong(durationString));
        } catch (Exception e) {
          throw new IllegalArgumentException("Invalid format of field 'duration'");
        }
      }

      if (isBlank(endDate) && isBlank(durationString)) {
        throw new IllegalArgumentException("Either 'end' or 'duration' must be specified");
      }

      if (inputs != null) {
        schedulingInfo.inputs = Opt.some(String.join(",", inputs));
      }
      if (isNotBlank(rrule)) {
        try {
          RRule parsedRrule = new RRule(rrule);
          parsedRrule.validate();
          schedulingInfo.rrule = Opt.some(parsedRrule);
        } catch (Exception e) {
          throw new IllegalArgumentException("Invalid RRule: " + rrule);
        }
        if (isBlank(durationString) || isBlank(startDate) || isBlank(endDate)) {
          throw new IllegalArgumentException("'start', 'end' and 'duration' must be specified when 'rrule' is specified");
        }
      }
      return schedulingInfo;
    }

    /**
     * Get the SchedulingInfo for the given event id.
     *
     * @param eventId
     *          The id of the event to get the SchedulingInfo for.
     * @param schedulerService
     *          The {@link SchedulerService} to query for the event id.
     *
     * @return The SchedulingInfo for the given event id.
     *
     * @throws UnauthorizedException
     *          If the {@link SchedulerService} cannot be queried due to missing authorization.
     * @throws SchedulerException
     *          In case internal errors occur within the {@link SchedulerService}.
     */
    public static SchedulingInfo of(String eventId, SchedulerService schedulerService)
        throws UnauthorizedException, SchedulerException {
      final SchedulingInfo result = new SchedulingInfo();
      try {
        final TechnicalMetadata technicalMetadata = schedulerService.getTechnicalMetadata(eventId);
        result.startDate = Opt.some(technicalMetadata.getStartDate());
        result.endDate = Opt.some(technicalMetadata.getEndDate());
        result.agentId = Opt.some(technicalMetadata.getAgentId());
        String inputs = technicalMetadata.getCaptureAgentConfiguration().get(CaptureParameters.CAPTURE_DEVICE_NAMES);
        if (isNotBlank(inputs)) {
          result.inputs = Opt.some(inputs);
        }
        return result;
      } catch (NotFoundException e) {
        return result;
      }
    }
  }

  /**
   * Convert the given list of {@link MediaPackage} elements to a JSON used to tell which events are causing conflicts.
   *
   * @param checkedEventId
   *          The id of the event which was checked for conflicts. May be empty if an rrule was checked.
   * @param mediaPackages
   *          The conflicting {@link MediaPackage}s.
   * @param indexService
   *          The {@link IndexService} for getting the corresponding events for the conflicting {@link MediaPackage}s.
   * @param elasticsearchIndex
   *          The index to use for getting the corresponding events for the conflicting MediaPackages.
   *
   * @return A List of conflicting events, represented as JSON objects.
   *
   * @throws SearchIndexException
   *          If an event cannot be found.
   */
  public static List convertConflictingEvents(
      Optional checkedEventId,
      List mediaPackages,
      IndexService indexService,
      ElasticsearchIndex elasticsearchIndex
  ) throws SearchIndexException {
    final List result = new ArrayList<>();
    for (MediaPackage mediaPackage : mediaPackages) {
      final Opt eventOpt = indexService.getEvent(mediaPackage.getIdentifier().toString(), elasticsearchIndex);
      if (eventOpt.isSome()) {
        final Event event = eventOpt.get();
        if (checkedEventId.isPresent() && checkedEventId.equals(event.getIdentifier())) {
          continue;
        }
        result.add(obj(f("start", v(event.getTechnicalStartTime())), f("end", v(event.getTechnicalEndTime())),
            f("title", v(event.getTitle()))));
      } else {
        logger.warn("Index out of sync! Conflicting event catalog {} not found on event index!",
            mediaPackage.getIdentifier().toString());
      }
    }
    return result;
  }

  /**
   * Get the conflicting events for the given SchedulingInfo.
   *
   * @param schedulingInfo
   *          The SchedulingInfo to check for conflicts.
   * @param agentStateService
   *          The {@link CaptureAgentStateService} to use for retrieving capture agents.
   * @param schedulerService
   *          The {@link SchedulerService} to use for conflict checking.
   * @return
   *          A list of {@link MediaPackage} elements which cause conflicts with the given SchedulingInfo.
   *
   * @throws NotFoundException
   *          If the capture agent cannot be found.
   * @throws UnauthorizedException
   *          If the {@link SchedulerService} cannot be queried due to missing authorization.
   * @throws SchedulerException
   *          In case internal errors occur within the {@link SchedulerService}.
   */
  public static List getConflictingEvents(
      SchedulingInfo schedulingInfo,
      CaptureAgentStateService agentStateService,
      SchedulerService schedulerService
  ) throws NotFoundException, UnauthorizedException, SchedulerException {

    if (schedulingInfo.getRrule().isSome()) {
      final Agent agent = agentStateService.getAgent(schedulingInfo.getAgentId().get());
      String timezone = agent.getConfiguration().getProperty("capture.device.timezone");
      if (StringUtils.isBlank(timezone)) {
        timezone = TimeZone.getDefault().getID();
        logger.warn("No 'capture.device.timezone' set on agent {}. The default server timezone {} will be used.",
            schedulingInfo.getAgentId().get(), timezone);
      }
      return schedulerService.findConflictingEvents(
          schedulingInfo.getAgentId().get(),
          schedulingInfo.getRrule().get(),
          schedulingInfo.getStartDate().get(),
          schedulingInfo.getEndDate().get(),
          schedulingInfo.getDuration().get(),
          TimeZone.getTimeZone(timezone)
      );
    }

    return schedulerService.findConflictingEvents(
        schedulingInfo.getAgentId().get(),
        schedulingInfo.getStartDate().get(),
        schedulingInfo.getEndDate().get()
    );
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy