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

org.opencastproject.series.impl.SeriesServiceImpl 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.series.impl;

import static org.opencastproject.util.EqualsUtil.bothNotNull;
import static org.opencastproject.util.EqualsUtil.eqListSorted;
import static org.opencastproject.util.EqualsUtil.eqListUnsorted;
import static org.opencastproject.util.RequireUtil.notNull;
import static org.opencastproject.util.data.Option.some;

import org.opencastproject.index.IndexProducer;
import org.opencastproject.mediapackage.EName;
import org.opencastproject.message.broker.api.MessageReceiver;
import org.opencastproject.message.broker.api.MessageSender;
import org.opencastproject.message.broker.api.index.AbstractIndexProducer;
import org.opencastproject.message.broker.api.index.IndexRecreateObject;
import org.opencastproject.message.broker.api.index.IndexRecreateObject.Service;
import org.opencastproject.message.broker.api.series.SeriesItem;
import org.opencastproject.metadata.dublincore.DublinCore;
import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
import org.opencastproject.metadata.dublincore.DublinCoreCatalogList;
import org.opencastproject.metadata.dublincore.DublinCoreValue;
import org.opencastproject.metadata.dublincore.DublinCoreXmlFormat;
import org.opencastproject.metadata.dublincore.EncodingSchemeUtils;
import org.opencastproject.metadata.dublincore.Precision;
import org.opencastproject.security.api.AccessControlList;
import org.opencastproject.security.api.AccessControlParser;
import org.opencastproject.security.api.DefaultOrganization;
import org.opencastproject.security.api.Organization;
import org.opencastproject.security.api.OrganizationDirectoryService;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.security.util.SecurityUtil;
import org.opencastproject.series.api.SeriesException;
import org.opencastproject.series.api.SeriesQuery;
import org.opencastproject.series.api.SeriesService;
import org.opencastproject.series.impl.persistence.SeriesEntity;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.data.Option;

import com.entwinemedia.fn.data.Opt;

import org.apache.commons.lang3.StringUtils;
import org.osgi.framework.ServiceException;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;

import javax.xml.parsers.ParserConfigurationException;

/**
 * Implements {@link SeriesService}. Uses {@link SeriesServiceDatabase} for permanent storage and
 * {@link SeriesServiceIndex} for searching.
 */
@Component(
  property = {
    "service.description=Series Service"
  },
  immediate = true,
  service = { SeriesService.class }
)
public class SeriesServiceImpl extends AbstractIndexProducer implements SeriesService {

  /** Logging utility */
  private static final Logger logger = LoggerFactory.getLogger(SeriesServiceImpl.class);

  /** Index for searching */
  protected SeriesServiceIndex index;

  /** Persistent storage */
  protected SeriesServiceDatabase persistence;

  /** The security service */
  protected SecurityService securityService;

  /** The organization directory */
  protected OrganizationDirectoryService orgDirectory;

  /** The message broker service sender */
  protected MessageSender messageSender;

  /** The message broker service receiver */
  protected MessageReceiver messageReceiver;

  /** The system user name */
  private String systemUserName;

  /** OSGi callback for setting index. */
  @Reference(name = "series-index")
  public void setIndex(SeriesServiceIndex index) {
    this.index = index;
  }

  /** OSGi callback for setting persistance. */
  @Reference(name = "series-persistence")
  public void setPersistence(SeriesServiceDatabase persistence) {
    this.persistence = persistence;
  }

  /** OSGi callback for setting the security service. */
  @Reference(name = "security-service")
  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  /** OSGi callback for setting the organization directory service. */
  @Reference(name = "orgDirectory")
  public void setOrgDirectory(OrganizationDirectoryService orgDirectory) {
    this.orgDirectory = orgDirectory;
  }

  /** OSGi callback for setting the message sender. */
  @Reference(name = "message-broker-sender")
  public void setMessageSender(MessageSender messageSender) {
    this.messageSender = messageSender;
  }

  /** OSGi callback for setting the message receiver. */
  @Reference(name = "message-broker-receiver")
  public void setMessageReceiver(MessageReceiver messageReceiver) {
    this.messageReceiver = messageReceiver;
  }

  /**
   * Activates Series Service. Checks whether we are using synchronous or asynchronous indexing. If asynchronous is
   * used, Executor service is set. If index is empty, persistent storage is queried if it contains any series. If that
   * is the case, series are retrieved and indexed.
   */
  @Activate
  public void activate(ComponentContext cc) throws Exception {
    logger.info("Activating Series Service");
    systemUserName = cc.getBundleContext().getProperty(SecurityUtil.PROPERTY_KEY_SYS_USER);
    populateSolr(systemUserName);
    super.activate();
  }

  @Deactivate
  public void deactivate() {
    super.deactivate();
  }

  /** If the solr index is empty, but there are series in the database, populate the solr index. */
  private void populateSolr(String systemUserName) {
    long instancesInSolr;
    try {
      instancesInSolr = index.count();
    } catch (Exception e) {
      throw new IllegalStateException("Repopulating series Solr index failed", e);
    }
    if (instancesInSolr != 0L) {
      return;
    }

    logger.info("The series index is empty. Populating it now with series");
    List allSeries = null;
    try {
      allSeries = persistence.getAllSeries();
    } catch (SeriesServiceDatabaseException ex) {
      throw new ServiceException("Unable to get all series from the database", ex);
    }
    final int total = allSeries.size();
    if (total == 0) {
      logger.info("No series found. Repopulating index finished.");
      return;
    }

    int current = 0;
    for (SeriesEntity series: allSeries) {
      current++;
      try {
        // Run as the superuser so we get all series, regardless of organization or role
        Organization organization = orgDirectory.getOrganization(series.getOrganization());
        securityService.setOrganization(organization);
        securityService.setUser(SecurityUtil.createSystemUser(systemUserName, organization));

        index.updateIndex(DublinCoreXmlFormat.read(series.getDublinCoreXML()));
        String aclStr = series.getAccessControl();
        if (StringUtils.isNotBlank(aclStr)) {
          AccessControlList acl = AccessControlParser.parseAcl(aclStr);
          index.updateSecurityPolicy(series.getSeriesId(), acl);
        }
      } catch (Exception ex) {
        logger.error("Unable to repopulate index for series {}", series.getSeriesId(), ex);
      } finally {
        securityService.setOrganization(null);
        securityService.setUser(null);
      }
      // log progress
      if (current % 100 == 0) {
        logger.info("Indexing series {}/{} ({} percent done)", current, total, current * 100 / total);
      }
    }
    logger.info("Finished populating series search index");
  }

  @Override
  public DublinCoreCatalog updateSeries(DublinCoreCatalog dc) throws SeriesException, UnauthorizedException {
    try {
      for (DublinCoreCatalog dublinCore : isNew(notNull(dc, "dc"))) {
        final String id = dublinCore.getFirst(DublinCore.PROPERTY_IDENTIFIER);

        if (!dublinCore.hasValue(DublinCore.PROPERTY_CREATED)) {
          DublinCoreValue date = EncodingSchemeUtils.encodeDate(new Date(), Precision.Minute);
          dublinCore.set(DublinCore.PROPERTY_CREATED, date);
          logger.debug("Setting series creation date to '{}'", date.getValue());
        }

        if (dublinCore.hasValue(DublinCore.PROPERTY_TITLE)) {
          if (dublinCore.getFirst(DublinCore.PROPERTY_TITLE).length() > 255) {
            dublinCore.set(DublinCore.PROPERTY_TITLE, dublinCore.getFirst(DublinCore.PROPERTY_TITLE).substring(0, 255));
            logger.warn("Title was longer than 255 characters. Cutting excess off.");
          }
        }

        logger.debug("Updating series {}", id);
        index.updateIndex(dublinCore);
        try {
          final AccessControlList acl = persistence.getAccessControlList(id);
          if (acl != null) {
            index.updateSecurityPolicy(id, acl);
          }
        } catch (NotFoundException ignore) {
          // Ignore not found since this is the first indexing
        }
        // Make sure store to persistence comes after index, return value can be null
        DublinCoreCatalog updated = persistence.storeSeries(dublinCore);
        messageSender.sendObjectMessage(SeriesItem.SERIES_QUEUE, MessageSender.DestinationType.Queue,
                SeriesItem.updateCatalog(dublinCore));
        return (updated == null) ? null : dublinCore;
      }
      return dc;
    } catch (Exception e) {
      throw new SeriesException(e);
    }
  }

  /** Check if dc is new and, if so, return an updated version ready to store. */
  private Option isNew(DublinCoreCatalog dc) throws SeriesServiceDatabaseException {
    final String id = dc.getFirst(DublinCore.PROPERTY_IDENTIFIER);
    if (id != null) {
      try {
        return equals(persistence.getSeries(id), dc) ? Option. none() : some(dc);
      } catch (NotFoundException e) {
        return some(dc);
      }
    } else {
      logger.info("Series Dublin Core does not contain identifier, generating one");
      dc.set(DublinCore.PROPERTY_IDENTIFIER, UUID.randomUUID().toString());
      return some(dc);
    }
  }

  @Override
  public boolean updateAccessControl(final String seriesId, final AccessControlList accessControl)
          throws NotFoundException, SeriesException {
    return updateAccessControl(seriesId, accessControl, false);
  }

  // todo method signature does not fit the three different possible return values
  @Override
  public boolean updateAccessControl(final String seriesId, final AccessControlList accessControl,
          boolean overrideEpisodeAcl)
          throws NotFoundException, SeriesException {
    if (StringUtils.isEmpty(seriesId)) {
      throw new IllegalArgumentException("Series ID parameter must not be null or empty.");
    }
    if (accessControl == null) {
      throw new IllegalArgumentException("ACL parameter must not be null");
    }
    if (needsUpdate(seriesId, accessControl) || overrideEpisodeAcl) {
      logger.debug("Updating ACL of series {}", seriesId);
      boolean updated;
      // not found is thrown if it doesn't exist
      try {
        index.updateSecurityPolicy(seriesId, accessControl);
      } catch (SeriesServiceDatabaseException e) {
        logger.error("Could not update series {} with access control rules: {}", seriesId, e.getMessage());
        throw new SeriesException(e);
      }

      try {
        updated = persistence.storeSeriesAccessControl(seriesId, accessControl);
        messageSender.sendObjectMessage(SeriesItem.SERIES_QUEUE, MessageSender.DestinationType.Queue,
                SeriesItem.updateAcl(seriesId, accessControl, overrideEpisodeAcl));
      } catch (SeriesServiceDatabaseException e) {
        logger.error("Could not update series {} with access control rules: {}", seriesId, e.getMessage());
        throw new SeriesException(e);
      }
      return updated;
    } else {
      // todo not the right return code
      return true;
    }
  }

  /** Check if acl needs to be updated for the given series. */
  private boolean needsUpdate(String seriesId, AccessControlList acl) throws SeriesException {
    try {
      return !equals(persistence.getAccessControlList(seriesId), acl);
    } catch (NotFoundException e) {
      return true;
    } catch (SeriesServiceDatabaseException e) {
      throw new SeriesException(e);
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see org.opencastproject.series.api.SeriesService#deleteSeries(java.lang.String)
   */
  @Override
  public void deleteSeries(final String seriesID) throws SeriesException, NotFoundException {
    try {
      persistence.deleteSeries(seriesID);
      messageSender.sendObjectMessage(SeriesItem.SERIES_QUEUE, MessageSender.DestinationType.Queue,
              SeriesItem.delete(seriesID));
    } catch (SeriesServiceDatabaseException e1) {
      logger.error("Could not delete series with id {} from persistence storage", seriesID);
      throw new SeriesException(e1);
    }

    try {
      index.delete(seriesID);
    } catch (SeriesServiceDatabaseException e) {
      logger.error("Unable to delete series with id {}: {}", seriesID, e.getMessage());
      throw new SeriesException(e);
    }
  }

  @Override
  public DublinCoreCatalogList getSeries(SeriesQuery query) throws SeriesException {
    try {
      return index.search(query);
    } catch (SeriesServiceDatabaseException e) {
      logger.error("Failed to execute search query: {}", e.getMessage());
      throw new SeriesException(e);
    }
  }

  @Override
  public Map getIdTitleMapOfAllSeries() throws SeriesException, UnauthorizedException {
    try {
      return index.queryIdTitleMap();
    } catch (SeriesServiceDatabaseException e) {
      logger.error("Failed to execute search query: {}", e.getMessage());
      throw new SeriesException(e);
    }
  }

  @Override
  public DublinCoreCatalog getSeries(String seriesID) throws SeriesException, NotFoundException {
    try {
      return index.getDublinCore(notNull(seriesID, "seriesID"));
    } catch (SeriesServiceDatabaseException e) {
      logger.error("Exception occured while retrieving series {}: {}", seriesID, e.getMessage());
      throw new SeriesException(e);
    }
  }

  @Override
  public AccessControlList getSeriesAccessControl(String seriesID) throws NotFoundException, SeriesException {
    try {
      return index.getAccessControl(notNull(seriesID, "seriesID"));
    } catch (SeriesServiceDatabaseException e) {
      logger.error("Exception occurred while retrieving access control rules for series {}: {}", seriesID,
              e.getMessage());
      throw new SeriesException(e);
    }
  }

  @Override
  public int getSeriesCount() throws SeriesException {
    try {
      return (int) index.count();
    } catch (SeriesServiceDatabaseException e) {
      logger.error("Exception occured while counting series.", e);
      throw new SeriesException(e);
    }
  }

  @Override
  public Map getSeriesProperties(String seriesID)
          throws SeriesException, NotFoundException, UnauthorizedException {
    try {
      return persistence.getSeriesProperties(seriesID);
    } catch (SeriesServiceDatabaseException e) {
      logger.error("Failed to get series properties for series with id '{}'", seriesID, e);
      throw new SeriesException(e);
    }
  }

  @Override
  public String getSeriesProperty(String seriesID, String propertyName)
          throws SeriesException, NotFoundException, UnauthorizedException {
    try {
      return persistence.getSeriesProperty(seriesID, propertyName);
    } catch (SeriesServiceDatabaseException e) {
      logger.error("Failed to get series property for series with series id '{}' and property name '{}'", seriesID,
              propertyName, e);
      throw new SeriesException(e);
    }
  }

  @Override
  public void updateSeriesProperty(String seriesID, String propertyName, String propertyValue)
          throws SeriesException, NotFoundException, UnauthorizedException {
    try {
      persistence.updateSeriesProperty(seriesID, propertyName, propertyValue);
      messageSender.sendObjectMessage(SeriesItem.SERIES_QUEUE, MessageSender.DestinationType.Queue,
              SeriesItem.updateProperty(seriesID, propertyName, propertyValue));
    } catch (SeriesServiceDatabaseException e) {
      logger.error(
              "Failed to get series property for series with series id '{}' and property name '{}' and value '{}'",
              seriesID, propertyName, propertyValue, e);
      throw new SeriesException(e);
    }
  }

  @Override
  public void deleteSeriesProperty(String seriesID, String propertyName)
          throws SeriesException, NotFoundException, UnauthorizedException {
    try {
      persistence.deleteSeriesProperty(seriesID, propertyName);
      messageSender.sendObjectMessage(SeriesItem.SERIES_QUEUE, MessageSender.DestinationType.Queue,
              SeriesItem.updateProperty(seriesID, propertyName, null));
    } catch (SeriesServiceDatabaseException e) {
      logger.error("Failed to delete series property for series with series id '{}' and property name '{}'",
              seriesID, propertyName, e);
      throw new SeriesException(e);
    }
  }

  /**
   * Define equality on DublinCoreCatalogs. Two DublinCores are considered equal if they have the same properties and if
   * each property has the same values in the same order.
   * 

* Note: As long as http://opencast.jira.com/browse/MH-8759 is not fixed, the encoding scheme of values is not * considered. *

* Implementation Note: DublinCores should not be compared by their string serialization since the ordering of * properties is not defined and cannot be guaranteed between serializations. */ public static boolean equals(DublinCoreCatalog a, DublinCoreCatalog b) { final Map> av = a.getValues(); final Map> bv = b.getValues(); if (av.size() == bv.size()) { for (Map.Entry> ave : av.entrySet()) { if (!eqListSorted(ave.getValue(), bv.get(ave.getKey()))) return false; } return true; } else { return false; } } /** * Define equality on AccessControlLists. Two AccessControlLists are considered equal if they contain the exact same * entries no matter in which order. */ public static boolean equals(AccessControlList a, AccessControlList b) { return bothNotNull(a, b) && eqListUnsorted(a.getEntries(), b.getEntries()); } @Override public Opt> getSeriesElements(String seriesId) throws SeriesException { try { return persistence.getSeriesElements(seriesId); } catch (SeriesServiceDatabaseException e) { throw new SeriesException(e); } } @Override public Opt getSeriesElementData(String seriesId, String type) throws SeriesException { try { return persistence.getSeriesElement(seriesId, type); } catch (SeriesServiceDatabaseException e) { throw new SeriesException(e); } } @Override public boolean addSeriesElement(String seriesID, String type, byte[] data) throws SeriesException { try { if (persistence.existsSeriesElement(seriesID, type)) { return false; } else { return persistence.storeSeriesElement(seriesID, type, data); } } catch (SeriesServiceDatabaseException e) { throw new SeriesException(e); } } @Override public boolean updateSeriesElement(String seriesID, String type, byte[] data) throws SeriesException { try { if (persistence.existsSeriesElement(seriesID, type) && persistence.storeSeriesElement(seriesID, type, data)) { messageSender.sendObjectMessage(SeriesItem.SERIES_QUEUE, MessageSender.DestinationType.Queue, SeriesItem.updateElement(seriesID, type, new String(data, StandardCharsets.UTF_8))); return true; } else { return false; } } catch (SeriesServiceDatabaseException e) { throw new SeriesException(e); } } @Override public boolean deleteSeriesElement(String seriesID, String type) throws SeriesException { try { if (persistence.existsSeriesElement(seriesID, type)) { return persistence.deleteSeriesElement(seriesID, type); } else { return false; } } catch (SeriesServiceDatabaseException e) { throw new SeriesException(e); } } @Override public void repopulate(final String indexName) { final String destinationId = SeriesItem.SERIES_QUEUE_PREFIX + indexName.substring(0, 1).toUpperCase() + indexName.substring(1); try { final int total = persistence.countSeries(); logger.info("Re-populating '{}' index with series. There are {} series to add to the index.", indexName, total); final int responseInterval = (total < 100) ? 1 : (total / 100); List databaseSeries = persistence.getAllSeries(); int current = 1; for (SeriesEntity series: databaseSeries) { Organization organization = orgDirectory.getOrganization(series.getOrganization()); SecurityUtil.runAs(securityService, organization, SecurityUtil.createSystemUser(systemUserName, organization), () -> { String id = series.getSeriesId(); logger.trace("Adding series '{}' for org '{}'", id, series.getOrganization()); DublinCoreCatalog catalog; try { catalog = DublinCoreXmlFormat.read(series.getDublinCoreXML()); } catch (IOException | ParserConfigurationException | SAXException e) { logger.error("Could not read dublincore XML", e); return; } messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue, SeriesItem.updateCatalog(catalog)); String aclStr = series.getAccessControl(); if (StringUtils.isNotBlank(aclStr)) { try { AccessControlList acl = AccessControlParser.parseAcl(aclStr); messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue, SeriesItem.updateAcl(id, acl, false)); } catch (Exception ex) { logger.error("Unable to parse series {} access control list", id, ex); } } try { for (Entry property : persistence.getSeriesProperties(id).entrySet()) { messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue, SeriesItem.updateProperty(id, property.getKey(), property.getValue())); } } catch (NotFoundException | SeriesServiceDatabaseException e) { logger.error("Error requesting series properties", e); } }); if ((current % responseInterval == 0) || (current == total)) { logger.info("Initializing {} series index rebuild {}/{}: {} percent", indexName, current, total, current * 100 / total); } current++; } logger.info("Finished initializing '{}' index rebuild", indexName); } catch (Exception e) { logger.warn("Unable to index series instances:", e); throw new ServiceException(e.getMessage()); } Organization org = new DefaultOrganization(); SecurityUtil.runAs(securityService, org, SecurityUtil.createSystemUser(systemUserName, org), () -> { messageSender.sendObjectMessage(IndexProducer.RESPONSE_QUEUE, MessageSender.DestinationType.Queue, IndexRecreateObject.end(indexName, IndexRecreateObject.Service.Series)); }); } @Override public MessageReceiver getMessageReceiver() { return messageReceiver; } @Override public Service getService() { return Service.Series; } @Override public String getClassName() { return SeriesServiceImpl.class.getName(); } @Override public MessageSender getMessageSender() { return messageSender; } @Override public SecurityService getSecurityService() { return securityService; } @Override public String getSystemUserName() { return systemUserName; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy