package io.lumify.core.version;
import io.lumify.core.util.LumifyLogger;
import io.lumify.core.util.LumifyLoggerFactory;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
*
*/
public class ProjectInfoScanner implements Iterable<ProjectInfo>, Iterator<ProjectInfo> {
private static final LumifyLogger LOG = LumifyLoggerFactory.getLogger(ProjectInfoScanner.class);
private static final Pattern LUMIFY_BUILD_INFO_PATTERN =
Pattern.compile(".*\\bMETA-INF[\\\\/]lumify[\\\\/].*-build.properties$");
private static final Pattern ARCHIVE_PATTERN = Pattern.compile(".*\\.(jar|war|ear)$");
private static final String[] TARGET_EXTENSIONS = new String[]{
"properties",
"jar",
"war",
"ear"
};
private final Deque<Iterator<Entry>> entryIterStack;
private ProjectInfo nextInfo;
public ProjectInfoScanner(final File file) {
this(Arrays.asList(new File[]{file}));
}
public ProjectInfoScanner(final Collection<File> roots) {
Set<File> fullTree = new TreeSet<File>();
for (File root : roots) {
if (root.isDirectory()) {
fullTree.addAll(FileUtils.listFiles(root, TARGET_EXTENSIONS, true));
} else {
fullTree.add(root);
}
}
List<Entry> scanRoots = new ArrayList<Entry>(fullTree.size());
for (File file : fullTree) {
scanRoots.add(new FileEntry(file));
}
entryIterStack = new LinkedList<Iterator<Entry>>();
entryIterStack.push(scanRoots.iterator());
advance();
}
@Override
public Iterator<ProjectInfo> iterator() {
return this;
}
@Override
public boolean hasNext() {
return nextInfo != null;
}
@Override
public ProjectInfo next() {
if (nextInfo == null) {
throw new NoSuchElementException();
}
ProjectInfo info = nextInfo;
advance();
return info;
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove is not supported");
}
private void advance() {
try {
nextInfo = findNext();
} catch (IOException ioe) {
LOG.debug("Error finding next Lumify build info.", ioe);
throw new IllegalStateException("Error finding next Lumify build info.", ioe);
}
}
private ProjectInfo findNext() throws IOException {
Iterator<Entry> currentIter;
Entry next;
while (!entryIterStack.isEmpty()) {
currentIter = entryIterStack.peek();
while (currentIter.hasNext()) {
next = currentIter.next();
if (next.isArchive()) {
entryIterStack.push(new ZipEntryContainer(next.getFullPath(), next.getInputStream()));
return findNext();
} else if (next.isProjectInfo()) {
Properties props = new Properties();
props.load(next.getInputStream());
Map<String, String> propMap = new HashMap<String, String>();
for (String key : props.stringPropertyNames()) {
propMap.put(key, props.getProperty(key, ""));
}
return new ProjectInfo(next.getFullPath(), propMap);
}
}
entryIterStack.pop();
}
return null;
}
private abstract static class Entry {
public abstract String getFullPath();
public abstract InputStream getInputStream() throws IOException;
@Override
public String toString() {
return getFullPath();
}
public boolean isArchive() {
return getFullPath() != null && ARCHIVE_PATTERN.matcher(getFullPath()).matches();
}
public boolean isProjectInfo() {
return getFullPath() != null && LUMIFY_BUILD_INFO_PATTERN.matcher(getFullPath()).matches();
}
}
private static class ZipEntryContainer implements Iterator<Entry> {
private final String parentName;
private final ZipInputStream zipStream;
private boolean hasNext;
public ZipEntryContainer(final String pName, final InputStream stream) throws IOException {
zipStream = new ZipInputStream(stream);
hasNext = true;
parentName = pName;
}
private Entry advance() throws IOException {
ZipEntry ze = zipStream.getNextEntry();
String entryName = String.format("%s::%s", parentName, ze != null ? ze.getName() : "");
return ze != null ? new ArchiveEntry(entryName, zipStream) : null;
}
@Override
public boolean hasNext() {
return hasNext;
}
@Override
public Entry next() {
if (!hasNext) {
throw new NoSuchElementException();
}
// assume we have entries until we reach the end of the zip file.
// if we advance to the next entry before processing the current
// entry, we can't read from the stream
try {
Entry entry = advance();
if (entry == null) {
entry = new ArchiveEntry(null, null);
hasNext = false;
}
return entry;
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove is not supported");
}
}
private static class ArchiveEntry extends Entry {
private final String name;
private final InputStream stream;
public ArchiveEntry(String name, InputStream stream) {
this.name = name;
this.stream = stream;
}
@Override
public String getFullPath() {
return name;
}
@Override
public InputStream getInputStream() throws IOException {
return stream;
}
}
private static class FileEntry extends Entry {
private final File file;
public FileEntry(File file) {
this.file = file;
}
@Override
public String getFullPath() {
return file.getAbsolutePath();
}
@Override
public InputStream getInputStream() throws IOException {
return new FileInputStream(file);
}
}
}