package org.sakaiproject.content.util;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import javax.activation.MimetypesFileTypeMap;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.component.cover.ServerConfigurationService;
import org.sakaiproject.content.api.ContentCollection;
import org.sakaiproject.content.api.ContentCollectionEdit;
import org.sakaiproject.content.cover.ContentHostingService;
import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.content.api.ContentResourceEdit;
import org.sakaiproject.entity.api.Entity;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.entity.api.ResourcePropertiesEdit;
import org.sakaiproject.event.api.NotificationService;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.IdUsedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.exception.ServerOverloadException;
import org.sakaiproject.exception.TypeException;
import org.sakaiproject.tool.api.ToolSession;
import org.sakaiproject.tool.cover.SessionManager;
@SuppressWarnings({ "deprecation", "restriction" })
public class ZipContentUtil {
protected static final Log LOG = LogFactory.getLog(ZipContentUtil.class);
private static final String ZIP_EXTENSION = ".zip";
private static final int BUFFER_SIZE = 32000;
private static final MimetypesFileTypeMap mime = new MimetypesFileTypeMap();
public static final String PREFIX = "resources.";
public static final String REQUEST = "request.";
private static final String STATE_HOME_COLLECTION_ID = PREFIX + REQUEST + "collection_home";
private static final String STATE_HOME_COLLECTION_DISPLAY_NAME = PREFIX + REQUEST + "collection_home_display_name";
public static final String STATE_MESSAGE = "message";
/**
* Maximum number of files to extract from a zip archive (1000)
*/
public static final int MAX_ZIP_EXTRACT_FILES_DEFAULT = 1000;
private static Integer MAX_ZIP_EXTRACT_FILES;
public static int getMaxZipExtractFiles() {
if(MAX_ZIP_EXTRACT_FILES == null){
MAX_ZIP_EXTRACT_FILES = ServerConfigurationService.getInt(org.sakaiproject.content.api.ContentHostingService.RESOURCES_ZIP_EXPAND_MAX,MAX_ZIP_EXTRACT_FILES_DEFAULT);
}
if (MAX_ZIP_EXTRACT_FILES <= 0) {
MAX_ZIP_EXTRACT_FILES = MAX_ZIP_EXTRACT_FILES_DEFAULT; // any less than this is useless so probably a mistake
LOG.warn("content.zip.expand.maxfiles is set to a value less than or equal to 0, defaulting to "+MAX_ZIP_EXTRACT_FILES_DEFAULT);
}
return MAX_ZIP_EXTRACT_FILES;
}
/**
* Compresses a ContentCollection to a new zip archive with the same folder name
*
* @param reference sakai entity reference
* @throws Exception on failure
*/
public void compressFolder(Reference reference) {
File temp = null;
FileInputStream fis = null;
ToolSession toolSession = SessionManager.getCurrentToolSession();
try {
// Create the compressed archive in the filesystem
ZipOutputStream out = null;
try {
temp = File.createTempFile("sakai_content-", ".tmp");
ContentCollection collection = ContentHostingService.getCollection(reference.getId());
out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(temp),BUFFER_SIZE));
storeContentCollection(reference.getId(),collection,out);
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
}
}
}
// Store the compressed archive in the repository
String resourceId = reference.getId().substring(0,reference.getId().lastIndexOf(Entity.SEPARATOR));
String resourceName = extractName(resourceId);
String homeCollectionId = (String) toolSession.getAttribute(STATE_HOME_COLLECTION_ID);
if(homeCollectionId != null && homeCollectionId.equals(reference.getId())){
//place the zip file into the home folder of the resource tool
resourceId = reference.getId() + resourceName;
String homeName = (String) toolSession.getAttribute(STATE_HOME_COLLECTION_DISPLAY_NAME);
if(homeName != null){
resourceName = homeName;
}
}
int count = 0;
ContentResourceEdit resourceEdit = null;
while(true){
try{
String newResourceId = resourceId;
String newResourceName = resourceName;
count++;
if(count > 1){
//previous naming convention failed, try another one
newResourceId += "_" + count;
newResourceName += "_" + count;
}
newResourceId += ZIP_EXTENSION;
newResourceName += ZIP_EXTENSION;
resourceEdit = ContentHostingService.addResource(newResourceId);
//success, so keep track of name/id
resourceId = newResourceId;
resourceName = newResourceName;
break;
}catch(IdUsedException e){
//do nothing, just let it loop again
}catch(Exception e){
throw new Exception(e);
}
}
fis = new FileInputStream(temp);
resourceEdit.setContent(fis);
resourceEdit.setContentType(mime.getContentType(resourceId));
ResourcePropertiesEdit props = resourceEdit.getPropertiesEdit();
props.addProperty(ResourcePropertiesEdit.PROP_DISPLAY_NAME, resourceName);
ContentHostingService.commitResource(resourceEdit, NotificationService.NOTI_NONE);
}
catch (PermissionException pE){
addAlert(toolSession, "You do not have the proper permissions for compressing to zip archive");
LOG.warn(pE);
}
catch (Exception e) {
addAlert(toolSession, "An error has occurred while compressing to zip archive");
LOG.error(e);
}
finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
}
}
if (temp != null && temp.exists()) {
if (!temp.delete()) {
LOG.warn("failed to remove temp file");
}
}
}
}
private void addAlert(ToolSession toolSession, String alert){
String errorMessage = (String) toolSession.getAttribute(STATE_MESSAGE);
if(errorMessage == null){
errorMessage = alert;
}else{
errorMessage += "\n\n" + alert;
}
toolSession.setAttribute(STATE_MESSAGE, errorMessage);
}
/**
* Extracts a compressed (zip) ContentResource to a new folder with the same name.
*
* @param reference the sakai entity reference
* @throws Exception on failure
* @deprecated 11 Oct 2011 -AZ, use {@link #extractArchive(String)} instead
*/
public void extractArchive(Reference reference) throws Exception {
if (reference == null) {
throw new IllegalArgumentException("reference cannot be null");
}
extractArchive(reference.getId());
}
/**
* Extracts a compressed (zip) ContentResource to a new folder with the same name.
*
* @param referenceId the sakai entity reference id
* @throws Exception on failure
*/
public void extractArchive(String referenceId) throws Exception {
ContentResource resource = ContentHostingService.getResource(referenceId);
String rootCollectionId = extractZipCollectionPrefix(resource);
// Prepare Collection
ContentCollectionEdit rootCollection = ContentHostingService.addCollection(rootCollectionId);
ResourcePropertiesEdit prop = rootCollection.getPropertiesEdit();
prop.addProperty(ResourcePropertiesEdit.PROP_DISPLAY_NAME, extractZipCollectionName(resource));
ContentHostingService.commitCollection(rootCollection);
// Extract Zip File
File temp = null;
try {
temp = exportResourceToFile(resource);
ZipFile zipFile = new ZipFile(temp,ZipFile.OPEN_READ);
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry nextElement = entries.nextElement();
if (nextElement.isDirectory()) {
createContentCollection(rootCollectionId, nextElement);
}
else {
createContentResource(rootCollectionId, nextElement, zipFile);
}
}
zipFile.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
temp.delete();
}
}
/**
* Get a list of the files in a zip and their size
* @param reference the sakai entity reference
* @return a map of file names to file sizes in the zip archive
* @deprecated 11 Oct 2011 -AZ, use {@link #getZipManifest(String)}
*/
public Map<String, Long> getZipManifest(Reference reference) {
if (reference == null) {
throw new IllegalArgumentException("reference cannot be null");
}
return getZipManifest(reference.getId());
}
/**
* Get a list of the files in a zip and their size
* @param referenceId the sakai entity reference id
* @return a map of file names to file sizes in the zip archive
*/
public Map<String, Long> getZipManifest(String referenceId) {
Map<String, Long> ret = new HashMap<String, Long>();
ContentResource resource;
try {
resource = ContentHostingService.getResource(referenceId);
} catch (PermissionException e1) {
return null;
} catch (IdUnusedException e1) {
return null;
} catch (TypeException e1) {
return null;
}
//String rootCollectionId = extractZipCollectionPrefix(resource);
// Extract Zip File
File temp = null;
try {
temp = exportResourceToFile(resource);
ZipFile zipFile = new ZipFile(temp,ZipFile.OPEN_READ);
Enumeration<? extends ZipEntry> entries = zipFile.entries();
int i = 0;
//use <= getMAX_ZIP_EXTRACT_SIZE() so the returned value will be
//larger than the max and then rejected
while (entries.hasMoreElements() && i <= getMaxZipExtractFiles()) {
ZipEntry nextElement = entries.nextElement();
ret.put(nextElement.getName(), nextElement.getSize());
i++;
}
zipFile.close();
}
catch (Exception e) {
e.printStackTrace();
}
finally {
if (temp.exists()) {
if (!temp.delete()) {
LOG.warn("uanble to delete temp file!");
}
}
}
return ret;
}
/**
* Creates a new ContentResource extracted from ZipFile
*
* @param rootCollectionId
* @param nextElement
* @param zipFile
* @throws Exception
*/
private void createContentResource(String rootCollectionId,
ZipEntry nextElement, ZipFile zipFile) throws Exception {
String resourceId = rootCollectionId + nextElement.getName();
String resourceName = extractName(nextElement.getName());
ContentResourceEdit resourceEdit = ContentHostingService.addResource(resourceId);
resourceEdit.setContent(zipFile.getInputStream(nextElement));
resourceEdit.setContentType(mime.getContentType(resourceName));
ResourcePropertiesEdit props = resourceEdit.getPropertiesEdit();
props.addProperty(ResourcePropertiesEdit.PROP_DISPLAY_NAME, resourceName);
ContentHostingService.commitResource(resourceEdit, NotificationService.NOTI_NONE);
}
/**
* Creates a new ContentCollection in the rootCollectionId with the element.getName()
*
* @param rootCollectionId
* @param element
* @throws Exception
*/
private void createContentCollection(String rootCollectionId,
ZipEntry element) throws Exception {
String resourceId = rootCollectionId + element.getName();
String resourceName = extractName(element.getName());
ContentCollectionEdit collection = ContentHostingService.addCollection(resourceId);
ResourcePropertiesEdit props = collection.getPropertiesEdit();
props.addProperty(ResourcePropertiesEdit.PROP_DISPLAY_NAME, resourceName);
ContentHostingService.commitCollection(collection);
}
/**
* Exports a the ContentResource zip file to the operating system
*
* @param resource
* @return
*/
private File exportResourceToFile(ContentResource resource) {
File temp = null;
FileOutputStream out = null;
try {
temp = File.createTempFile("sakai_content-", ".tmp");
temp.deleteOnExit();
// Write content to file
out = new FileOutputStream(temp);
IOUtils.copy(resource.streamContent(),out);
out.flush();
} catch (IOException e) {
e.printStackTrace();
} catch (ServerOverloadException e) {
e.printStackTrace();
}
finally {
if (out !=null) {
try {
out.close();
} catch (IOException e) {
}
}
}
return temp;
}
/**
* Iterates the collection.getMembers() and streams content resources recursively to the ZipOutputStream
*
* @param rootId
* @param collection
* @param out
* @throws Exception
*/
private void storeContentCollection(String rootId, ContentCollection collection, ZipOutputStream out) throws Exception {
List<String> members = collection.getMembers();
for (String memberId: members) {
if (memberId.endsWith(Entity.SEPARATOR)) {
ContentCollection memberCollection = ContentHostingService.getCollection(memberId);
storeContentCollection(rootId,memberCollection,out);
}
else {
ContentResource resource = ContentHostingService.getResource(memberId);
storeContentResource(rootId, resource, out);
}
}
}
/**
* Streams content resource to the ZipOutputStream
*
* @param rootId
* @param resource
* @param out
* @throws Exception
*/
private void storeContentResource(String rootId, ContentResource resource, ZipOutputStream out) throws Exception {
String filename = resource.getId().substring(rootId.length(),resource.getId().length());
ZipEntry zipEntry = new ZipEntry(filename);
zipEntry.setSize(resource.getContentLength());
out.putNextEntry(zipEntry);
InputStream contentStream = null;
try {
contentStream = resource.streamContent();
IOUtils.copy(contentStream, out);
} finally {
if (contentStream != null) {
contentStream.close();
}
}
}
private String extractZipCollectionPrefix(ContentResource resource) {
String idPrefix = resource.getContainingCollection().getId() +
extractZipCollectionName(resource) +
Entity.SEPARATOR;
return idPrefix;
}
private String extractName(String collectionName) {
String[] tmp = collectionName.split(Entity.SEPARATOR);
return tmp[tmp.length-1];
}
private String extractZipCollectionName(ContentResource resource) {
String tmp = extractName(resource.getId());
return tmp.substring(0, tmp.lastIndexOf("."));
}
}