/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache 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.apache.org/licenses/LICENSE-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 org.springframework.boot.loader.archive;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.jar.Manifest;
/**
* {@link Archive} implementation backed by an exploded archive directory.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class ExplodedArchive implements Archive {
private static final Set<String> SKIPPED_NAMES = new HashSet<>(
Arrays.asList(".", ".."));
private final File root;
private final boolean recursive;
private File manifestFile;
private Manifest manifest;
/**
* Create a new {@link ExplodedArchive} instance.
* @param root the root folder
*/
public ExplodedArchive(File root) {
this(root, true);
}
/**
* Create a new {@link ExplodedArchive} instance.
* @param root the root folder
* @param recursive if recursive searching should be used to locate the manifest.
* Defaults to {@code true}, folders with a large tree might want to set this to
* {@code
* false}.
*/
public ExplodedArchive(File root, boolean recursive) {
if (!root.exists() || !root.isDirectory()) {
throw new IllegalArgumentException("Invalid source folder " + root);
}
this.root = root;
this.recursive = recursive;
this.manifestFile = getManifestFile(root);
}
private File getManifestFile(File root) {
File metaInf = new File(root, "META-INF");
return new File(metaInf, "MANIFEST.MF");
}
@Override
public URL getUrl() throws MalformedURLException {
return this.root.toURI().toURL();
}
@Override
public Manifest getManifest() throws IOException {
if (this.manifest == null && this.manifestFile.exists()) {
FileInputStream inputStream = new FileInputStream(this.manifestFile);
try {
this.manifest = new Manifest(inputStream);
}
finally {
inputStream.close();
}
}
return this.manifest;
}
@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
List<Archive> nestedArchives = new ArrayList<>();
for (Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
}
@Override
public Iterator<Entry> iterator() {
return new FileEntryIterator(this.root, this.recursive);
}
protected Archive getNestedArchive(Entry entry) throws IOException {
File file = ((FileEntry) entry).getFile();
return (file.isDirectory() ? new ExplodedArchive(file)
: new JarFileArchive(file));
}
@Override
public String toString() {
try {
return getUrl().toString();
}
catch (Exception ex) {
return "exploded archive";
}
}
/**
* File based {@link Entry} {@link Iterator}.
*/
private static class FileEntryIterator implements Iterator<Entry> {
private final Comparator<File> entryComparator = new EntryComparator();
private final File root;
private final boolean recursive;
private final Deque<Iterator<File>> stack = new LinkedList<>();
private File current;
FileEntryIterator(File root, boolean recursive) {
this.root = root;
this.recursive = recursive;
this.stack.add(listFiles(root));
this.current = poll();
}
@Override
public boolean hasNext() {
return this.current != null;
}
@Override
public Entry next() {
if (this.current == null) {
throw new NoSuchElementException();
}
File file = this.current;
if (file.isDirectory()
&& (this.recursive || file.getParentFile().equals(this.root))) {
this.stack.addFirst(listFiles(file));
}
this.current = poll();
String name = file.toURI().getPath()
.substring(this.root.toURI().getPath().length());
return new FileEntry(name, file);
}
private Iterator<File> listFiles(File file) {
File[] files = file.listFiles();
if (files == null) {
return Collections.<File>emptyList().iterator();
}
Arrays.sort(files, this.entryComparator);
return Arrays.asList(files).iterator();
}
private File poll() {
while (!this.stack.isEmpty()) {
while (this.stack.peek().hasNext()) {
File file = this.stack.peek().next();
if (!SKIPPED_NAMES.contains(file.getName())) {
return file;
}
}
this.stack.poll();
}
return null;
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove");
}
/**
* {@link Comparator} that orders {@link File} entries by their absolute paths.
*/
private static class EntryComparator implements Comparator<File> {
@Override
public int compare(File o1, File o2) {
return o1.getAbsolutePath().compareTo(o2.getAbsolutePath());
}
}
}
/**
* {@link Entry} backed by a File.
*/
private static class FileEntry implements Entry {
private final String name;
private final File file;
FileEntry(String name, File file) {
this.name = name;
this.file = file;
}
public File getFile() {
return this.file;
}
@Override
public boolean isDirectory() {
return this.file.isDirectory();
}
@Override
public String getName() {
return this.name;
}
}
}