com.sun.enterprise.deployment.deploy.shared.InputJarArchive Maven / Gradle / Ivy
Show all versions of payara-micro Show documentation
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2006-2013 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
// Portions Copyright [2022] Payara Foundation and/or affiliates
package com.sun.enterprise.deployment.deploy.shared;
import com.sun.enterprise.util.i18n.StringManager;
import com.sun.enterprise.util.io.FileUtils;
import org.glassfish.api.deployment.archive.ReadableArchive;
import org.jvnet.hk2.annotations.Service;
import org.glassfish.hk2.api.PerLookup;
import java.io.*;
import org.glassfish.hk2.utilities.CleanerFactory;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.net.URI;
import java.net.URISyntaxException;
import org.glassfish.logging.annotation.LogMessageInfo;
/**
* This implementation of the Archive deal with reading
* jar files either from a JarFile or from a JarInputStream
*
* @author Jerome Dochez
*/
@Service(name="jar")
@PerLookup
public class InputJarArchive extends JarArchive implements ReadableArchive {
public static final Logger deplLogger = org.glassfish.deployment.common.DeploymentContextImpl.deplLogger;
@LogMessageInfo(message = " file open failure; file = {0}", level="WARNING")
private static final String FILE_OPEN_FAILURE = "NCLS-DEPLOYMENT-00019";
@LogMessageInfo(message = "exception message: {0} -- invalid zip file: {1}", level="WARNING")
private static final String INVALID_ZIP_FILE = "NCLS-DEPLOYMENT-00020";
// the file we are currently mapped to
volatile protected JarFile jarFile;
// in case this abstraction is dealing with a jar file
// within a jar file, the jarFile will be null and this
// JarInputStream will contain the
volatile protected JarInputStream jarIS;
// the archive Uri
volatile private URI uri;
// parent jar file for embedded jar
private InputJarArchive parentArchive;
private static StringManager localStrings = StringManager.getManager(InputJarArchive.class);
// track entry enumerations to close them if needed when the archive is closed
private final WeakHashMap entryEnumerations =
new WeakHashMap();
/**
* Get the size of the archive
* @return tje the size of this archive or -1 on error
*/
public long getArchiveSize() throws NullPointerException, SecurityException {
if(uri == null) {
return -1;
}
return new File(uri).length();
}
/** @return an @see java.io.OutputStream for a new entry in this
* current abstract archive.
* @param name the entry name
*/
public OutputStream addEntry(String name) throws IOException {
throw new UnsupportedOperationException("Cannot write to an JAR archive open for reading");
}
/**
* close the abstract archive
*/
public synchronized void close() throws IOException {
for (EntryEnumeration e : entryEnumerations.keySet()) {
e.closeNoRemove();
}
entryEnumerations.clear();
if (jarFile != null) {
jarFile.close();
jarFile = null;
}
if (jarIS != null) {
jarIS.close();
jarIS = null;
}
}
private synchronized EntryEnumeration recordEntryEnumeration(final EntryEnumeration e) {
entryEnumerations.put(e, null);
return e;
}
/**
* Returns the collection of first level directories in this
* archive.
*
* Avoid having to fetch all the entries if we can avoid it. The only time
* we must do that is if size() is invoked on the collection. Use
* the CollectionWrappedEnumeration for this optimization which will
* create a full in-memory list of the entries only if and when needed
* to satisfy the size() method.
*
* @return collection of directories under the root of this archive
*/
@Override
public Collection getDirectories() throws IOException {
return new CollectionWrappedEnumeration(
new CollectionWrappedEnumeration.EnumerationFactory() {
@Override
public Enumeration enumeration() {
return entries(true);
}
});
}
/**
* creates a new abstract archive with the given path
*
* @param uri the path to create the archive
*/
public void create(URI uri) throws IOException {
throw new UnsupportedOperationException("Cannot write to an JAR archive open for reading");
}
@Override
public Enumeration entries() {
return entries(false);
}
/**
* Returns an enumeration of the entry names in the archive.
*
* @param topLevelDirectoriesOnly whether to report directories only or non-directories only
* @return enumeration of the matching entry names, excluding the manifest
*/
private Enumeration entries(final boolean topLevelDirectoriesOnly) {
try {
/*
* We have two decisions to make:
*
* 1. whether the caller wants top-level directory entries or all
* non-directory entries enumerated, and
*
* 2. how to obtain the sequence of JarEntry objects which we filter
* before returning their names.
*
*/
return recordEntryEnumeration(createEntryEnumeration(topLevelDirectoriesOnly));
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
/**
* @return an @see java.util.Enumeration of entries in this abstract
* archive, providing the list of embedded archive to not count their
* entries as part of this archive
*/
public Enumeration entries(Enumeration embeddedArchives) {
// jar file are not recursive
return entries();
}
public JarEntry getJarEntry(String name) {
if (jarFile == null) {
return null;
}
return jarFile.getJarEntry(name);
}
/**
* Returns the existence of the given entry name
* The file name must be relative to the root of the module.
*
* @param name the file name relative to the root of the module. * @return the existence the given entry name.
*/
public boolean exists(String name) throws IOException {
if (jarFile == null) {
return false;
}
return jarFile.getEntry(name) != null;
}
/**
* @return a @see java.io.InputStream for an existing entry in
* the current abstract archive
* @param entryName entry name
*/
public InputStream getEntry(String entryName) throws IOException {
//for the momment this is a fix to prevent inputstream issues
if(jarFile == null) {
open(this.getURI());
}
if (jarFile != null) {
ZipEntry ze = jarFile.getEntry(entryName);
if(ze == null) {
return null;
}
return new BufferedInputStream(jarFile.getInputStream(ze));
}
if (parentArchive == null || parentArchive.jarFile == null) {
return null;
}
// close the current input stream
if (jarIS != null) {
jarIS.close();
}
// reopen the embedded archive and position the input stream
// at the beginning of the desired element
JarEntry archiveJarEntry = (uri != null) ? parentArchive.jarFile.getJarEntry(uri.getSchemeSpecificPart()) : null;
if (archiveJarEntry == null) {
return null;
}
JarEntry je;
jarIS = new JarInputStream(parentArchive.jarFile.getInputStream(archiveJarEntry));
do {
je = jarIS.getNextJarEntry();
} while (je != null && !je.getName().equals(entryName));
if(je == null) {
return null;
}
return new BufferedInputStream(jarIS);
}
/**
* Returns the entry size for a given entry name or 0 if not known
*
* @param name the entry name
* @return the entry size
*/
public long getEntrySize(String name) {
if (jarFile == null) {
return 0;
}
ZipEntry ze = jarFile.getEntry(name);
if (ze == null) {
return 0;
}
return ze.getSize();
}
/** Open an abstract archive
* @param uri the path to the archive
*/
public void open(URI uri) throws IOException {
this.uri = uri;
jarFile = getJarFile(uri);
}
/**
* @return a JarFile instance for a file path
*/
protected static JarFile getJarFile(URI uri) throws IOException {
JarFile jarFile = null;
try {
File file = new File(uri);
if (file.exists()) {
jarFile = new JarFile(file);
}
} catch (IOException e) {
deplLogger.log(Level.WARNING, FILE_OPEN_FAILURE, new Object[]{uri});
// add the additional information about the path
// since the IOException from jdk doesn't include that info
String additionalInfo = localStrings.getString("enterprise.deployment.invalid_zip_file", uri);
deplLogger.log(Level.WARNING, INVALID_ZIP_FILE, new Object[]{e.getLocalizedMessage(), additionalInfo});
}
return jarFile;
}
/**
* @return the manifest information for this abstract archive
*/
public Manifest getManifest() throws IOException {
if (jarFile != null) {
return jarFile.getManifest();
}
if (parentArchive == null) {
return null;
}
// close the current input stream
if (jarIS != null) {
jarIS.close();
}
// reopen the embedded archive and position the input stream
// at the beginning of the desired element
if (jarIS == null) {
jarIS = new JarInputStream(parentArchive.jarFile.getInputStream(parentArchive.jarFile.
getJarEntry(uri.getSchemeSpecificPart())));
}
Manifest manifest = jarIS.getManifest();
if (manifest == null) {
java.io.InputStream is = getEntry(java.util.jar.JarFile.MANIFEST_NAME);
if (is != null) {
manifest = new Manifest();
manifest.read(is);
is.close();
}
}
return manifest;
}
/**
* Returns the path used to create or open the underlying archive
*
* @return the path for this archive.
*/
public URI getURI() {
return uri;
}
/**
* @return true if this abstract archive maps to an existing
* jar file
*/
public boolean exists() {
return jarFile != null;
}
/**
* deletes the underlying jar file
*/
public boolean delete() {
if (jarFile == null) {
return false;
}
try {
jarFile.close();
jarFile = null;
} catch (IOException ioe) {
return false;
}
return FileUtils.deleteFile(new File(uri));
}
/**
* rename the underlying jar file
*/
public boolean renameTo(String name) {
if (jarFile == null) {
return false;
}
try {
jarFile.close();
jarFile = null;
} catch (IOException ioe) {
return false;
}
return FileUtils.renameFile(new File(uri), new File(name));
}
/**
* @return an Archive for an embedded archive indentified with
* the name parameter
*/
public ReadableArchive getSubArchive(String name) throws IOException {
if (jarFile != null) {
// for now, I only support one level down embedded archives
InputJarArchive ija = new InputJarArchive();
JarEntry je = jarFile.getJarEntry(name);
if (je != null) {
JarInputStream jis = new JarInputStream(new BufferedInputStream(jarFile.getInputStream(je)));
try {
ija.uri = new URI("jar", name, null);
} catch (URISyntaxException e) {
// do nothing
}
ija.jarIS = jis;
ija.parentArchive = this;
return ija;
}
}
return null;
}
/**
* Creates the correct type of entry enumeration, depending on whether the
* current archive is nested or not and depending on whether the caller
* requested top-level directory entries or all non-directory entries be returned
* in the enumeration.
*
* @param uriToReadForEntries
* @param topLevelDirectoriesOnly
* @return
* @throws FileNotFoundException
* @throws IOException
*/
private EntryEnumeration createEntryEnumeration(final boolean topLevelDirectoriesOnly) throws FileNotFoundException, IOException {
final JarEntrySource source = (parentArchive == null ?
new ArchiveJarEntrySource(uri) :
new SubarchiveJarEntrySource(parentArchive.jarFile, uri));
if (topLevelDirectoriesOnly) {
return new TopLevelDirectoryEntryEnumeration(source);
} else {
return new NonDirectoryEntryEnumeration(source);
}
}
/**
* Logic for enumerations of the entry names that is common between the
* top-level directory entry enumeration and the full non-directory
* enumeration.
*
* The goal is to wrap an Enumeration around the underlying entries
* available in the archive. This avoids collecting all
* the entry names first and then returning an enumeration of the collection;
* that can be very costly for large JARs.
*
* But, the trade-off is that we need to be careful because we leave a stream
* opened to the JAR. So, even though the finalizer is not guaranteed to be
* invoked, we still provide one to close up the JarFile. This should help
* reduce the chance for locked JARs on Windows due to open streams.
*/
private abstract class EntryEnumeration implements Enumeration {
/* look-ahead of one entry */
private JarEntry nextMatchingEntry;
/* source of JarEntry objects for building the enumeration values */
private final JarEntrySource jarEntrySource;
private EntryEnumeration(final JarEntrySource jarEntrySource) {
this.jarEntrySource = jarEntrySource;
registerCloseEvent();
}
/**
* Finishes the initialization for the enumeration; MUST be invoked
* from the subclass constructor after super(...).
*/
protected void completeInit() {
nextMatchingEntry = skipToNextMatchingEntry();
}
@Override
public boolean hasMoreElements() {
return nextMatchingEntry != null;
}
@Override
public String nextElement() {
if (nextMatchingEntry == null) {
throw new NoSuchElementException();
}
final String answer = nextMatchingEntry.getName();
nextMatchingEntry = skipToNextMatchingEntry();
return answer;
}
protected JarEntry getNextJarEntry() throws IOException {
return jarEntrySource.getNextJarEntry();
}
/**
* Returns the next JarEntry available from the archive.
*
* Different concrete subclasses implement this differently so as to
* enumerate the correct sequence of entry names.
*
* @return the next available JarEntry; null if no more are available
*/
protected abstract JarEntry skipToNextMatchingEntry();
private void closeNoRemove() {
try {
jarEntrySource.close();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
void close() {
closeNoRemove();
entryEnumerations.remove(this);
}
public final void registerCloseEvent() {
CleanerFactory.create().register(this, () -> {
close();
});
}
}
/**
* Defines behavior for sources of JarEntry objects for EntryEnumeration
* implementations.
*
* The implementation must be different for top-level archives vs.
* subarchives.
*/
private interface JarEntrySource {
/**
* Returns the next JarEntry from the raw entries() enumeration
* of the archive or subarchive.
* @return JarEntry for the next entry in the JarArchive
* @throws IOException
*/
JarEntry getNextJarEntry() throws IOException;
/**
* Closes the source of the JarEntry objects.
* @throws IOException
*/
void close() throws IOException;
}
/**
* Source of JarEntry objects for a top-level archive (as opposed to a
* subarchive).
*/
private static class ArchiveJarEntrySource implements JarEntrySource {
private JarFile sourceJarFile;
private Enumeration jarEntries;
private ArchiveJarEntrySource(final URI archiveURI) throws IOException {
sourceJarFile = getJarFile(archiveURI);
if (sourceJarFile == null){
throw new IOException(localStrings.getString("enterprise.deployment.invalid_zip_file", archiveURI));
}
jarEntries = sourceJarFile.entries();
}
@Override
public JarEntry getNextJarEntry() {
return (jarEntries.hasMoreElements()) ? jarEntries.nextElement() : null;
}
@Override
public void close() throws IOException {
sourceJarFile.close();
}
}
/**
* Source of JarEntry objects for a subarchive.
*/
private static class SubarchiveJarEntrySource implements JarEntrySource {
private JarInputStream jis;
private SubarchiveJarEntrySource(final JarFile jf, final URI uri) throws IOException {
final JarEntry subarchiveJarEntry = jf.getJarEntry(uri.getSchemeSpecificPart());
jis = new JarInputStream(jf.getInputStream(subarchiveJarEntry));
}
@Override
public JarEntry getNextJarEntry() throws IOException {
return jis.getNextJarEntry();
}
@Override
public void close() throws IOException {
jis.close();
}
}
/**
* Enumerates the top-level directory entries.
*
* This implementation uses the enumeration of JarEntry objects from the
* JarFile itself.
*/
private class TopLevelDirectoryEntryEnumeration extends EntryEnumeration {
private TopLevelDirectoryEntryEnumeration(final JarEntrySource jarEntrySource) throws FileNotFoundException, IOException {
super(jarEntrySource);
completeInit();
}
@Override
protected JarEntry skipToNextMatchingEntry() {
JarEntry candidateNextEntry;
try {
/*
* The next entry should be returned (and not skipped) if the entry is a
* directory entry and it contains only a single slash at the
* end of the entry name.
*/
while ((candidateNextEntry = getNextJarEntry()) != null) {
final String candidateNextEntryName = candidateNextEntry.getName();
if (candidateNextEntry.isDirectory()
&& (candidateNextEntryName.indexOf('/') == candidateNextEntryName.lastIndexOf('/'))
&& (candidateNextEntryName.indexOf('/') == candidateNextEntryName.length() - 1)) {
break;
}
}
return candidateNextEntry;
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
/**
* Enumerates entries in the archive that are not directories.
*/
private class NonDirectoryEntryEnumeration extends EntryEnumeration {
private NonDirectoryEntryEnumeration(final JarEntrySource jarEntrySource) throws IOException {
super(jarEntrySource);
completeInit();
}
@Override
protected JarEntry skipToNextMatchingEntry() {
JarEntry candidateNextEntry;
try {
/*
* The next entry should be returned (and not skipped) if the entry is
* not a directory entry and if it also not the manifest.
*/
while ((candidateNextEntry = getNextJarEntry()) != null) {
final String candidateNextEntryName = candidateNextEntry.getName();
if (!candidateNextEntry.isDirectory() && !candidateNextEntryName.equals(JarFile.MANIFEST_NAME)) {
break;
}
}
return candidateNextEntry;
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
/**
* A Collection which wraps an Enumeration.
*
* Note that the nextSlot field is always updated, even if we are using
* the original enumeration to return the next value from the iterator. This
* is so that, if the caller invokes size() which causes us to build the
* ArrayList containing all the elements -- even if that invocation comes while
* the iterator is being used to return values -- the subsequent invocations
* of hasNext and next will use the correct place in the newly-constructed
* ArrayList of values.
*
* @param
*/
static class CollectionWrappedEnumeration extends AbstractCollection {
/** Used only if size is invoked */
private ArrayList entries = null;
/** always updated, even if we use the enumeration */
private int nextSlot = 0;
private final EnumerationFactory factory;
private Enumeration e;
static interface EnumerationFactory {
public Enumeration enumeration();
}
CollectionWrappedEnumeration(final EnumerationFactory factory) {
this.factory = factory;
e = factory.enumeration();
}
@Override
public Iterator iterator() {
return new Iterator() {
@Override
public boolean hasNext() {
return (entries != null) ? nextSlot < entries.size() : e.hasMoreElements();
}
@Override
public T next() {
T result = null;
if (entries != null) {
if (nextSlot >= entries.size()) {
throw new NoSuchElementException();
}
result = entries.get(nextSlot++);
} else {
result = e.nextElement();
nextSlot++;
}
return result;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
public int size() {
if (entries == null) {
populateEntries();
};
return entries.size();
}
private void populateEntries() {
entries = new ArrayList();
/*
* Fill up the with data from
* a new enumeration.
*/
for (Enumeration newE = factory.enumeration(); newE.hasMoreElements(); ) {
entries.add(newE.nextElement());
}
e = null;
}
}
}