/*******************************************************************************
* Copyright (c) 2013 GoPivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* GoPivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.wizard.content;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.springframework.ide.eclipse.boot.wizard.BootWizardActivator;
import org.springsource.ide.eclipse.commons.frameworks.core.downloadmanager.DownloadableItem;
import org.springsource.ide.eclipse.commons.frameworks.core.downloadmanager.UIThreadDownloadDisallowed;
/**
* A CodeSet stored in a Downloadable Zip File. The interesting data in the
* code set may be somewhere inside the zip file and is pointed to by
* a path relative to the zipfile root.
*/
public class ZipFileCodeSet extends CodeSet {
private static final boolean DEBUG = false; //(""+Platform.getLocation()).contains("kdvolder");
private void debug(String msg) {
if (DEBUG) {
System.out.println(msg);
}
}
private final DownloadableItem zipDownload;
private final IPath root;
private Map<String, CodeSetEntry> entries = null;
/**
* Ensures that zip file is downloaded and entries are parsed
* into a map. Only the first time this method is called do
* we do any actual work. The zip entries are cached
* after that.
*/
private synchronized void ensureEntryCache() throws Exception, UIThreadDownloadDisallowed {
//Careful... if called in UIThred this may throw an exception because downloading content
// in the UI thread is not allowed. Callers generally know how to deal with that but
// we should take care not to accidentally leave an empty hashmap around as well.
// I.e. take care not to install the map unless reading download etc of the
// zip succeeded.
if (entries==null) {
debug(">>> caching codeset entries "+this);
final HashMap<String, CodeSetEntry> newEntries = new HashMap<String, CodeSetEntry>(1024);
each(new Processor<Void>() {
@Override
public Void doit(CodeSetEntry e) throws Exception {
debug(e.getPath().toString());
newEntries.put(e.getPath().toString(), e);
return null;
}
});
//Nothing bad happened. The cache map is ready now.
this.entries = newEntries;
debug("<<< cached codeset entries "+entries.size());
}
}
ZipFileCodeSet(String name, DownloadableItem zip, IPath root) {
super(name);
this.zipDownload = zip;
this.root = root.makeRelative();
}
@Override
public String toString() {
return "ZipCodeSet("+name+", "+zipDownload.getURL()+"@"+root+")";
}
@Override
public boolean exists() throws Exception {
ensureEntryCache();
return !entries.isEmpty();
}
@Override
public boolean hasFile(IPath path) throws UIThreadDownloadDisallowed {
try {
ensureEntryCache();
return entries.containsKey(fileKey(path));
} catch (UIThreadDownloadDisallowed e) {
throw e;
} catch (Exception e) {
BootWizardActivator.log(e);
}
return false;
}
@Override
public boolean hasFolder(IPath path) {
try {
ensureEntryCache();
return entries.containsKey(folderKey(path));
} catch (Exception e) {
BootWizardActivator.log(e);
}
return false;
}
private String folderKey(IPath _path) {
String path = fileKey(_path);
if (!path.endsWith("/")) {
//ZipEntries for dirs always end with a "/"
path = path+"/";
}
return path;
}
private String fileKey(IPath path) {
path = path.makeRelative();
return path.toString();
}
@Override
public <T> T each(Processor<T> processor) throws Exception {
T result = null;
ZipFile zip = new ZipFile(zipDownload.getFile());
try {
Enumeration<ZipArchiveEntry> iter = zip.getEntries();
while (iter.hasMoreElements() && result==null) {
ZipArchiveEntry el = iter.nextElement();
Path zipPath = new Path(el.getName());
if (root.isPrefixOf(zipPath)) {
String key = zipPath.removeFirstSegments(root.segmentCount()).toString();
if ("".equals(key)) {
//path maches exactly, this means we hit the root of the
// code set. Do not store it because the root of a codeset
// is not actually an element of the codeset!
} else {
CodeSetEntry cse = csEntry(zip, el);
result = processor.doit(cse);
if (result!=null) {
//Bail out early when result found
return result;
}
}
}
}
return result;
} finally {
try {
zip.close();
} catch (IOException e) {
}
}
}
@Override
public <T> T readFileEntry(String path, Processor<T> processor) throws Exception {
ZipFile zip = new ZipFile(zipDownload.getFile());
try {
String entryName = root.append(path).toString();
ZipArchiveEntry entry = zip.getEntry(entryName);
return processor.doit(entry==null?null:csEntry(zip, entry));
} finally {
try {
zip.close();
} catch (IOException e) {
}
}
}
/**
* Create a CodeSetEntry that wraps a ZipEntry
*/
private CodeSetEntry csEntry(final ZipFile zip, final ZipArchiveEntry e) {
IPath zipPath = new Path(e.getName()); //path relative to zip file
Assert.isTrue(root.isPrefixOf(zipPath));
final IPath csPath = zipPath.removeFirstSegments(root.segmentCount());
return new CodeSetEntry() {
@Override
public IPath getPath() {
return csPath;
}
@Override
public String toString() {
return getPath()+" in "+zipDownload;
}
@Override
public boolean isDirectory() {
return e.isDirectory();
}
@Override
public int getUnixMode() {
return e.getUnixMode();
}
@Override
public InputStream getData() throws IOException {
return zip.getInputStream(e);
}
};
}
}