//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.util;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Stream;
Utility class to handle a Multi Release Jar file
/**
* <p>Utility class to handle a Multi Release Jar file</p>
*/
public class MultiReleaseJarFile implements Closeable
{
private static final String META_INF_VERSIONS = "META-INF/versions/";
private final JarFile jarFile;
private final int platform;
private final boolean multiRelease;
/* Map to hold unversioned name to VersionedJarEntry */
private final Map<String, VersionedJarEntry> entries;
Construct a multi release jar file for the current JVM version, ignoring directories.
Params: - file – The file to open
Throws: - IOException – if the jar file cannot be read
/**
* Construct a multi release jar file for the current JVM version, ignoring directories.
*
* @param file The file to open
* @throws IOException if the jar file cannot be read
*/
public MultiReleaseJarFile(File file) throws IOException
{
this(file, JavaVersion.VERSION.getPlatform(), false);
}
Construct a multi release jar file
Params: - file – The file to open
- javaPlatform – The JVM platform to apply when selecting a version.
- includeDirectories – true if any directory entries should not be ignored
Throws: - IOException – if the jar file cannot be read
/**
* Construct a multi release jar file
*
* @param file The file to open
* @param javaPlatform The JVM platform to apply when selecting a version.
* @param includeDirectories true if any directory entries should not be ignored
* @throws IOException if the jar file cannot be read
*/
public MultiReleaseJarFile(File file, int javaPlatform, boolean includeDirectories) throws IOException
{
if (file == null || !file.exists() || !file.canRead() || file.isDirectory())
throw new IllegalArgumentException("bad jar file: " + file);
jarFile = new JarFile(file, true, JarFile.OPEN_READ);
this.platform = javaPlatform;
Manifest manifest = jarFile.getManifest();
if (manifest == null)
multiRelease = false;
else
multiRelease = Boolean.parseBoolean(String.valueOf(manifest.getMainAttributes().getValue("Multi-Release")));
Map<String, VersionedJarEntry> map = new TreeMap<>();
jarFile.stream()
.map(VersionedJarEntry::new)
.filter(e -> (includeDirectories || !e.isDirectory()) && e.isApplicable())
.forEach(e -> map.compute(e.name, (k, v) -> v == null || v.isReplacedBy(e) ? e : v));
for (Iterator<Map.Entry<String, VersionedJarEntry>> i = map.entrySet().iterator(); i.hasNext(); )
{
Map.Entry<String, VersionedJarEntry> e = i.next();
VersionedJarEntry entry = e.getValue();
if (entry.inner)
{
VersionedJarEntry outer = entry.outer == null ? null : map.get(entry.outer);
if (outer == null || outer.version != entry.version)
i.remove();
}
}
entries = Collections.unmodifiableMap(map);
}
Returns: true IFF the jar is a multi release jar
/**
* @return true IFF the jar is a multi release jar
*/
public boolean isMultiRelease()
{
return multiRelease;
}
Returns: The major version applied to this jar for the purposes of selecting entries
/**
* @return The major version applied to this jar for the purposes of selecting entries
*/
public int getVersion()
{
return platform;
}
Returns: A stream of versioned entries from the jar, excluded any that are not applicable
/**
* @return A stream of versioned entries from the jar, excluded any that are not applicable
*/
public Stream<VersionedJarEntry> stream()
{
return entries.values().stream();
}
Get a versioned resource entry by name
Params: - name – The unversioned name of the resource
Returns: The versioned entry of the resource
/**
* Get a versioned resource entry by name
*
* @param name The unversioned name of the resource
* @return The versioned entry of the resource
*/
public VersionedJarEntry getEntry(String name)
{
return entries.get(name);
}
@Override
public void close() throws IOException
{
if (jarFile != null)
jarFile.close();
}
@Override
public String toString()
{
return String.format("%s[%b,%d]", jarFile.getName(), isMultiRelease(), getVersion());
}
A versioned Jar entry
/**
* A versioned Jar entry
*/
public class VersionedJarEntry
{
final JarEntry entry;
final String name;
final int version;
final boolean inner;
final String outer;
VersionedJarEntry(JarEntry entry)
{
int v = 0;
String name = entry.getName();
if (name.startsWith(META_INF_VERSIONS))
{
v = -1;
int index = name.indexOf('/', META_INF_VERSIONS.length());
if (index > META_INF_VERSIONS.length() && index < name.length())
{
try
{
v = TypeUtil.parseInt(name, META_INF_VERSIONS.length(), index - META_INF_VERSIONS.length(), 10);
name = name.substring(index + 1);
}
catch (NumberFormatException x)
{
throw new RuntimeException("illegal version in " + jarFile, x);
}
}
}
this.entry = entry;
this.name = name;
this.version = v;
this.inner = name.contains("$") && name.toLowerCase(Locale.ENGLISH).endsWith(".class");
this.outer = inner ? name.substring(0, name.indexOf('$')) + ".class" : null;
}
Returns: the unversioned name of the resource
/**
* @return the unversioned name of the resource
*/
public String getName()
{
return name;
}
Returns: The name of the resource within the jar, which could be versioned
/**
* @return The name of the resource within the jar, which could be versioned
*/
public String getNameInJar()
{
return entry.getName();
}
Returns: The version of the resource or 0 for a base version
/**
* @return The version of the resource or 0 for a base version
*/
public int getVersion()
{
return version;
}
Returns: True iff the entry is not from the base version
/**
* @return True iff the entry is not from the base version
*/
public boolean isVersioned()
{
return version > 0;
}
Returns: True iff the entry is a directory
/**
* @return True iff the entry is a directory
*/
public boolean isDirectory()
{
return entry.isDirectory();
}
Throws: - IOException – if something goes wrong!
Returns: An input stream of the content of the versioned entry.
/**
* @return An input stream of the content of the versioned entry.
* @throws IOException if something goes wrong!
*/
public InputStream getInputStream() throws IOException
{
return jarFile.getInputStream(entry);
}
boolean isApplicable()
{
if (multiRelease)
return (this.version == 0 || this.version == platform) && name.length() > 0;
return this.version == 0;
}
boolean isReplacedBy(VersionedJarEntry entry)
{
if (isDirectory())
return entry.version == 0;
return this.name.equals(entry.name) && entry.version > version;
}
@Override
public String toString()
{
return String.format("%s->%s[%d]", name, entry.getName(), version);
}
}
}