/*
* Copyright 2012 Jason Miller
*
* 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 jj;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.CodeSource;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.jar.Manifest;
/**
* <p>
* Component to wrap up a directory full of jars so the contents of those jars can
* be accessed efficiently
*
*
* @author jason
*
*/
public class Jars implements ResourceResolver {
private static final String META_INF = "/META-INF";
private static final String MANIFEST_PATH = META_INF + "/MANIFEST.MF";
private static final String JAR_GLOB = "*.jar";
private final Path libPath;
private static final class FileSystemNode {
final FileSystem fileSystem;
final Path jarPath;
FileSystemNode next;
FileSystemNode(final Path jarPath, final FileSystem fileSystem) throws IOException {
this.jarPath = jarPath;
this.fileSystem = fileSystem;
}
}
private final Map<String, FileSystemNode> jars;
private final Map<FileSystem, CodeSource> codeSources;
/**
* A path pointing to a directory, presumably full of jars
*/
public Jars(final Path libPath) throws IOException {
assert Files.isDirectory(libPath) : "this component is to read and index a directory of jars";
this.libPath = libPath;
this.jars = indexJars();
this.codeSources = makeCodeSources();
}
@Override
public Path pathForFile(String file) throws IOException {
FileSystemNode fs = jarsForFile(file);
Path attempt = null;
while (attempt == null && fs != null) {
attempt = fs.fileSystem.getPath(file);
if (!Files.exists(attempt)) {
attempt = null;
fs = fs.next;
}
}
return attempt;
}
public Path[] pathsForFile(String file) throws IOException {
ArrayList<Path> result = new ArrayList<>();
FileSystemNode fs = jarsForFile(file);
while (fs != null) {
Path attempt = fs.fileSystem.getPath(file);
if (Files.exists(attempt)) {
result.add(attempt);
}
fs = fs.next;
}
return result.toArray(new Path[result.size()]);
}
public CodeSource codeSourceForFile(String file) throws IOException {
FileSystemNode fs = jarsForFile(file);
Path attempt = null;
while (attempt == null && fs != null) {
attempt = fs.fileSystem.getPath(file);
if (!Files.exists(attempt)) {
attempt = null;
fs = fs.next;
}
}
return (fs != null) ? codeSources.get(fs.fileSystem) : null;
}
public Manifest jarManifestForFile(String file) throws IOException {
Manifest result = null;
FileSystemNode fs = jarsForFile(file);
if (fs != null) {
Path manifest = fs.fileSystem.getPath(MANIFEST_PATH);
if (Files.exists(manifest)) {
try (InputStream is = Files.newInputStream(manifest)) {
result = new Manifest(is);
}
}
}
return result;
}
private FileSystemNode jarsForFile(String file) {
String key = Paths.get(file).getParent().toString() + "/";
return jars.get(key);
}
private final DirectoryStream.Filter<Path> directoryFilter = entry -> Files.isDirectory(entry) && !entry.startsWith(META_INF);
private boolean hasFiles(Path directory) throws IOException {
boolean result = false;
try (DirectoryStream<Path> entries = Files.newDirectoryStream(directory)) {
for (Path entry : entries) {
result = result || Files.isRegularFile(entry);
}
}
return result;
}
private void addDirectoriesAndRecurse(Path basePath, HashSet<Path> paths) throws IOException {
try (DirectoryStream<Path> directories = Files.newDirectoryStream(basePath, directoryFilter)) {
for (Path directory : directories) {
Path nextDirectory = basePath.resolve(directory);
if (hasFiles(nextDirectory)) {
paths.add(nextDirectory);
}
addDirectoriesAndRecurse(nextDirectory, paths);
}
}
}
private Map<FileSystem, CodeSource> makeCodeSources() throws IOException {
Map<FileSystem, CodeSource> result = new HashMap<>();
for (FileSystemNode node : jars.values()) {
while (node != null) {
if (!result.containsKey(node.fileSystem)) {
// should actually read the certs! if any
CodeSource codeSource = new CodeSource(node.jarPath.toUri().toURL(), (Certificate[])null);
result.put(node.fileSystem, codeSource);
}
node = node.next;
}
}
return Collections.unmodifiableMap(result);
}
private Map<String, FileSystemNode> indexJars() throws IOException {
Map<String, FileSystemNode> result = new HashMap<>();
try (DirectoryStream<Path> libDir = Files.newDirectoryStream(libPath, JAR_GLOB)) {
for (Path jarPath : libDir) {
FileSystem myJarFS = FileSystems.newFileSystem(jarPath, null);
HashSet<Path> paths = new HashSet<>();
for (Path root : myJarFS.getRootDirectories()) {
addDirectoriesAndRecurse(root, paths);
}
for (Path path : paths) {
String p = path.toString();
if (result.containsKey(p)) {
result.get(p).next = new FileSystemNode(jarPath, myJarFS);
} else {
result.put(p, new FileSystemNode(jarPath, myJarFS));
}
}
}
}
return Collections.unmodifiableMap(result);
}
}