org.opencastproject.index.service.impl.IndexServiceImpl Maven / Gradle / Ivy
The 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.index.service.impl;
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 static org.opencastproject.security.api.DefaultOrganization.DEFAULT_ORGANIZATION_ID;
import static org.opencastproject.workflow.api.ConfiguredWorkflow.workflow;
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.assetmanager.util.WorkflowPropertiesUtil;
import org.opencastproject.assetmanager.util.Workflows;
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.elasticsearch.api.SearchIndexException;
import org.opencastproject.elasticsearch.api.SearchResult;
import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
import org.opencastproject.elasticsearch.index.objects.event.Event;
import org.opencastproject.elasticsearch.index.objects.event.EventSearchQuery;
import org.opencastproject.elasticsearch.index.objects.series.Series;
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.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.exception.UnsupportedAssetException;
import org.opencastproject.index.service.impl.util.EventHttpServletRequest;
import org.opencastproject.index.service.impl.util.EventUtils;
import org.opencastproject.index.service.impl.util.Retraction;
import org.opencastproject.index.service.impl.util.RetractionListener;
import org.opencastproject.index.service.util.JSONUtils;
import org.opencastproject.index.service.util.RequestUtils;
import org.opencastproject.index.service.util.RestUtils;
import org.opencastproject.ingest.api.IngestException;
import org.opencastproject.ingest.api.IngestService;
import org.opencastproject.list.api.ListProvidersService;
import org.opencastproject.mediapackage.Attachment;
import org.opencastproject.mediapackage.Catalog;
import org.opencastproject.mediapackage.EName;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElement;
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.metadata.dublincore.DCMIPeriod;
import org.opencastproject.metadata.dublincore.DublinCore;
import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
import org.opencastproject.metadata.dublincore.DublinCoreMetadataCollection;
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.MetadataField;
import org.opencastproject.metadata.dublincore.MetadataJson;
import org.opencastproject.metadata.dublincore.MetadataList;
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.Permissions;
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.security.util.SecurityUtil;
import org.opencastproject.series.api.SeriesException;
import org.opencastproject.series.api.SeriesService;
import org.opencastproject.util.Checksum;
import org.opencastproject.util.ChecksumType;
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.Tuple;
import org.opencastproject.workflow.api.ConfiguredWorkflow;
import org.opencastproject.workflow.api.WorkflowDatabaseException;
import org.opencastproject.workflow.api.WorkflowDefinition;
import org.opencastproject.workflow.api.WorkflowException;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowInstance.WorkflowState;
import org.opencastproject.workflow.api.WorkflowParsingException;
import org.opencastproject.workflow.api.WorkflowService;
import org.opencastproject.workspace.api.Workspace;
import com.entwinemedia.fn.data.Opt;
import com.google.common.net.MediaType;
import net.fortuna.ical4j.model.Period;
import net.fortuna.ical4j.model.property.RRule;
import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadException;
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.DateTimeZone;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
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.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
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.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
@Component(
immediate = true,
service = IndexService.class,
property = {
"service.description=Index Services Implementation"
}
)
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);
private final List eventCatalogUIAdapters = new ArrayList<>();
private final List seriesCatalogUIAdapters = new ArrayList<>();
/** A parser for handling JSON documents inside the body of a request. **/
private static final JSONParser parser = new JSONParser();
private String attachmentRegex = "^attachment.*";
private String catalogRegex = "^catalog.*";
private String trackRegex = "^track.*";
private String numberedAssetRegex = "^\\*$";
private Pattern patternAttachment = Pattern.compile(attachmentRegex);
private Pattern patternCatalog = Pattern.compile(catalogRegex);
private Pattern patternTrack = Pattern.compile(trackRegex);
private Pattern patternNumberedAsset = Pattern.compile(numberedAssetRegex);
private AclServiceFactory aclServiceFactory;
private AuthorizationService authorizationService;
private CaptureAgentStateService captureAgentStateService;
private EventCommentService eventCommentService;
private IngestService ingestService;
private ListProvidersService listProvidersService;
private AssetManager assetManager;
private SchedulerService schedulerService;
private SecurityService securityService;
private SeriesService seriesService;
private UserDirectoryService userDirectoryService;
private WorkflowService workflowService;
private Workspace workspace;
private ElasticsearchIndex elasticsearchIndex;
/** The single thread executor service */
private ExecutorService executorService = Executors.newSingleThreadExecutor();
private Map retractions = new ConcurrentHashMap<>();
/**
* OSGi DI.
*
* @param aclServiceFactory
* the factory to set
*/
@Reference
public void setAclServiceFactory(AclServiceFactory aclServiceFactory) {
this.aclServiceFactory = aclServiceFactory;
}
@Reference
public void setElasticsearchIndex(ElasticsearchIndex elasticsearchIndex) {
this.elasticsearchIndex = elasticsearchIndex;
}
/**
* OSGi DI.
*
* @param authorizationService
* the service to set
*/
@Reference
public void setAuthorizationService(AuthorizationService authorizationService) {
this.authorizationService = authorizationService;
}
/**
* OSGi DI.
*
* @param captureAgentStateService
* the service to set
*/
@Reference
public void setCaptureAgentStateService(CaptureAgentStateService captureAgentStateService) {
this.captureAgentStateService = captureAgentStateService;
}
/**
* OSGi callback for the event comment service.
*
* @param eventCommentService
* the service to set
*/
@Reference
public void setEventCommentService(EventCommentService eventCommentService) {
this.eventCommentService = eventCommentService;
}
/**
* OSGi callback to add {@link EventCatalogUIAdapter} instance.
*
* @param catalogUIAdapter
* the adapter to add
*/
@Reference(
name = "EventCatalogUIAdapter",
cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC,
unbind = "removeCatalogUIAdapter"
)
public void addCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) {
eventCatalogUIAdapters.add(catalogUIAdapter);
}
/**
* OSGi callback to remove {@link EventCatalogUIAdapter} instance.
*
* @param catalogUIAdapter
* the adapter to remove
*/
public void removeCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) {
eventCatalogUIAdapters.remove(catalogUIAdapter);
}
/**
* OSGi callback to add {@link SeriesCatalogUIAdapter} instance.
*
* @param catalogUIAdapter
* the adapter to add
*/
@Reference(
name = "SeriesCatalogUIAdapter",
cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC,
unbind = "removeCatalogUIAdapter"
)
public void addCatalogUIAdapter(SeriesCatalogUIAdapter catalogUIAdapter) {
seriesCatalogUIAdapters.add(catalogUIAdapter);
}
/**
* OSGi callback to remove {@link SeriesCatalogUIAdapter} instance.
*
* @param catalogUIAdapter
* the adapter to remove
*/
public void removeCatalogUIAdapter(SeriesCatalogUIAdapter catalogUIAdapter) {
seriesCatalogUIAdapters.remove(catalogUIAdapter);
}
/**
* OSGi DI.
*
* @param ingestService
* the service to set
*/
@Reference
public void setIngestService(IngestService ingestService) {
this.ingestService = ingestService;
}
/**
* OSGi DI.
*
* @param listProvidersService
* the service to set
*/
@Reference
public void setListProvidersService(ListProvidersService listProvidersService) {
this.listProvidersService = listProvidersService;
}
/**
* OSGi DI.
*
* @param assetManager
* the manager to set
*/
@Reference
public void setAssetManager(AssetManager assetManager) {
this.assetManager = assetManager;
}
/**
* OSGi DI.
*
* @param schedulerService
* the service to set
*/
@Reference
public void setSchedulerService(SchedulerService schedulerService) {
this.schedulerService = schedulerService;
}
/**
* OSGi DI.
*
* @param securityService
* the service to set
*/
@Reference
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
/**
* OSGi DI.
*
* @param seriesService
* the service to set
*/
@Reference
public void setSeriesService(SeriesService seriesService) {
this.seriesService = seriesService;
}
/**
* OSGi DI.
*
* @param workflowService
* the service to set
*/
@Reference
public void setWorkflowService(WorkflowService workflowService) {
this.workflowService = workflowService;
}
/**
* OSGi DI.
*
* @param workspace
* the workspace to set
*/
@Reference
public void setWorkspace(Workspace workspace) {
this.workspace = workspace;
}
/**
* OSGi DI.
*
* @param userDirectoryService
* the service to set
*/
@Reference
public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
this.userDirectoryService = userDirectoryService;
}
/**
*
* @return the acl service
*/
public AclService getAclService() {
return aclServiceFactory.serviceFor(securityService.getOrganization());
}
public List getEventCatalogUIAdapters(String organization) {
return eventCatalogUIAdapters.stream().filter(a -> a.handlesOrganization(organization))
.collect(Collectors.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 getSeriesCatalogUIAdapters(String organization) {
return seriesCatalogUIAdapters.stream().filter(a -> a.handlesOrganization(organization))
.collect(Collectors.toList());
}
public EventCatalogUIAdapter getCommonEventCatalogUIAdapter(String organization) {
Optional orgEventCatalogUIAdapter = eventCatalogUIAdapters.stream()
.filter(a -> a instanceof CommonEventCatalogUIAdapter)
.filter(a -> a.handlesOrganization(organization))
.findFirst();
if (orgEventCatalogUIAdapter.isPresent()) {
return orgEventCatalogUIAdapter.get();
} else if (!organization.equals(DEFAULT_ORGANIZATION_ID)) {
return getCommonEventCatalogUIAdapter(DEFAULT_ORGANIZATION_ID);
} else {
throw new IllegalStateException("Common event metadata for " + DEFAULT_ORGANIZATION_ID + " needs to be "
+ "configured!");
}
}
public SeriesCatalogUIAdapter getCommonSeriesCatalogUIAdapter(String organization) {
Optional orgSeriesCatalogUIAdapter = seriesCatalogUIAdapters.stream()
.filter(a -> a instanceof CommonSeriesCatalogUIAdapter)
.filter(a -> a.handlesOrganization(organization))
.findFirst();
if (orgSeriesCatalogUIAdapter.isPresent()) {
return orgSeriesCatalogUIAdapter.get();
} else if (!organization.equals(DEFAULT_ORGANIZATION_ID)) {
return getCommonSeriesCatalogUIAdapter(DEFAULT_ORGANIZATION_ID);
} else {
throw new IllegalStateException("Common series metadata for " + DEFAULT_ORGANIZATION_ID + " needs to be "
+ "configured!");
}
}
@Override
public List getEventCatalogUIAdapters() {
return new ArrayList<>(getEventCatalogUIAdapters(securityService.getOrganization().getId()));
}
@Override
public List getExtendedEventCatalogUIAdapters() {
String organization = securityService.getOrganization().getId();
return eventCatalogUIAdapters.stream().filter(a -> !(a instanceof CommonEventCatalogUIAdapter))
.filter(a -> a.handlesOrganization(organization)).collect(Collectors.toList());
}
@Override
public List getSeriesCatalogUIAdapters() {
return new LinkedList<>(getSeriesCatalogUIAdapters(securityService.getOrganization().getId()));
}
@Override
public EventCatalogUIAdapter getCommonEventCatalogUIAdapter() {
return getCommonEventCatalogUIAdapter(securityService.getOrganization().getId());
}
@Override
public SeriesCatalogUIAdapter getCommonSeriesCatalogUIAdapter() {
return getCommonSeriesCatalogUIAdapter(securityService.getOrganization().getId());
}
@Activate
public void activate(ComponentContext cc) {
workflowService.addWorkflowListener(new RetractionListener(this, securityService, retractions));
}
@Deactivate
public void deactivate(ComponentContext cc) {
executorService.shutdown();
}
@Override
public String createEvent(HttpServletRequest request) throws IndexServiceException, UnsupportedAssetException {
JSONObject metadataJson = null;
MediaPackage mp = null;
// regex for form field name matching an attachment or a catalog
// The first sub items identifies if the file is an attachment or catalog
// The second is the item flavor
// Example form field names: "catalog/captions/timedtext" and "attachment/captions/vtt"
// The prefix of field name for attachment and catalog
List assetList = new LinkedList();
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) new JSONParser().parse(metadata);
// in case of scheduling: Check if user has access to the CA
if (metadataJson.containsKey("source")) {
final JSONObject sourceJson = (JSONObject) metadataJson.get("source");
if (sourceJson.containsKey("metadata")) {
final JSONObject sourceMetadataJson = (JSONObject) sourceJson.get("metadata");
if (sourceMetadataJson.containsKey("device")) {
SecurityUtil.checkAgentAccess(securityService, (String) sourceMetadataJson.get("device"));
}
}
}
} catch (Exception e) {
logger.warn("Unable to parse metadata {}", metadata);
throw new IllegalArgumentException("Unable to parse metadata");
}
}
} else {
// AngularJS file upload lib appends ".0" to field name, so we cut that off
fieldName = fieldName.substring(0, fieldName.lastIndexOf("."));
final MediaType mediaType = MediaType.parse(item.getContentType());
final boolean accepted = RequestUtils.typeIsAccepted(item.getName(), fieldName, mediaType,
listProvidersService);
if (!accepted) {
throw new UnsupportedAssetException("Provided file format " + mediaType.toString() + " not allowed.");
}
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);
// For dynamic uploads, cannot get flavor at this point, so saving with temporary flavor
} else if (item.getFieldName().toLowerCase().matches(attachmentRegex)) {
assetList.add(item.getFieldName());
mp = ingestService.addAttachment(item.openStream(), item.getName(),
new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
} else if (item.getFieldName().toLowerCase().matches(catalogRegex)) {
// Cannot get flavor at this point, so saving with temporary flavor
assetList.add(item.getFieldName());
mp = ingestService.addCatalog(item.openStream(), item.getName(),
new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
} else if (item.getFieldName().toLowerCase().matches(trackRegex)) {
// Cannot get flavor at this point, so saving with temporary flavor
assetList.add(item.getFieldName());
mp = ingestService.addTrack(item.openStream(), item.getName(),
new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
} else {
logger.warn("Unknown field name found {}", item.getFieldName());
}
}
}
// MH-12085 update the flavors of any newly added assets.
try {
JSONArray assetMetadata = (JSONArray)((JSONObject) metadataJson.get("assets")).get("options");
if (assetMetadata != null) {
mp = updateMpAssetFlavor(assetList, mp, assetMetadata);
}
} catch (Exception e) {
// Assuming a parse error versus a file error and logging the error type
logger.warn("Unable to process asset metadata {}", metadataJson.get("assets"), e);
throw new IllegalArgumentException("Unable to parse metadata", e);
}
} 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 (FileUploadException | UnauthorizedException | ParseException | IngestException | SchedulerException
| MediaPackageException | IOException | NotFoundException e) {
logger.error("Unable to create event:", e);
throw new IndexServiceException("Unable to create event", e);
}
}
@Override
public String updateEventAssets(MediaPackage mp, HttpServletRequest request) throws IndexServiceException, UnsupportedAssetException {
JSONObject metadataJson = null;
// regex for form field name matching an attachment or a catalog
// The first sub items identifies if the file is an attachment or catalog
// The second is the item flavor
// Example form field names: "catalog/captions/timedtext" and "attachment/captions/vtt"
// The prefix of field name for attachment and catalog
// The metadata is expected to contain a workflow definition id and
// asset metadata mapped to the asset field id.
List assetList = new LinkedList();
// 1. save assets with temporary flavors
try {
if (!ServletFileUpload.isMultipartContent(request)) {
throw new IllegalArgumentException("No multipart content");
}
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 {
// AngularJS file upload lib appends ".0" to field name, so we cut that off
fieldName = fieldName.substring(0, fieldName.lastIndexOf("."));
final MediaType mediaType = MediaType.parse(item.getContentType());
final boolean accepted = RequestUtils.typeIsAccepted(item.getName(), fieldName, mediaType,
listProvidersService);
if (!accepted) {
throw new UnsupportedAssetException("Provided file format " + mediaType.toString() + " not allowed.");
}
if (item.getFieldName().toLowerCase().matches(attachmentRegex)) {
assetList.add(item.getFieldName());
// Add attachment with field name as temporary flavor
mp = ingestService.addAttachment(item.openStream(), item.getName(),
new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
} else if (item.getFieldName().toLowerCase().matches(catalogRegex)) {
assetList.add(item.getFieldName());
// Add catalog with field name as temporary flavor
mp = ingestService.addCatalog(item.openStream(), item.getName(),
new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
} else if (item.getFieldName().toLowerCase().matches(trackRegex)) {
// Cannot get flavor at this point, so saving with temporary flavor
assetList.add(item.getFieldName());
mp = ingestService.addTrack(item.openStream(), item.getName(),
new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
} else {
logger.warn("Unknown field name found {}", item.getFieldName());
}
}
}
// 2. remove existing assets of the new flavor
// and correct the temporary flavor to the new flavor.
try {
JSONArray assetMetadata = (JSONArray)((JSONObject) metadataJson.get("assets")).get("options");
if (assetMetadata != null) {
mp = updateMpAssetFlavor(assetList, mp, assetMetadata);
} else {
logger.warn("The asset option mapping parameter was not found");
throw new IndexServiceException("The asset option mapping parameter was not found");
}
} catch (Exception e) {
// Assuming a parse error versus a file error and logging the error type
logger.warn("Unable to process asset metadata {}", metadataJson.get("assets"), e);
throw new IllegalArgumentException("Unable to parse metadata", e);
}
return startAddAssetWorkflow(metadataJson, mp);
} catch (MediaPackageException | FileUploadException | IOException | IngestException e) {
logger.error("Unable to create event:", e);
throw new IndexServiceException("Unable to create event", e);
}
}
/**
* Parses the processing information, including the workflowDefinitionId, from the metadataJson and starts the
* workflow with the passed mediapackage.
* Example of processing json:
* ...., "processing": { "workflow": "full", "configuration": { "videoPreview": "false", "trimHold": "false",
* "captionHold": "false", "archiveOp": "true", "publishEngage": "true", "publishHarvesting": "true" } }, ....
*
* @param metadataJson
* @param mediaPackage
* @return the created workflow instance id
* @throws IndexServiceException
*/
private String startAddAssetWorkflow(JSONObject metadataJson, MediaPackage mediaPackage)
throws IndexServiceException {
String wfId = null;
String mpId = mediaPackage.getIdentifier().toString();
JSONObject processing = (JSONObject) metadataJson.get("processing");
if (processing == null)
throw new IllegalArgumentException("No processing field in metadata");
String workflowDefId = (String) processing.get("workflow");
if (workflowDefId == null)
throw new IllegalArgumentException("No workflow definition field in processing metadata");
JSONObject configJson = (JSONObject) processing.get("configuration");
try {
// Start the new workflow on the snapshot
// Workflow params are assumed to be String (not mixed with Number)
Map params = new HashMap();
if (configJson != null) {
for (Object key: configJson.keySet()) {
params.put((String)key, (String) configJson.get(key));
}
}
WorkflowInstance workflowInstance = workflowService.start(
workflowService.getWorkflowDefinitionById(workflowDefId), mediaPackage, params);
logger.info("Asset update and publish workflow {} scheduled for mp {}", workflowInstance.getId(), mpId);
} catch (AssetManagerException | WorkflowParsingException | UnauthorizedException e) {
throw new IndexServiceException("Unable to start workflow " + workflowDefId + " on " + mpId);
} catch (WorkflowDatabaseException e) {
logger.warn("Unable to load workflow '{}' from workflow service:", wfId, e);
} catch (NotFoundException e) {
logger.warn("Workflow '{}' not found", wfId);
}
return wfId;
}
/**
* 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) {
throw new IllegalArgumentException("Unable to parse access control list: " + accessJson.toJSONString());
}
}
return acl;
}
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();
MetadataJson.fillListFromJson(metadataList, allEventMetadataJson);
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());
DublinCoreMetadataCollection eventMetadata = eventHttpServletRequest.getMetadataList().get()
.getMetadataByAdapter(getCommonEventCatalogUIAdapter());
Date currentStartDate = null;
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");
}
if (StringUtils.isNotEmpty((String) sourceMetadata.get("start"))) {
currentStartDate = EncodingSchemeUtils.decodeDate((String) sourceMetadata.get("start"));
}
}
MetadataField startDate = eventMetadata.getOutputFields().get("startDate");
if (startDate != null && startDate.isUpdated() && startDate.getValue() != null) {
SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(startDate.getPattern());
currentStartDate = sdf.parse((String) startDate.getValue());
} else if (currentStartDate != null) {
eventMetadata.removeField(startDate);
MetadataField newStartDate = new MetadataField(startDate);
newStartDate.setValue(EncodingSchemeUtils.encodeDate(currentStartDate, Precision.Fraction).getValue());
eventMetadata.addField(newStartDate);
}
// This field is null when it is not used in the Admin UI event details metadata tab.
// If used, set it to the the start Date or a new date.
// Note, even though this field borrows the DublinCore.PROPERTY_CREATED key,
// the startDate is used to update the DublinCore catalog PROPERTY_CREATED field,
// event, and mediapackage start fields.
MetadataField created = eventMetadata.getOutputFields().get(DublinCore.PROPERTY_CREATED.getLocalName());
if (created != null && (!created.isUpdated() || created.getValue() == null)) {
eventMetadata.removeField(created);
MetadataField newCreated = new MetadataField(created);
if (currentStartDate != null) {
newCreated.setValue(EncodingSchemeUtils.encodeDate(currentStartDate, Precision.Second).getValue());
} else {
newCreated.setValue(EncodingSchemeUtils.encodeDate(new Date(), Precision.Second).getValue());
}
eventMetadata.addField(newCreated);
}
// Get presenter usernames for use as technical presenters
Set presenterUsernames = new HashSet<>();
Opt> technicalPresenters = updatePresenters(eventMetadata);
if (technicalPresenters.isSome()) {
presenterUsernames = technicalPresenters.get();
}
eventHttpServletRequest.getMetadataList().get().add(getCommonEventCatalogUIAdapter(), 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"), 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.debug(
"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 configuration = new HashMap<>();
if (eventHttpServletRequest.getProcessing().get().get("configuration") != null) {
configuration = new HashMap<>((JSONObject) eventHttpServletRequest.getProcessing().get().get("configuration"));
}
for (Entry 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());
MediaPackage mediaPackage;
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().toString();
case SCHEDULE_SINGLE:
mediaPackage = updateDublincCoreCatalog(eventHttpServletRequest.getMediaPackage().get(), dc);
eventHttpServletRequest.setMediaPackage(mediaPackage);
try {
schedulerService.addEvent(start.toDate(), start.plus(duration).toDate(), captureAgentId, presenterUsernames,
mediaPackage, configuration, (Map) caProperties, Opt. none());
} finally {
for (MediaPackageElement mediaPackageElement : mediaPackage.getElements()) {
try {
workspace.delete(mediaPackage.getIdentifier().toString(), mediaPackageElement.getIdentifier());
} catch (NotFoundException | IOException e) {
logger.warn("Failed to delete media package element", e);
}
}
}
return mediaPackage.getIdentifier().toString();
case SCHEDULE_MULTIPLE:
final Map scheduled = schedulerService.addMultipleEvents(rRule, start.toDate(), end.toDate(), duration, tz, captureAgentId,
presenterUsernames, eventHttpServletRequest.getMediaPackage().get(), configuration, (Map) caProperties, Opt.none());
return StringUtils.join(scheduled.keySet(), ",");
default:
throw new IllegalArgumentException("Unknown source type: " + 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;
Optional dcOpt = DublinCoreUtil.loadEpisodeDublinCore(workspace,
eventHttpServletRequest.getMediaPackage().get());
if (dcOpt.isPresent()) {
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 DublinCoreMetadataCollection} to have friendly names loaded by the
* {@link UserDirectoryService} and return the usernames of the presenters.
*
* @param eventMetadata
* The {@link DublinCoreMetadataCollection} 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> updatePresenters(DublinCoreMetadataCollection eventMetadata) {
MetadataField presentersMetadataField = eventMetadata.getOutputFields()
.get(DublinCore.PROPERTY_CREATOR.getLocalName());
if (presentersMetadataField.isUpdated()) {
Tuple, Set> updatedPresenters = getTechnicalPresenters(eventMetadata);
Set presenterUsernames = updatedPresenters.getB();
eventMetadata.removeField(presentersMetadataField);
MetadataField newPresentersMetadataField = new MetadataField(presentersMetadataField);
newPresentersMetadataField.setValue(updatedPresenters.getA());
eventMetadata.addField(newPresentersMetadataField);
return Opt.some(presenterUsernames);
} else {
return Opt.none();
}
}
/**
*
* @param mp
* the mediapackage to update
* @param dc
* the dublincore metadata to use to update the mediapackage
* @return the updated mediapackage
* @throws IOException
* Thrown if an IO error occurred adding the dc catalog file
* @throws MediaPackageException
* Thrown if an error occurred updating the mediapackage
* @throws IngestException
* Thrown if an error occurred attaching the catalog to the mediapackage
*/
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;
}
/**
* Update the flavor of newly added asset with the passed metadata
*
* @param assetList
* the list of assets to update
* @param mp
* the mediapackage to update
* @param assetMetadata
* a set of mapping metadata for the asset list
* @return mediapackage updated with assets
*/
@SuppressWarnings("unchecked")
protected MediaPackage updateMpAssetFlavor(List assetList, MediaPackage mp, JSONArray assetMetadata) {
// Create JSONObject data map
JSONObject assetDataMap = new JSONObject();
for (int i = 0; i < assetMetadata.size(); i++) {
try {
assetDataMap.put(((JSONObject) assetMetadata.get(i)).get("id"), assetMetadata.get(i));
} catch (Exception e) {
throw new IllegalArgumentException("Unable to parse metadata", e);
}
}
// Find the correct flavor for each asset.
for (String assetOrig: assetList) {
// expecting file assets to contain postfix "track_trackpart.0"
String asset = assetOrig;
String assetNumber = null;
String[] assetNameParts = asset.split(Pattern.quote("."));
if (assetNameParts.length > 1) {
asset = assetNameParts[0];
assetNumber = assetNameParts[1];
}
try {
if ((assetMetadata != null) && (assetDataMap.get(asset) != null)) {
String type = (String)((JSONObject) assetDataMap.get(asset)).get("type");
String flavorType = (String)((JSONObject) assetDataMap.get(asset)).get("flavorType");
String flavorSubType = (String)((JSONObject) assetDataMap.get(asset)).get("flavorSubType");
String tags = (String)((JSONObject) assetDataMap.get(asset)).get("tags");
String[] tagsArray = null;
// Captions may have lang:LANG_CODE tag set.
String langTag = null;
if (tags != null) {
tagsArray = tags.split(",");
for (String tag : tagsArray) {
if (StringUtils.startsWith(StringUtils.trimToEmpty(tag), "lang:")) {
langTag = StringUtils.trimToEmpty(tag);
break;
}
}
}
// Use 'multiple' setting to allow multiple elements with same flavor or not.
boolean overwriteExisting = !(Boolean) ((JSONObject) assetDataMap.get(asset)).getOrDefault("multiple", false);
if (patternNumberedAsset.matcher(flavorSubType).matches() && (assetNumber != null)) {
flavorSubType = assetNumber;
}
MediaPackageElementFlavor newElemflavor = new MediaPackageElementFlavor(flavorType, flavorSubType);
if (patternAttachment.matcher(type).matches()) {
if (overwriteExisting) {
// remove existing attachments of the new flavor
Attachment[] existing = mp.getAttachments(newElemflavor);
for (int i = 0; i < existing.length; i++) {
// if lang tag is set, we should only remove elements with the same lang tag
if (null == langTag || existing[i].containsTag(langTag)) {
mp.remove(existing[i]);
logger.info("Overwriting existing asset {} {}", type, newElemflavor);
}
}
}
// correct the flavor of the new attachment
Attachment[] elArray = mp.getAttachments(new MediaPackageElementFlavor(assetOrig, "*"));
elArray[0].setFlavor(newElemflavor);
if (tags != null && tagsArray.length > 0) {
for (String tag : tagsArray) {
elArray[0].addTag(tag);
}
}
logger.info("Updated asset {} {}", type, newElemflavor);
} else if (patternCatalog.matcher(type).matches()) {
if (overwriteExisting) {
// remove existing catalogs of the new flavor
Catalog[] existing = mp.getCatalogs(newElemflavor);
for (int i = 0; i < existing.length; i++) {
// if lang tag is set, we should only remove elements with the same lang tag
if (null == langTag || existing[i].containsTag(langTag)) {
mp.remove(existing[i]);
logger.info("Overwriting existing asset {} {}", type, newElemflavor);
}
}
}
Catalog[] catArray = mp.getCatalogs(new MediaPackageElementFlavor(assetOrig, "*"));
if (catArray.length > 1) {
throw new IllegalArgumentException("More than one " + asset + " found, only one expected.");
}
catArray[0].setFlavor(newElemflavor);
if (tags != null && tagsArray.length > 0) {
for (String tag : tagsArray) {
catArray[0].addTag(tag);
}
}
logger.info("Update asset {} {}", type, newElemflavor);
} else if (patternTrack.matcher(type).matches()) {
if (overwriteExisting) {
// remove existing catalogs of the new flavor
Track[] existing = mp.getTracks(newElemflavor);
for (int i = 0; i < existing.length; i++) {
// if lang tag is set, we should only remove elements with the same lang tag
if (null == langTag || existing[i].containsTag(langTag)) {
mp.remove(existing[i]);
logger.info("Overwriting existing asset {} {}", type, newElemflavor);
}
}
}
Track[] trackArray = mp.getTracks(new MediaPackageElementFlavor(assetOrig, "*"));
if (trackArray.length > 1) {
throw new IllegalArgumentException("More than one " + asset + " found, only one expected.");
}
trackArray[0].setFlavor(newElemflavor);
if (tags != null && tagsArray.length > 0) {
for (String tag : tagsArray) {
trackArray[0].addTag(tag);
}
}
logger.info("Update asset {} {}", type, newElemflavor);
} else {
logger.warn("Unknown asset type {} {} for field {}", type, newElemflavor, asset);
}
}
} catch (Exception e) {
// Assuming a parse error versus a file error and logging the error type
throw new IllegalArgumentException("Unable to parse metadata: " + assetMetadata.toJSONString(), e);
}
}
return mp;
}
@Override
public MetadataList updateAllEventMetadata(
final String id, final String metadataJSON, final ElasticsearchIndex index)
throws IllegalArgumentException, IndexServiceException, NotFoundException, SearchIndexException,
UnauthorizedException {
final MetadataList metadataList;
try {
metadataList = getMetadataListWithAllEventCatalogUIAdapters();
MetadataJson.fillListFromJson(metadataList, (JSONArray) new JSONParser().parse(metadataJSON));
} catch (final org.json.simple.parser.ParseException 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 {
MediaPackage mediaPackage = getEventMediapackage(event);
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:
try {
Optional workflowInstance = workflowService.
getRunningWorkflowInstanceByMediaPackage(event.getIdentifier(), Permissions.Action.WRITE.toString());
if (workflowInstance.isEmpty()) {
throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
}
WorkflowInstance instance = workflowInstance.get();
instance.setMediaPackage(mediaPackage);
updateWorkflowInstance(instance);
} catch (WorkflowException e) {
throw new IndexServiceException("Unable to remove catalog with flavor '" + flavor
+ "' by updating workflow event " + event.getIdentifier(), e);
}
break;
case ARCHIVE:
assetManager.takeSnapshot(mediaPackage);
break;
case SCHEDULE:
try {
schedulerService.updateEvent(event.getIdentifier(), Opt.none(), Opt.none(), Opt.none(), Opt.none(),
Opt.some(mediaPackage), Opt.none(), Opt.none());
} catch (SchedulerException e) {
throw new IndexServiceException("Unable to remove catalog with flavor " + flavor + " by updating scheduled "
+ "event " + event.getIdentifier(), e);
}
break;
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, ElasticsearchIndex index)
throws IndexServiceException, SearchIndexException, NotFoundException, UnauthorizedException {
Opt optEvent = getEvent(id, index);
if (optEvent.isNone())
throw new NotFoundException("Cannot find an event with id " + id);
Event event = optEvent.get();
MediaPackage mediaPackage = getEventMediapackage(event);
updateMediaPackageMetadata(mediaPackage, metadataList);
switch (getEventSource(event)) {
case WORKFLOW:
try {
Optional workflowInstance = workflowService.
getRunningWorkflowInstanceByMediaPackage(event.getIdentifier(), Permissions.Action.WRITE.toString());
if (workflowInstance.isEmpty()) {
throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
}
WorkflowInstance instance = workflowInstance.get();
instance.setMediaPackage(mediaPackage);
updateWorkflowInstance(instance);
} catch (WorkflowException e) {
throw new IndexServiceException("Unable to update workflow event " + id + " with metadata "
+ RestUtils.getJsonStringSilent(MetadataJson.listToJson(metadataList, true)), e);
}
break;
case ARCHIVE:
assetManager.takeSnapshot(mediaPackage);
break;
case SCHEDULE:
DublinCoreMetadataCollection eventCatalog = metadataList.getMetadataByAdapter(getCommonEventCatalogUIAdapter());
Opt> presenters = eventCatalog == null ? Opt.none() : updatePresenters(eventCatalog);
try {
schedulerService.updateEvent(id, Opt.none(), Opt.none(), Opt.none(), presenters, Opt.some(mediaPackage),
Opt.none(), Opt.none());
} catch (SchedulerException e) {
throw new IndexServiceException("Unable to update scheduled event " + id + " with metadata "
+ RestUtils.getJsonStringSilent(MetadataJson.listToJson(metadataList, true)), e);
}
break;
default:
logger.error("Unknown 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, Set> getTechnicalPresenters(DublinCoreMetadataCollection eventMetadata) {
MetadataField presentersMetadataField = eventMetadata.getOutputFields()
.get(DublinCore.PROPERTY_CREATOR.getLocalName());
List presenters = new ArrayList<>();
Set 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, ElasticsearchIndex index)
throws IllegalArgumentException, IndexServiceException, SearchIndexException, NotFoundException,
UnauthorizedException {
Opt optEvent = getEvent(id, index);
if (optEvent.isNone())
throw new NotFoundException("Cannot find an event with id " + id);
Event event = optEvent.get();
MediaPackage mediaPackage = getEventMediapackage(event);
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:
try {
mediaPackage = authorizationService.setAcl(mediaPackage, AclScope.Episode, acl).getA();
} catch (MediaPackageException e) {
throw new IndexServiceException("Unable to update acl", e);
}
assetManager.takeSnapshot(mediaPackage);
return acl;
case SCHEDULE:
try {
mediaPackage = authorizationService.setAcl(mediaPackage, AclScope.Episode, acl).getA();
schedulerService.updateEvent(id, Opt.none(), Opt.none(), Opt.none(), Opt.none(), Opt.some(mediaPackage),
Opt.none(), Opt.none());
} catch (SchedulerException | MediaPackageException e) {
throw new IndexServiceException("Unable to update the acl for the scheduled event", e);
}
return acl;
default:
throw new IndexServiceException(
String.format("Unable to update the ACL as '%s' is an unknown event source.", getEventSource(event)));
}
}
private boolean hasSnapshots(String eventId) {
AQueryBuilder q = assetManager.createQuery();
return !enrich(q.select(q.snapshot()).where(q.mediaPackageId(eventId).and(q.version().isLatest())).run()).getSnapshots().isEmpty();
}
@Override
public Map> getEventWorkflowProperties(final List eventIds) {
return WorkflowPropertiesUtil.getLatestWorkflowPropertiesForEvents(assetManager, eventIds);
}
@Override
public Opt getEvent(String id, ElasticsearchIndex index) throws SearchIndexException {
SearchResult 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 EventRemovalResult removeEvent(Event event, String retractWorkflowId)
throws UnauthorizedException, WorkflowDatabaseException, NotFoundException {
final boolean hasOnlyEngageLive = event.getPublications().size() == 1
&& EventUtils.ENGAGE_LIVE_CHANNEL_ID.equals(event.getPublications().get(0).getChannel());
final boolean retract = event.hasPreview()
|| (!event.getPublications().isEmpty() && !hasOnlyEngageLive && this.hasSnapshots(event.getIdentifier()));
if (retract) {
retractAndRemoveEvent(event.getIdentifier(), retractWorkflowId);
return EventRemovalResult.RETRACTING;
} else {
try {
final boolean success = removeEvent(event.getIdentifier());
return success ? EventRemovalResult.SUCCESS : EventRemovalResult.GENERAL_FAILURE;
} catch (NotFoundException e) {
return EventRemovalResult.NOT_FOUND;
}
}
}
private void retractAndRemoveEvent(String id, String retractWorkflowId)
throws WorkflowDatabaseException, NotFoundException {
final WorkflowDefinition wfd = workflowService.getWorkflowDefinitionById(retractWorkflowId);
final Workflows workflows = new Workflows(assetManager, workflowService);
final ConfiguredWorkflow workflow = workflow(wfd);
final List result = workflows.applyWorkflowToLatestVersion(Collections.singleton(id), workflow).toList();
if (result.size() != 1) {
throw new IllegalStateException("Couldn't start workflow to retract media package" + id);
}
this.retractions.put(
result.get(0).getId(),
new Retraction(securityService.getUser(), securityService.getOrganization())
);
}
@Override
public boolean removeEvent(String id) throws NotFoundException, UnauthorizedException {
boolean unauthorizedWorkflow = false;
boolean notFoundWorkflow = false;
boolean removedWorkflow = false;
try {
List workflowInstances = workflowService.getWorkflowInstancesByMediaPackage(id);
if (workflowInstances.isEmpty()) {
notFoundWorkflow = true;
} else {
for (WorkflowInstance instance : workflowInstances) {
workflowService.stop(instance.getId());
workflowService.remove(instance.getId());
}
removedWorkflow = true;
}
} catch (NotFoundException e) {
notFoundWorkflow = true;
} catch (UnauthorizedException e) {
unauthorizedWorkflow = true;
} catch (WorkflowException e) {
logger.error("Unable to remove the event '{}' because removing workflow failed:", id, e);
}
boolean unauthorizedScheduler = false;
boolean notFoundScheduler = false;
boolean removedScheduler = false;
try {
schedulerService.removeEvent(id);
removedScheduler = true;
} catch (NotFoundException e) {
notFoundScheduler = true;
} catch (UnauthorizedException e) {
unauthorizedScheduler = true;
} catch (SchedulerException e) {
logger.error("Unable to remove the event '{}' from scheduler service:", id, e);
}
boolean unauthorizedArchive = false;
boolean notFoundArchive = false;
boolean removedArchive = false;
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();
removedArchive = true;
} else {
notFoundArchive = true;
}
} catch (AssetManagerException e) {
if (e.getCause() instanceof UnauthorizedException) {
unauthorizedArchive = true;
} else if (e.getCause() instanceof NotFoundException) {
notFoundArchive = true;
} else {
logger.error("Unable to remove the event '{}' from the archive:", id, e);
}
}
if (unauthorizedScheduler || unauthorizedWorkflow || unauthorizedArchive)
throw new UnauthorizedException("Not authorized to remove event id " + id);
// if all three services either removed the event successfully or couldn't find it, make sure it's also removed
// from the index
if ((removedScheduler || notFoundScheduler) && (removedWorkflow || notFoundWorkflow)
&& (removedArchive || notFoundArchive)) {
try {
elasticsearchIndex.deleteEvent(id, securityService.getOrganization().getId());
} catch (SearchIndexException e) {
logger.error("Removing event {} from the {} index failed", id, elasticsearchIndex.getIndexName(), e);
}
}
try {
eventCommentService.deleteComments(id);
} catch (EventCommentException e) {
logger.error("Unable to remove comments for event '{}':", id, e);
}
if (notFoundScheduler && notFoundWorkflow && notFoundArchive)
throw new NotFoundException("Event id " + id + " not found.");
return ((removedScheduler || notFoundScheduler) && (removedWorkflow || notFoundWorkflow)
&& (removedArchive || notFoundArchive));
}
private 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 MediaPackage getEventMediapackage(Event event) throws IndexServiceException {
switch (getEventSource(event)) {
case WORKFLOW:
try {
Optional currentWorkflowInstance = workflowService.
getRunningWorkflowInstanceByMediaPackage(event.getIdentifier(), Permissions.Action.READ.toString());
if (currentWorkflowInstance.isEmpty()) {
throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
}
return currentWorkflowInstance.get().getMediaPackage();
} catch (WorkflowDatabaseException e) {
throw new IndexServiceException("Unable to get current workflow instance for event with id " + event.getIdentifier() + " from workflow service", e);
} catch (UnauthorizedException e) {
throw new IndexServiceException("Not authorized to read media package " + event.getIdentifier() + " from workflow", e);
} catch (WorkflowException e) {
throw new IndexServiceException("Unable to get event media package " + event.getIdentifier() + " from WorkflowService because", e);
}
case ARCHIVE:
Optional mpOpt = assetManager.getMediaPackage(event.getIdentifier());
if (mpOpt.isPresent()) {
logger.debug("Found event in archive with id {}", event.getIdentifier());
return mpOpt.get();
}
throw new IndexServiceException("No archived event found with id " + event.getIdentifier());
case SCHEDULE:
try {
MediaPackage mediaPackage = schedulerService.getMediaPackage(event.getIdentifier());
logger.debug("Found event in scheduler with id {}", event.getIdentifier());
return mediaPackage;
} catch (NotFoundException e) {
throw new IndexServiceException("No scheduled event with id " + event.getIdentifier(), e);
} catch (UnauthorizedException e) {
throw new IndexServiceException("Unauthorized to get event " + event.getIdentifier() + " from scheduler", e);
} catch (SchedulerException e) {
throw new IndexServiceException("Unable to get event " + event.getIdentifier() + " from scheduler", e);
}
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;
} else if (event.isScheduledEvent() && !event.hasRecordingStarted()) {
return Source.SCHEDULE;
} else if (event.getArchiveVersion() != null) {
return Source.ARCHIVE;
} else if (event.getWorkflowId() != null) {
return Source.WORKFLOW;
} else {
return Source.SCHEDULE;
}
}
private void updateMediaPackageMetadata(MediaPackage mp, MetadataList metadataList) {
String oldSeriesId = mp.getSeries();
for (EventCatalogUIAdapter catalogUIAdapter : getEventCatalogUIAdapters()) {
final DublinCoreMetadataCollection metadata = metadataList.getMetadataByAdapter(catalogUIAdapter);
if (metadata != null && metadata.isUpdated()) {
catalogUIAdapter.storeFields(mp, metadata);
}
}
// update series catalogs
if (!StringUtils.equals(oldSeriesId, mp.getSeries())) {
List seriesDcTags = new ArrayList<>();
List seriesAclTags = new ArrayList<>();
Map> seriesExtDcTags = new HashMap<>();
if (StringUtils.isNotBlank(oldSeriesId)) {
// remove series dublincore from the media package
for (MediaPackageElement mpe : mp.getElementsByFlavor(MediaPackageElements.SERIES)) {
mp.remove(mpe);
seriesDcTags.addAll(Arrays.asList(mpe.getTags()));
}
// remove series ACL from the media package
for (MediaPackageElement mpe : mp.getElementsByFlavor(MediaPackageElements.XACML_POLICY_SERIES)) {
mp.remove(mpe);
seriesAclTags.addAll(Arrays.asList(mpe.getTags()));
}
// remove series extended metadata from the media package
try {
Opt