/**
* 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.workflow.handler.workflow;
import org.opencastproject.job.api.JobContext;
import org.opencastproject.mediapackage.Attachment;
import org.opencastproject.mediapackage.DefaultMediaPackageSerializerImpl;
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.MediaPackageException;
import org.opencastproject.mediapackage.MediaPackageParser;
import org.opencastproject.mediapackage.MediaPackageSerializer;
import org.opencastproject.util.Checksum;
import org.opencastproject.util.ChecksumType;
import org.opencastproject.util.FileSupport;
import org.opencastproject.util.MimeTypes;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.ZipUtil;
import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowOperationException;
import org.opencastproject.workflow.api.WorkflowOperationInstance;
import org.opencastproject.workflow.api.WorkflowOperationResult;
import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
import org.opencastproject.workspace.api.Workspace;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Produces a zipped archive of a mediapackage, places it in the archive collection, and removes the rest of the
* mediapackage elements from both the mediapackage xml and if possible, from storage altogether.
*/
public class ZipWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
/** The logger */
private static final Logger logger = LoggerFactory.getLogger(ZipWorkflowOperationHandler.class);
/** The workflow operation's property to consult to determine the collection to use to store an archive */
public static final String ZIP_COLLECTION_PROPERTY = "zip-collection";
/** The element flavors to include in the zip file */
public static final String INCLUDE_FLAVORS_PROPERTY = "include-flavors";
/** The zip archive's target element flavor */
public static final String TARGET_FLAVOR_PROPERTY = "target-flavor";
/** The zip archive's target tags */
public static final String TARGET_TAGS_PROPERTY = "target-tags";
/** The property indicating whether to apply compression to the archive */
public static final String COMPRESS_PROPERTY = "compression";
/** The default collection in the working file repository to store archives */
public static final String DEFAULT_ZIP_COLLECTION = "zip";
/** The default location to use when building an zip archive relative to the
* storage directory */
public static final String DEFAULT_ZIP_ARCHIVE_TEMP_DIR = "tmp/zip";
/** Key for configuring the location of the archive-temp folder */
public static final String ZIP_ARCHIVE_TEMP_DIR_CFG_KEY =
"org.opencastproject.workflow.handler.workflow.ZipWorkflowOperationHandler.tmpdir";
/** The default flavor to use for a mediapackage archive */
public static final MediaPackageElementFlavor DEFAULT_ARCHIVE_FLAVOR = MediaPackageElementFlavor
.parseFlavor("archive/zip");
/** The temporary storage location */
protected File tempStorageDir = null;
/** The configuration properties */
protected SortedMap<String, String> configurationOptions = null;
/**
* The workspace to use in retrieving and storing files.
*/
protected Workspace workspace;
/** The default no-arg constructor builds the configuration options set */
public ZipWorkflowOperationHandler() {
configurationOptions = new TreeMap<String, String>();
configurationOptions.put(ZIP_COLLECTION_PROPERTY,
"The configuration key that specifies the zip archive collection. Defaults to " + DEFAULT_ZIP_COLLECTION);
configurationOptions.put(COMPRESS_PROPERTY,
"The configuration key that specifies whether to compress the zip archive. Defaults to false.");
configurationOptions.put(INCLUDE_FLAVORS_PROPERTY,
"The configuration key that specifies the element flavors to include in the zipped mediapackage archive");
configurationOptions.put(TARGET_FLAVOR_PROPERTY, "The target flavor for the zip archive element, defaulting to '"
+ DEFAULT_ARCHIVE_FLAVOR + "'");
configurationOptions.put(TARGET_FLAVOR_PROPERTY, "The target flavor for the zip archive element, defaulting to '"
+ DEFAULT_ARCHIVE_FLAVOR + "'");
configurationOptions.put(TARGET_TAGS_PROPERTY, "The target tags for the zip archive element");
}
/**
* Sets the workspace to use.
*
* @param workspace
* the workspace
*/
public void setWorkspace(Workspace workspace) {
this.workspace = workspace;
}
/**
* Activate the component, generating the temporary storage directory for
* building zip archives if necessary.
*
* {@inheritDoc}
*
* @see org.opencastproject.workflow.api.AbstractWorkflowOperationHandler#activate(org.osgi.service.component.ComponentContext)
*/
protected void activate(ComponentContext cc) {
tempStorageDir = StringUtils.isNotBlank(cc.getBundleContext().getProperty(ZIP_ARCHIVE_TEMP_DIR_CFG_KEY))
? new File(cc.getBundleContext().getProperty(ZIP_ARCHIVE_TEMP_DIR_CFG_KEY))
: new File(cc.getBundleContext().getProperty("org.opencastproject.storage.dir"), DEFAULT_ZIP_ARCHIVE_TEMP_DIR);
// create directory
try {
FileUtils.forceMkdir(tempStorageDir);
} catch (IOException e) {
logger.error("Could not create temporary directory for ZIP archives: `{}`", tempStorageDir.getAbsolutePath());
throw new IllegalStateException(e);
}
// Clean up tmp dir on start-up
try {
FileUtils.cleanDirectory(tempStorageDir);
} catch (IOException e) {
logger.error("Could not clean temporary directory for ZIP archives: `{}`", tempStorageDir.getAbsolutePath());
throw new IllegalStateException(e);
}
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.workflow.api.AbstractWorkflowOperationHandler#start(org.opencastproject.workflow.api.WorkflowInstance,
* JobContext)
*/
@Override
public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
throws WorkflowOperationException {
if (workflowInstance == null) {
throw new WorkflowOperationException("Invalid workflow instance");
}
final MediaPackage mediaPackage = workflowInstance.getMediaPackage();
final WorkflowOperationInstance currentOperation = workflowInstance.getCurrentOperation();
if (currentOperation == null) {
throw new WorkflowOperationException("Cannot get current workflow operation");
}
String flavors = currentOperation.getConfiguration(INCLUDE_FLAVORS_PROPERTY);
final List<MediaPackageElementFlavor> flavorsToZip = new ArrayList<MediaPackageElementFlavor>();
MediaPackageElementFlavor targetFlavor = DEFAULT_ARCHIVE_FLAVOR;
// Read the target flavor
String targetFlavorOption = currentOperation.getConfiguration(TARGET_FLAVOR_PROPERTY);
try {
targetFlavor = targetFlavorOption == null ? DEFAULT_ARCHIVE_FLAVOR : MediaPackageElementFlavor.parseFlavor(targetFlavorOption);
logger.trace("Using '{}' as the target flavor for the zip archive of recording {}", targetFlavor, mediaPackage);
} catch (IllegalArgumentException e) {
throw new WorkflowOperationException("Flavor '" + targetFlavorOption + "' is not valid", e);
}
// Read the target tags
String targetTagsOption = StringUtils.trimToEmpty(currentOperation.getConfiguration(TARGET_TAGS_PROPERTY));
String[] targetTags = StringUtils.split(targetTagsOption, ",");
// If the configuration does not specify flavors, just zip them all
if (flavors == null) {
flavorsToZip.add(MediaPackageElementFlavor.parseFlavor("*/*"));
} else {
for (String flavor : asList(flavors)) {
flavorsToZip.add(MediaPackageElementFlavor.parseFlavor(flavor));
}
}
logger.info("Archiving mediapackage {} in workflow {}", mediaPackage, workflowInstance);
String compressProperty = currentOperation.getConfiguration(COMPRESS_PROPERTY);
boolean compress = compressProperty == null ? false : Boolean.valueOf(compressProperty);
// Zip the contents of the mediapackage
File zip = null;
try {
logger.info("Creating zipped archive of recording {}", mediaPackage);
zip = zip(mediaPackage, flavorsToZip, compress);
} catch (Exception e) {
throw new WorkflowOperationException("Unable to create a zip archive from mediapackage " + mediaPackage, e);
}
// Get the collection for storing the archived mediapackage
String configuredCollectionId = currentOperation.getConfiguration(ZIP_COLLECTION_PROPERTY);
String collectionId = configuredCollectionId == null ? DEFAULT_ZIP_COLLECTION : configuredCollectionId;
// Add the zip as an attachment to the mediapackage
logger.info("Moving zipped archive of recording {} to the working file repository collection '{}'", mediaPackage,
collectionId);
InputStream in = null;
URI uri = null;
try {
in = new FileInputStream(zip);
uri = workspace.putInCollection(collectionId, mediaPackage.getIdentifier().compact() + ".zip", in);
logger.info("Zipped archive of recording {} is available from {}", mediaPackage, uri);
} catch (FileNotFoundException e) {
throw new WorkflowOperationException("zip file " + zip + " not found", e);
} catch (IOException e) {
throw new WorkflowOperationException(e);
} finally {
IOUtils.closeQuietly(in);
}
Attachment attachment = (Attachment) MediaPackageElementBuilderFactory.newInstance().newElementBuilder()
.elementFromURI(uri, Type.Attachment, targetFlavor);
try {
attachment.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, zip));
} catch (IOException e) {
throw new WorkflowOperationException(e);
}
attachment.setMimeType(MimeTypes.ZIP);
// Apply the target tags
for (String tag : targetTags) {
attachment.addTag(tag);
logger.trace("Tagging the archive of recording '{}' with '{}'", mediaPackage, tag);
}
attachment.setMimeType(MimeTypes.ZIP);
// The zip file is safely in the archive, so it's now safe to attempt to remove the original zip
try {
FileUtils.forceDelete(zip);
} catch (Exception e) {
throw new WorkflowOperationException(e);
}
mediaPackage.add(attachment);
return createResult(mediaPackage, Action.CONTINUE);
}
/**
* Creates a zip archive of all elements in a mediapackage.
*
* @param mediaPackage
* the mediapackage to zip
*
* @return the zip file
*
* @throws IOException
* If an IO exception occurs
* @throws NotFoundException
* If a file referenced in the mediapackage can not be found
* @throws MediaPackageException
* If the mediapackage can not be serialized to xml
* @throws WorkflowOperationException
* If the mediapackage is invalid
*/
protected File zip(MediaPackage mediaPackage, List<MediaPackageElementFlavor> flavorsToZip, boolean compress)
throws IOException, NotFoundException, MediaPackageException, WorkflowOperationException {
if (mediaPackage == null) {
throw new WorkflowOperationException("Invalid mediapackage");
}
// Create the temp directory
File mediaPackageDir = new File(tempStorageDir, mediaPackage.getIdentifier().compact());
FileUtils.forceMkdir(mediaPackageDir);
// Link or copy each matching element's file from the workspace to the temp directory
MediaPackageSerializer serializer = new DefaultMediaPackageSerializerImpl(mediaPackageDir);
MediaPackage clone = (MediaPackage) mediaPackage.clone();
for (MediaPackageElement element : clone.getElements()) {
// remove the element if it doesn't match the flavors to zip
boolean remove = true;
for (MediaPackageElementFlavor flavor : flavorsToZip) {
if (flavor.matches(element.getFlavor())) {
remove = false;
break;
}
}
if (remove) {
clone.remove(element);
continue;
}
File elementDir = new File(mediaPackageDir, element.getIdentifier());
FileUtils.forceMkdir(elementDir);
File workspaceFile = workspace.get(element.getURI());
File linkedFile = FileSupport.link(workspaceFile, new File(elementDir, workspaceFile.getName()), true);
try {
element.setURI(serializer.encodeURI(linkedFile.toURI()));
} catch (URISyntaxException e) {
throw new MediaPackageException("unable to serialize a mediapackage element", e);
}
}
// Add the manifest
FileUtils.writeStringToFile(new File(mediaPackageDir, "manifest.xml"), MediaPackageParser.getAsXml(clone), "UTF-8");
// Zip the directory
File zip = new File(tempStorageDir, clone.getIdentifier().compact() + ".zip");
int compressValue = compress ? ZipUtil.DEFAULT_COMPRESSION : ZipUtil.NO_COMPRESSION;
long startTime = System.currentTimeMillis();
ZipUtil.zip(new File[] { mediaPackageDir }, zip, true, compressValue);
long stopTime = System.currentTimeMillis();
logger.debug("Zip file creation took {} seconds", (stopTime - startTime) / 1000);
// Remove the directory
FileUtils.forceDelete(mediaPackageDir);
// Return the zip
return zip;
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.workflow.api.WorkflowOperationHandler#getConfigurationOptions()
*/
@Override
public SortedMap<String, String> getConfigurationOptions() {
return configurationOptions;
}
}