/*
* Copyright 2003-2010 Tufts University Licensed 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://www.osedu.org/licenses/ECL-2.0
*
* 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 tufts.vue.action;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.zip.*;
import tufts.Util;
import tufts.vue.DEBUG;
import tufts.vue.VueUtil;
import tufts.vue.Version;
import tufts.vue.VUE;
import tufts.vue.Resource;
import tufts.vue.PropertyEntry;
import tufts.vue.URLResource;
import tufts.vue.Images;
import tufts.vue.IMSCP;
import tufts.vue.LWComponent;
import tufts.vue.LWMap;
import static tufts.vue.Resource.*;
/**
* Code related to identifying, creating and unpacking VUE archives.
*
* @version $Revision: 1.14 $ / $Date: 2010-02-03 19:13:45 $ / $Author: mike $
*/
public class Archive
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(Archive.class);
private static final String ZIP_IMPORT_LABEL ="Imported";
private static final String MAP_ARCHIVE_KEY = "@(#)TUFTS-VUE-ARCHIVE";
private static final String SPEC_KEY = "spec=";
private static final int SPEC_KEY_LEN = SPEC_KEY.length();
public static boolean isVueIMSCPArchive(File file) {
if (!file.getName().toLowerCase().endsWith(".zip"))
return false;
try {
ZipFile zipFile = new ZipFile(file);
return zipFile.getEntry(IMSCP.MAP_FILE) != null && zipFile.getEntry(IMSCP.MANIFEST_FILE) != null;
} catch (Throwable t) {
Log.warn(t);
return false;
}
}
public static boolean isVuePackage(File file) {
return file.getName().toLowerCase().endsWith(VueUtil.VueArchiveExtension);
}
/**
* @return true if we can create files in the given directory
* File.canWrite is insufficient to ensure this. If the filesystem
* the directory is on is not writeable, we wont know this until
* we attempt to create a file there, and it fails.
*/
public static boolean canCreateFiles(File directory) {
if (directory == null)
return false;
// Window's Vista as of April 2008 can apparently lie about this. E.g., it is
// claiming the Desktop folder is not writeable... -- SMF 2008-04-04
// if (!directory.canWrite()) {
// Log.info("Cannot write: " + directory);
// return false;
// }
File tmp = null;
try {
tmp = directory.createTempFile(".vueFScheck", "", directory);
} catch (Throwable t) {
Log.info("Cannot write to filesystem inside: " + directory + "; " + t);
}
if (tmp != null) {
if (DEBUG.Enabled) Log.debug("Created test file: " + tmp);
try {
tmp.delete();
} catch (Throwable t) {
Log.error("Couldn't delete tmp file " + tmp, t);
}
return true;
}
return false;
}
/**
* @param zipFile should be a File pointing to a VUE Package -- a Zip Archive created by VUE
*/
public static LWMap openVuePackage(final File zipFile)
throws java.io.IOException,
java.util.zip.ZipException
{
Log.info("Unpacking VUE zip archive: " + zipFile);
final String unpackingDir;
//File folder = new File(VueUtil.getDefaultUserFolder().getAbsolutePath()+File.separator+"VueMapArchives";
final File parentDirectory = zipFile.getParentFile();
if (false && canCreateFiles(parentDirectory)) // for now, always unpack into the temp directory
unpackingDir = parentDirectory.toString();
else
unpackingDir = VUE.getSystemProperty("java.io.tmpdir");
Log.info("Unpacking location: " + unpackingDir);
final ZipInputStream zin = new ZipInputStream(new FileInputStream(zipFile));
final Map<String,String> packagedResources = new HashMap();
ZipEntry entry;
ZipEntry mapEntry = null;
String mapFile = null;
while ( (entry = zin.getNextEntry()) != null ) {
final String location = unzipEntryToFile(zin, entry, unpackingDir);
final String comment = Archive.getComment(entry);
if (comment != null) {
if (comment.startsWith(MAP_ARCHIVE_KEY)) {
mapEntry = entry;
mapFile = location;
Log.info("Identified map entry: " + comment + " (" + entry.getName() + ")");
//Log.info("Found map: " + entry + "; at " + location);
} else {
String spec = comment.substring(comment.indexOf(SPEC_KEY) + SPEC_KEY_LEN);
Log.info(" [" + spec + "]"); // todo: revert to debug level eventually
if (packagedResources.put(spec, location) != null)
Log.warn("repeated resource spec in archive! [" + spec + "]");
//Log.debug(" spec= " + spec);
}
} else {
Log.warn("ENTRY WITH NO COMMENT: " + entry);
}
}
zin.close();
// If this package map is being unmarshalled on the same machine it was created
// on, all the URLResource's will initialize themseleves normally to their
// original source files, not their package files. This is a bit inefficient /
// might confuse things, so we need to be careful. For now we're just going to
// completely re-init the Resources from scratch, so they'll init twice. It
// would be more ideal to pass in a special UnmarshalListener that would handle
// setting the PACKAGE_FILE property on the Resources before they init, or at
// least pass in a context object to the default VueUnmarshalListener, that
// could be passed to URLResource.XML_completed, which it could check to know if
// it should wait on attempting to initialize.
final LWMap map =
ActionUtil.unmarshallMap(new File(mapFile),
new ArchiveMapUnmarshalHandler(zipFile + "(" + mapEntry + ")",
zipFile,
packagedResources));
map.setFile(zipFile);
map.markAsSaved();
return map;
}
private static class ArchiveMapUnmarshalHandler extends MapUnmarshalHandler
{
final Map<String,String> packagedResources;
final File archiveFile;
ArchiveMapUnmarshalHandler(Object source, File archiveFile, Map<String,String> resourcesFoundInPackage) {
super(source, Resource.MANAGED_UNMARSHALLING);
this.packagedResources = resourcesFoundInPackage;
this.archiveFile = archiveFile;
}
/** skip setFile -- we're going to use the package file */
@Override
void notifyFile(final LWMap map, final File file)
{
super.map = map;
super.file = file;
map.setArchiveMap(true);
// skip setFile: we'll handle it ourselves for the archive
}
/** this impl is so we can patch up resources first before completing the restore */
@Override
void notifyUnmarshallingCompleted()
{
map.runResourceDeserializeInits(map.getAllResources());
patchResourcesForPackage();
//map.runResourceFinalInits(allResources);
map.completeXMLRestore(context);
//super.notifyUnmarshallingCompleted();
}
private void patchResourcesForPackage() {
for (Resource r : map.getAllResources()) {
final String packageCacheFile = packagedResources.get(r.getSpec());
if (packageCacheFile != null) {
//Log.debug("Found packaged resource: " + r + "; " + packageCacheFile);
if (DEBUG.Enabled) Log.debug("patching packaged resource: " + packageCacheFile + "; into " + r);
// This will convert "/" from the zip-entry package name to "\" on Windows
// (ZipEntry pathnames always use '/', no matter what the platform).
final File localFile = new File(packageCacheFile);
final String localPath = localFile.toString();
if (DEBUG.RESOURCE && !localPath.equals(packageCacheFile))
Log.info(" localized file path: " + localPath);
if (r instanceof URLResource) {
((URLResource)r).setPackageFile(localFile, archiveFile);
} else {
Log.warn("package file fallback for unknown resource type, impl in question: " + Util.tags(r) + "; " + localFile);
r.setProperty(PACKAGE_FILE, localFile);
r.setCached(true);
}
} else {
if (DEBUG.Enabled) Log.debug("No archive entry matching: " + r.getSpec());
}
}
}
}
/**
* @param location -- if null, entry will be unzipped in local (current) working directory,
* otherwise, entry will be unzipped at the given path location in the file system.
* @return filename of unzipped file
*/
public static String unzipEntryToFile(ZipInputStream zin, ZipEntry entry, String location)
throws IOException
{
final String filename;
if (location == null) {
filename = entry.getName();
} else {
if (location.endsWith(File.separator))
filename = location + entry.getName();
else
filename = location + File.separator + entry.getName();
}
if (true||DEBUG.IO) {
// Note: entry.getSize() is not known until the entry is unpacked
//final String comment = Archive.getComment(entry);
String msg = "Unzipping to " + filename + " from entry " + entry;
//if (comment != null) msg += "\n\t[" + comment + "]";
Log.info(msg);
//Log.debug(msg);
}
final File newFile = createFile(filename);
//Log.info("UNPACKING " + newFile);
final FileOutputStream out = new FileOutputStream(newFile);
byte [] b = new byte[1024];
int len = 0;
int wrote = 0;
while ( (len=zin.read(b))!= -1 ) {
wrote += len;
out.write(b,0,len);
}
out.close();
if (DEBUG.IO) {
Log.debug(" Unzipped " + filename + "; wrote=" + wrote + "; size=" + entry.getSize());
}
return filename;
}
public static File createFile(String name)
throws IOException
{
final File file = new File(name);
File parent = file;
while ( (parent = parent.getParentFile()) != null) {
//Log.debug("Parent: " + parent);
if (parent.getPath().equals("/")) {
//Log.debug("skipping " + parent);
break;
}
if (!parent.exists()) {
Log.debug("Creating: " + parent);
parent.mkdir();
}
}
file.createNewFile();
return file;
}
private static void unzipIMSCP(ZipInputStream zin, ZipEntry entry)
throws IOException
{
unzipEntryToFile(zin, entry, VueUtil.getDefaultUserFolder().getAbsolutePath());
// String fname = VueUtil.getDefaultUserFolder().getAbsolutePath()+File.separator+s;
// if (DEBUG.IO) System.out.println("unzipping " + s + " to " + fname);
// FileOutputStream out = new FileOutputStream(fname);
// byte [] b = new byte[512];
// int len = 0;
// while ( (len=zin.read(b))!= -1 ) {
// out.write(b,0,len);
// }
// out.close();
}
public static LWMap loadVueIMSCPArchive(File file)
throws java.io.FileNotFoundException,
java.util.zip.ZipException,
java.io.IOException
{
Log.info("Unpacking VUE IMSCP zip archive: " + file);
ZipFile zipFile = new ZipFile(file);
Vector<Resource> resourceVector = new Vector();
File resourceFolder = new File(VueUtil.getDefaultUserFolder().getAbsolutePath()+File.separator+IMSCP.RESOURCE_FILES);
if(resourceFolder.exists() || resourceFolder.mkdir()) {
ZipInputStream zin = new ZipInputStream(new FileInputStream(file));
ZipEntry e;
while ((e=zin.getNextEntry()) != null) {
unzipIMSCP(zin, e);
//if (DEBUG.IO) System.out.println("ZipEntry: " + e.getName());
if(!e.getName().equalsIgnoreCase(IMSCP.MAP_FILE) && !e.getName().equalsIgnoreCase(IMSCP.MANIFEST_FILE)){
// todo: may want to add a Resource.Factory.get(ZipEntry) method
Resource resource = Resource.getFactory().get(e.getName());
resourceVector.add(resource);
//if (DEBUG.IO) System.out.println("Resource: " + resource);
}
}
zin.close();
}
File mapFile = new File(VueUtil.getDefaultUserFolder().getAbsolutePath()+File.separator+IMSCP.MAP_FILE);
LWMap map = ActionUtil.unmarshallMap(mapFile);
map.setFile(null);
map.setLabel(ZIP_IMPORT_LABEL);
for (Resource r : resourceVector) {
replaceResource(map, r,
Resource.getFactory().get(VueUtil.getDefaultUserFolder().getAbsolutePath()+File.separator+r.getSpec()));
//new URLResource(VueUtil.getDefaultUserFolder().getAbsolutePath()+File.separator+r.getSpec()));
}
map.markAsSaved();
return map;
}
public static void replaceResource(LWMap map,Resource r1,Resource r2) {
for (LWComponent component : map.getAllDescendents()) {
if(component.hasResource()){
Resource resource = component.getResource();
if(resource.getSpec().equals(r1.getSpec()))
component.setResource(r2);
}
}
}
private static final String COMMENT_ENCODING = "UTF-8";
/**
*
* There's a java bug (STILL!) as of JAN 2008: comments are encoded in the zip file,
* but not extractable via any method call in any JDK. See:
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6646605
*
* This method encapsulates workaround comment setting code. We add comments anyway
* for easy debug (e.g., unzip -l), and then encode them again as "extra" zip entry
* bytes, which we can extract later as the comment.
*
* Note also that for special characters to make it through this process across
* multiple platforms, the same, platform-neutral encoding must be used
* both when setting and getting.
*
*/
private static void setComment(ZipEntry entry, String comment) {
// Using the default ZipEntry.setComment here is currently
// only useful in that it provides for visual inspection of
// archive entry comments, using, for example the "unzip"
// command line tool.
entry.setComment(comment);
try {
//entry.setComment(new String(comment.getBytes(), COMMENT_ENCODING));
//entry.setComment(new String(comment.getBytes(COMMENT_ENCODING), COMMENT_ENCODING));
entry.setExtra(comment.getBytes(COMMENT_ENCODING));
} catch (Throwable t) {
Log.warn("Couldn't " + COMMENT_ENCODING + " encode 'extra' bytes into ZipEntry comment; " + entry + "; [" + comment + "]", t);
entry.setExtra(comment.getBytes());
}
}
/**
* Extract comments from the given ZipEntry.
* @See setComment
*/
public static String getComment(ZipEntry entry) {
// See setComment for why we do this this way:
byte[] extra = entry.getExtra();
String comment = null;
if (extra != null && extra.length > 0) {
if (DEBUG.IO && DEBUG.META) Log.debug("getComment found " + extra.length + " extra bytes");
try {
comment = new String(extra, COMMENT_ENCODING);
} catch (Throwable t) {
Log.warn("Couldn't " + COMMENT_ENCODING + " decode 'extra' bytes from ZipEntry comment; " + entry, t);
comment = new String(extra);
}
//comment = "extra(" + new String(extra) + ")";
}
return comment;
}
private static int UniqueNameFailsafeCount = 1;
/**
* Generate a package file name from the given URLResource. We could just as easily
* generate random names, but we base it on the URL for easy debugging and
* and exploring of the package in Finder/Explorer (e.g., we also try to make
* sure the documents have appropriate extensions so the OS shell applications
* can generate appropriate icons, etc).
*
* @param existingNames -- if provided, will put the result of generated names
* in this set, and will be used to ensure that no repeated names are generated
* on future calls.
*/
private static String generatePackageFileName(Resource r, Set<String> existingNames) {
String packageName = null;
try {
packageName = generateInformativePackageFileName(r);
} catch (Throwable t) {
Log.warn("Failed to create informative package name for " + r, t);
}
if (packageName != null && packageName.length() > 250) {
// 255 is the max length on modern Mac's an PC's
// We truncate to 250 in case we need a few extra chars for establishing uniqueness.
Log.info("Truncating long name: " + packageName);
packageName = packageName.substring(0, 250);
}
if (packageName == null) {
packageName = String.format("vuedata%03d", UniqueNameFailsafeCount++); // note: count is static: not thread-safe
} else if (existingNames != null) {
if (existingNames.contains(packageName)) {
//if (DEBUG.Enabled) Log.debug("Existing names already contains [" + packageName + "]; " + existingNames);
Log.info("repeated name [" + packageName + "]");
int cnt = 1;
String uniqueName = packageName;
int lastDot = packageName.lastIndexOf('.');
if (lastDot > 0) {
// if there's an extension for the filename, introduce the unique-ifying
// index before it, so the filename will still have a recognizable extension
// to OS shell applications.
final String preDot = packageName.substring(0, lastDot);
final String postDot = packageName.substring(lastDot);
do {
uniqueName = String.format("%s.%03d%s", preDot, cnt++, postDot);
} while (existingNames.contains(uniqueName));
} else {
do {
uniqueName = String.format("%s.%03d", packageName, cnt++);
} while (existingNames.contains(uniqueName));
}
packageName = uniqueName;
Log.info("uniqified package name: " + packageName);
}
existingNames.add(packageName);
}
return packageName;
}
private static String generateInformativePackageFileName(Resource r)
throws java.io.UnsupportedEncodingException
{
if (DEBUG.IO) Log.debug("Generating package file name from " + r + "; " + r.getProperties());
try {
if (r.hasProperty(PACKAGE_FILE)) {
File pf = (File) r.getPropertyValue(PACKAGE_FILE);
String name = pf.getName();
// this prevents cascading encodings of special chars (e.g., $20 becomces $2420 each
// time the package is saved, growing longer each time)
if (DEBUG.IO) Log.debug("Using pre-existing package file name: " + name);
return name;
}
} catch (Throwable t) {
Log.warn(t);
}
final Object imageSource = r.getImageSource();
//final URL url = r.getImageSource(); // better as URI?
if (imageSource == null)
return r.getSpec(); // failsafe
String packageName;
if (imageSource instanceof File){
packageName = ((File)imageSource).getName();
} else if (imageSource instanceof URL) {
final URL url = (URL) imageSource;
packageName = url.toString(); // this could be very messy with queries...
if (packageName.startsWith("http://")) {
// strip off the most common case -- this not informative (can be assumed),
// and makes package file names easier to read
packageName = packageName.substring(7);
}
// packageName = url.getHost() + url.getFile();
// If the resource is image content, and the generated name doesn't
// look like something that has an extension that most OS shell
// applications would recognize as an image (e.g., Finder, Explorer),
// add an extension so that when looking at unpacked archives directories,
// image icons can easily be seen.
if (r.isImage() && r.hasProperty(IMAGE_FORMAT) && !Resource.looksLikeImageFile(packageName))
packageName += "." + r.getProperty(IMAGE_FORMAT).toLowerCase();
} else {
throw new IllegalArgumentException("image source is neither URL or File: " + Util.tags(imageSource));
}
// String packageName;
// if ("file".equals(url.getProtocol())) {
// File file = new File(url.getFile());
// packageName = file.getName();
// } else {
// packageName = url.toString(); // this could be very messy with queries...
// if (packageName.startsWith("http://")) {
// // strip off the most common case -- this not informative (can be assumed),
// // and makes package file names easier to read
// packageName = packageName.substring(7);
// }
// // packageName = url.getHost() + url.getFile();
// // If the resource is image content, and the generated name doesn't
// // look like something that has an extension that most OS shell
// // applications would recognize as an image (e.g., Finder, Explorer),
// // add an extension so that when looking at unpacked archives directories,
// // image icons can easily be seen.
// if (r.isImage() && r.hasProperty(IMAGE_FORMAT) && !Resource.looksLikeImageFile(packageName))
// packageName += "." + r.getProperty(IMAGE_FORMAT).toLowerCase();
// }
if (DEBUG.IO) Log.debug(" decoding " + packageName);
// Decode (to prevent any redundant encoding), then re-encode
packageName = java.net.URLDecoder.decode(packageName, "UTF-8");
if (DEBUG.IO) Log.debug(" decoded to " + packageName);
packageName = java.net.URLEncoder.encode(packageName, "UTF-8");
if (DEBUG.IO) Log.debug("re-encoded to " + packageName);
// now "lock-in" the encoding: as this is now a fixed file-name, we don't ever want it to be
// accidentally decoded, which might create something that looks like a path when we don't want it to.
packageName = packageName.replace('%', '$');
if (DEBUG.IO) Log.debug(" locked in at " + packageName);
// if (URLResource.ALLOW_URI_WHITESPACE) {
// // TODO: may be able to just decode these '+' encodings back to the actual
// // space character, tho would need to do lots of testing of the entire
// // workflow code path on multiple platforms. This would be especially nice
// // at least for document names (e.g., non-images), as they'll often have
// // spaces, and '$20' in the middle of the document name is pretty ugly to look
// // at if they open the document (e.g., PDF, Word, Excel etc).
// // 2008-03-31 Not currently working, at least on the mac: finding the local files eventually fails
// packageName = packageName.replace('+', ' ');
// } else {
// So Mac openURL doesn't decode these space indicators later when opening:
packageName = packageName.replaceAll("\\+", "\\$20");
// Replacing '+' with '-' is a friendler whitespace replacement (more
// readable), tho it's "destructive" in that the original URL could no
// longer be reliably reverse engineered from the filename. We don't
// actually depend on being able to do that, but it's handy for
// debugging, and could be useful if we ever have to deal with any kind of
// recovery from data corruption.
//packageName = packageName.replace('+', '-');
// }
return packageName;
}
private static class Item {
final ZipEntry entry;
final Resource resource;
final File dataFile;
Item(ZipEntry e, Resource r, File f) {
entry = e; resource = r; dataFile = f;
}
public String toString() {
return "Item[" + entry.toString() + "; " + resource + "; " + dataFile + "]";
}
}
/**
* Write the map to the given file as a Zip archive, along with all unique resources
* for which data can be found locally (local user files, or local image cache).
* Entries for Resource data in the zip archive are annotated with their original
* Resource spec, so they can be identified on unpacking, and associated with their
* original aResources.
*/
public static void writeArchive(LWMap map, File archive)
throws java.io.IOException
{
Log.info("Writing archive package " + archive);
// final String label = map.getLabel();
// final String mapName;
// if (label.endsWith(".vue"))
// mapName = label.substring(0, label.length() - 4);
// else
// mapName = label;
final String label = archive.getName();
final String mapName;
if (label.endsWith(VueUtil.VueArchiveExtension))
mapName = label.substring(0, label.length() - 4);
else
mapName = label;
final String dirName = mapName + ".vdr";
//-----------------------------------------------------------------------------
// Find source data-files for all unique resources
//-----------------------------------------------------------------------------
final Collection<Resource> uniqueResources = map.getAllUniqueResources();
final Collection<PropertyEntry> manifest = new ArrayList();
final List<Item> items = new ArrayList();
final Set<String> uniqueEntryNames = new HashSet();
Archive.UniqueNameFailsafeCount = 1; // note: static variable -- not threadsafe
for (Resource r : uniqueResources) {
try {
final File sourceFile = r.getActiveDataFile();
final String description = "" + (DEBUG.Enabled ? r : r.getSpec());
// todo: if source file is a .vue file, could load the map (faster if is
// already open and we find it), then archive THAT out as a tmp .vpk file,
// and add that to to archive uncompressed, converting the .vue resource
// to a .vpk resource.
if (sourceFile == null) {
Log.info("skipped: " + description);
continue;
} else if (!sourceFile.exists()) {
Log.warn("Missing local file: " + sourceFile + "; for " + r);
continue;
}
final String packageEntryName = generatePackageFileName(r, uniqueEntryNames);
final ZipEntry entry = new ZipEntry(dirName + "/" + packageEntryName);
Archive.setComment(entry, "\t" + SPEC_KEY + r.getSpec());
final Item item = new Item(entry, r, sourceFile);
//Log.info("created: " + entry + "; " + description);
items.add(item);
manifest.add(new PropertyEntry(r.getSpec(), packageEntryName));
if (DEBUG.Enabled) Log.info("created: " + item);
} catch (Throwable t) {
Log.error("writeArchive: failed to handle " + Util.tags(r), t);
}
}
//-----------------------------------------------------------------------------
// Write the map to the archive
//-----------------------------------------------------------------------------
final ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(archive)));
final ZipEntry mapEntry = new ZipEntry(dirName + "/" + mapName + "$map.vue");
final String comment = MAP_ARCHIVE_KEY + "; VERSION: 2;"
+ " Saved " + new Date() + " by " + VUE.getName() + " built " + Version.AllInfo + "; items=" + items.size() + ";"
+ ">" // /usr/bin/what terminatior
//+ "\n\tmap-name(" + mapName + ")"
//+ "\n\tunique-resources(" + resources.size() + ")"
;
Archive.setComment(mapEntry, comment);
zos.putNextEntry(mapEntry);
final Writer mapOut = new OutputStreamWriter(zos);
// TODO: need to handle a map marshalling failure, in which case we want to
// abandon writing the entire archive -- this will require a big reorg here --
// we need to write the entire archive to a tmp archive first, and only create
// the new file if everything succeeds.
try {
map.setArchiveManifest(manifest);
ActionUtil.marshallMapToWriter(map, mapOut);
} catch (Throwable t) {
Log.error(t);
throw new RuntimeException(t);
} finally {
// TODO: do NOT reset this if this map is already a packaged map...
map.setArchiveManifest(null);
}
//-----------------------------------------------------------------------------
// Write the resources to the archive
//-----------------------------------------------------------------------------
for (Item item : items) {
if (DEBUG.Enabled)
Log.debug("writing: " + item);
else
Log.info("writing: " + item.entry);
try {
zos.putNextEntry(item.entry);
copyBytesToZip(item.dataFile, zos);
} catch (Throwable t) {
Log.error("Failed to archive item: " + item, t);
}
}
zos.closeEntry();
zos.close();
Log.info("Wrote " + archive);
}
// /**
// * @deprecated - doesn't need to be this complicated, and makes ensuring uniquely named
// * archive entries surprisingly difficult -- SMF 2008-04-01
// *
// * Create a ZIP archive that contains the given map, as well as all resources who's
// * data is currently available. This means currently only data in local user files,
// * or in the image cache can be archived. Non-image remote data (e.g., documents:
// * Word, Excel, PDF, etc) cannot currently be archived. The resources in the map
// * will be annotated with package cache information before the map is written out.
// *
// * STEPS:
// *
// * 1 - All resources in the map are cloned, and all LWComponents on the map are
// * temporarily assigned these cloned resources, so we may make special
// * modifications to them for the archived map (e.g., the resources are tagged with
// * the name of their archive file).
// *
// * 2 - The map, with it's temporary set of modified resources, is marshalled
// * directly to the zip archive. It's always the first item in the archive, tho
// * this is not currently a requrement. However, it is essential that it be tagged
// * in the archive with the MapArchiveKey (via a zip entry comment), so it
// * can be identified during extraction.
// *
// * 3 - The original resources are restored to the map. We're done with the clones.
// *
// * 4 - Of the resource data available, the unique set of them is identified,
// * and they are written to the zip archive.
// *
// * This code is not thread-safe. The map should not be modified during
// * this codepath. (E.g., if ever used as part of an auto-save feature,
// * it would not be safe to let it run in a background thread).
// */
// private static void writeAnnotatedArchive(LWMap map, File archive)
// throws java.io.IOException
// {
// Log.info("Writing map-annotated archive package " + archive);
// String mapName = map.getLabel();
// if (mapName.endsWith(".vue"))
// mapName = mapName.substring(0, mapName.length() - 4);
// final String dirName = mapName + ".vdr";
// final ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(archive)));
// final ZipEntry mapEntry = new ZipEntry(dirName + "/" + mapName + "$map.vue");
// final String comment = MAP_ARCHIVE_KEY + "; VERSION: 1;"
// + " Saved " + new Date() + " by " + VUE.getName() + " built " + Version.AllInfo
// //+ "\n\tmap-name(" + mapName + ")"
// //+ "\n\tunique-resources(" + resources.size() + ")"
// ;
// Archive.setComment(mapEntry, comment);
// zos.putNextEntry(mapEntry);
// final Map<LWComponent,Resource> savedResources = new IdentityHashMap();
// final Map<Resource,Resource> clonedResources = new IdentityHashMap();
// final Map<Resource,File> onDiskFiles = new IdentityHashMap();
// UniqueNameFailsafeCount = 1; // note: static variable -- not threadsafe
// for (LWComponent c : map.getAllDescendents(LWComponent.ChildKind.ANY)) {
// final Resource resource = c.getResource();
// if (resource == null)
// continue;
// if (resource instanceof URLResource == false) {
// Log.error("UNHANDLED NON-URLResource: " + Util.tags(resource));
// continue;
// }
// final URLResource r = (URLResource) resource;
// File sourceFile = r.getActiveDataFile();
// boolean wasLocal = r.isLocalFile();
// //if (DEBUG.Enabled) Log.debug(r + "; sourceDataFile=" + sourceFile);
// if (sourceFile != null && sourceFile.exists()) {
// savedResources.put(c, r);
// final URLResource cloned = (URLResource) r.clone();
// onDiskFiles.put(cloned, sourceFile);
// final String packageName = generatePackageFileName(r, null);
// cloned.setProperty(PACKAGE_KEY_DEPRECATED, packageName);
// if (wasLocal) {
// //Log.info("STORING LOCAL PROPERTY: " + r.getSpec());
// cloned.setHiddenProperty("Package.orig", r.getSpec());
// //Log.info("STORED LOCAL PROPERTY: " + cloned.getProperty("@package.orig"));
// }
// clonedResources.put(r, cloned);
// c.takeResource(cloned);
// Log.debug("Clone: " + cloned);
// } else {
// if (sourceFile == null)
// Log.info("No cache file for: " + r);
// else
// Log.info("Missing local file: " + sourceFile);
// }
// }
// //-----------------------------------------------------------------------------
// // Archive up the map with it's re-written resources
// //-----------------------------------------------------------------------------
// final Writer mapOut = new OutputStreamWriter(zos);
// try {
// ActionUtil.marshallMapToWriter(map, mapOut);
// } catch (Throwable t) {
// Log.error(t);
// throw new RuntimeException(t);
// }
// //-----------------------------------------------------------------------------
// // Restore original resources to the map:
// //-----------------------------------------------------------------------------
// for (Map.Entry<LWComponent,Resource> e : savedResources.entrySet())
// e.getKey().takeResource(e.getValue());
// //-----------------------------------------------------------------------------
// // Write out all UNIQUE resources -- we only want to write the data once
// // no matter how many times the resource is on the map (and we don't currently
// // support single unique instance ensuring resource factories).
// //-----------------------------------------------------------------------------
// final Collection<Resource> uniqueResources = map.getAllUniqueResources();
// //ActionUtil.marshallMap(File.createTempFile("vv-" + file.getName(), ".vue"), map);
// // TODO: this might be much simpler if we just processed the same list of
// // of resources, with precomputed files, and just kept a separate map
// // of zip entries and skipped any at the last moment if they'd already been added.
// // final Set<String> uniqueEntryNames = new HashSet();
// for (Resource r : uniqueResources) {
// final Resource cloned = clonedResources.get(r);
// final File sourceFile = onDiskFiles.get(cloned);
// final String packageFileName = (cloned == null ? "[missing clone!]" : cloned.getProperty(Resource.PACKAGE_KEY_DEPRECATED));
// ZipEntry entry = null;
// if (sourceFile != null) {
// entry = new ZipEntry(dirName + "/" + packageFileName);
// }
// // TODO: to get the re-written resources to unpack, weather we specially
// // encode the SPEC, or add another special property for the local cache file
// // access (prob better), the Images.java code will need to keep the resource
// // around more, so we can decide to go to package cache or original source.
// // which could be handled also maybe via UrlAuth, tho really, we should just
// // be converting the Resource to provide the data fetch, tho whoa, THAT is a
// // problem if not all unique resources, because we still need to check the
// // cache for remote URL's... Okay, this really isn't that big of a deal.
// final String debug = "" + (DEBUG.Enabled ? r : r.getSpec());
// if (entry == null) {
// Log.info("skipped: " + debug);
// } else {
// Log.info("writing: " + entry + "; " + debug);
// Archive.setComment(entry, "\t" + SPEC_KEY + r.getSpec());
// try {
// zos.putNextEntry(entry);
// copyBytesToZip(sourceFile, zos);
// } catch (Throwable t) {
// Log.error("Failed to archive entry: " + entry + "; for " + r, t);
// }
// }
// }
// zos.closeEntry();
// zos.close();
// Log.info("Wrote " + archive);
// }
private static void copyBytesToZip(File file, ZipOutputStream zos)
throws java.io.IOException
{
final BufferedInputStream fis = new BufferedInputStream(new FileInputStream(file));
byte[] buf = new byte[2048];
int len;
int total = 0;
while ((len = fis.read(buf)) > 0) {
if (DEBUG.IO && DEBUG.META) System.err.print(".");
zos.write(buf, 0, len);
total += len;
}
//Log.debug("wrote " + total + " bytes for " + file);
fis.close();
}
}