/**
* 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.EncodingSchemeUtils;
import org.opencastproject.metadata.dublincore.Precision;
import org.opencastproject.security.api.AccessControlList;
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.util.NotFoundException;
import org.opencastproject.util.data.Effect0;
import org.opencastproject.util.data.Function0;
import org.opencastproject.util.data.FunctionException;
import org.opencastproject.util.data.Option;
import org.opencastproject.util.data.Tuple;
import com.entwinemedia.fn.data.Opt;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.text.WordUtils;
import org.osgi.framework.ServiceException;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
/**
* Implements {@link SeriesService}. Uses {@link SeriesServiceDatabase} for permanent storage and
* {@link SeriesServiceIndex} for searching.
*/
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. */
public void setIndex(SeriesServiceIndex index) {
this.index = index;
}
/** OSGi callback for setting persistance. */
public void setPersistence(SeriesServiceDatabase persistence) {
this.persistence = persistence;
}
/** OSGi callback for setting the security service. */
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
/** OSGi callback for setting the organization directory service. */
public void setOrgDirectory(OrganizationDirectoryService orgDirectory) {
this.orgDirectory = orgDirectory;
}
/** OSGi callback for setting the message sender. */
public void setMessageSender(MessageSender messageSender) {
this.messageSender = messageSender;
}
/** OSGi callback for setting the message 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.
*/
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();
}
/** If the solr index is empty, but there are series in the database, populate the solr index. */
private void populateSolr(String systemUserName) {
long instancesInSolr = 0L;
try {
instancesInSolr = index.count();
} catch (Exception e) {
throw new IllegalStateException(e);
}
if (instancesInSolr == 0L) {
try {
Iterator<Tuple<DublinCoreCatalog, String>> databaseSeries = persistence.getAllSeries();
if (databaseSeries.hasNext()) {
logger.info("The series index is empty. Populating it now with series");
while (databaseSeries.hasNext()) {
Tuple<DublinCoreCatalog, String> series = databaseSeries.next();
// Run as the superuser so we get all series, regardless of organization or role
Organization organization = orgDirectory.getOrganization(series.getB());
securityService.setOrganization(organization);
securityService.setUser(SecurityUtil.createSystemUser(systemUserName, organization));
index.updateIndex(series.getA());
String id = series.getA().getFirst(DublinCore.PROPERTY_IDENTIFIER);
AccessControlList acl = persistence.getAccessControlList(id);
if (acl != null)
index.updateSecurityPolicy(id, acl);
index.updateOptOutStatus(id, persistence.isOptOut(id));
}
logger.info("Finished populating series search index");
}
} catch (Exception e) {
logger.warn("Unable to index series instances: {}", e);
throw new ServiceException(e.getMessage());
} finally {
securityService.setOrganization(null);
securityService.setUser(null);
}
}
}
@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
}
try {
index.updateOptOutStatus(id, persistence.isOptOut(id));
} 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 rethrow(e);
}
}
private static Error rethrow(Exception e) throws SeriesException, UnauthorizedException {
if (e instanceof FunctionException) {
final Throwable cause = e.getCause();
if (cause instanceof UnauthorizedException) {
throw ((UnauthorizedException) cause);
} else {
throw new SeriesException(e);
}
} else {
throw new SeriesException(e);
}
}
/** Check if <code>dc</code> is new and, if so, return an updated version ready to store. */
private Option<DublinCoreCatalog> isNew(DublinCoreCatalog dc) throws SeriesServiceDatabaseException {
final String id = dc.getFirst(DublinCore.PROPERTY_IDENTIFIER);
if (id != null) {
try {
return equals(persistence.getSeries(id), dc) ? Option.<DublinCoreCatalog> 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);
}
}
// todo method signature does not fit the three different possible return values
@Override
public boolean updateAccessControl(final String seriesId, final AccessControlList accessControl)
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)) {
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));
} 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 <code>acl</code> 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 {
this.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 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 boolean isOptOut(String seriesId) throws NotFoundException, SeriesException {
try {
return index.isOptOut(seriesId);
} catch (SeriesServiceDatabaseException e) {
logger.error("Exception occured while getting opt out status of series '{}': {}", seriesId,
ExceptionUtils.getStackTrace(e));
throw new SeriesException(e);
}
}
@Override
public void updateOptOutStatus(String seriesId, boolean optOut) throws NotFoundException, SeriesException {
try {
persistence.updateOptOutStatus(seriesId, optOut);
index.updateOptOutStatus(seriesId, optOut);
messageSender.sendObjectMessage(SeriesItem.SERIES_QUEUE, MessageSender.DestinationType.Queue,
SeriesItem.updateOptOut(seriesId, optOut));
} catch (SeriesServiceDatabaseException e) {
logger.error("Failed to update opt out status of series with id '{}': {}", seriesId,
ExceptionUtils.getStackTrace(e));
throw new SeriesException(e);
}
}
@Override
public Map<String, String> 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,
ExceptionUtils.getStackTrace(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 '{}': {}",
new Object[] { seriesID, propertyName, ExceptionUtils.getStackTrace(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 '{}': {}",
new Object[] { seriesID, propertyName, propertyValue, ExceptionUtils.getStackTrace(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 '{}': {}",
new Object[] { seriesID, propertyName, ExceptionUtils.getStackTrace(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.
* <p>
* Note: As long as http://opencast.jira.com/browse/MH-8759 is not fixed, the encoding scheme of values is not
* considered.
* <p>
* 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<EName, List<DublinCoreValue>> av = a.getValues();
final Map<EName, List<DublinCoreValue>> bv = b.getValues();
if (av.size() == bv.size()) {
for (Map.Entry<EName, List<DublinCoreValue>> 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<Map<String, byte[]>> getSeriesElements(String seriesId) throws SeriesException {
try {
return persistence.getSeriesElements(seriesId);
} catch (SeriesServiceDatabaseException e) {
throw new SeriesException(e);
}
}
@Override
public Opt<byte[]> 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)) {
return persistence.storeSeriesElement(seriesID, type, data);
} 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 + WordUtils.capitalize(indexName);
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);
Iterator<Tuple<DublinCoreCatalog, String>> databaseSeries = persistence.getAllSeries();
final int[] current = new int[1];
current[0] = 1;
while (databaseSeries.hasNext()) {
final Tuple<DublinCoreCatalog, String> series = databaseSeries.next();
Organization organization = orgDirectory.getOrganization(series.getB());
SecurityUtil.runAs(securityService, organization, SecurityUtil.createSystemUser(systemUserName, organization),
new Function0.X<Void>() {
@Override
public Void xapply() throws Exception {
String id = series.getA().getFirst(DublinCore.PROPERTY_IDENTIFIER);
logger.trace("Adding series '{}' for org '{}'", id, series.getB());
messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue,
SeriesItem.updateCatalog(series.getA()));
AccessControlList acl = persistence.getAccessControlList(id);
if (acl != null) {
messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue,
SeriesItem.updateAcl(id, acl));
}
messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue,
SeriesItem.updateOptOut(id, persistence.isOptOut(id)));
for (Entry<String, String> property : persistence.getSeriesProperties(id).entrySet()) {
messageSender.sendObjectMessage(destinationId, MessageSender.DestinationType.Queue,
SeriesItem.updateProperty(id, property.getKey(), property.getValue()));
}
if (((current[0] % responseInterval) == 0) || (current[0] == total)) {
messageSender.sendObjectMessage(IndexProducer.RESPONSE_QUEUE, MessageSender.DestinationType.Queue,
IndexRecreateObject
.update(indexName, IndexRecreateObject.Service.Series, total, current[0]));
}
current[0] += 1;
return null;
}
});
}
logger.info("Finished populating '{}' index with series.", indexName);
} catch (Exception e) {
logger.warn("Unable to index series instances: {}", e);
throw new ServiceException(e.getMessage());
}
Organization organization = new DefaultOrganization();
SecurityUtil.runAs(securityService, organization, SecurityUtil.createSystemUser(systemUserName, organization),
new Effect0() {
@Override
protected void run() {
messageSender.sendObjectMessage(destinationId, 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;
}
}