aQute.bnd.osgi.Jar Maven / Gradle / Ivy
Show all versions of bndlib Show documentation
package aQute.bnd.osgi;
import static aQute.lib.io.IO.*;
import java.io.*;
import java.net.*;
import java.security.*;
import java.util.*;
import java.util.jar.*;
import java.util.regex.*;
import java.util.zip.*;
import aQute.lib.base64.*;
import aQute.lib.io.*;
import aQute.service.reporter.*;
public class Jar implements Closeable {
public enum Compression {
DEFLATE, STORE
}
public static final Object[] EMPTY_ARRAY = new Jar[0];
final Map resources = new TreeMap();
final Map> directories = new TreeMap>();
Manifest manifest;
boolean manifestFirst;
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
* @return
*/
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("META-INF/MANIFEST.MF")) {
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();
if (!key.endsWith(".java")) {
duplicates |= putResource(key, entry.getValue(), overwrite);
}
}
return duplicates;
}
public Manifest getManifest() throws Exception {
check();
if (manifest == null) {
Resource manifestResource = getResource("META-INF/MANIFEST.MF");
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 write(File file) throws Exception {
check();
try {
OutputStream out = new FileOutputStream(file);
try {
write(out);
}
finally {
IO.close(out);
}
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) {
// 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(getName(), ".jar");
System.out.println("Created tmp file " + f);
write(f);
Jar tmp = new Jar(f);
try {
tmp.calcChecksums(algorithms);
tmp.write(out);
}
finally {
f.delete();
tmp.close();
}
}
finally {
algorithms = algs;
}
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("META-INF/MANIFEST.MF");
if (r != null) {
writeResource(jout, directories, "META-INF/MANIFEST.MF", r);
done.add("META-INF/MANIFEST.MF");
}
} 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();
}
private void doManifest(Set done, ZipOutputStream jout) throws Exception {
check();
if (nomanifest)
return;
JarEntry ze = new JarEntry("META-INF/MANIFEST.MF");
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();
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!
*/
static byte[] CONTINUE = new byte[] {
'\r', '\n', ' '
};
/**
* 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) {
write(out, 0, "\r\n");
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 n = write(out, 0, name + ": ");
write(out, n, value);
write(out, 0, "\r\n");
}
/**
* Convert a string to bytes with UTF8 and then output in max 72 bytes
*
* @param out
* the output string
* @param i
* 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 i, String s) throws IOException {
byte[] bytes = s.getBytes("UTF8");
return write(out, i, 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) { // we need to add the \n\r!
out.write(CONTINUE);
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