/**
* 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.index.service.impl;
import static org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace;
import static org.opencastproject.assetmanager.api.AssetManager.DEFAULT_OWNER;
import static org.opencastproject.assetmanager.api.fn.Enrichments.enrich;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_IDENTIFIER;
import org.opencastproject.assetmanager.api.AssetManager;
import org.opencastproject.assetmanager.api.AssetManagerException;
import org.opencastproject.assetmanager.api.query.AQueryBuilder;
import org.opencastproject.assetmanager.api.query.AResult;
import org.opencastproject.assetmanager.api.query.Predicate;
import org.opencastproject.authorization.xacml.manager.api.AclService;
import org.opencastproject.authorization.xacml.manager.api.AclServiceFactory;
import org.opencastproject.capture.CaptureParameters;
import org.opencastproject.capture.admin.api.CaptureAgentStateService;
import org.opencastproject.event.comment.EventComment;
import org.opencastproject.event.comment.EventCommentException;
import org.opencastproject.event.comment.EventCommentParser;
import org.opencastproject.event.comment.EventCommentService;
import org.opencastproject.index.service.api.IndexService;
import org.opencastproject.index.service.catalog.adapter.DublinCoreMetadataUtil;
import org.opencastproject.index.service.catalog.adapter.MetadataList;
import org.opencastproject.index.service.catalog.adapter.MetadataUtils;
import org.opencastproject.index.service.catalog.adapter.events.CommonEventCatalogUIAdapter;
import org.opencastproject.index.service.catalog.adapter.series.CommonSeriesCatalogUIAdapter;
import org.opencastproject.index.service.exception.IndexServiceException;
import org.opencastproject.index.service.impl.index.AbstractSearchIndex;
import org.opencastproject.index.service.impl.index.event.Event;
import org.opencastproject.index.service.impl.index.event.EventHttpServletRequest;
import org.opencastproject.index.service.impl.index.event.EventSearchQuery;
import org.opencastproject.index.service.impl.index.group.Group;
import org.opencastproject.index.service.impl.index.group.GroupIndexSchema;
import org.opencastproject.index.service.impl.index.group.GroupSearchQuery;
import org.opencastproject.index.service.impl.index.series.Series;
import org.opencastproject.index.service.impl.index.series.SeriesSearchQuery;
import org.opencastproject.index.service.resources.list.query.GroupsListQuery;
import org.opencastproject.index.service.util.JSONUtils;
import org.opencastproject.index.service.util.RestUtils;
import org.opencastproject.ingest.api.IngestException;
import org.opencastproject.ingest.api.IngestService;
import org.opencastproject.matterhorn.search.SearchIndexException;
import org.opencastproject.matterhorn.search.SearchResult;
import org.opencastproject.matterhorn.search.SortCriterion;
import org.opencastproject.mediapackage.Catalog;
import org.opencastproject.mediapackage.EName;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElement.Type;
import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageElements;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.mediapackage.Track;
import org.opencastproject.mediapackage.identifier.Id;
import org.opencastproject.mediapackage.identifier.IdImpl;
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.DublinCoreUtil;
import org.opencastproject.metadata.dublincore.DublinCoreValue;
import org.opencastproject.metadata.dublincore.DublinCores;
import org.opencastproject.metadata.dublincore.EncodingSchemeUtils;
import org.opencastproject.metadata.dublincore.EventCatalogUIAdapter;
import org.opencastproject.metadata.dublincore.MetadataCollection;
import org.opencastproject.metadata.dublincore.MetadataField;
import org.opencastproject.metadata.dublincore.MetadataParsingException;
import org.opencastproject.metadata.dublincore.Precision;
import org.opencastproject.metadata.dublincore.SeriesCatalogUIAdapter;
import org.opencastproject.scheduler.api.SchedulerException;
import org.opencastproject.scheduler.api.SchedulerService;
import org.opencastproject.security.api.AccessControlList;
import org.opencastproject.security.api.AccessControlParser;
import org.opencastproject.security.api.AclScope;
import org.opencastproject.security.api.AuthorizationService;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.security.api.User;
import org.opencastproject.security.api.UserDirectoryService;
import org.opencastproject.security.util.SecurityContext;
import org.opencastproject.series.api.SeriesException;
import org.opencastproject.series.api.SeriesQuery;
import org.opencastproject.series.api.SeriesService;
import org.opencastproject.userdirectory.JpaGroupRoleProvider;
import org.opencastproject.util.DateTimeSupport;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.XmlNamespaceBinding;
import org.opencastproject.util.XmlNamespaceContext;
import org.opencastproject.util.data.Effect0;
import org.opencastproject.util.data.Tuple;
import org.opencastproject.workflow.api.WorkflowDatabaseException;
import org.opencastproject.workflow.api.WorkflowException;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowInstance.WorkflowState;
import org.opencastproject.workflow.api.WorkflowQuery;
import org.opencastproject.workflow.api.WorkflowService;
import org.opencastproject.workflow.api.WorkflowSet;
import org.opencastproject.workspace.api.Workspace;
import com.entwinemedia.fn.Fn2;
import com.entwinemedia.fn.Stream;
import com.entwinemedia.fn.data.Opt;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.Period;
import net.fortuna.ical4j.model.parameter.Value;
import net.fortuna.ical4j.model.property.RRule;
import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jettison.json.JSONException;
import org.joda.time.DateTimeConstants;
import org.joda.time.DateTimeZone;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
public class IndexServiceImpl implements IndexService {
private static final String WORKFLOW_CONFIG_PREFIX = "org.opencastproject.workflow.config.";
public static final String THEME_PROPERTY_NAME = "theme";
/** The logging facility */
private static final Logger logger = LoggerFactory.getLogger(IndexServiceImpl.class);
/** A parser for handling JSON documents inside the body of a request. **/
private static final JSONParser parser = new JSONParser();
private final List<EventCatalogUIAdapter> eventCatalogUIAdapters = new ArrayList<>();
private final List<SeriesCatalogUIAdapter> seriesCatalogUIAdapters = new ArrayList<>();
private EventCatalogUIAdapter eventCatalogUIAdapter;
private SeriesCatalogUIAdapter seriesCatalogUIAdapter;
private AclServiceFactory aclServiceFactory;
private AuthorizationService authorizationService;
private CaptureAgentStateService captureAgentStateService;
private EventCommentService eventCommentService;
private IngestService ingestService;
private AssetManager assetManager;
private SchedulerService schedulerService;
private SecurityService securityService;
private JpaGroupRoleProvider jpaGroupRoleProvider;
private SeriesService seriesService;
private UserDirectoryService userDirectoryService;
private WorkflowService workflowService;
private Workspace workspace;
/** The single thread executor service */
private ExecutorService executorService = Executors.newSingleThreadExecutor();
/** OSGi DI. */
public void setAclServiceFactory(AclServiceFactory aclServiceFactory) {
this.aclServiceFactory = aclServiceFactory;
}
/** OSGi DI. */
public void setAuthorizationService(AuthorizationService authorizationService) {
this.authorizationService = authorizationService;
}
/** OSGi DI. */
public void setCaptureAgentStateService(CaptureAgentStateService captureAgentStateService) {
this.captureAgentStateService = captureAgentStateService;
}
/** OSGi callback for the event comment service. */
public void setEventCommentService(EventCommentService eventCommentService) {
this.eventCommentService = eventCommentService;
}
/** OSGi callback to add the event dublincore {@link EventCatalogUIAdapter} instance. */
public void setCommonEventCatalogUIAdapter(CommonEventCatalogUIAdapter eventCatalogUIAdapter) {
this.eventCatalogUIAdapter = eventCatalogUIAdapter;
}
/** OSGi callback to add the series dublincore {@link SeriesCatalogUIAdapter} instance. */
public void setCommonSeriesCatalogUIAdapter(CommonSeriesCatalogUIAdapter seriesCatalogUIAdapter) {
this.seriesCatalogUIAdapter = seriesCatalogUIAdapter;
}
/** OSGi callback to add {@link EventCatalogUIAdapter} instance. */
public void addCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) {
eventCatalogUIAdapters.add(catalogUIAdapter);
}
/** OSGi callback to remove {@link EventCatalogUIAdapter} instance. */
public void removeCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) {
eventCatalogUIAdapters.remove(catalogUIAdapter);
}
/** OSGi callback to add {@link SeriesCatalogUIAdapter} instance. */
public void addCatalogUIAdapter(SeriesCatalogUIAdapter catalogUIAdapter) {
seriesCatalogUIAdapters.add(catalogUIAdapter);
}
/** OSGi callback to remove {@link SeriesCatalogUIAdapter} instance. */
public void removeCatalogUIAdapter(SeriesCatalogUIAdapter catalogUIAdapter) {
seriesCatalogUIAdapters.remove(catalogUIAdapter);
}
/** OSGi DI. */
public void setIngestService(IngestService ingestService) {
this.ingestService = ingestService;
}
/** OSGi DI. */
public void setAssetManager(AssetManager assetManager) {
this.assetManager = assetManager;
}
/** OSGi DI. */
public void setSchedulerService(SchedulerService schedulerService) {
this.schedulerService = schedulerService;
}
/** OSGi DI. */
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
/** OSGi DI. */
public void setSeriesService(SeriesService seriesService) {
this.seriesService = seriesService;
}
/** OSGi DI. */
public void setWorkflowService(WorkflowService workflowService) {
this.workflowService = workflowService;
}
/** OSGi DI. */
public void setWorkspace(Workspace workspace) {
this.workspace = workspace;
}
/** OSGi DI. */
public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
this.userDirectoryService = userDirectoryService;
}
/** OSGi DI. */
public void setGroupRoleProvider(JpaGroupRoleProvider jpaGroupRoleProvider) {
this.jpaGroupRoleProvider = jpaGroupRoleProvider;
}
public AclService getAclService() {
return aclServiceFactory.serviceFor(securityService.getOrganization());
}
private static final Fn2<EventCatalogUIAdapter, String, Boolean> eventOrganizationFilter = new Fn2<EventCatalogUIAdapter, String, Boolean>() {
@Override
public Boolean apply(EventCatalogUIAdapter catalogUIAdapter, String organization) {
return organization.equals(catalogUIAdapter.getOrganization());
}
};
private static final Fn2<SeriesCatalogUIAdapter, String, Boolean> seriesOrganizationFilter = new Fn2<SeriesCatalogUIAdapter, String, Boolean>() {
@Override
public Boolean apply(SeriesCatalogUIAdapter catalogUIAdapter, String organization) {
return catalogUIAdapter.getOrganization().equals(organization);
}
};
public List<EventCatalogUIAdapter> getEventCatalogUIAdapters(String organization) {
return Stream.$(eventCatalogUIAdapters).filter(eventOrganizationFilter._2(organization)).toList();
}
/**
* @param organization
* The organization to filter the results with.
* @return A {@link List} of {@link SeriesCatalogUIAdapter} that provide the metadata to the front end.
*/
public List<SeriesCatalogUIAdapter> getSeriesCatalogUIAdapters(String organization) {
return Stream.$(seriesCatalogUIAdapters).filter(seriesOrganizationFilter._2(organization)).toList();
}
@Override
public List<EventCatalogUIAdapter> getEventCatalogUIAdapters() {
return new ArrayList<>(getEventCatalogUIAdapters(securityService.getOrganization().getId()));
}
@Override
public List<SeriesCatalogUIAdapter> getSeriesCatalogUIAdapters() {
return new LinkedList<>(getSeriesCatalogUIAdapters(securityService.getOrganization().getId()));
}
@Override
public EventCatalogUIAdapter getCommonEventCatalogUIAdapter() {
return eventCatalogUIAdapter;
}
@Override
public SeriesCatalogUIAdapter getCommonSeriesCatalogUIAdapter() {
return seriesCatalogUIAdapter;
}
@Override
public String createEvent(HttpServletRequest request) throws IndexServiceException {
JSONObject metadataJson = null;
MediaPackage mp = null;
try {
if (ServletFileUpload.isMultipartContent(request)) {
mp = ingestService.createMediaPackage();
for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
FileItemStream item = iter.next();
String fieldName = item.getFieldName();
if (item.isFormField()) {
if ("metadata".equals(fieldName)) {
String metadata = Streams.asString(item.openStream());
try {
metadataJson = (JSONObject) parser.parse(metadata);
} catch (Exception e) {
logger.warn("Unable to parse metadata {}", metadata);
throw new IllegalArgumentException("Unable to parse metadata");
}
}
} else {
if ("presenter".equals(item.getFieldName())) {
mp = ingestService.addTrack(item.openStream(), item.getName(), MediaPackageElements.PRESENTER_SOURCE, mp);
} else if ("presentation".equals(item.getFieldName())) {
mp = ingestService.addTrack(item.openStream(), item.getName(), MediaPackageElements.PRESENTATION_SOURCE,
mp);
} else if ("audio".equals(item.getFieldName())) {
mp = ingestService.addTrack(item.openStream(), item.getName(),
new MediaPackageElementFlavor("presenter-audio", "source"), mp);
} else {
logger.warn("Unknown field name found {}", item.getFieldName());
}
}
}
} else {
throw new IllegalArgumentException("No multipart content");
}
// MH-10834 If there is only an audio track, change the flavor from presenter-audio/source to presenter/source.
if (mp.getTracks().length == 1
&& mp.getTracks()[0].getFlavor().equals(new MediaPackageElementFlavor("presenter-audio", "source"))) {
Track audioTrack = mp.getTracks()[0];
mp.remove(audioTrack);
audioTrack.setFlavor(MediaPackageElements.PRESENTER_SOURCE);
mp.add(audioTrack);
}
return createEvent(metadataJson, mp);
} catch (Exception e) {
logger.error("Unable to create event: {}", getStackTrace(e));
throw new IndexServiceException(e.getMessage());
}
}
/**
* Get the type of the source that is creating the event.
*
* @param source
* The source of the event e.g. upload, single scheduled, multi scheduled
* @return The type of the source
* @throws IllegalArgumentException
* Thrown if unable to get the source from the json object.
*/
private SourceType getSourceType(JSONObject source) {
SourceType type;
try {
type = SourceType.valueOf((String) source.get("type"));
} catch (Exception e) {
logger.error("Unknown source type '{}'", source.get("type"));
throw new IllegalArgumentException("Unknown source type");
}
return type;
}
/**
* Get the access control list from a JSON representation
*
* @param metadataJson
* The {@link JSONObject} that has the access json
* @return An {@link AccessControlList}
* @throws IllegalArgumentException
* Thrown if unable to parse the access control list
*/
private AccessControlList getAccessControlList(JSONObject metadataJson) {
AccessControlList acl = new AccessControlList();
JSONObject accessJson = (JSONObject) metadataJson.get("access");
if (accessJson != null) {
try {
acl = AccessControlParser.parseAcl(accessJson.toJSONString());
} catch (Exception e) {
logger.warn("Unable to parse access control list: {}", accessJson.toJSONString());
throw new IllegalArgumentException("Unable to parse access control list!");
}
}
return acl;
}
@Override
public String createEvent(JSONObject metadataJson, MediaPackage mp) throws ParseException, IOException,
MediaPackageException, IngestException, NotFoundException, SchedulerException, UnauthorizedException {
if (metadataJson == null)
throw new IllegalArgumentException("No metadata set");
JSONObject source = (JSONObject) metadataJson.get("source");
if (source == null)
throw new IllegalArgumentException("No source field in metadata");
JSONObject processing = (JSONObject) metadataJson.get("processing");
if (processing == null)
throw new IllegalArgumentException("No processing field in metadata");
JSONArray allEventMetadataJson = (JSONArray) metadataJson.get("metadata");
if (allEventMetadataJson == null)
throw new IllegalArgumentException("No metadata field in metadata");
AccessControlList acl = getAccessControlList(metadataJson);
MetadataList metadataList = getMetadataListWithAllEventCatalogUIAdapters();
try {
metadataList.fromJSON(allEventMetadataJson.toJSONString());
} catch (MetadataParsingException e) {
logger.warn("Unable to parse event metadata {}", allEventMetadataJson.toJSONString());
throw new IllegalArgumentException("Unable to parse metadata set");
}
EventHttpServletRequest eventHttpServletRequest = new EventHttpServletRequest();
eventHttpServletRequest.setAcl(acl);
eventHttpServletRequest.setMetadataList(metadataList);
eventHttpServletRequest.setMediaPackage(mp);
eventHttpServletRequest.setProcessing(processing);
eventHttpServletRequest.setSource(source);
return createEvent(eventHttpServletRequest);
}
@Override
public String createEvent(EventHttpServletRequest eventHttpServletRequest) throws ParseException, IOException,
MediaPackageException, IngestException, NotFoundException, SchedulerException, UnauthorizedException {
// Preconditions
if (eventHttpServletRequest.getAcl().isNone()) {
throw new IllegalArgumentException("No access control list available to create new event.");
}
if (eventHttpServletRequest.getMediaPackage().isNone()) {
throw new IllegalArgumentException("No mediapackage available to create new event.");
}
if (eventHttpServletRequest.getMetadataList().isNone()) {
throw new IllegalArgumentException("No metadata list available to create new event.");
}
if (eventHttpServletRequest.getProcessing().isNone()) {
throw new IllegalArgumentException("No processing metadata available to create new event.");
}
if (eventHttpServletRequest.getSource().isNone()) {
throw new IllegalArgumentException("No source field metadata available to create new event.");
}
// Get Workflow
String workflowTemplate = (String) eventHttpServletRequest.getProcessing().get().get("workflow");
if (workflowTemplate == null)
throw new IllegalArgumentException("No workflow template in metadata");
// Get Type of Source
SourceType type = getSourceType(eventHttpServletRequest.getSource().get());
MetadataCollection eventMetadata = eventHttpServletRequest.getMetadataList().get()
.getMetadataByAdapter(eventCatalogUIAdapter).get();
JSONObject sourceMetadata = (JSONObject) eventHttpServletRequest.getSource().get().get("metadata");
if (sourceMetadata != null
&& (type.equals(SourceType.SCHEDULE_SINGLE) || type.equals(SourceType.SCHEDULE_MULTIPLE))) {
try {
MetadataField<?> current = eventMetadata.getOutputFields().get("location");
eventMetadata.updateStringField(current, (String) sourceMetadata.get("device"));
} catch (Exception e) {
logger.warn("Unable to parse device {}", sourceMetadata.get("device"));
throw new IllegalArgumentException("Unable to parse device");
}
}
MetadataField<?> created = eventMetadata.getOutputFields().get(DublinCore.PROPERTY_CREATED.getLocalName());
if (created == null || !created.isUpdated() || created.getValue().isNone()) {
eventMetadata.removeField(created);
MetadataField<String> newCreated = MetadataUtils.copyMetadataField(created);
newCreated.setValue(EncodingSchemeUtils.encodeDate(new Date(), Precision.Second).getValue());
eventMetadata.addField(newCreated);
}
// Get presenter usernames for use as technical presenters
Set<String> presenterUsernames = new HashSet<>();
Opt<Set<String>> technicalPresenters = updatePresenters(eventMetadata);
if (technicalPresenters.isSome()) {
presenterUsernames = technicalPresenters.get();
}
eventHttpServletRequest.getMetadataList().get().add(eventCatalogUIAdapter, eventMetadata);
updateMediaPackageMetadata(eventHttpServletRequest.getMediaPackage().get(),
eventHttpServletRequest.getMetadataList().get());
DublinCoreCatalog dc = getDublinCoreCatalog(eventHttpServletRequest);
String captureAgentId = null;
TimeZone tz = null;
org.joda.time.DateTime start = null;
org.joda.time.DateTime end = null;
long duration = 0L;
Properties caProperties = new Properties();
RRule rRule = null;
if (sourceMetadata != null
&& (type.equals(SourceType.SCHEDULE_SINGLE) || type.equals(SourceType.SCHEDULE_MULTIPLE))) {
Properties configuration;
try {
captureAgentId = (String) sourceMetadata.get("device");
configuration = captureAgentStateService.getAgentConfiguration((String) sourceMetadata.get("device"));
} catch (Exception e) {
logger.warn("Unable to parse device {}: because: {}", sourceMetadata.get("device"), getStackTrace(e));
throw new IllegalArgumentException("Unable to parse device");
}
String durationString = (String) sourceMetadata.get("duration");
if (StringUtils.isBlank(durationString))
throw new IllegalArgumentException("No duration in source metadata");
// Create timezone based on CA's reported TZ.
String agentTimeZone = configuration.getProperty("capture.device.timezone");
if (StringUtils.isNotBlank(agentTimeZone)) {
tz = TimeZone.getTimeZone(agentTimeZone);
dc.set(DublinCores.OC_PROPERTY_AGENT_TIMEZONE, tz.getID());
} else { // No timezone was present, assume the serve's local timezone.
tz = TimeZone.getDefault();
logger.warn(
"The field 'capture.device.timezone' has not been set in the agent configuration. The default server timezone will be used.");
}
org.joda.time.DateTime now = new org.joda.time.DateTime(DateTimeZone.UTC);
start = now.withMillis(DateTimeSupport.fromUTC((String) sourceMetadata.get("start")));
end = now.withMillis(DateTimeSupport.fromUTC((String) sourceMetadata.get("end")));
duration = Long.parseLong(durationString);
DublinCoreValue period = EncodingSchemeUtils
.encodePeriod(new DCMIPeriod(start.toDate(), start.plus(duration).toDate()), Precision.Second);
String inputs = (String) sourceMetadata.get("inputs");
caProperties.putAll(configuration);
dc.set(DublinCore.PROPERTY_TEMPORAL, period);
caProperties.put(CaptureParameters.CAPTURE_DEVICE_NAMES, inputs);
}
if (type.equals(SourceType.SCHEDULE_MULTIPLE)) {
rRule = new RRule((String) sourceMetadata.get("rrule"));
}
Map<String, String> configuration = new HashMap<>();
if (eventHttpServletRequest.getProcessing().get().get("configuration") != null) {
configuration = new HashMap<>((JSONObject) eventHttpServletRequest.getProcessing().get().get("configuration"));
}
for (Entry<String, String> entry : configuration.entrySet()) {
caProperties.put(WORKFLOW_CONFIG_PREFIX.concat(entry.getKey()), entry.getValue());
}
caProperties.put(CaptureParameters.INGEST_WORKFLOW_DEFINITION, workflowTemplate);
eventHttpServletRequest.setMediaPackage(authorizationService.setAcl(eventHttpServletRequest.getMediaPackage().get(),
AclScope.Episode, eventHttpServletRequest.getAcl().get()).getA());
switch (type) {
case UPLOAD:
case UPLOAD_LATER:
eventHttpServletRequest
.setMediaPackage(updateDublincCoreCatalog(eventHttpServletRequest.getMediaPackage().get(), dc));
configuration.put("workflowDefinitionId", workflowTemplate);
WorkflowInstance ingest = ingestService.ingest(eventHttpServletRequest.getMediaPackage().get(),
workflowTemplate, configuration);
return eventHttpServletRequest.getMediaPackage().get().getIdentifier().compact();
case SCHEDULE_SINGLE:
eventHttpServletRequest
.setMediaPackage(updateDublincCoreCatalog(eventHttpServletRequest.getMediaPackage().get(), dc));
Long id = schedulerService.addEvent(dc, configuration);
schedulerService.updateCaptureAgentMetadata(caProperties, Tuple.tuple(id, dc));
schedulerService.updateAccessControlList(id, eventHttpServletRequest.getAcl().get());
return eventHttpServletRequest.getMediaPackage().get().getIdentifier().compact();
case SCHEDULE_MULTIPLE:
List<Period> periods = calculatePeriods(start.toDate(), end.toDate(), duration, rRule, tz);
int i = 1;
int length = Integer.toString(periods.size()).length();
List<String> ids = new ArrayList<>();
String initialTitle = dc.getFirst(DublinCore.PROPERTY_TITLE);
for (Period period : periods) {
Date startDate = new Date(period.getStart().getTime());
Date endDate = new Date(period.getEnd().getTime());
Id idImpl = new IdImpl(UUID.randomUUID().toString());
// Set the new media package identifier
eventHttpServletRequest.getMediaPackage().get().setIdentifier(idImpl);
// Update dublincore title and temporal
String newTitle = initialTitle + String.format(" %0" + length + "d", i++);
dc.set(DublinCore.PROPERTY_TITLE, newTitle);
DublinCoreValue eventTime = EncodingSchemeUtils.encodePeriod(new DCMIPeriod(startDate, endDate),
Precision.Second);
dc.set(DublinCore.PROPERTY_TEMPORAL, eventTime);
eventHttpServletRequest
.setMediaPackage(updateDublincCoreCatalog(eventHttpServletRequest.getMediaPackage().get(), dc));
eventHttpServletRequest.getMediaPackage().get().setTitle(newTitle);
Long schedulerId = schedulerService.addEvent(dc, configuration);
schedulerService.updateCaptureAgentMetadata(caProperties, Tuple.tuple(schedulerId, dc));
schedulerService.updateAccessControlList(schedulerId, eventHttpServletRequest.getAcl().get());
ids.add(idImpl.compact());
}
return StringUtils.join(ids, ",");
default:
logger.warn("Unknown source type {}", type);
throw new IllegalArgumentException("Unknown source type");
}
}
/**
* Get the {@link DublinCoreCatalog} from an {@link EventHttpServletRequest}.
*
* @param eventHttpServletRequest
* The request to extract the {@link DublinCoreCatalog} from.
* @return The {@link DublinCoreCatalog}
*/
private DublinCoreCatalog getDublinCoreCatalog(EventHttpServletRequest eventHttpServletRequest) {
DublinCoreCatalog dc;
Opt<DublinCoreCatalog> dcOpt = DublinCoreUtil.loadEpisodeDublinCore(workspace,
eventHttpServletRequest.getMediaPackage().get());
if (dcOpt.isSome()) {
dc = dcOpt.get();
// make sure to bind the OC_PROPERTY namespace
dc.addBindings(XmlNamespaceContext
.mk(XmlNamespaceBinding.mk(DublinCores.OC_PROPERTY_NS_PREFIX, DublinCores.OC_PROPERTY_NS_URI)));
} else {
dc = DublinCores.mkOpencastEpisode().getCatalog();
}
return dc;
}
/**
* Update the presenters field in the event {@link MetadataCollection} to have friendly names loaded by the
* {@link UserDirectoryService} and return the usernames of the presenters.
*
* @param eventMetadata
* The {@link MetadataCollection} to update the presenters (creator field) with full names.
* @return If the presenters (creator) field has been updated, the set of user names, if any, of the presenters. None
* if it wasn't updated.
*/
private Opt<Set<String>> updatePresenters(MetadataCollection eventMetadata) {
MetadataField<?> presentersMetadataField = eventMetadata.getOutputFields()
.get(DublinCore.PROPERTY_CREATOR.getLocalName());
if (presentersMetadataField.isUpdated()) {
Set<String> presenterUsernames = new HashSet<>();
Tuple<List<String>, Set<String>> updatedPresenters = getTechnicalPresenters(eventMetadata);
presenterUsernames = updatedPresenters.getB();
eventMetadata.removeField(presentersMetadataField);
MetadataField<Iterable<String>> newPresentersMetadataField = MetadataUtils
.copyMetadataField(presentersMetadataField);
newPresentersMetadataField.setValue(updatedPresenters.getA());
eventMetadata.addField(newPresentersMetadataField);
return Opt.some(presenterUsernames);
} else {
return Opt.none();
}
}
private MediaPackage updateDublincCoreCatalog(MediaPackage mp, DublinCoreCatalog dc)
throws IOException, MediaPackageException, IngestException {
try (InputStream inputStream = IOUtils.toInputStream(dc.toXmlString(), "UTF-8")) {
// Update dublincore catalog
Catalog[] catalogs = mp.getCatalogs(MediaPackageElements.EPISODE);
if (catalogs.length > 0) {
Catalog catalog = catalogs[0];
URI uri = workspace.put(mp.getIdentifier().toString(), catalog.getIdentifier(), "dublincore.xml", inputStream);
catalog.setURI(uri);
// setting the URI to a new source so the checksum will most like be invalid
catalog.setChecksum(null);
} else {
mp = ingestService.addCatalog(inputStream, "dublincore.xml", MediaPackageElements.EPISODE, mp);
}
}
return mp;
}
/**
* Giving a start time and end time with a recurrence rule and a timezone, all periods of the recurrence rule are
* calculated taken daylight saving time into account.
*
*
* @param start
* the start date time
* @param end
* the end date
* @param duration
* the duration
* @param rRule
* the recurrence rule
* @param tz
* @return a list of scheduling periods
*/
protected List<Period> calculatePeriods(Date start, Date end, long duration, RRule rRule, TimeZone tz) {
final TimeZone utc = TimeZone.getTimeZone("UTC");
TimeZone.setDefault(tz);
DateTime seed = new DateTime(start);
DateTime period = new DateTime();
Calendar endCalendar = Calendar.getInstance(utc);
endCalendar.setTime(end);
Calendar calendar = Calendar.getInstance(utc);
calendar.setTime(seed);
calendar.set(Calendar.DAY_OF_MONTH, endCalendar.get(Calendar.DAY_OF_MONTH));
calendar.set(Calendar.MONTH, endCalendar.get(Calendar.MONTH));
calendar.set(Calendar.YEAR, endCalendar.get(Calendar.YEAR));
period.setTime(calendar.getTime().getTime() + duration);
duration = duration % (DateTimeConstants.MILLIS_PER_DAY);
List<Period> periods = new ArrayList<>();
TimeZone.setDefault(utc);
for (Object date : rRule.getRecur().getDates(seed, period, Value.DATE_TIME)) {
Date d = (Date) date;
Calendar cDate = Calendar.getInstance(utc);
// Adjust for DST, if start of event
if (tz.inDaylightTime(seed)) { // Event starts in DST
if (!tz.inDaylightTime(d)) { // Date not in DST?
d.setTime(d.getTime() + tz.getDSTSavings()); // Adjust for Fall back one hour
}
} else { // Event doesn't start in DST
if (tz.inDaylightTime(d)) {
d.setTime(d.getTime() - tz.getDSTSavings()); // Adjust for Spring forward one hour
}
}
cDate.setTime(d);
periods.add(new Period(new DateTime(cDate.getTime()), new DateTime(cDate.getTimeInMillis() + duration)));
}
TimeZone.setDefault(null);
return periods;
}
@Override
public MetadataList updateCommonEventMetadata(String id, String metadataJSON, AbstractSearchIndex index)
throws IllegalArgumentException, IndexServiceException, SearchIndexException, NotFoundException,
UnauthorizedException {
MetadataList metadataList;
try {
metadataList = getMetadataListWithCommonEventCatalogUIAdapter();
metadataList.fromJSON(metadataJSON);
} catch (Exception e) {
logger.warn("Not able to parse the event metadata {}: {}", metadataJSON, getStackTrace(e));
throw new IllegalArgumentException("Not able to parse the event metadata " + metadataJSON, e);
}
return updateEventMetadata(id, metadataList, index);
}
@Override
public MetadataList updateAllEventMetadata(String id, String metadataJSON, AbstractSearchIndex index)
throws IllegalArgumentException, IndexServiceException, NotFoundException, SearchIndexException,
UnauthorizedException {
MetadataList metadataList;
try {
metadataList = getMetadataListWithAllEventCatalogUIAdapters();
metadataList.fromJSON(metadataJSON);
} catch (Exception e) {
logger.warn("Not able to parse the event metadata {}: {}", metadataJSON, getStackTrace(e));
throw new IllegalArgumentException("Not able to parse the event metadata " + metadataJSON, e);
}
return updateEventMetadata(id, metadataList, index);
}
@Override
public void removeCatalogByFlavor(Event event, MediaPackageElementFlavor flavor)
throws IndexServiceException, NotFoundException, UnauthorizedException {
Opt<MediaPackage> mpOpt = getEventMediapackage(event);
if (mpOpt.isSome()) {
MediaPackage mediaPackage = mpOpt.get();
Catalog[] catalogs = mediaPackage.getCatalogs(flavor);
if (catalogs.length == 0) {
throw new NotFoundException(String.format("Cannot find a catalog with flavor '%s' for event with id '%s'.",
flavor.toString(), event.getIdentifier()));
}
for (Catalog catalog : catalogs) {
mediaPackage.remove(catalog);
}
switch (getEventSource(event)) {
case WORKFLOW:
Opt<WorkflowInstance> workflowInstance = getCurrentWorkflowInstance(event.getIdentifier());
if (workflowInstance.isNone()) {
logger.error("No workflow instance for event {} found!", event.getIdentifier());
throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
}
try {
WorkflowInstance instance = workflowInstance.get();
instance.setMediaPackage(mediaPackage);
updateWorkflowInstance(instance);
} catch (WorkflowException e) {
logger.error("Unable to remove catalog with flavor {} by updating workflow event {} because {}",
new Object[] { flavor, event.getIdentifier(), getStackTrace(e) });
throw new IndexServiceException("Unable to update workflow event " + event.getIdentifier());
}
break;
case ARCHIVE:
assetManager.takeSnapshot(DEFAULT_OWNER, mediaPackage);
break;
case SCHEDULE:
// Ignoring as there are no mediapackages attached to scheduled items
throw new IllegalStateException(
"Unable to remove a catalog from a Scheduled event as there is no mediapackage.");
default:
throw new IndexServiceException(
String.format("Unable to handle event source type '%s'", getEventSource(event)));
}
}
}
@Override
public void removeCatalogByFlavor(Series series, MediaPackageElementFlavor flavor)
throws NotFoundException, IndexServiceException {
if (series == null) {
throw new IllegalArgumentException("The series cannot be null.");
}
if (flavor == null) {
throw new IllegalArgumentException("The flavor cannot be null.");
}
boolean found = false;
try {
found = seriesService.deleteSeriesElement(series.getIdentifier(), flavor.getType());
} catch (SeriesException e) {
throw new IndexServiceException(String.format("Unable to delete catalog from series '%s' with type '%s'",
series.getIdentifier(), flavor.getType()), e);
}
if (!found) {
throw new NotFoundException(String.format("Unable to find a catalog for series '%s' with flavor '%s'",
series.getIdentifier(), flavor));
}
}
@Override
public MetadataList updateEventMetadata(String id, MetadataList metadataList, AbstractSearchIndex index)
throws IndexServiceException, SearchIndexException, NotFoundException, UnauthorizedException {
Opt<Event> optEvent = getEvent(id, index);
if (optEvent.isNone())
throw new NotFoundException("Cannot find an event with id " + id);
Event event = optEvent.get();
Opt<MediaPackage> mpOpt = getEventMediapackage(event);
MediaPackage mediaPackage;
Opt<Set<String>> presenters = Opt.none();
Opt<MetadataCollection> eventCatalog = metadataList.getMetadataByAdapter(getCommonEventCatalogUIAdapter());
if (eventCatalog.isSome()) {
presenters = updatePresenters(eventCatalog.get());
}
switch (getEventSource(event)) {
case WORKFLOW:
Opt<WorkflowInstance> workflowInstance = getCurrentWorkflowInstance(event.getIdentifier());
if (workflowInstance.isNone()) {
logger.error("No workflow instance for event {} found!", event.getIdentifier());
throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
}
try {
if (mpOpt.isNone()) {
logger.error("No mediapackage found for workflow event {}!", id);
throw new IndexServiceException("No mediapackage found for workflow event {}!" + id);
}
mediaPackage = mpOpt.get();
WorkflowInstance instance = workflowInstance.get();
instance.setMediaPackage(mediaPackage);
updateWorkflowInstance(instance);
} catch (WorkflowException e) {
logger.error("Unable to update workflow event {} with metadata {} because {}",
new Object[] { id, RestUtils.getJsonStringSilent(metadataList.toJSON()), getStackTrace(e) });
throw new IndexServiceException("Unable to update workflow event " + id);
}
break;
case ARCHIVE:
if (mpOpt.isNone()) {
logger.error("No mediapackage found for archived event {}!", id);
throw new IndexServiceException("No mediapackage found for archived event {}!" + id);
}
mediaPackage = mpOpt.get();
updateMediaPackageMetadata(mediaPackage, metadataList);
assetManager.takeSnapshot(DEFAULT_OWNER, mediaPackage);
break;
case SCHEDULE:
try {
Long eventId = schedulerService.getEventId(event.getIdentifier());
DublinCoreCatalog dc = schedulerService.getEventDublinCore(eventId);
Opt<MetadataCollection> abstractMetadata = metadataList.getMetadataByAdapter(eventCatalogUIAdapter);
if (abstractMetadata.isSome()) {
DublinCoreMetadataUtil.updateDublincoreCatalog(dc, abstractMetadata.get());
}
schedulerService.updateEvent(eventId, dc, new HashMap<String, String>());
} catch (SchedulerException e) {
logger.error("Unable to update scheduled event {} with metadata {} because {}",
new Object[] { id, RestUtils.getJsonStringSilent(metadataList.toJSON()), getStackTrace(e) });
throw new IndexServiceException("Unable to update scheduled event " + id);
}
break;
default:
logger.error("Unkown event source!");
}
return metadataList;
}
/**
* Processes the combined usernames and free text entries of the presenters (creator) field into a list of presenters
* using the full names of the users if available and adds the usernames to a set of technical presenters.
*
* @param eventMetadata
* The metadata list that has the presenter (creator) field to pull the list of presenters from.
* @return A {@link Tuple} with a list of friendly presenter names and a set of user names if available for the
* presenters.
*/
protected Tuple<List<String>, Set<String>> getTechnicalPresenters(MetadataCollection eventMetadata) {
MetadataField<?> presentersMetadataField = eventMetadata.getOutputFields()
.get(DublinCore.PROPERTY_CREATOR.getLocalName());
List<String> presenters = new ArrayList<>();
Set<String> technicalPresenters = new HashSet<>();
for (String presenter : MetadataUtils.getIterableStringMetadata(presentersMetadataField)) {
User user = userDirectoryService.loadUser(presenter);
if (user == null) {
presenters.add(presenter);
} else {
String fullname = StringUtils.isNotBlank(user.getName()) ? user.getName() : user.getUsername();
presenters.add(fullname);
technicalPresenters.add(user.getUsername());
}
}
return Tuple.tuple(presenters, technicalPresenters);
}
@Override
public AccessControlList updateEventAcl(String id, AccessControlList acl, AbstractSearchIndex index)
throws IllegalArgumentException, IndexServiceException, SearchIndexException, NotFoundException,
UnauthorizedException {
Opt<Event> optEvent = getEvent(id, index);
if (optEvent.isNone())
throw new NotFoundException("Cannot find an event with id " + id);
Event event = optEvent.get();
Opt<MediaPackage> mpOpt = getEventMediapackage(event);
MediaPackage mediaPackage;
switch (getEventSource(event)) {
case WORKFLOW:
// Not updating the acl as the workflow might have already passed the point of distribution.
throw new IllegalArgumentException("Unable to update the ACL of this event as it is currently processing.");
case ARCHIVE:
if (mpOpt.isNone()) {
logger.error("No mediapackage found for archived event {}!", id);
throw new IndexServiceException("No mediapackage found for archived event {}!" + id);
}
mediaPackage = mpOpt.get();
mediaPackage = authorizationService.setAcl(mediaPackage, AclScope.Episode, acl).getA();
assetManager.takeSnapshot(DEFAULT_OWNER, mediaPackage);
return acl;
case SCHEDULE:
try {
Long eventId = schedulerService.getEventId(event.getIdentifier());
schedulerService.updateAccessControlList(eventId, acl);
} catch (SchedulerException e) {
throw new IndexServiceException("Unable to update the acl for the scheduled event", e);
}
return acl;
default:
logger.error("Unknown event source '{}' unable to update ACL!", getEventSource(event));
throw new IndexServiceException(
String.format("Unable to update the ACL as '{}' is an unknown event source.", getEventSource(event)));
}
}
@Override
public SearchResult<Group> getGroups(String filter, Opt<Integer> optLimit, Opt<Integer> optOffset,
Opt<String> optSort, AbstractSearchIndex index) throws SearchIndexException {
GroupSearchQuery query = new GroupSearchQuery(securityService.getOrganization().getId(), securityService.getUser());
// Parse the filters
if (StringUtils.isNotBlank(filter)) {
for (String f : filter.split(",")) {
String[] filterTuple = f.split(":");
if (filterTuple.length < 2) {
logger.info("No value for filter {} in filters list: {}", filterTuple[0], filter);
continue;
}
String name = filterTuple[0];
String value = filterTuple[1];
if (GroupsListQuery.FILTER_NAME_NAME.equals(name))
query.withName(value);
}
}
if (optSort.isSome()) {
Set<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
for (SortCriterion criterion : sortCriteria) {
switch (criterion.getFieldName()) {
case GroupIndexSchema.NAME:
query.sortByName(criterion.getOrder());
break;
case GroupIndexSchema.DESCRIPTION:
query.sortByDescription(criterion.getOrder());
break;
case GroupIndexSchema.ROLE:
query.sortByRole(criterion.getOrder());
break;
case GroupIndexSchema.MEMBERS:
query.sortByMembers(criterion.getOrder());
break;
case GroupIndexSchema.ROLES:
query.sortByRoles(criterion.getOrder());
break;
default:
throw new WebApplicationException(Status.BAD_REQUEST);
}
}
}
if (optLimit.isSome())
query.withLimit(optLimit.get());
if (optOffset.isSome())
query.withOffset(optOffset.get());
return index.getByQuery(query);
}
@Override
public Opt<Group> getGroup(String id, AbstractSearchIndex index) throws SearchIndexException {
SearchResult<Group> result = index
.getByQuery(new GroupSearchQuery(securityService.getOrganization().getId(), securityService.getUser())
.withIdentifier(id));
// If the results list if empty, we return already a response.
if (result.getPageSize() == 0) {
logger.debug("Didn't find event with id {}", id);
return Opt.none();
}
return Opt.some(result.getItems()[0].getSource());
}
@Override
public Response removeGroup(String id) throws NotFoundException {
return jpaGroupRoleProvider.removeGroup(id);
}
@Override
public Response updateGroup(String id, String name, String description, String roles, String members)
throws NotFoundException {
return jpaGroupRoleProvider.updateGroup(id, name, description, roles, members);
}
@Override
public Response createGroup(String name, String description, String roles, String members) {
if (StringUtils.isEmpty(roles))
roles = "";
if (StringUtils.isEmpty(members))
members = "";
return jpaGroupRoleProvider.createGroup(name, description, roles, members);
}
/**
* Get a single event
*
* @param id
* the mediapackage id
* @return an event or none if not found wrapped in an option
* @throws SearchIndexException
*/
@Override
public Opt<Event> getEvent(String id, AbstractSearchIndex index) throws SearchIndexException {
SearchResult<Event> result = index
.getByQuery(new EventSearchQuery(securityService.getOrganization().getId(), securityService.getUser())
.withIdentifier(id));
// If the results list if empty, we return already a response.
if (result.getPageSize() == 0) {
logger.debug("Didn't find event with id {}", id);
return Opt.none();
}
return Opt.some(result.getItems()[0].getSource());
}
@Override
public boolean removeEvent(String id) throws NotFoundException, UnauthorizedException {
boolean unauthorizedScheduler = false;
boolean notFoundScheduler = false;
boolean removedScheduler = true;
try {
schedulerService.removeEvent(schedulerService.getEventId(id));
} catch (NotFoundException e) {
notFoundScheduler = true;
} catch (UnauthorizedException e) {
unauthorizedScheduler = true;
} catch (SchedulerException e) {
removedScheduler = false;
logger.error("Unable to remove the event '{}' from scheduler service: {}", id, getStackTrace(e));
}
boolean unauthorizedWorkflow = false;
boolean notFoundWorkflow = false;
boolean removedWorkflow = true;
try {
WorkflowQuery workflowQuery = new WorkflowQuery().withMediaPackage(id);
WorkflowSet workflowSet = workflowService.getWorkflowInstances(workflowQuery);
if (workflowSet.size() == 0)
notFoundWorkflow = true;
for (WorkflowInstance instance : workflowSet.getItems()) {
workflowService.stop(instance.getId());
workflowService.remove(instance.getId());
}
} catch (NotFoundException e) {
notFoundWorkflow = true;
} catch (UnauthorizedException e) {
unauthorizedWorkflow = true;
} catch (WorkflowDatabaseException e) {
removedWorkflow = false;
logger.error("Unable to remove the event '{}' because removing workflow failed: {}", id, getStackTrace(e));
} catch (WorkflowException e) {
removedWorkflow = false;
logger.error("Unable to remove the event '{}' because removing workflow failed: {}", id, getStackTrace(e));
}
boolean unauthorizedArchive = false;
boolean notFoundArchive = false;
boolean removedArchive = true;
try {
final AQueryBuilder q = assetManager.createQuery();
final Predicate p = q.organizationId().eq(securityService.getOrganization().getId()).and(q.mediaPackageId(id));
final AResult r = q.select(q.nothing()).where(p).run();
if (r.getSize() > 0) {
q.delete(DEFAULT_OWNER, q.snapshot()).where(p).run();
} else {
notFoundArchive = true;
}
} catch (AssetManagerException e) {
if (e.getCause() instanceof UnauthorizedException) {
unauthorizedArchive = true;
} else if (e.getCause() instanceof NotFoundException) {
notFoundArchive = true;
} else {
removedArchive = false;
logger.error("Unable to remove the event '{}' from the archive: {}", id, getStackTrace(e));
}
}
if (notFoundScheduler && notFoundWorkflow && notFoundArchive)
throw new NotFoundException("Event id " + id + " not found.");
if (unauthorizedScheduler || unauthorizedWorkflow || unauthorizedArchive)
throw new UnauthorizedException("Not authorized to remove event id " + id);
try {
eventCommentService.deleteComments(id);
} catch (EventCommentException e) {
logger.error("Unable to remove comments for event '{}': {}", id, getStackTrace(e));
}
return removedScheduler && removedWorkflow && removedArchive;
}
@Override
public void updateWorkflowInstance(WorkflowInstance workflowInstance)
throws WorkflowException, UnauthorizedException {
// Only update the workflow if the instance is in a working state
if (WorkflowInstance.WorkflowState.FAILED.equals(workflowInstance.getState())
|| WorkflowInstance.WorkflowState.FAILING.equals(workflowInstance.getState())
|| WorkflowInstance.WorkflowState.STOPPED.equals(workflowInstance.getState())
|| WorkflowInstance.WorkflowState.SUCCEEDED.equals(workflowInstance.getState())) {
logger.info("Skip updating {} workflow mediapackage {} with updated comments catalog",
workflowInstance.getState(), workflowInstance.getMediaPackage().getIdentifier().toString());
return;
}
workflowService.update(workflowInstance);
}
@Override
public Opt<MediaPackage> getEventMediapackage(Event event) throws IndexServiceException {
switch (getEventSource(event)) {
case WORKFLOW:
Opt<WorkflowInstance> currentWorkflowInstance = getCurrentWorkflowInstance(event.getIdentifier());
if (currentWorkflowInstance.isSome()) {
logger.debug("Found event in workflow with id {}", event.getIdentifier());
return Opt.some(currentWorkflowInstance.get().getMediaPackage());
}
return Opt.none();
case ARCHIVE:
final AQueryBuilder q = assetManager.createQuery();
final AResult r = q.select(q.snapshot())
.where(q.mediaPackageId(event.getIdentifier()).and(q.version().isLatest())).run();
if (r.getSize() > 0) {
logger.debug("Found event in archive with id {}", event.getIdentifier());
return Opt.some(enrich(r).getSnapshots().head2().getMediaPackage());
}
logger.error("No event with id {} found from archive!", event.getIdentifier());
throw new IndexServiceException("No archived event found with id " + event.getIdentifier());
case SCHEDULE:
return Opt.none();
default:
throw new IllegalStateException("Unknown event type!");
}
}
/**
* Determines in a very basic way what kind of source the event is
*
* @param event
* the event
* @return the source type
*/
@Override
public Source getEventSource(Event event) {
if (event.getWorkflowId() != null && isWorkflowActive(event.getWorkflowState()))
return Source.WORKFLOW;
if (event.getSchedulingStatus() != null && !event.hasRecordingStarted())
return Source.SCHEDULE;
if (event.getArchiveVersion() != null)
return Source.ARCHIVE;
if (event.getWorkflowId() != null)
return Source.WORKFLOW;
return Source.SCHEDULE;
}
@Override
public Opt<WorkflowInstance> getCurrentWorkflowInstance(String mpId) throws IndexServiceException {
WorkflowQuery query = new WorkflowQuery().withMediaPackage(mpId);
WorkflowSet workflowInstances;
try {
workflowInstances = workflowService.getWorkflowInstances(query);
if (workflowInstances.size() == 0) {
logger.info("No workflow instance found for mediapackage {}.", mpId);
return Opt.none();
}
} catch (WorkflowDatabaseException e) {
logger.error("Unable to get workflows for event {} because {}", mpId, getStackTrace(e));
throw new IndexServiceException("Unable to get current workflow for event " + mpId);
}
// Get the newest workflow instance
// TODO This presuppose knowledge of the Database implementation and should be fixed sooner or later!
WorkflowInstance workflowInstance = workflowInstances.getItems()[0];
for (WorkflowInstance instance : workflowInstances.getItems()) {
if (instance.getId() > workflowInstance.getId())
workflowInstance = instance;
}
return Opt.some(workflowInstance);
}
private void updateMediaPackageMetadata(MediaPackage mp, MetadataList metadataList) {
List<EventCatalogUIAdapter> catalogUIAdapters = getEventCatalogUIAdapters();
if (catalogUIAdapters.size() > 0) {
for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
Opt<MetadataCollection> metadata = metadataList.getMetadataByAdapter(catalogUIAdapter);
if (metadata.isSome() && metadata.get().isUpdated()) {
catalogUIAdapter.storeFields(mp, metadata.get());
}
}
}
}
@Override
public String createSeries(MetadataList metadataList, Map<String, String> options, Opt<AccessControlList> optAcl,
Opt<Long> optThemeId) throws IndexServiceException {
DublinCoreCatalog dc = DublinCores.mkOpencastSeries().getCatalog();
dc.set(PROPERTY_IDENTIFIER, UUID.randomUUID().toString());
dc.set(DublinCore.PROPERTY_CREATED, EncodingSchemeUtils.encodeDate(new Date(), Precision.Second));
for (Entry<String, String> entry : options.entrySet()) {
dc.set(new EName(DublinCores.OC_PROPERTY_NS_URI, entry.getKey()), entry.getValue());
}
Opt<MetadataCollection> seriesMetadata = metadataList.getMetadataByFlavor(MediaPackageElements.SERIES.toString());
if (seriesMetadata.isSome()) {
DublinCoreMetadataUtil.updateDublincoreCatalog(dc, seriesMetadata.get());
}
AccessControlList acl;
if (optAcl.isSome()) {
acl = optAcl.get();
} else {
acl = new AccessControlList();
}
String seriesId;
try {
DublinCoreCatalog createdSeries = seriesService.updateSeries(dc);
seriesId = createdSeries.getFirst(PROPERTY_IDENTIFIER);
seriesService.updateAccessControl(seriesId, acl);
for (Long id : optThemeId)
seriesService.updateSeriesProperty(seriesId, THEME_PROPERTY_NAME, Long.toString(id));
} catch (Exception e) {
logger.error("Unable to create new series: {}", getStackTrace(e));
throw new IndexServiceException("Unable to create new series");
}
updateSeriesMetadata(seriesId, metadataList);
return seriesId;
}
@Override
public String createSeries(String metadata)
throws IllegalArgumentException, IndexServiceException, UnauthorizedException {
JSONObject metadataJson = null;
try {
metadataJson = (JSONObject) new JSONParser().parse(metadata);
} catch (Exception e) {
logger.warn("Unable to parse metadata {}", metadata);
throw new IllegalArgumentException("Unable to parse metadata" + metadata);
}
if (metadataJson == null)
throw new IllegalArgumentException("No metadata set to create series");
JSONArray seriesMetadataJson = (JSONArray) metadataJson.get("metadata");
if (seriesMetadataJson == null)
throw new IllegalArgumentException("No metadata field in metadata");
JSONObject options = (JSONObject) metadataJson.get("options");
if (options == null)
throw new IllegalArgumentException("No options field in metadata");
Opt<Long> themeId = Opt.none();
Long theme = (Long) metadataJson.get("theme");
if (theme != null) {
themeId = Opt.some(theme);
}
Map<String, String> optionsMap;
try {
optionsMap = JSONUtils.toMap(new org.codehaus.jettison.json.JSONObject(options.toJSONString()));
} catch (JSONException e) {
logger.warn("Unable to parse options to map: {}", getStackTrace(e));
throw new IllegalArgumentException("Unable to parse options to map");
}
DublinCoreCatalog dc = DublinCores.mkOpencastSeries().getCatalog();
dc.set(PROPERTY_IDENTIFIER, UUID.randomUUID().toString());
dc.set(DublinCore.PROPERTY_CREATED, EncodingSchemeUtils.encodeDate(new Date(), Precision.Second));
for (Entry<String, String> entry : optionsMap.entrySet()) {
dc.set(new EName(DublinCores.OC_PROPERTY_NS_URI, entry.getKey()), entry.getValue());
}
MetadataList metadataList;
try {
metadataList = getMetadataListWithAllSeriesCatalogUIAdapters();
metadataList.fromJSON(seriesMetadataJson.toJSONString());
} catch (Exception e) {
logger.warn("Not able to parse the series metadata {}: {}", seriesMetadataJson, getStackTrace(e));
throw new IllegalArgumentException("Not able to parse the series metadata");
}
Opt<MetadataCollection> seriesMetadata = metadataList.getMetadataByFlavor(MediaPackageElements.SERIES.toString());
if (seriesMetadata.isSome()) {
DublinCoreMetadataUtil.updateDublincoreCatalog(dc, seriesMetadata.get());
}
AccessControlList acl = getAccessControlList(metadataJson);
String seriesId;
try {
DublinCoreCatalog createdSeries = seriesService.updateSeries(dc);
seriesId = createdSeries.getFirst(PROPERTY_IDENTIFIER);
seriesService.updateAccessControl(seriesId, acl);
for (Long id : themeId)
seriesService.updateSeriesProperty(seriesId, THEME_PROPERTY_NAME, Long.toString(id));
} catch (Exception e) {
logger.error("Unable to create new series: {}", getStackTrace(e));
throw new IndexServiceException("Unable to create new series");
}
updateSeriesMetadata(seriesId, metadataList);
return seriesId;
}
@Override
public Opt<Series> getSeries(String seriesId, AbstractSearchIndex searchIndex) throws SearchIndexException {
SearchResult<Series> result = searchIndex
.getByQuery(new SeriesSearchQuery(securityService.getOrganization().getId(), securityService.getUser())
.withIdentifier(seriesId));
// If the results list if empty, we return already a response.
if (result.getPageSize() == 0) {
logger.debug("Didn't find series with id {}", seriesId);
return Opt.none();
}
return Opt.some(result.getItems()[0].getSource());
}
@Override
public void removeSeries(String id) throws NotFoundException, SeriesException, UnauthorizedException {
SeriesQuery seriesQuery = new SeriesQuery();
seriesQuery.setSeriesId(id);
DublinCoreCatalogList dublinCoreCatalogList = seriesService.getSeries(seriesQuery);
if (dublinCoreCatalogList.size() == 0) {
throw new NotFoundException();
}
seriesService.deleteSeries(id);
}
@Override
public MetadataList updateCommonSeriesMetadata(String id, String metadataJSON, AbstractSearchIndex index)
throws IllegalArgumentException, IndexServiceException, NotFoundException, UnauthorizedException {
MetadataList metadataList = getMetadataListWithCommonSeriesCatalogUIAdapters();
return updateSeriesMetadata(id, metadataJSON, index, metadataList);
}
@Override
public MetadataList updateAllSeriesMetadata(String id, String metadataJSON, AbstractSearchIndex index)
throws IllegalArgumentException, IndexServiceException, NotFoundException, UnauthorizedException {
MetadataList metadataList = getMetadataListWithAllSeriesCatalogUIAdapters();
return updateSeriesMetadata(id, metadataJSON, index, metadataList);
}
@Override
public MetadataList updateAllSeriesMetadata(String id, MetadataList metadataList, AbstractSearchIndex index)
throws IndexServiceException, NotFoundException, UnauthorizedException {
checkSeriesExists(id, index);
updateSeriesMetadata(id, metadataList);
return metadataList;
}
@Override
public void updateCommentCatalog(final Event event, final List<EventComment> comments) throws Exception {
final Opt<MediaPackage> mpOpt = getEventMediapackage(event);
if (mpOpt.isNone())
return;
final SecurityContext securityContext = new SecurityContext(securityService, securityService.getOrganization(),
securityService.getUser());
executorService.execute(new Runnable() {
@Override
public void run() {
securityContext.runInContext(new Effect0() {
@Override
protected void run() {
try {
MediaPackage mediaPackage = mpOpt.get();
updateMediaPackageCommentCatalog(mediaPackage, comments);
switch (getEventSource(event)) {
case WORKFLOW:
logger.info("Update workflow mediapacakge {} with updated comments catalog.", event.getIdentifier());
Opt<WorkflowInstance> workflowInstance = getCurrentWorkflowInstance(event.getIdentifier());
if (workflowInstance.isNone()) {
logger.error("No workflow instance for event {} found!", event.getIdentifier());
throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
}
WorkflowInstance instance = workflowInstance.get();
instance.setMediaPackage(mediaPackage);
updateWorkflowInstance(instance);
break;
case ARCHIVE:
logger.info("Update archive mediapacakge {} with updated comments catalog.", event.getIdentifier());
assetManager.takeSnapshot(DEFAULT_OWNER, mediaPackage);
break;
default:
logger.error("Unkown event source {}!", event.getSource().toString());
}
} catch (Exception e) {
logger.error("Unable to update event {} comment catalog: {}", event.getIdentifier(), getStackTrace(e));
}
}
});
}
});
}
private void updateMediaPackageCommentCatalog(MediaPackage mediaPackage, List<EventComment> comments)
throws EventCommentException, IOException {
// Get the comments catalog
Catalog[] commentCatalogs = mediaPackage.getCatalogs(MediaPackageElements.COMMENTS);
Catalog c = null;
if (commentCatalogs.length == 1)
c = commentCatalogs[0];
if (comments.size() > 0) {
// If no comments catalog found, create a new one
if (c == null) {
c = (Catalog) MediaPackageElementBuilderFactory.newInstance().newElementBuilder().newElement(Type.Catalog,
MediaPackageElements.COMMENTS);
c.setIdentifier(UUID.randomUUID().toString());
mediaPackage.add(c);
}
// Update comments catalog
InputStream in = null;
try {
String commentCatalog = EventCommentParser.getAsXml(comments);
in = IOUtils.toInputStream(commentCatalog, "UTF-8");
URI uri = workspace.put(mediaPackage.getIdentifier().toString(), c.getIdentifier(), "comments.xml", in);
c.setURI(uri);
// setting the URI to a new source so the checksum will most like be invalid
c.setChecksum(null);
} finally {
IOUtils.closeQuietly(in);
}
} else {
// Remove comments catalog
if (c != null) {
mediaPackage.remove(c);
try {
workspace.delete(c.getURI());
} catch (NotFoundException e) {
logger.warn("Comments catalog {} not found to delete!", c.getURI());
}
}
}
}
@Override
public void changeOptOutStatus(String eventId, boolean optout, AbstractSearchIndex index)
throws NotFoundException, SchedulerException, SearchIndexException, UnauthorizedException {
Opt<Event> optEvent = getEvent(eventId, index);
if (optEvent.isNone())
throw new NotFoundException("Cannot find an event with id " + eventId);
schedulerService.updateOptOutStatus(eventId, optout);
logger.debug("Setting event {} to opt out status of {}", eventId, optout);
}
/**
* Checks to see if a given series exists.
*
* @param seriesID
* The id of the series.
* @param index
* The index to check for the particular series.
* @throws NotFoundException
* Thrown if unable to find the series.
* @throws IndexServiceException
* Thrown if unable to access the index to get the series.
*/
private void checkSeriesExists(String seriesID, AbstractSearchIndex index)
throws NotFoundException, IndexServiceException {
try {
Opt<Series> optSeries = getSeries(seriesID, index);
if (optSeries.isNone())
throw new NotFoundException("Cannot find a series with id " + seriesID);
} catch (SearchIndexException e) {
logger.error("Unable to get a series with id {} because: {}", seriesID, getStackTrace(e));
throw new IndexServiceException("Cannot use search service to find Series");
}
}
private MetadataList updateSeriesMetadata(String seriesID, String metadataJSON, AbstractSearchIndex index,
MetadataList metadataList)
throws IllegalArgumentException, IndexServiceException, NotFoundException, UnauthorizedException {
checkSeriesExists(seriesID, index);
try {
metadataList.fromJSON(metadataJSON);
} catch (Exception e) {
logger.warn("Not able to parse the event metadata {}: {}", metadataJSON, getStackTrace(e));
throw new IllegalArgumentException("Not able to parse the event metadata");
}
updateSeriesMetadata(seriesID, metadataList);
return metadataList;
}
/**
* @return A {@link MetadataList} with only the common SeriesCatalogUIAdapter's empty {@link MetadataCollection}
* available
*/
private MetadataList getMetadataListWithCommonSeriesCatalogUIAdapters() {
MetadataList metadataList = new MetadataList();
metadataList.add(seriesCatalogUIAdapter.getFlavor(), seriesCatalogUIAdapter.getUITitle(),
seriesCatalogUIAdapter.getRawFields());
return metadataList;
}
/**
* @return A {@link MetadataList} with all of the available CatalogUIAdapters empty {@link MetadataCollection}
* available
*/
@Override
public MetadataList getMetadataListWithAllSeriesCatalogUIAdapters() {
MetadataList metadataList = new MetadataList();
for (SeriesCatalogUIAdapter adapter : getSeriesCatalogUIAdapters()) {
metadataList.add(adapter.getFlavor(), adapter.getUITitle(), adapter.getRawFields());
}
return metadataList;
}
private MetadataList getMetadataListWithCommonEventCatalogUIAdapter() {
MetadataList metadataList = new MetadataList();
metadataList.add(eventCatalogUIAdapter, eventCatalogUIAdapter.getRawFields());
return metadataList;
}
@Override
public MetadataList getMetadataListWithAllEventCatalogUIAdapters() {
MetadataList metadataList = new MetadataList();
for (EventCatalogUIAdapter catalogUIAdapter : getEventCatalogUIAdapters()) {
metadataList.add(catalogUIAdapter, catalogUIAdapter.getRawFields());
}
return metadataList;
}
/**
* Checks the list of metadata for updated fields and stores/updates them in the respective metadata catalog.
*
* @param seriesId
* The series identifier
* @param metadataList
* The metadata list
*/
private void updateSeriesMetadata(String seriesId, MetadataList metadataList) {
for (SeriesCatalogUIAdapter adapter : seriesCatalogUIAdapters) {
Opt<MetadataCollection> metadata = metadataList.getMetadataByFlavor(adapter.getFlavor());
if (metadata.isSome() && metadata.get().isUpdated()) {
adapter.storeFields(seriesId, metadata.get());
}
}
}
public boolean isWorkflowActive(String workflowState) {
return WorkflowState.INSTANTIATED.toString().equals(workflowState)
|| WorkflowState.RUNNING.toString().equals(workflowState)
|| WorkflowState.PAUSED.toString().equals(workflowState);
}
@Override
public boolean hasActiveTransaction(String eventId)
throws NotFoundException, UnauthorizedException, IndexServiceException {
return false;
}
}