blob: c8b235928a6faae45ff4f5c7bb63ccc20c212f8b [file] [log] [blame]
Stuart McCulloch26e7a5a2011-10-17 10:31:43 +00001package aQute.lib.osgi;
2
3import static aQute.lib.io.IO.*;
4
5import java.io.*;
6import java.security.*;
7import java.util.*;
8import java.util.jar.*;
9import java.util.regex.*;
10import java.util.zip.*;
11
12import aQute.lib.base64.*;
13import aQute.libg.reporter.*;
14
15public class Jar implements Closeable {
16 public static final Object[] EMPTY_ARRAY = new Jar[0];
17 Map<String, Resource> resources = new TreeMap<String, Resource>();
18 Map<String, Map<String, Resource>> directories = new TreeMap<String, Map<String, Resource>>();
19 Manifest manifest;
20 boolean manifestFirst;
21 String name;
22 File source;
23 ZipFile zipFile;
24 long lastModified;
25 String lastModifiedReason;
26 Reporter reporter;
27 boolean doNotTouchManifest;
28 boolean nomanifest;
29
30 public Jar(String name) {
31 this.name = name;
32 }
33
34 public Jar(String name, File dirOrFile, Pattern doNotCopy) throws ZipException, IOException {
35 this(name);
36 source = dirOrFile;
37 if (dirOrFile.isDirectory())
38 FileResource.build(this, dirOrFile, doNotCopy);
39 else if (dirOrFile.isFile()) {
40 zipFile = ZipResource.build(this, dirOrFile);
41 } else {
42 throw new IllegalArgumentException("A Jar can only accept a valid file or directory: "
43 + dirOrFile);
44 }
45 }
46
47 public Jar(String name, InputStream in, long lastModified) throws IOException {
48 this(name);
49 EmbeddedResource.build(this, in, lastModified);
50 }
51
52 public Jar(String name, String path) throws IOException {
53 this(name);
54 File f = new File(path);
55 InputStream in = new FileInputStream(f);
56 EmbeddedResource.build(this, in, f.lastModified());
57 in.close();
58 }
59
60 public Jar(File f) throws IOException {
61 this(getName(f), f, null);
62 }
63
64 /**
65 * Make the JAR file name the project name if we get a src or bin directory.
66 *
67 * @param f
68 * @return
69 */
70 private static String getName(File f) {
71 f = f.getAbsoluteFile();
72 String name = f.getName();
73 if (name.equals("bin") || name.equals("src"))
74 return f.getParentFile().getName();
75 else {
76 if (name.endsWith(".jar"))
77 name = name.substring(0, name.length() - 4);
78 return name;
79 }
80 }
81
82 public Jar(String string, InputStream resourceAsStream) throws IOException {
83 this(string, resourceAsStream, 0);
84 }
85
86 public Jar(String string, File file) throws ZipException, IOException {
87 this(string, file, Pattern.compile(Constants.DEFAULT_DO_NOT_COPY));
88 }
89
90 public void setName(String name) {
91 this.name = name;
92 }
93
94 public String toString() {
95 return "Jar:" + name;
96 }
97
98 public boolean putResource(String path, Resource resource) {
99 return putResource(path, resource, true);
100 }
101
102 public boolean putResource(String path, Resource resource, boolean overwrite) {
103 updateModified(resource.lastModified(), path);
104 while (path.startsWith("/"))
105 path = path.substring(1);
106
107 if (path.equals("META-INF/MANIFEST.MF")) {
108 manifest = null;
109 if (resources.isEmpty())
110 manifestFirst = true;
111 }
112 String dir = getDirectory(path);
113 Map<String, Resource> s = directories.get(dir);
114 if (s == null) {
115 s = new TreeMap<String, Resource>();
116 directories.put(dir, s);
117 int n = dir.lastIndexOf('/');
118 while (n > 0) {
119 String dd = dir.substring(0, n);
120 if (directories.containsKey(dd))
121 break;
122 directories.put(dd, null);
123 n = dd.lastIndexOf('/');
124 }
125 }
126 boolean duplicate = s.containsKey(path);
127 if (!duplicate || overwrite) {
128 resources.put(path, resource);
129 s.put(path, resource);
130 }
131 return duplicate;
132 }
133
134 public Resource getResource(String path) {
135 return resources.get(path);
136 }
137
138 private String getDirectory(String path) {
139 int n = path.lastIndexOf('/');
140 if (n < 0)
141 return "";
142
143 return path.substring(0, n);
144 }
145
146 public Map<String, Map<String, Resource>> getDirectories() {
147 return directories;
148 }
149
150 public Map<String, Resource> getResources() {
151 return resources;
152 }
153
154 public boolean addDirectory(Map<String, Resource> directory, boolean overwrite) {
155 boolean duplicates = false;
156 if (directory == null)
157 return false;
158
159 for (Map.Entry<String, Resource> entry : directory.entrySet()) {
160 String key = entry.getKey();
161 if (!key.endsWith(".java")) {
162 duplicates |= putResource(key, (Resource) entry.getValue(), overwrite);
163 }
164 }
165 return duplicates;
166 }
167
168 public Manifest getManifest() throws Exception {
169 if (manifest == null) {
170 Resource manifestResource = getResource("META-INF/MANIFEST.MF");
171 if (manifestResource != null) {
172 InputStream in = manifestResource.openInputStream();
173 manifest = new Manifest(in);
174 in.close();
175 }
176 }
177 return manifest;
178 }
179
180 public boolean exists(String path) {
181 return resources.containsKey(path);
182 }
183
184 public void setManifest(Manifest manifest) {
185 manifestFirst = true;
186 this.manifest = manifest;
187 }
188
189 public void write(File file) throws Exception {
190 try {
191 OutputStream out = new FileOutputStream(file);
192 write(out);
193 out.close();
194 return;
195
196 } catch (Exception t) {
197 file.delete();
198 throw t;
199 }
200 }
201
202 public void write(String file) throws Exception {
203 write(new File(file));
204 }
205
206 public void write(OutputStream out) throws Exception {
207 ZipOutputStream jout = nomanifest || doNotTouchManifest ? new ZipOutputStream(out)
208 : new JarOutputStream(out);
209 Set<String> done = new HashSet<String>();
210
211 Set<String> directories = new HashSet<String>();
212 if (doNotTouchManifest) {
213 Resource r = getResource("META-INF/MANIFEST.MF");
214 if (r != null) {
215 writeResource(jout, directories, "META-INF/MANIFEST.MF", r);
216 done.add("META-INF/MANIFEST.MF");
217 }
218 } else
219 doManifest(done, jout);
220
221 for (Map.Entry<String, Resource> entry : getResources().entrySet()) {
222 // Skip metainf contents
223 if (!done.contains(entry.getKey()))
224 writeResource(jout, directories, (String) entry.getKey(),
225 (Resource) entry.getValue());
226 }
227 jout.finish();
228 }
229
230 private void doManifest(Set<String> done, ZipOutputStream jout) throws Exception {
231 if (nomanifest)
232 return;
233
234 JarEntry ze = new JarEntry("META-INF/MANIFEST.MF");
235 jout.putNextEntry(ze);
236 writeManifest(jout);
237 jout.closeEntry();
238 done.add(ze.getName());
239 }
240
241 /**
242 * Cleanup the manifest for writing. Cleaning up consists of adding a space
243 * after any \n to prevent the manifest to see this newline as a delimiter.
244 *
245 * @param out
246 * Output
247 * @throws IOException
248 */
249
250 public void writeManifest(OutputStream out) throws Exception {
251 writeManifest(getManifest(), out);
252 }
253
254 public static void writeManifest(Manifest manifest, OutputStream out) throws IOException {
255 if (manifest == null)
256 return;
257
258 manifest = clean(manifest);
259 outputManifest(manifest, out);
260 }
261
262 /**
263 * Unfortunately we have to write our own manifest :-( because of a stupid
264 * bug in the manifest code. It tries to handle UTF-8 but the way it does it
265 * it makes the bytes platform dependent.
266 *
267 * So the following code outputs the manifest.
268 *
269 * A Manifest consists of
270 *
271 * <pre>
272 * 'Manifest-Version: 1.0\r\n'
273 * main-attributes *
274 * \r\n
275 * name-section
276 *
277 * main-attributes ::= attributes
278 * attributes ::= key ': ' value '\r\n'
279 * name-section ::= 'Name: ' name '\r\n' attributes
280 * </pre>
281 *
282 * Lines in the manifest should not exceed 72 bytes (! this is where the
283 * manifest screwed up as well when 16 bit unicodes were used).
284 *
285 * <p>
286 * As a bonus, we can now sort the manifest!
287 */
288 static byte[] CONTINUE = new byte[] {'\r','\n', ' '};
289
290 /**
291 * Main function to output a manifest properly in UTF-8.
292 *
293 * @param manifest
294 * The manifest to output
295 * @param out
296 * The output stream
297 * @throws IOException
298 * when something fails
299 */
300 public static void outputManifest(Manifest manifest, OutputStream out) throws IOException {
301 writeEntry(out, "Manifest-Version", "1.0");
302 attributes(manifest.getMainAttributes(), out);
303
304 TreeSet<String> keys = new TreeSet<String>();
305 for (Object o : manifest.getEntries().keySet())
306 keys.add(o.toString());
307
308 for (String key : keys) {
309 write(out, 0, "\r\n");
310 writeEntry(out, "Name", key);
311 attributes(manifest.getAttributes(key), out);
312 }
313 }
314
315 /**
316 * Write out an entry, handling proper unicode and line length constraints
317 *
318 */
319 private static void writeEntry(OutputStream out, String name, String value) throws IOException {
320 int n = write(out, 0, name + ": ");
321 n = write(out, n, value);
322 write(out, 0, "\r\n");
323 }
324
325 /**
326 * Convert a string to bytes with UTF8 and then output in max 72 bytes
327 *
328 * @param out
329 * the output string
330 * @param i
331 * the current width
332 * @param s
333 * the string to output
334 * @return the new width
335 * @throws IOException
336 * when something fails
337 */
338 private static int write(OutputStream out, int i, String s) throws IOException {
339 byte[] bytes = s.getBytes("UTF8");
340 return write(out, i, bytes);
341 }
342
343 /**
344 * Write the bytes but ensure that the line length does not exceed 72
345 * characters. If it is more than 70 characters, we just put a cr/lf +
346 * space.
347 *
348 * @param out
349 * The output stream
350 * @param width
351 * The nr of characters output in a line before this method
352 * started
353 * @param bytes
354 * the bytes to output
355 * @return the nr of characters in the last line
356 * @throws IOException
357 * if something fails
358 */
359 private static int write(OutputStream out, int width, byte[] bytes) throws IOException {
360 for (int i = 0; i < bytes.length; i++) {
361 if (width >= 72) { // we need to add the \n\r!
362 out.write(CONTINUE);
363 width = 1;
364 }
365 out.write(bytes[i]);
366 width++;
367 }
368 return width;
369 }
370
371 /**
372 * Output an Attributes map. We will sort this map before outputing.
373 *
374 * @param value
375 * the attrbutes
376 * @param out
377 * the output stream
378 * @throws IOException
379 * when something fails
380 */
381 private static void attributes(Attributes value, OutputStream out) throws IOException {
382 TreeMap<String, String> map = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
383 for (Map.Entry<Object, Object> entry : value.entrySet()) {
384 map.put(entry.getKey().toString(), entry.getValue().toString());
385 }
386
387 map.remove("Manifest-Version"); // get rid of
388 // manifest
389 // version
390 for (Map.Entry<String, String> entry : map.entrySet()) {
391 writeEntry(out, entry.getKey(), entry.getValue());
392 }
393 }
394
395 private static Manifest clean(Manifest org) {
396
397 Manifest result = new Manifest();
398 for (Map.Entry<?, ?> entry : org.getMainAttributes().entrySet()) {
399 String nice = clean((String) entry.getValue());
400 result.getMainAttributes().put(entry.getKey(), nice);
401 }
402 for (String name : org.getEntries().keySet()) {
403 Attributes attrs = result.getAttributes(name);
404 if (attrs == null) {
405 attrs = new Attributes();
406 result.getEntries().put(name, attrs);
407 }
408
409 for (Map.Entry<?, ?> entry : org.getAttributes(name).entrySet()) {
410 String nice = clean((String) entry.getValue());
411 attrs.put((Attributes.Name) entry.getKey(), nice);
412 }
413 }
414 return result;
415 }
416
417 private static String clean(String s) {
418 if (s.indexOf('\n') < 0)
419 return s;
420
421 StringBuffer sb = new StringBuffer(s);
422 for (int i = 0; i < sb.length(); i++) {
423 if (sb.charAt(i) == '\n')
424 sb.insert(++i, ' ');
425 }
426 return sb.toString();
427 }
428
429 private void writeResource(ZipOutputStream jout, Set<String> directories, String path,
430 Resource resource) throws Exception {
431 if (resource == null)
432 return;
433
434 createDirectories(directories, jout, path);
435 ZipEntry ze = new ZipEntry(path);
436 ze.setMethod(ZipEntry.DEFLATED);
437 long lastModified = resource.lastModified();
438 if (lastModified == 0L) {
439 lastModified = System.currentTimeMillis();
440 }
441 ze.setTime(lastModified);
442 if (resource.getExtra() != null)
443 ze.setExtra(resource.getExtra().getBytes());
444 jout.putNextEntry(ze);
445 resource.write(jout);
446 jout.closeEntry();
447 }
448
449 void createDirectories(Set<String> directories, ZipOutputStream zip, String name)
450 throws IOException {
451 int index = name.lastIndexOf('/');
452 if (index > 0) {
453 String path = name.substring(0, index);
454 if (directories.contains(path))
455 return;
456 createDirectories(directories, zip, path);
457 ZipEntry ze = new ZipEntry(path + '/');
458 zip.putNextEntry(ze);
459 zip.closeEntry();
460 directories.add(path);
461 }
462 }
463
464 public String getName() {
465 return name;
466 }
467
468 /**
469 * Add all the resources in the given jar that match the given filter.
470 *
471 * @param sub
472 * the jar
473 * @param filter
474 * a pattern that should match the resoures in sub to be added
475 */
476 public boolean addAll(Jar sub, Instruction filter) {
477 return addAll(sub, filter, "");
478 }
479
480 /**
481 * Add all the resources in the given jar that match the given filter.
482 *
483 * @param sub
484 * the jar
485 * @param filter
486 * a pattern that should match the resoures in sub to be added
487 */
488 public boolean addAll(Jar sub, Instruction filter, String destination) {
489 boolean dupl = false;
490 for (String name : sub.getResources().keySet()) {
491 if ("META-INF/MANIFEST.MF".equals(name))
492 continue;
493
494 if (filter == null || filter.matches(name) != filter.isNegated())
495 dupl |= putResource(Processor.appendPath(destination, name), sub.getResource(name),
496 true);
497 }
498 return dupl;
499 }
500
501 public void close() {
502 if (zipFile != null)
503 try {
504 zipFile.close();
505 } catch (IOException e) {
506 // Ignore
507 }
508 resources = null;
509 directories = null;
510 manifest = null;
511 source = null;
512 }
513
514 public long lastModified() {
515 return lastModified;
516 }
517
518 public void updateModified(long time, String reason) {
519 if (time > lastModified) {
520 lastModified = time;
521 lastModifiedReason = reason;
522 }
523 }
524
525 public void setReporter(Reporter reporter) {
526 this.reporter = reporter;
527 }
528
529 public boolean hasDirectory(String path) {
530 return directories.get(path) != null;
531 }
532
533 public List<String> getPackages() {
534 List<String> list = new ArrayList<String>(directories.size());
535
536 for (Map.Entry<String, Map<String, Resource>> i : directories.entrySet()) {
537 if (i.getValue() != null) {
538 String path = i.getKey();
539 String pack = path.replace('/', '.');
540 list.add(pack);
541 }
542 }
543 return list;
544 }
545
546 public File getSource() {
547 return source;
548 }
549
550 public boolean addAll(Jar src) {
551 return addAll(src, null);
552 }
553
554 public boolean rename(String oldPath, String newPath) {
555 Resource resource = remove(oldPath);
556 if (resource == null)
557 return false;
558
559 return putResource(newPath, resource);
560 }
561
562 public Resource remove(String path) {
563 Resource resource = resources.remove(path);
564 String dir = getDirectory(path);
565 Map<String, Resource> mdir = directories.get(dir);
566 // must be != null
567 mdir.remove(path);
568 return resource;
569 }
570
571 /**
572 * Make sure nobody touches the manifest! If the bundle is signed, we do not
573 * want anybody to touch the manifest after the digests have been
574 * calculated.
575 */
576 public void setDoNotTouchManifest() {
577 doNotTouchManifest = true;
578 }
579
580 /**
581 * Calculate the checksums and set them in the manifest.
582 */
583
584 public void calcChecksums(String algorithms[]) throws Exception {
585 if (algorithms == null)
586 algorithms = new String[] { "SHA", "MD5" };
587
588 Manifest m = getManifest();
589 if ( m == null) {
590 m= new Manifest();
591 setManifest(m);
592 }
593
594 MessageDigest digests[] = new MessageDigest[algorithms.length];
595 int n = 0;
596 for (String algorithm : algorithms)
597 digests[n++] = MessageDigest.getInstance(algorithm);
598
599 byte buffer[] = new byte[30000];
600
601 for (Map.Entry<String, Resource> entry : resources.entrySet()) {
602
603 // Skip the manifest
604 if (entry.getKey().equals("META-INF/MANIFEST.MF"))
605 continue;
606
607 Resource r = entry.getValue();
608 Attributes attributes = m.getAttributes(entry.getKey());
609 if (attributes == null) {
610 attributes = new Attributes();
611 getManifest().getEntries().put(entry.getKey(), attributes);
612 }
613 InputStream in = r.openInputStream();
614 try {
615 for (MessageDigest d : digests)
616 d.reset();
617 int size = in.read(buffer);
618 while (size > 0) {
619 for (MessageDigest d : digests)
620 d.update(buffer, 0, size);
621 size = in.read(buffer);
622 }
623 } finally {
624 in.close();
625 }
626 for (MessageDigest d : digests)
627 attributes.putValue(d.getAlgorithm() + "-Digest", Base64.encodeBase64(d.digest()));
628 }
629 }
630
631 Pattern BSN = Pattern.compile("\\s*([-\\w\\d\\._]+)\\s*;?.*");
632
633 public String getBsn() throws Exception {
634 Manifest m = getManifest();
635 if (m == null)
636 return null;
637
638 String s = m.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
639 Matcher matcher = BSN.matcher(s);
640 if (matcher.matches()) {
641 return matcher.group(1);
642 }
643 return null;
644 }
645
646 public String getVersion() throws Exception {
647 Manifest m = getManifest();
648 if (m == null)
649 return null;
650
651 String s = m.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
652 if (s == null)
653 return null;
654
655 return s.trim();
656 }
657
658 /**
659 * Expand the JAR file to a directory.
660 *
661 * @param dir
662 * the dst directory, is not required to exist
663 * @throws Exception
664 * if anything does not work as expected.
665 */
666 public void expand(File dir) throws Exception {
667 dir = dir.getAbsoluteFile();
668 dir.mkdirs();
669 if (!dir.isDirectory()) {
670 throw new IllegalArgumentException("Not a dir: " + dir.getAbsolutePath());
671 }
672
673 for (Map.Entry<String, Resource> entry : getResources().entrySet()) {
674 File f = getFile(dir, entry.getKey());
675 f.getParentFile().mkdirs();
676 copy(entry.getValue().openInputStream(), f);
677 }
678 }
679
680 /**
681 * Make sure we have a manifest
682 * @throws Exception
683 */
684 public void ensureManifest() throws Exception {
685 if ( getManifest() != null)
686 return;
687 manifest = new Manifest();
688 }
689
690}