blob: 80afa3eb8791ce920017b014b0d7d66c3eb9fdc6 [file] [log] [blame]
Stuart McCulloch2bcdb182008-08-06 15:52:20 +00001/* Copyright 2006 aQute SARL
2 * Licensed under the Apache License, Version 2.0, see http://www.apache.org/licenses/LICENSE-2.0 */
3package aQute.lib.osgi;
4
5import java.io.*;
6import java.util.*;
7import java.util.jar.*;
8import java.util.regex.*;
9import java.util.zip.*;
10
11/**
12 * Include-Resource: ( [name '=' ] file )+
13 *
14 * Private-Package: package-decl ( ',' package-decl )*
15 *
16 * Export-Package: package-decl ( ',' package-decl )*
17 *
18 * Import-Package: package-decl ( ',' package-decl )*
19 *
20 * @version $Revision: 1.4 $
21 */
22public class Builder extends Analyzer {
23 private static final int SPLIT_MERGE_LAST = 1;
24 private static final int SPLIT_MERGE_FIRST = 2;
25 private static final int SPLIT_ERROR = 3;
26 private static final int SPLIT_FIRST = 4;
27 private static final int SPLIT_DEFAULT = 0;
28
Stuart McCullochc8f55952008-08-06 16:18:48 +000029 List tempJars = new ArrayList();
Stuart McCulloch2bcdb182008-08-06 15:52:20 +000030 boolean sources = false;
31 File[] sourcePath;
32 Pattern NAME_URL = Pattern
33 .compile("(.*)(http://.*)");
34
35 public Jar build() throws Exception {
36 begin();
37
38 dot = new Jar("dot");
39 doPrebuild(dot, parseHeader(getProperty("-prebuild")));
40 doExpand(dot);
41 doIncludeResources(dot);
42
43 doConditional(dot);
44
45 dot.setManifest(calcManifest());
46 // This must happen after we analyzed so
47 // we know what it is on the classpath
48 addSources(dot);
49 if (getProperty(POM) != null)
50 doPom(dot);
51
52 doVerify(dot);
53 if (dot.getResources().isEmpty())
54 error("The JAR is empty");
55
56 dot.updateModified(lastModified());
57 return dot;
58 }
59
60 void begin() {
61 super.begin();
62 if (getProperty(IMPORT_PACKAGE) == null)
63 setProperty(IMPORT_PACKAGE, "*");
64
65 sources = getProperty(SOURCES) != null;
66 }
67
68 private void doConditional(Jar dot) throws IOException {
69 Map conditionals = getHeader(CONDITIONAL_PACKAGE);
70 if (conditionals != null && conditionals.size() > 0) {
71 int size;
72 do {
73 size = dot.getDirectories().size();
74 analyze();
75 analyzed = false;
76 Map imports = getImports();
77
78 Map filtered = merge(CONDITIONAL_PACKAGE, conditionals,
79 imports, new HashSet());
80
81 // remove existing packages to prevent merge errors
82 filtered.keySet().removeAll(dot.getPackages());
83 filtered = replaceWithPattern(filtered);
84 doExpand(dot, CONDITIONAL_PACKAGE, filtered);
85 } while (dot.getDirectories().size() > size);
86 }
87 }
88
89 /**
90 * Intercept the call to analyze and cleanup versions after we have analyzed
91 * the setup. We do not want to cleanup if we are going to verify.
92 */
93
94 public void analyze() throws IOException {
95 super.analyze();
96 cleanupVersion(imports);
97 cleanupVersion(exports);
98 String version = getProperty(BUNDLE_VERSION);
99 if (version != null)
100 setProperty(BUNDLE_VERSION, cleanupVersion(version));
101 }
102
103 public void cleanupVersion(Map mapOfMap) {
104 for (Iterator e = mapOfMap.entrySet().iterator(); e.hasNext();) {
105 Map.Entry entry = (Map.Entry) e.next();
106 Map attributes = (Map) entry.getValue();
107 if (attributes.containsKey("version")) {
108 attributes.put("version", cleanupVersion((String) attributes
109 .get("version")));
110 }
111 }
112 }
113
114 /**
115 * Clean up version parameters. Other builders use more fuzzy definitions of
116 * the version syntax. This method cleans up such a version to match an OSGi
117 * version.
118 *
119 * @param version
120 * @return
121 */
122 static Pattern fuzzyVersion = Pattern
123 .compile(
124 "(\\d+)(\\.(\\d+)(\\.(\\d+))?)?([^a-zA-Z0-9](.*))?",
125 Pattern.DOTALL);
126 static Pattern fuzzyModifier = Pattern.compile("(\\d+[.-])*(.*)",
127 Pattern.DOTALL);
128
129 static Pattern nummeric = Pattern.compile("\\d*");
130
131 static public String cleanupVersion(String version) {
132 Matcher m = fuzzyVersion.matcher(version);
133 if (m.matches()) {
134 StringBuffer result = new StringBuffer();
135 String d1 = m.group(1);
136 String d2 = m.group(3);
137 String d3 = m.group(5);
138 String qualifier = m.group(7);
139
140 if (d1 != null) {
141 result.append(d1);
142 if (d2 != null) {
143 result.append(".");
144 result.append(d2);
145 if (d3 != null) {
146 result.append(".");
147 result.append(d3);
148 if (qualifier != null) {
149 result.append(".");
150 cleanupModifier(result, qualifier);
151 }
152 } else if (qualifier != null) {
153 result.append(".0.");
154 cleanupModifier(result, qualifier);
155 }
156 } else if (qualifier != null) {
157 result.append(".0.0.");
158 cleanupModifier(result, qualifier);
159 }
160 return result.toString();
161 }
162 }
163 return version;
164 }
165
166 static void cleanupModifier(StringBuffer result, String modifier) {
167 Matcher m = fuzzyModifier.matcher(modifier);
168 if (m.matches())
169 modifier = m.group(2);
170
171 for (int i = 0; i < modifier.length(); i++) {
172 char c = modifier.charAt(i);
173 if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')
174 || (c >= 'A' && c <= 'Z') || c == '_' || c == '-')
175 result.append(c);
176 }
177 }
178
179 /**
180 *
181 */
182 private void addSources(Jar dot) {
183 if (!sources)
184 return;
185
186 try {
187 ByteArrayOutputStream out = new ByteArrayOutputStream();
188 getProperties().store(out, "Generated by BND, at " + new Date());
189 dot.putResource("OSGI-OPT/bnd.bnd", new EmbeddedResource(out
190 .toByteArray(), 0));
191 out.close();
192 } catch (Exception e) {
193 error("Can not embed bnd file in JAR: " + e);
194 }
195
196 for (Iterator cpe = classspace.keySet().iterator(); cpe.hasNext();) {
197 String path = (String) cpe.next();
198 path = path.substring(0, path.length() - ".class".length())
199 + ".java";
200
201 for (int i = 0; i < sourcePath.length; i++) {
202 File root = sourcePath[i];
203 File f = getFile(root, path);
204 if (f.exists()) {
205 dot
206 .putResource("OSGI-OPT/src/" + path,
207 new FileResource(f));
208 }
209 }
210 }
211 }
212
213 private void doVerify(Jar dot) throws Exception {
214 Verifier verifier = new Verifier(dot, getProperties());
215 verifier.setPedantic(isPedantic());
216 verifier.verify();
217 errors.addAll(verifier.getErrors());
218 warnings.addAll(verifier.getWarnings());
219 }
220
221 private void doExpand(Jar jar) throws IOException {
222 if (classpath.size() == 0
223 && (getProperty(EXPORT_PACKAGE) != null || getProperty(PRIVATE_PACKAGE) != null))
224 warning("Classpath is empty. Private-Package and Export-Package can only expand from the classpath when there is one");
225
226 Map prive = replaceWithPattern(getHeader(Analyzer.PRIVATE_PACKAGE));
227 Map export = replaceWithPattern(getHeader(Analyzer.EXPORT_PACKAGE));
228 if (prive.isEmpty() && export.isEmpty()) {
229 warnings
230 .add("Neither Export-Package nor Private-Package is set, therefore no packages will be included");
231 }
232 doExpand(jar, EXPORT_PACKAGE, export);
233 doExpand(jar, PRIVATE_PACKAGE, prive);
234 }
235
236 private void doExpand(Jar jar, String name, Map instructions) {
237 Set superfluous = new HashSet(instructions.keySet());
238 for (Iterator c = classpath.iterator(); c.hasNext();) {
239 Jar now = (Jar) c.next();
240 doExpand(jar, instructions, now, superfluous);
241 }
242 if (superfluous.size() > 0) {
243 StringBuffer sb = new StringBuffer();
244 String del = "Instructions for " + name + " that are never used: ";
245 for (Iterator i = superfluous.iterator(); i.hasNext();) {
246 Instruction p = (Instruction) i.next();
247 sb.append(del);
248 sb.append(p.getPattern());
249 del = ", ";
250 }
251 warning(sb.toString());
252 }
253 }
254
255 /**
256 * Iterate over each directory in the class path entry and check if that
257 * directory is a desired package.
258 *
259 * @param included
260 * @param classpathEntry
261 */
262 private void doExpand(Jar jar, Map included, Jar classpathEntry,
263 Set superfluous) {
264
265 loop: for (Iterator p = classpathEntry.getDirectories().entrySet()
266 .iterator(); p.hasNext();) {
267 Map.Entry directory = (Map.Entry) p.next();
268 String path = (String) directory.getKey();
269
270 if (doNotCopy.matcher(getName(path)).matches())
271 continue;
272
273 if (directory.getValue() == null)
274 continue;
275
276 String pack = path.replace('/', '.');
277 Instruction instr = matches(included, pack, superfluous);
278 if (instr != null) {
279 // System.out.println("Pattern match: " + pack + " " +
280 // instr.getPattern() + " " + instr.isNegated());
281 if (!instr.isNegated()) {
282 Map contents = (Map) directory.getValue();
283
284 // What to do with split packages? Well if this
285 // directory already exists, we will check the strategy
286 // and react accordingly.
287 boolean overwriteResource = true;
288 if (jar.hasDirectory(path)) {
289 Map directives = (Map) included.get(instr);
290
291 switch (getSplitStrategy((String) directives
292 .get(SPLIT_PACKAGE_DIRECTIVE))) {
293 case SPLIT_MERGE_LAST:
294 overwriteResource = true;
295 break;
296
297 case SPLIT_MERGE_FIRST:
298 overwriteResource = false;
299 break;
300
301 case SPLIT_ERROR:
302 error("Split package generates error: " + pack);
303 continue loop;
304
305 case SPLIT_FIRST:
306 continue loop;
307
308 default:
309 // Default is like merge-first, but with a warning
310 warning("There are split packages, use directive -split-package:=(merge-first|merge-last) on instruction to get rid of this warning: "
311 + pack
312 + ", classpath: "
313 + classpath
314 + " from: " + classpathEntry.source);
315 overwriteResource = false;
316 break;
317 }
318 }
319 jar.addDirectory(contents, overwriteResource);
320 }
321 }
322 }
323 }
324
325 private int getSplitStrategy(String type) {
326 if (type == null)
327 return SPLIT_DEFAULT;
328
329 if (type.equals("merge-last"))
330 return SPLIT_MERGE_LAST;
331
332 if (type.equals("merge-first"))
333 return SPLIT_MERGE_FIRST;
334
335 if (type.equals("error"))
336 return SPLIT_ERROR;
337
338 if (type.equals("first"))
339 return SPLIT_FIRST;
340
341 error("Invalid strategy for split-package: " + type);
342 return SPLIT_DEFAULT;
343 }
344
345 private Map replaceWithPattern(Map header) {
346 Map map = new LinkedHashMap();
347 for (Iterator e = header.entrySet().iterator(); e.hasNext();) {
348 Map.Entry entry = (Map.Entry) e.next();
349 String pattern = (String) entry.getKey();
350 Instruction instr = Instruction.getPattern(pattern);
351 map.put(instr, entry.getValue());
352 }
353 return map;
354 }
355
356 private Instruction matches(Map instructions, String pack,
357 Set superfluousPatterns) {
358 for (Iterator i = instructions.keySet().iterator(); i.hasNext();) {
359 Instruction pattern = (Instruction) i.next();
360 if (pattern.matches(pack)) {
361 superfluousPatterns.remove(pattern);
362 return pattern;
363 }
364 }
365 return null;
366 }
367
368 private Map getHeader(String string) {
369 if (string == null)
370 return new LinkedHashMap();
371 return parseHeader(getProperty(string));
372 }
373
374 /**
375 * Parse the Bundle-Includes header. Files in the bundles Include header are
376 * included in the jar. The source can be a directory or a file.
377 *
378 * @throws IOException
379 * @throws FileNotFoundException
380 */
381 private void doIncludeResources(Jar jar) throws Exception {
382 Macro macro = new Macro(getProperties(), this);
383
384 String includes = getProperty("Bundle-Includes");
385 if (includes == null)
386 includes = getProperty("Include-Resource");
387 else
388 warnings
389 .add("Please use Include-Resource instead of Bundle-Includes");
390
391 if (includes == null)
392 return;
393
394 for (Iterator i = getClauses(includes).iterator(); i.hasNext();) {
395 boolean preprocess = false;
396 String clause = (String) i.next();
397 if (clause.startsWith("{") && clause.endsWith("}")) {
398 preprocess = true;
399 clause = clause.substring(1, clause.length() - 1).trim();
400 }
401
402 Map extra = new HashMap();
403 int n = clause.indexOf(';');
404 if (n > 0) {
405 String attributes = clause.substring(n + 1);
406 String parts[] = attributes.split("\\s*;\\s*");
407 for (int j = 0; j < parts.length; j++) {
408 String assignment[] = parts[j].split("\\s*=\\s*");
409 if (assignment.length == 2)
410 extra.put(assignment[0], assignment[1]);
411 else
412 error("Invalid attribute on Include-Resource: "
413 + clause);
414 }
415 clause = clause.substring(0, n);
416 }
417
418 if (clause.startsWith("@")) {
419 extractFromJar(jar, clause.substring(1));
420 } else {
421 String parts[] = clause.split("\\s*=\\s*");
422
423 String source;
424 File sourceFile;
425 String destinationPath;
426
427 if (parts.length == 1) {
428 // Just a copy, destination path defined by
429 // source path.
430 source = parts[0];
431 sourceFile = getFile(base, source);
432 // Directories should be copied to the root
433 // but files to their file name ...
434 if (sourceFile.isDirectory())
435 destinationPath = "";
436 else
437 destinationPath = sourceFile.getName();
438 } else {
439 source = parts[1];
440 sourceFile = getFile(base, source);
441 destinationPath = parts[0];
442 }
443
444 // Some people insist on ending a directory with
445 // a slash ... it now also works if you do /=dir
446 if (destinationPath.endsWith("/"))
447 destinationPath = destinationPath.substring(0,
448 destinationPath.length() - 1);
449
450 if (!sourceFile.exists()) {
451 Jar src = getJarFromName(source, "Include-Resource "
452 + source);
453 if (src != null) {
454 JarResource jarResource = new JarResource(src);
455 jar.putResource(destinationPath, jarResource);
456 } else {
457 error("Input file does not exist: " + source);
458 }
459 } else
460 copy(jar, destinationPath, sourceFile, preprocess ? macro
461 : null, extra);
462 }
463 }
464 }
465
466 /**
467 * Extra resources from a Jar and add them to the given jar. The clause is
468 * the
469 *
470 * @param jar
471 * @param clauses
472 * @param i
473 * @throws ZipException
474 * @throws IOException
475 */
476 private void extractFromJar(Jar jar, String name) throws ZipException,
477 IOException {
478 // Inline all resources and classes from another jar
479 // optionally appended with a modified regular expression
480 // like @zip.jar!/META-INF/MANIFEST.MF
481 int n = name.lastIndexOf("!/");
482 Pattern filter = null;
483 if (n > 0) {
484 String fstring = name.substring(n + 2);
485 name = name.substring(0, n);
486 filter = wildcard(fstring);
487 }
488 Jar sub = getJarFromName(name, "extract from jar");
489 if (sub == null)
490 error("Can not find JAR file " + name);
Stuart McCulloch008eee02008-08-06 15:57:57 +0000491 else {
Stuart McCulloch2bcdb182008-08-06 15:52:20 +0000492 jar.addAll(sub, filter);
Stuart McCullochc8f55952008-08-06 16:18:48 +0000493 tempJars.add(sub);
Stuart McCulloch008eee02008-08-06 15:57:57 +0000494 }
Stuart McCulloch2bcdb182008-08-06 15:52:20 +0000495 }
496
497 private Pattern wildcard(String spec) {
498 StringBuffer sb = new StringBuffer();
499 for (int j = 0; j < spec.length(); j++) {
500 char c = spec.charAt(j);
501 switch (c) {
502 case '.':
503 sb.append("\\.");
504 break;
505
506 case '*':
507 // test for ** (all directories)
508 if (j < spec.length() - 1 && spec.charAt(j + 1) == '*') {
509 sb.append(".*");
510 j++;
511 } else
512 sb.append("[^/]*");
513 break;
514 default:
515 sb.append(c);
516 break;
517 }
518 }
519 String s = sb.toString();
520 try {
521 return Pattern.compile(s);
522 } catch (Exception e) {
523 error("Invalid regular expression on wildcarding: " + spec
524 + " used *");
525 }
526 return null;
527 }
528
529 private void copy(Jar jar, String path, File from, Macro macro, Map extra)
530 throws Exception {
531 if (doNotCopy.matcher(from.getName()).matches())
532 return;
533
534 if (from.isDirectory()) {
535 String next = path;
536 if (next.length() != 0)
537 next += '/';
538
539 File files[] = from.listFiles();
540 for (int i = 0; i < files.length; i++) {
541 copy(jar, next + files[i].getName(), files[i], macro, extra);
542 }
543 } else {
544 if (from.exists()) {
545 if (macro != null) {
546 String content = read(from);
547 content = macro.process(content);
548 Resource resource = new EmbeddedResource(content
549 .getBytes("UTF-8"), from.lastModified());
550
551 String x = (String) extra.get("extra");
552 if (x != null)
553 resource.setExtra(x);
554 jar.putResource(path, resource);
555 } else
556 jar.putResource(path, new FileResource(from));
557 } else {
558 error("Input file does not exist: " + from);
559 }
560 }
561 }
562
563 private String read(File from) throws Exception {
564 long size = from.length();
565 byte[] buffer = new byte[(int) size];
566 FileInputStream in = new FileInputStream(from);
567 in.read(buffer);
568 in.close();
569 return new String(buffer, "UTF-8");
570 }
571
572 private String getName(String where) {
573 int n = where.lastIndexOf('/');
574 if (n < 0)
575 return where;
576
577 return where.substring(n + 1);
578 }
579
580 public void setSourcepath(File[] files) {
581 sourcePath = files;
582 }
583
584 /**
585 * Create a POM reseource for Maven containing as much information as
586 * possible from the manifest.
587 *
588 * @param output
589 * @param builder
590 * @throws FileNotFoundException
591 * @throws IOException
592 */
593 public void doPom(Jar dot) throws FileNotFoundException, IOException {
594 {
595 Manifest manifest = dot.getManifest();
596 String name = manifest.getMainAttributes().getValue(
597 Analyzer.BUNDLE_NAME);
598 String description = manifest.getMainAttributes().getValue(
599 Analyzer.BUNDLE_DESCRIPTION);
600 String docUrl = manifest.getMainAttributes().getValue(
601 Analyzer.BUNDLE_DOCURL);
602 String version = manifest.getMainAttributes().getValue(
603 Analyzer.BUNDLE_VERSION);
604 String bundleVendor = manifest.getMainAttributes().getValue(
605 Analyzer.BUNDLE_VENDOR);
606 ByteArrayOutputStream s = new ByteArrayOutputStream();
607 PrintStream ps = new PrintStream(s);
608 String bsn = manifest.getMainAttributes().getValue(
609 Analyzer.BUNDLE_SYMBOLICNAME);
610 String licenses = manifest.getMainAttributes().getValue(
611 BUNDLE_LICENSE);
612
613 if (bsn == null) {
614 errors
615 .add("Can not create POM unless Bundle-SymbolicName is set");
616 return;
617 }
618
619 bsn = bsn.trim();
620 int n = bsn.lastIndexOf('.');
621 if (n <= 0) {
622 errors
623 .add("Can not create POM unless Bundle-SymbolicName contains a .");
624 ps.close();
625 s.close();
626 return;
627 }
628 String groupId = bsn.substring(0, n);
629 String artifactId = bsn.substring(n + 1);
630 ps
631 .println("<project xmlns='http://maven.apache.org/POM/4.0.0' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd'>");
632 ps.println(" <modelVersion>4.0.0</modelVersion>");
633 ps.println(" <groupId>" + groupId + "</groupId>");
634
635 n = artifactId.indexOf(';');
636 if (n > 0)
637 artifactId = artifactId.substring(0, n).trim();
638
639 ps.println(" <artifactId>" + artifactId + "</artifactId>");
640 ps.println(" <version>" + version + "</version>");
641 if (description != null) {
642 ps.println(" <description>");
643 ps.print(" ");
644 ps.println(description);
645 ps.println(" </description>");
646 }
647 if (name != null) {
648 ps.print(" <name>");
649 ps.print(name);
650 ps.println("</name>");
651 }
652 if (docUrl != null) {
653 ps.print(" <url>");
654 ps.print(docUrl);
655 ps.println("</url>");
656 }
657
658 if (bundleVendor != null) {
659 Matcher m = NAME_URL.matcher(bundleVendor);
660 String namePart = bundleVendor;
661 String urlPart = null;
662 if (m.matches()) {
663 namePart = m.group(1);
664 urlPart = m.group(2);
665 }
666 ps.println(" <organization>");
667 ps.print(" <name>");
668 ps.print(namePart.trim());
669 ps.println("</name>");
670 if (urlPart != null) {
671 ps.print(" <url>");
672 ps.print(urlPart.trim());
673 ps.println("</url>");
674 }
675 ps.println(" </organization>");
676 }
677 if (licenses != null) {
678 ps.println(" <licenses>");
679 Map map = parseHeader(licenses);
680 for (Iterator e = map.entrySet().iterator(); e.hasNext();) {
681 Map.Entry entry = (Map.Entry) e.next();
682 ps.println(" <license>");
683 Map values = (Map) entry.getValue();
684 print(ps, values, "name", "name", (String) values
685 .get("url"));
686 print(ps, values, "url", "url", null);
687 print(ps, values, "distribution", "distribution", "repo");
688 ps.println(" </license>");
689 }
690 ps.println(" </licenses>");
691 }
692 ps.println("</project>");
693 ps.close();
694 s.close();
695 dot
696 .putResource("pom.xml", new EmbeddedResource(s
697 .toByteArray(), 0));
698 }
699 }
700
701 /**
702 * NEW
703 *
704 */
705 void doPrebuild(Jar dot, Map clauses) {
706 for (Iterator i = clauses.entrySet().iterator(); i.hasNext();) {
707 Map.Entry entry = (Map.Entry) i.next();
708 String name = (String) entry.getKey();
709 Map args = (Map) entry.getValue();
710 File file = getFile(base, name);
711 File dir = file.getParentFile();
712 if (!dir.exists())
713 error("No directory for prebuild: " + file.getAbsolutePath());
714 else {
715 Instruction instr = Instruction.getPattern(file.getName());
716 File[] children = dir.listFiles();
717 for (int c = 0; c < children.length; c++) {
718 File child = children[c];
719 if (instr.matches(child.getName()) && !instr.isNegated()) {
720 try {
721 progress("Prebuilding "+child);
722 doPrebuild(dot, child, args);
723 } catch (Exception e) {
724 error("Can not build: "+child);
725 }
726 }
727 }
728 }
729 }
730 }
731
732 void doPrebuild(Jar dot, File f, Map args) throws Exception {
733 Builder builder = new Builder();
734 builder.setBase(base);
735 builder.setProperties(f);
736
737 Properties p = new Properties();
738 p.putAll(getProperties());
739 p.keySet().removeAll(builder.getProperties().keySet());
740 builder.getProperties().putAll(p);
741
742 Jar jar = builder.build();
743 String path = (String) args.get("path");
744 if (path != null)
745 path = f.getName();
746
747 dot.putResource(path, new JarResource(jar));
748 }
749
750 /**
751 * Utility function to print a tag from a map
752 *
753 * @param ps
754 * @param values
755 * @param string
756 * @param tag
757 * @param object
758 */
759 private void print(PrintStream ps, Map values, String string, String tag,
760 String object) {
761 String value = (String) values.get(string);
762 if (value == null)
763 value = object;
764 if (value == null)
765 return;
766 ps.println(" <" + tag + ">" + value.trim() + "</" + tag + ">");
767 }
768
769 public void close() {
Stuart McCullochc8f55952008-08-06 16:18:48 +0000770 for (Iterator j = tempJars.iterator(); j.hasNext();) {
Stuart McCulloch008eee02008-08-06 15:57:57 +0000771 Jar jar = (Jar) j.next();
772 jar.close();
773 }
Stuart McCulloch2bcdb182008-08-06 15:52:20 +0000774 super.close();
775 }
776}