Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.joshworks.snappy.loader.jar.JarFile Maven / Gradle / Ivy
/*
* Copyright 2012-2016 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 io.joshworks.snappy.loader.jar;
import io.joshworks.snappy.loader.data.RandomAccessData;
import io.joshworks.snappy.loader.data.RandomAccessData.ResourceAccess;
import io.joshworks.snappy.loader.data.RandomAccessDataFile;
import io.joshworks.snappy.loader.util.AsciiBytes;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
/**
* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
* offers the following additional functionality.
*
* New filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created}
* from existing files.
* A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
* on any directory entry.
* A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
* embedded JAR files (as long as their entry is not compressed).
* Entry data can be accessed as {@link RandomAccessData}.
*
*
* @author Phillip Webb
*/
public class JarFile extends java.util.jar.JarFile implements Iterable {
private static final AsciiBytes META_INF = new AsciiBytes("META-INF/");
private static final AsciiBytes MANIFEST_MF = new AsciiBytes("META-INF/MANIFEST.MF");
private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF");
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "io.joshworks.boot.loader";
private static final AsciiBytes SLASH = new AsciiBytes("/");
private final RandomAccessDataFile rootFile;
private final String pathFromRoot;
private final RandomAccessData data;
private final List entries;
private SoftReference> entriesByName;
private boolean signed;
private JarEntryData manifestEntry;
private SoftReference manifest;
private URL url;
/**
* Create a new {@link JarFile} backed by the specified file.
*
* @param file the root jar file
* @throws IOException if the file cannot be read
*/
public JarFile(File file) throws IOException {
this(new RandomAccessDataFile(file));
}
/**
* Create a new {@link JarFile} backed by the specified file.
*
* @param file the root jar file
* @throws IOException if the file cannot be read
*/
JarFile(RandomAccessDataFile file) throws IOException {
this(file, "", file);
}
/**
* Private constructor used to create a new {@link JarFile} either directly or from a
* nested entry.
*
* @param rootFile the root jar file
* @param pathFromRoot the name of this file
* @param data the underlying data
* @throws IOException if the file cannot be read
*/
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot,
RandomAccessData data) throws IOException {
super(rootFile.getFile());
CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data);
this.rootFile = rootFile;
this.pathFromRoot = pathFromRoot;
this.data = getArchiveData(endRecord, data);
this.entries = loadJarEntries(endRecord);
}
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot,
RandomAccessData data, List entries, JarEntryFilter... filters)
throws IOException {
super(rootFile.getFile());
this.rootFile = rootFile;
this.pathFromRoot = pathFromRoot;
this.data = data;
this.entries = filterEntries(entries, filters);
}
/**
* Register a {@literal 'java.protocol.handler.pkgs'} property so that a
* {@link URLStreamHandler} will be located to deal with jar URLs.
*/
public static void registerUrlProtocolHandler() {
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
: handlers + "|" + HANDLERS_PACKAGE));
resetCachedUrlHandlers();
}
/**
* Reset any cached handlers just in case a jar protocol has already been used. We
* reset the handler by trying to set a null {@link URLStreamHandlerFactory} which
* should have no effect other than clearing the handlers cache.
*/
private static void resetCachedUrlHandlers() {
try {
URL.setURLStreamHandlerFactory(null);
} catch (Error ex) {
// Ignore
}
}
private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord,
RandomAccessData data) {
long offset = endRecord.getStartOfArchive(data);
if (offset == 0) {
return data;
}
return data.getSubsection(offset, data.getSize() - offset);
}
private List loadJarEntries(CentralDirectoryEndRecord endRecord)
throws IOException {
RandomAccessData centralDirectory = endRecord.getCentralDirectory(this.data);
int numberOfRecords = endRecord.getNumberOfRecords();
List entries = new ArrayList(numberOfRecords);
InputStream inputStream = centralDirectory.getInputStream(ResourceAccess.ONCE);
try {
JarEntryData entry = JarEntryData.fromInputStream(this, inputStream);
while (entry != null) {
entries.add(entry);
processEntry(entry);
entry = JarEntryData.fromInputStream(this, inputStream);
}
} finally {
inputStream.close();
}
return entries;
}
private List filterEntries(List entries,
JarEntryFilter[] filters) {
List filteredEntries = new ArrayList(entries.size());
for (JarEntryData entry : entries) {
AsciiBytes name = entry.getName();
for (JarEntryFilter filter : filters) {
name = (filter == null || name == null ? name
: filter.apply(name, entry));
}
if (name != null) {
JarEntryData filteredCopy = entry.createFilteredCopy(this, name);
filteredEntries.add(filteredCopy);
processEntry(filteredCopy);
}
}
return filteredEntries;
}
private void processEntry(JarEntryData entry) {
AsciiBytes name = entry.getName();
if (name.startsWith(META_INF)) {
processMetaInfEntry(name, entry);
}
}
private void processMetaInfEntry(AsciiBytes name, JarEntryData entry) {
if (name.equals(MANIFEST_MF)) {
this.manifestEntry = entry;
}
if (name.endsWith(SIGNATURE_FILE_EXTENSION)) {
this.signed = true;
}
}
protected final RandomAccessDataFile getRootJarFile() {
return this.rootFile;
}
RandomAccessData getData() {
return this.data;
}
@Override
public Manifest getManifest() throws IOException {
if (this.manifestEntry == null) {
return null;
}
Manifest manifest = (this.manifest == null ? null : this.manifest.get());
if (manifest == null) {
InputStream inputStream = this.manifestEntry.getInputStream();
try {
manifest = new Manifest(inputStream);
} finally {
inputStream.close();
}
this.manifest = new SoftReference(manifest);
}
return manifest;
}
@Override
public Enumeration entries() {
final Iterator iterator = iterator();
return new Enumeration() {
@Override
public boolean hasMoreElements() {
return iterator.hasNext();
}
@Override
public java.util.jar.JarEntry nextElement() {
return iterator.next().asJarEntry();
}
};
}
@Override
public Iterator iterator() {
return this.entries.iterator();
}
@Override
public JarEntry getJarEntry(String name) {
return (JarEntry) getEntry(name);
}
@Override
public ZipEntry getEntry(String name) {
JarEntryData jarEntryData = getJarEntryData(name);
return (jarEntryData == null ? null : jarEntryData.asJarEntry());
}
public JarEntryData getJarEntryData(String name) {
if (name == null) {
return null;
}
return getJarEntryData(new AsciiBytes(name));
}
public JarEntryData getJarEntryData(AsciiBytes name) {
if (name == null) {
return null;
}
Map entriesByName = (this.entriesByName == null ? null
: this.entriesByName.get());
if (entriesByName == null) {
entriesByName = new HashMap();
for (JarEntryData entry : this.entries) {
entriesByName.put(entry.getName(), entry);
}
this.entriesByName = new SoftReference>(
entriesByName);
}
JarEntryData entryData = entriesByName.get(name);
if (entryData == null && !name.endsWith(SLASH)) {
entryData = entriesByName.get(name.append(SLASH));
}
return entryData;
}
boolean isSigned() {
return this.signed;
}
void setupEntryCertificates() {
// Fallback to JarInputStream to obtain certificates, not fast but hopefully not
// happening that often.
try {
JarInputStream inputStream = new JarInputStream(
getData().getInputStream(ResourceAccess.ONCE));
try {
java.util.jar.JarEntry entry = inputStream.getNextJarEntry();
while (entry != null) {
inputStream.closeEntry();
JarEntry jarEntry = getJarEntry(entry.getName());
if (jarEntry != null) {
jarEntry.setupCertificates(entry);
}
entry = inputStream.getNextJarEntry();
}
} finally {
inputStream.close();
}
} catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
@Override
public synchronized InputStream getInputStream(ZipEntry ze) throws IOException {
return getContainedEntry(ze).getSource().getInputStream();
}
/**
* Return a nested {@link JarFile} loaded from the specified entry.
*
* @param entry the zip entry
* @return a {@link JarFile} for the entry
* @throws IOException if the nested jar file cannot be read
*/
public synchronized JarFile getNestedJarFile(final ZipEntry entry)
throws IOException {
return getNestedJarFile(getContainedEntry(entry).getSource());
}
/**
* Return a nested {@link JarFile} loaded from the specified entry.
*
* @param sourceEntry the zip entry
* @return a {@link JarFile} for the entry
* @throws IOException if the nested jar file cannot be read
*/
public synchronized JarFile getNestedJarFile(JarEntryData sourceEntry)
throws IOException {
try {
if (sourceEntry.nestedJar == null) {
sourceEntry.nestedJar = createJarFileFromEntry(sourceEntry);
}
return sourceEntry.nestedJar;
} catch (IOException ex) {
throw new IOException(
"Unable to open nested jar file '" + sourceEntry.getName() + "'", ex);
}
}
private JarFile createJarFileFromEntry(JarEntryData sourceEntry) throws IOException {
if (sourceEntry.isDirectory()) {
return createJarFileFromDirectoryEntry(sourceEntry);
}
return createJarFileFromFileEntry(sourceEntry);
}
private JarFile createJarFileFromDirectoryEntry(JarEntryData sourceEntry)
throws IOException {
final AsciiBytes sourceName = sourceEntry.getName();
JarEntryFilter filter = new JarEntryFilter() {
@Override
public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) {
if (name.startsWith(sourceName) && !name.equals(sourceName)) {
return name.substring(sourceName.length());
}
return null;
}
};
return new JarFile(this.rootFile,
this.pathFromRoot + "!/"
+ sourceEntry.getName().substring(0, sourceName.length() - 1),
this.data, this.entries, filter);
}
private JarFile createJarFileFromFileEntry(JarEntryData sourceEntry)
throws IOException {
if (sourceEntry.getMethod() != ZipEntry.STORED) {
throw new IllegalStateException("Unable to open nested entry '"
+ sourceEntry.getName() + "'. It has been compressed and nested "
+ "jar files must be stored without compression. Please check the "
+ "mechanism used to create your executable jar file");
}
return new JarFile(this.rootFile,
this.pathFromRoot + "!/" + sourceEntry.getName(), sourceEntry.getData());
}
/**
* Return a new jar based on the filtered contents of this file.
*
* @param filters the set of jar entry filters to be applied
* @return a filtered {@link JarFile}
* @throws IOException if the jar file cannot be read
*/
public synchronized JarFile getFilteredJarFile(JarEntryFilter... filters)
throws IOException {
return new JarFile(this.rootFile, this.pathFromRoot, this.data, this.entries,
filters);
}
private JarEntry getContainedEntry(ZipEntry zipEntry) throws IOException {
if (zipEntry instanceof JarEntry
&& ((JarEntry) zipEntry).getSource().getSource() == this) {
return (JarEntry) zipEntry;
}
throw new IllegalArgumentException("ZipEntry must be contained in this file");
}
@Override
public int size() {
return (int) this.data.getSize();
}
@Override
public void close() throws IOException {
this.rootFile.close();
}
/**
* Return a URL that can be used to access this JAR file. NOTE: the specified URL
* cannot be serialized and or cloned.
*
* @return the URL
* @throws MalformedURLException if the URL is malformed
*/
public URL getUrl() throws MalformedURLException {
if (this.url == null) {
Handler handler = new Handler(this);
String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/";
file = file.replace("file:////", "file://"); // Fix UNC paths
this.url = new URL("jar", "", -1, file, handler);
}
return this.url;
}
@Override
public String toString() {
return getName();
}
@Override
public String getName() {
String path = this.pathFromRoot;
return this.rootFile.getFile() + path;
}
}