aQute.bnd.osgi.Jar Maven / Gradle / Ivy
Show all versions of biz.aQute.bndlib Show documentation
package aQute.bnd.osgi;
import static aQute.lib.io.IO.getFile;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import aQute.bnd.version.Version;
import aQute.lib.base64.Base64;
import aQute.lib.io.IO;
import aQute.lib.io.IOConstants;
import aQute.lib.zip.ZipUtil;
import aQute.service.reporter.Reporter;
public class Jar implements Closeable {
static final int BUFFER_SIZE = IOConstants.PAGE_SIZE * 16;
public enum Compression {
DEFLATE, STORE
}
static final String DEFAULT_MANIFEST_NAME = "META-INF/MANIFEST.MF";
public static final Object[] EMPTY_ARRAY = new Jar[0];
final TreeMap resources = new TreeMap();
final TreeMap> directories = new TreeMap>();
Manifest manifest;
boolean manifestFirst;
String manifestName = DEFAULT_MANIFEST_NAME;
String name;
File source;
ZipFile zipFile;
long lastModified;
String lastModifiedReason;
Reporter reporter;
boolean doNotTouchManifest;
boolean nomanifest;
Compression compression = Compression.DEFLATE;
boolean closed;
String[] algorithms;
public Jar(String name) {
this.name = name;
}
public Jar(String name, File dirOrFile, Pattern doNotCopy) throws ZipException, IOException {
this(name);
source = dirOrFile;
if (dirOrFile.isDirectory())
FileResource.build(this, dirOrFile, doNotCopy);
else if (dirOrFile.isFile()) {
zipFile = ZipResource.build(this, dirOrFile);
} else {
throw new IllegalArgumentException("A Jar can only accept a valid file or directory: " + dirOrFile);
}
}
public Jar(String name, InputStream in, long lastModified) throws IOException {
this(name);
EmbeddedResource.build(this, in, lastModified);
}
public Jar(String name, String path) throws IOException {
this(name);
File f = new File(path);
InputStream in = new FileInputStream(f);
EmbeddedResource.build(this, in, f.lastModified());
in.close();
}
public Jar(File f) throws IOException {
this(getName(f), f, null);
}
/**
* Make the JAR file name the project name if we get a src or bin directory.
*
* @param f
*/
private static String getName(File f) {
f = f.getAbsoluteFile();
String name = f.getName();
if (name.equals("bin") || name.equals("src"))
return f.getParentFile().getName();
if (name.endsWith(".jar"))
name = name.substring(0, name.length() - 4);
return name;
}
public Jar(String string, InputStream resourceAsStream) throws IOException {
this(string, resourceAsStream, 0);
}
public Jar(String string, File file) throws ZipException, IOException {
this(string, file, Pattern.compile(Constants.DEFAULT_DO_NOT_COPY));
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Jar:" + name;
}
public boolean putResource(String path, Resource resource) {
check();
return putResource(path, resource, true);
}
public boolean putResource(String path, Resource resource, boolean overwrite) {
check();
updateModified(resource.lastModified(), path);
while (path.startsWith("/"))
path = path.substring(1);
if (path.equals(manifestName)) {
manifest = null;
if (resources.isEmpty())
manifestFirst = true;
}
String dir = getDirectory(path);
Map s = directories.get(dir);
if (s == null) {
s = new TreeMap();
directories.put(dir, s);
int n = dir.lastIndexOf('/');
while (n > 0) {
String dd = dir.substring(0, n);
if (directories.containsKey(dd))
break;
directories.put(dd, null);
n = dd.lastIndexOf('/');
}
}
boolean duplicate = s.containsKey(path);
if (!duplicate || overwrite) {
resources.put(path, resource);
s.put(path, resource);
}
return duplicate;
}
public Resource getResource(String path) {
check();
if (resources == null)
return null;
return resources.get(path);
}
private String getDirectory(String path) {
check();
int n = path.lastIndexOf('/');
if (n < 0)
return "";
return path.substring(0, n);
}
public Map> getDirectories() {
check();
return directories;
}
public Map getResources() {
check();
return resources;
}
public boolean addDirectory(Map directory, boolean overwrite) {
check();
boolean duplicates = false;
if (directory == null)
return false;
for (Map.Entry entry : directory.entrySet()) {
String key = entry.getKey();
//
// Previous version did not copy JAVA files but
// I think this is very old (everybody seems to separate the
// sources from the binaries nowadays) and it is a fix
// on the wrong level. Lets see if someone whines.
//
duplicates |= putResource(key, entry.getValue(), overwrite);
}
return duplicates;
}
public Manifest getManifest() throws Exception {
check();
if (manifest == null) {
Resource manifestResource = getResource(manifestName);
if (manifestResource != null) {
InputStream in = manifestResource.openInputStream();
manifest = new Manifest(in);
in.close();
}
}
return manifest;
}
public boolean exists(String path) {
check();
return resources.containsKey(path);
}
public void setManifest(Manifest manifest) {
check();
manifestFirst = true;
this.manifest = manifest;
}
public void setManifest(File file) throws IOException {
check();
FileInputStream fin = new FileInputStream(file);
try {
Manifest m = new Manifest(fin);
setManifest(m);
} finally {
fin.close();
}
}
public void setManifestName(String manifestName) {
check();
if (manifestName == null || manifestName.length() == 0)
throw new IllegalArgumentException("Manifest name cannot be null or empty!");
this.manifestName = manifestName;
}
public void write(File file) throws Exception {
check();
try {
OutputStream out = new FileOutputStream(file);
try {
write(out);
} finally {
IO.close(out);
}
file.setLastModified(lastModified);
return;
} catch (Exception t) {
file.delete();
throw t;
}
}
public void write(String file) throws Exception {
check();
write(new File(file));
}
public void write(OutputStream out) throws Exception {
check();
if (!doNotTouchManifest && !nomanifest && algorithms != null) {
doChecksums(out);
return;
}
ZipOutputStream jout = nomanifest || doNotTouchManifest ? new ZipOutputStream(out) : new JarOutputStream(out);
switch (compression) {
case STORE :
jout.setMethod(ZipOutputStream.DEFLATED);
break;
default :
// default is DEFLATED
}
Set done = new HashSet();
Set directories = new HashSet();
if (doNotTouchManifest) {
Resource r = getResource(manifestName);
if (r != null) {
writeResource(jout, directories, manifestName, r);
done.add(manifestName);
}
} else
doManifest(done, jout);
for (Map.Entry entry : getResources().entrySet()) {
// Skip metainf contents
if (!done.contains(entry.getKey()))
writeResource(jout, directories, entry.getKey(), entry.getValue());
}
jout.finish();
}
public void writeFolder(File dir) throws Exception {
dir.mkdirs();
if (!dir.exists())
throw new IllegalArgumentException(
"The directory " + dir + " to write the JAR " + this + " could not be created");
if (!dir.isDirectory())
throw new IllegalArgumentException(
"The directory " + dir + " to write the JAR " + this + " to is not a directory");
check();
Set done = new HashSet();
Set directories = new HashSet();
if (doNotTouchManifest) {
Resource r = getResource(manifestName);
if (r != null) {
copyResource(dir, manifestName, r);
done.add(manifestName);
}
} else {
File file = IO.getFile(dir, manifestName);
file.getParentFile().mkdirs();
try (FileOutputStream fout = new FileOutputStream(file);) {
writeManifest(fout);
done.add(manifestName);
}
}
for (Map.Entry entry : getResources().entrySet()) {
String path = entry.getKey();
if (done.contains(path))
continue;
Resource resource = entry.getValue();
copyResource(dir, path, resource);
}
}
private void copyResource(File dir, String path, Resource resource) throws IOException, Exception {
File to = IO.getFile(dir, path);
to.getParentFile().mkdirs();
IO.copy(resource.openInputStream(), to);
}
public void doChecksums(OutputStream out) throws IOException, Exception {
// ok, we have a request to create digests
// of the resources. Since we have to output
// the manifest first, we have a slight problem.
// We can also not make multiple passes over the resource
// because some resources are not idempotent and/or can
// take significant time. So we just copy the jar
// to a temporary file, read it in again, calculate
// the checksums and save.
String[] algs = algorithms;
algorithms = null;
try {
File f = File.createTempFile(padString(getName(), 3, '_'), ".jar");
write(f);
Jar tmp = new Jar(f);
try {
tmp.calcChecksums(algorithms);
tmp.write(out);
} finally {
f.delete();
tmp.close();
}
} finally {
algorithms = algs;
}
}
private String padString(String s, int length, char pad) {
if (s == null)
s = "";
if (s.length() >= length)
return s;
char[] cs = new char[length];
Arrays.fill(cs, pad);
char[] orig = s.toCharArray();
System.arraycopy(orig, 0, cs, 0, orig.length);
return new String(cs);
}
private void doManifest(Set done, ZipOutputStream jout) throws Exception {
check();
if (nomanifest)
return;
JarEntry ze = new JarEntry(manifestName);
ZipUtil.setModifiedTime(ze, lastModified);
jout.putNextEntry(ze);
writeManifest(jout);
jout.closeEntry();
done.add(ze.getName());
}
/**
* Cleanup the manifest for writing. Cleaning up consists of adding a space
* after any \n to prevent the manifest to see this newline as a delimiter.
*
* @param out Output
* @throws IOException
*/
public void writeManifest(OutputStream out) throws Exception {
check();
stripSignatures();
writeManifest(getManifest(), out);
}
public static void writeManifest(Manifest manifest, OutputStream out) throws IOException {
if (manifest == null)
return;
manifest = clean(manifest);
outputManifest(manifest, out);
}
/**
* Unfortunately we have to write our own manifest :-( because of a stupid
* bug in the manifest code. It tries to handle UTF-8 but the way it does it
* it makes the bytes platform dependent. So the following code outputs the
* manifest. A Manifest consists of
*
*
* 'Manifest-Version: 1.0\r\n'
* main-attributes * \r\n name-section main-attributes ::= attributes
* attributes ::= key ': ' value '\r\n' name-section ::= 'Name: ' name
* '\r\n' attributes
*
*
* Lines in the manifest should not exceed 72 bytes (! this is where the
* manifest screwed up as well when 16 bit unicodes were used).
*
* As a bonus, we can now sort the manifest!
*/
private final static byte[] EOL = new byte[] {
'\r', '\n'
};
private final static byte[] SEPARATOR = new byte[] {
':', ' '
};
/**
* Main function to output a manifest properly in UTF-8.
*
* @param manifest The manifest to output
* @param out The output stream
* @throws IOException when something fails
*/
public static void outputManifest(Manifest manifest, OutputStream out) throws IOException {
writeEntry(out, "Manifest-Version", "1.0");
attributes(manifest.getMainAttributes(), out);
TreeSet keys = new TreeSet();
for (Object o : manifest.getEntries().keySet())
keys.add(o.toString());
for (String key : keys) {
out.write(EOL);
writeEntry(out, "Name", key);
attributes(manifest.getAttributes(key), out);
}
out.flush();
}
/**
* Write out an entry, handling proper unicode and line length constraints
*/
private static void writeEntry(OutputStream out, String name, String value) throws IOException {
int width = write(out, 0, name);
width = write(out, width, SEPARATOR);
write(out, width, value);
out.write(EOL);
}
/**
* Convert a string to bytes with UTF-8 and then output in max 72 bytes
*
* @param out the output string
* @param width the current width
* @param s the string to output
* @return the new width
* @throws IOException when something fails
*/
private static int write(OutputStream out, int width, String s) throws IOException {
byte[] bytes = s.getBytes("UTF-8");
return write(out, width, bytes);
}
/**
* Write the bytes but ensure that the line length does not exceed 72
* characters. If it is more than 70 characters, we just put a cr/lf +
* space.
*
* @param out The output stream
* @param width The nr of characters output in a line before this method
* started
* @param bytes the bytes to output
* @return the nr of characters in the last line
* @throws IOException if something fails
*/
private static int write(OutputStream out, int width, byte[] bytes) throws IOException {
int w = width;
for (int i = 0; i < bytes.length; i++) {
if (w >= 72 - EOL.length) { // we need to add the EOL!
out.write(EOL);
out.write(' ');
w = 1;
}
out.write(bytes[i]);
w++;
}
return w;
}
/**
* Output an Attributes map. We will sort this map before outputing.
*
* @param value the attrbutes
* @param out the output stream
* @throws IOException when something fails
*/
private static void attributes(Attributes value, OutputStream out) throws IOException {
TreeMap map = new TreeMap(String.CASE_INSENSITIVE_ORDER);
for (Map.Entry