blob: c3ab0cf7c2a4b34303c2f9d2fae938b8a25a25e8 [file] [log] [blame]
Stuart McCulloch26e7a5a2011-10-17 10:31:43 +00001package aQute.lib.osgi;
2
3/**
4 * This class can calculate the required headers for a (potential) JAR file. It
5 * analyzes a directory or JAR for the packages that are contained and that are
6 * referred to by the bytecodes. The user can the use regular expressions to
7 * define the attributes and directives. The matching is not fully regex for
8 * convenience. A * and ? get a . prefixed and dots are escaped.
9 *
10 * <pre>
11 * *;auto=true any
12 * org.acme.*;auto=true org.acme.xyz
13 * org.[abc]*;auto=true org.acme.xyz
14 * </pre>
15 *
16 * Additional, the package instruction can start with a '=' or a '!'. The '!'
17 * indicates negation. Any matching package is removed. The '=' is literal, the
18 * expression will be copied verbatim and no matching will take place.
19 *
20 * Any headers in the given properties are used in the output properties.
21 */
22import java.io.*;
23import java.net.*;
24import java.util.*;
25import java.util.jar.*;
26import java.util.jar.Attributes.Name;
27import java.util.regex.*;
28
29import aQute.bnd.annotation.*;
30import aQute.bnd.service.*;
31import aQute.lib.collections.*;
32import aQute.lib.filter.*;
33import aQute.lib.osgi.Clazz.QUERY;
34import aQute.libg.generics.*;
35import aQute.libg.header.*;
36import aQute.libg.tarjan.*;
37import aQute.libg.version.Version;
38
39public class Analyzer extends Processor {
40
41 static String version;
42 static Pattern versionPattern = Pattern
43 .compile("(\\d+\\.\\d+)\\.\\d+.*");
44 final Map<String, Map<String, String>> contained = newHashMap(); // package
45 final Map<String, Map<String, String>> referred = newHashMap(); // refers
46 // package
47 final Map<String, Set<String>> uses = newHashMap(); // package
48 Map<String, Clazz> classspace;
49 Map<String, Clazz> importedClassesCache = newMap();
50 Map<String, Map<String, String>> exports;
51 Map<String, Map<String, String>> imports;
52 Map<String, Map<String, String>> bundleClasspath; // Bundle
53 final Map<String, Map<String, String>> ignored = newHashMap(); // Ignored
54 // packages
55 Jar dot;
56 Map<String, Map<String, String>> classpathExports;
57
58 String activator;
59
60 final List<Jar> classpath = newList();
61
62 static Properties bndInfo;
63
64 boolean analyzed;
65 String bsn;
66 String versionPolicyUses;
67 String versionPolicyImplemented;
68 boolean diagnostics = false;
69 SortedSet<Clazz.JAVA> formats = new TreeSet<Clazz.JAVA>();
70 private boolean inited;
71
72 public Analyzer(Processor parent) {
73 super(parent);
74 }
75
76 public Analyzer() {
77 }
78
79 /**
80 * Specifically for Maven
81 *
82 * @param properties
83 * the properties
84 */
85
86 public static Properties getManifest(File dirOrJar) throws Exception {
87 Analyzer analyzer = new Analyzer();
88 try {
89 analyzer.setJar(dirOrJar);
90 Properties properties = new Properties();
91 properties.put(IMPORT_PACKAGE, "*");
92 properties.put(EXPORT_PACKAGE, "*");
93 analyzer.setProperties(properties);
94 Manifest m = analyzer.calcManifest();
95 Properties result = new Properties();
96 for (Iterator<Object> i = m.getMainAttributes().keySet().iterator(); i.hasNext();) {
97 Attributes.Name name = (Attributes.Name) i.next();
98 result.put(name.toString(), m.getMainAttributes().getValue(name));
99 }
100 return result;
101 } finally {
102 analyzer.close();
103 }
104 }
105
106 /**
107 * Calculates the data structures for generating a manifest.
108 *
109 * @throws IOException
110 */
111 public void analyze() throws Exception {
112 if (!analyzed) {
113 analyzed = true;
114 activator = getProperty(BUNDLE_ACTIVATOR);
115 bundleClasspath = parseHeader(getProperty(BUNDLE_CLASSPATH));
116
117 analyzeClasspath();
118
119 classspace = analyzeBundleClasspath(dot, bundleClasspath, contained, referred, uses);
120
121 for (AnalyzerPlugin plugin : getPlugins(AnalyzerPlugin.class)) {
122 if (plugin instanceof AnalyzerPlugin) {
123 AnalyzerPlugin analyzer = (AnalyzerPlugin) plugin;
124 try {
125 boolean reanalyze = analyzer.analyzeJar(this);
126 if (reanalyze)
127 classspace = analyzeBundleClasspath(dot, bundleClasspath, contained,
128 referred, uses);
129 } catch (Exception e) {
130 error("Plugin Analyzer " + analyzer + " throws exception " + e);
131 e.printStackTrace();
132 }
133 }
134 }
135
136 if (activator != null) {
137 // Add the package of the activator to the set
138 // of referred classes. This must be done before we remove
139 // contained set.
140 int n = activator.lastIndexOf('.');
141 if (n > 0) {
142 referred.put(activator.substring(0, n), new LinkedHashMap<String, String>());
143 }
144 }
145
146 referred.keySet().removeAll(contained.keySet());
147 if (referred.containsKey(".")) {
148 error("The default package '.' is not permitted by the Import-Package syntax. \n"
149 + " This can be caused by compile errors in Eclipse because Eclipse creates \n"
150 + "valid class files regardless of compile errors.\n"
151 + "The following package(s) import from the default package "
152 + getUsedBy("."));
153 }
154
155 Map<String, Map<String, String>> exportInstructions = parseHeader(getProperty(EXPORT_PACKAGE));
156 Map<String, Map<String, String>> additionalExportInstructions = parseHeader(getProperty(EXPORT_CONTENTS));
157 exportInstructions.putAll(additionalExportInstructions);
158 Map<String, Map<String, String>> importInstructions = parseHeader(getImportPackages());
159 Map<String, Map<String, String>> dynamicImports = parseHeader(getProperty(DYNAMICIMPORT_PACKAGE));
160
161 if (dynamicImports != null) {
162 // Remove any dynamic imports from the referred set.
163 referred.keySet().removeAll(dynamicImports.keySet());
164 }
165
166 Map<String, Map<String, String>> superfluous = newHashMap();
167 // Tricky!
168 for (Iterator<String> i = exportInstructions.keySet().iterator(); i.hasNext();) {
169 String instr = i.next();
170 if (!instr.startsWith("!"))
171 superfluous.put(instr, exportInstructions.get(instr));
172 }
173
174 exports = merge("export-package", exportInstructions, contained, superfluous.keySet(),
175 null);
176
177 // disallow export of default package
178 exports.remove(".");
179
180 for (Iterator<Map.Entry<String, Map<String, String>>> i = superfluous.entrySet()
181 .iterator(); i.hasNext();) {
182 // It is possible to mention metadata directories in the export
183 // explicitly, they are then exported and removed from the
184 // warnings. Note that normally metadata directories are not
185 // exported.
186 Map.Entry<String, Map<String, String>> entry = i.next();
187 String pack = entry.getKey();
188 if (isDuplicate(pack))
189 i.remove();
190 else if (isMetaData(pack)) {
191 exports.put(pack, entry.getValue());
192 i.remove();
193 }
194 }
195
196 if (!superfluous.isEmpty()) {
197 warning("Superfluous export-package instructions: " + superfluous.keySet());
198 }
199
200 // Add all exports that do not have an -noimport: directive
201 // to the imports.
202 Map<String, Map<String, String>> referredAndExported = newMap(referred);
203 referredAndExported.putAll(doExportsToImports(exports));
204
205 // match the imports to the referred and exported packages,
206 // merge the info for matching packages
207 Set<String> extra = new TreeSet<String>(importInstructions.keySet());
208 imports = merge("import-package", importInstructions, referredAndExported, extra,
209 ignored);
210
211 // Instructions that have not been used could be superfluous
212 // or if they do not contain wildcards, should be added
213 // as extra imports, the user knows best.
214 for (Iterator<String> i = extra.iterator(); i.hasNext();) {
215 String p = i.next();
216 if (p.startsWith("!") || p.indexOf('*') >= 0 || p.indexOf('?') >= 0
217 || p.indexOf('[') >= 0) {
218 if (!isResourceOnly() && !(p.equals("*")))
219 warning("Did not find matching referal for " + p);
220 } else {
221 Map<String, String> map = importInstructions.get(p);
222 imports.put(p, map);
223 }
224 }
225
226 // See what information we can find to augment the
227 // exports. I.e. look on the classpath
228 augmentExports();
229
230 // See what information we can find to augment the
231 // imports. I.e. look on the classpath
232 augmentImports();
233
234 // Add the uses clause to the exports
235 doUses(exports, uses, imports);
236 }
237 }
238
239 /**
240 * Copy the input collection into an output set but skip names that have
241 * been marked as duplicates or are optional.
242 *
243 * @param superfluous
244 * @return
245 */
246 Set<Instruction> removeMarkedDuplicates(Collection<Instruction> superfluous) {
247 Set<Instruction> result = new HashSet<Instruction>();
248 for (Iterator<Instruction> i = superfluous.iterator(); i.hasNext();) {
249 Instruction instr = (Instruction) i.next();
250 if (!isDuplicate(instr.getPattern()) && !instr.isOptional())
251 result.add(instr);
252 }
253 return result;
254 }
255
256 /**
257 * Analyzer has an empty default but the builder has a * as default.
258 *
259 * @return
260 */
261 protected String getImportPackages() {
262 return getProperty(IMPORT_PACKAGE);
263 }
264
265 /**
266 *
267 * @return
268 */
269 boolean isResourceOnly() {
270 return isTrue(getProperty(RESOURCEONLY));
271 }
272
273 /**
274 * Answer the list of packages that use the given package.
275 */
276 Set<String> getUsedBy(String pack) {
277 Set<String> set = newSet();
278 for (Iterator<Map.Entry<String, Set<String>>> i = uses.entrySet().iterator(); i.hasNext();) {
279 Map.Entry<String, Set<String>> entry = i.next();
280 Set<String> used = entry.getValue();
281 if (used.contains(pack))
282 set.add(entry.getKey());
283 }
284 return set;
285 }
286
287 /**
288 * One of the main workhorses of this class. This will analyze the current
289 * setp and calculate a new manifest according to this setup. This method
290 * will also set the manifest on the main jar dot
291 *
292 * @return
293 * @throws IOException
294 */
295 public Manifest calcManifest() throws Exception {
296 analyze();
297 Manifest manifest = new Manifest();
298 Attributes main = manifest.getMainAttributes();
299
300 main.put(Attributes.Name.MANIFEST_VERSION, "1.0");
301 main.putValue(BUNDLE_MANIFESTVERSION, "2");
302
303 boolean noExtraHeaders = "true".equalsIgnoreCase(getProperty(NOEXTRAHEADERS));
304
305 if (!noExtraHeaders) {
306 main.putValue(CREATED_BY,
307 System.getProperty("java.version") + " (" + System.getProperty("java.vendor")
308 + ")");
309 main.putValue(TOOL, "Bnd-" + getBndVersion());
310 main.putValue(BND_LASTMODIFIED, "" + System.currentTimeMillis());
311 }
312
313 String exportHeader = printClauses(exports, true);
314
315 if (exportHeader.length() > 0)
316 main.putValue(EXPORT_PACKAGE, exportHeader);
317 else
318 main.remove(EXPORT_PACKAGE);
319
320 Map<String, Map<String, String>> temp = removeKeys(imports, "java.");
321 if (!temp.isEmpty()) {
322 main.putValue(IMPORT_PACKAGE, printClauses(temp));
323 } else {
324 main.remove(IMPORT_PACKAGE);
325 }
326
327 temp = newMap(contained);
328 temp.keySet().removeAll(exports.keySet());
329
330 if (!temp.isEmpty())
331 main.putValue(PRIVATE_PACKAGE, printClauses(temp));
332 else
333 main.remove(PRIVATE_PACKAGE);
334
335 if (!ignored.isEmpty()) {
336 main.putValue(IGNORE_PACKAGE, printClauses(ignored));
337 } else {
338 main.remove(IGNORE_PACKAGE);
339 }
340
341 if (bundleClasspath != null && !bundleClasspath.isEmpty())
342 main.putValue(BUNDLE_CLASSPATH, printClauses(bundleClasspath));
343 else
344 main.remove(BUNDLE_CLASSPATH);
345
346 for (Enumeration<?> h = getProperties().propertyNames(); h.hasMoreElements();) {
347 String header = (String) h.nextElement();
348 if (header.trim().length() == 0) {
349 warning("Empty property set with value: " + getProperties().getProperty(header));
350 continue;
351 }
352
353 if (isMissingPlugin(header.trim())) {
354 error("Missing plugin for command %s", header);
355 }
356 if (!Character.isUpperCase(header.charAt(0))) {
357 if (header.charAt(0) == '@')
358 doNameSection(manifest, header);
359 continue;
360 }
361
362 if (header.equals(BUNDLE_CLASSPATH) || header.equals(EXPORT_PACKAGE)
363 || header.equals(IMPORT_PACKAGE))
364 continue;
365
366 if (header.equalsIgnoreCase("Name")) {
367 error("Your bnd file contains a header called 'Name'. This interferes with the manifest name section.");
368 continue;
369 }
370
371 if (Verifier.HEADER_PATTERN.matcher(header).matches()) {
372 String value = getProperty(header);
373 if (value != null && main.getValue(header) == null) {
374 if (value.trim().length() == 0)
375 main.remove(header);
376 else if (value.trim().equals(EMPTY_HEADER))
377 main.putValue(header, "");
378 else
379 main.putValue(header, value);
380 }
381 } else {
382 // TODO should we report?
383 }
384 }
385
386 //
387 // Calculate the bundle symbolic name if it is
388 // not set.
389 // 1. set
390 // 2. name of properties file (must be != bnd.bnd)
391 // 3. name of directory, which is usualy project name
392 //
393 String bsn = getBsn();
394 if (main.getValue(BUNDLE_SYMBOLICNAME) == null) {
395 main.putValue(BUNDLE_SYMBOLICNAME, bsn);
396 }
397
398 //
399 // Use the same name for the bundle name as BSN when
400 // the bundle name is not set
401 //
402 if (main.getValue(BUNDLE_NAME) == null) {
403 main.putValue(BUNDLE_NAME, bsn);
404 }
405
406 if (main.getValue(BUNDLE_VERSION) == null)
407 main.putValue(BUNDLE_VERSION, "0");
408
409 // Copy old values into new manifest, when they
410 // exist in the old one, but not in the new one
411 merge(manifest, dot.getManifest());
412
413 // Remove all the headers mentioned in -removeheaders
414 Map<String, Map<String, String>> removes = parseHeader(getProperty(REMOVEHEADERS));
415 Set<Instruction> matchers = Instruction.replaceWithInstruction(removes).keySet();
416
417 Collection<Object> toBeRemoved = Instruction.select(matchers, main.keySet());
418 Iterator<Object> i = main.keySet().iterator();
419 while (i.hasNext())
420 if (toBeRemoved.contains(i.next()))
421 i.remove();
422
423 dot.setManifest(manifest);
424 return manifest;
425 }
426
427 /**
428 * This method is called when the header starts with a @, signifying a name
429 * section header. The name part is defined by replacing all the @ signs to
430 * a /, removing the first and the last, and using the last part as header
431 * name:
432 *
433 * <pre>
434 * &#064;org@osgi@service@event@Implementation-Title
435 * </pre>
436 *
437 * This will be the header Implementation-Title in the
438 * org/osgi/service/event named section.
439 *
440 * @param manifest
441 * @param header
442 */
443 private void doNameSection(Manifest manifest, String header) {
444 String path = header.replace('@', '/');
445 int n = path.lastIndexOf('/');
446 // Must succeed because we start with @
447 String name = path.substring(n + 1);
448 // Skip first /
449 path = path.substring(1, n);
450 if (name.length() != 0 && path.length() != 0) {
451 Attributes attrs = manifest.getAttributes(path);
452 if (attrs == null) {
453 attrs = new Attributes();
454 manifest.getEntries().put(path, attrs);
455 }
456 attrs.putValue(name, getProperty(header));
457 } else {
458 warning("Invalid header (starts with @ but does not seem to be for the Name section): %s",
459 header);
460 }
461 }
462
463 /**
464 * Clear the key part of a header. I.e. remove everything from the first ';'
465 *
466 * @param value
467 * @return
468 */
469 public String getBsn() {
470 String value = getProperty(BUNDLE_SYMBOLICNAME);
471 if (value == null) {
472 if (getPropertiesFile() != null)
473 value = getPropertiesFile().getName();
474
475 String projectName = getBase().getName();
476 if (value == null || value.equals("bnd.bnd")) {
477 value = projectName;
478 } else if (value.endsWith(".bnd")) {
479 value = value.substring(0, value.length() - 4);
480 if (!value.startsWith(getBase().getName()))
481 value = projectName + "." + value;
482 }
483 }
484
485 if (value == null)
486 return "untitled";
487
488 int n = value.indexOf(';');
489 if (n > 0)
490 value = value.substring(0, n);
491 return value.trim();
492 }
493
494 public String _bsn(String args[]) {
495 return getBsn();
496 }
497
498 /**
499 * Calculate an export header solely based on the contents of a JAR file
500 *
501 * @param bundle
502 * The jar file to analyze
503 * @return
504 */
505 public String calculateExportsFromContents(Jar bundle) {
506 String ddel = "";
507 StringBuffer sb = new StringBuffer();
508 Map<String, Map<String, Resource>> map = bundle.getDirectories();
509 for (Iterator<String> i = map.keySet().iterator(); i.hasNext();) {
510 String directory = (String) i.next();
511 if (directory.equals("META-INF") || directory.startsWith("META-INF/"))
512 continue;
513 if (directory.equals("OSGI-OPT") || directory.startsWith("OSGI-OPT/"))
514 continue;
515 if (directory.equals("/"))
516 continue;
517
518 if (directory.endsWith("/"))
519 directory = directory.substring(0, directory.length() - 1);
520
521 directory = directory.replace('/', '.');
522 sb.append(ddel);
523 sb.append(directory);
524 ddel = ",";
525 }
526 return sb.toString();
527 }
528
529 public Map<String, Map<String, String>> getBundleClasspath() {
530 return bundleClasspath;
531 }
532
533 public Map<String, Map<String, String>> getContained() {
534 return contained;
535 }
536
537 public Map<String, Map<String, String>> getExports() {
538 return exports;
539 }
540
541 public Map<String, Map<String, String>> getImports() {
542 return imports;
543 }
544
545 public Jar getJar() {
546 return dot;
547 }
548
549 public Map<String, Map<String, String>> getReferred() {
550 return referred;
551 }
552
553 /**
554 * Return the set of unreachable code depending on exports and the bundle
555 * activator.
556 *
557 * @return
558 */
559 public Set<String> getUnreachable() {
560 Set<String> unreachable = new HashSet<String>(uses.keySet()); // all
561 for (Iterator<String> r = exports.keySet().iterator(); r.hasNext();) {
562 String packageName = r.next();
563 removeTransitive(packageName, unreachable);
564 }
565 if (activator != null) {
566 String pack = activator.substring(0, activator.lastIndexOf('.'));
567 removeTransitive(pack, unreachable);
568 }
569 return unreachable;
570 }
571
572 public Map<String, Set<String>> getUses() {
573 return uses;
574 }
575
576 /**
577 * Get the version for this bnd
578 *
579 * @return version or unknown.
580 */
581 public String getBndVersion() {
582 return getBndInfo("version", "1.42.1");
583 }
584
585 public long getBndLastModified() {
586 String time = getBndInfo("modified", "0");
587 try {
588 return Long.parseLong(time);
589 } catch (Exception e) {
590 }
591 return 0;
592 }
593
594 public String getBndInfo(String key, String defaultValue) {
595 if (bndInfo == null) {
596 bndInfo = new Properties();
597 try {
598 InputStream in = Analyzer.class.getResourceAsStream("bnd.info");
599 if (in != null) {
600 bndInfo.load(in);
601 in.close();
602 }
603 } catch (IOException ioe) {
604 warning("Could not read bnd.info in " + Analyzer.class.getPackage() + ioe);
605 }
606 }
607 return bndInfo.getProperty(key, defaultValue);
608 }
609
610 /**
611 * Merge the existing manifest with the instructions.
612 *
613 * @param manifest
614 * The manifest to merge with
615 * @throws IOException
616 */
617 public void mergeManifest(Manifest manifest) throws IOException {
618 if (manifest != null) {
619 Attributes attributes = manifest.getMainAttributes();
620 for (Iterator<Object> i = attributes.keySet().iterator(); i.hasNext();) {
621 Name name = (Name) i.next();
622 String key = name.toString();
623 // Dont want instructions
624 if (key.startsWith("-"))
625 continue;
626
627 if (getProperty(key) == null)
628 setProperty(key, (String) attributes.get(name));
629 }
630 }
631 }
632
633 public void setBase(File file) {
634 super.setBase(file);
635 getProperties().put("project.dir", getBase().getAbsolutePath());
636 }
637
638 /**
639 * Set the classpath for this analyzer by file.
640 *
641 * @param classpath
642 * @throws IOException
643 */
644 public void setClasspath(File[] classpath) throws IOException {
645 List<Jar> list = new ArrayList<Jar>();
646 for (int i = 0; i < classpath.length; i++) {
647 if (classpath[i].exists()) {
648 Jar current = new Jar(classpath[i]);
649 list.add(current);
650 } else {
651 error("Missing file on classpath: %s", classpath[i]);
652 }
653 }
654 for (Iterator<Jar> i = list.iterator(); i.hasNext();) {
655 addClasspath(i.next());
656 }
657 }
658
659 public void setClasspath(Jar[] classpath) {
660 for (int i = 0; i < classpath.length; i++) {
661 addClasspath(classpath[i]);
662 }
663 }
664
665 public void setClasspath(String[] classpath) {
666 for (int i = 0; i < classpath.length; i++) {
667 Jar jar = getJarFromName(classpath[i], " setting classpath");
668 if (jar != null)
669 addClasspath(jar);
670 }
671 }
672
673 /**
674 * Set the JAR file we are going to work in. This will read the JAR in
675 * memory.
676 *
677 * @param jar
678 * @return
679 * @throws IOException
680 */
681 public Jar setJar(File jar) throws IOException {
682 Jar jarx = new Jar(jar);
683 addClose(jarx);
684 return setJar(jarx);
685 }
686
687 /**
688 * Set the JAR directly we are going to work on.
689 *
690 * @param jar
691 * @return
692 */
693 public Jar setJar(Jar jar) {
694 this.dot = jar;
695 return jar;
696 }
697
698 protected void begin() {
699 if (inited == false) {
700 inited = true;
701 super.begin();
702
703 updateModified(getBndLastModified(), "bnd last modified");
704 verifyManifestHeadersCase(getProperties());
705
706 }
707 }
708
709 /**
710 * Check if the given class or interface name is contained in the jar.
711 *
712 * @param interfaceName
713 * @return
714 */
715
716 public boolean checkClass(String interfaceName) {
717 String path = Clazz.fqnToPath(interfaceName);
718 if (classspace.containsKey(path))
719 return true;
720
721 if (interfaceName.startsWith("java."))
722 return true;
723
724 if (imports != null && !imports.isEmpty()) {
725 String pack = interfaceName;
726 int n = pack.lastIndexOf('.');
727 if (n > 0)
728 pack = pack.substring(0, n);
729 else
730 pack = ".";
731
732 if (imports.containsKey(pack))
733 return true;
734 }
735 int n = interfaceName.lastIndexOf('.');
736 if (n > 0 && n + 1 < interfaceName.length()
737 && Character.isUpperCase(interfaceName.charAt(n + 1))) {
738 return checkClass(interfaceName.substring(0, n) + '$' + interfaceName.substring(n + 1));
739 }
740 return false;
741 }
742
743 /**
744 * Try to get a Jar from a file name/path or a url, or in last resort from
745 * the classpath name part of their files.
746 *
747 * @param name
748 * URL or filename relative to the base
749 * @param from
750 * Message identifying the caller for errors
751 * @return null or a Jar with the contents for the name
752 */
753 Jar getJarFromName(String name, String from) {
754 File file = new File(name);
755 if (!file.isAbsolute())
756 file = new File(getBase(), name);
757
758 if (file.exists())
759 try {
760 Jar jar = new Jar(file);
761 addClose(jar);
762 return jar;
763 } catch (Exception e) {
764 error("Exception in parsing jar file for " + from + ": " + name + " " + e);
765 }
766 // It is not a file ...
767 try {
768 // Lets try a URL
769 URL url = new URL(name);
770 Jar jar = new Jar(fileName(url.getPath()));
771 addClose(jar);
772 URLConnection connection = url.openConnection();
773 InputStream in = connection.getInputStream();
774 long lastModified = connection.getLastModified();
775 if (lastModified == 0)
776 // We assume the worst :-(
777 lastModified = System.currentTimeMillis();
778 EmbeddedResource.build(jar, in, lastModified);
779 in.close();
780 return jar;
781 } catch (IOException ee) {
782 // Check if we have files on the classpath
783 // that have the right name, allows us to specify those
784 // names instead of the full path.
785 for (Iterator<Jar> cp = getClasspath().iterator(); cp.hasNext();) {
786 Jar entry = cp.next();
787 if (entry.source != null && entry.source.getName().equals(name)) {
788 return entry;
789 }
790 }
791 // error("Can not find jar file for " + from + ": " + name);
792 }
793 return null;
794 }
795
796 private String fileName(String path) {
797 int n = path.lastIndexOf('/');
798 if (n > 0)
799 return path.substring(n + 1);
800 return path;
801 }
802
803 /**
804 *
805 * @param manifests
806 * @throws Exception
807 */
808 void merge(Manifest result, Manifest old) throws IOException {
809 if (old != null) {
810 for (Iterator<Map.Entry<Object, Object>> e = old.getMainAttributes().entrySet()
811 .iterator(); e.hasNext();) {
812 Map.Entry<Object, Object> entry = e.next();
813 Attributes.Name name = (Attributes.Name) entry.getKey();
814 String value = (String) entry.getValue();
815 if (name.toString().equalsIgnoreCase("Created-By"))
816 name = new Attributes.Name("Originally-Created-By");
817 if (!result.getMainAttributes().containsKey(name))
818 result.getMainAttributes().put(name, value);
819 }
820
821 // do not overwrite existing entries
822 Map<String, Attributes> oldEntries = old.getEntries();
823 Map<String, Attributes> newEntries = result.getEntries();
824 for (Iterator<Map.Entry<String, Attributes>> e = oldEntries.entrySet().iterator(); e
825 .hasNext();) {
826 Map.Entry<String, Attributes> entry = e.next();
827 if (!newEntries.containsKey(entry.getKey())) {
828 newEntries.put(entry.getKey(), entry.getValue());
829 }
830 }
831 }
832 }
833
834 String stem(String name) {
835 int n = name.lastIndexOf('.');
836 if (n > 0)
837 return name.substring(0, n);
838 else
839 return name;
840 }
841
842 /**
843 * Bnd is case sensitive for the instructions so we better check people are
844 * not using an invalid case. We do allow this to set headers that should
845 * not be processed by us but should be used by the framework.
846 *
847 * @param properties
848 * Properties to verify.
849 */
850
851 void verifyManifestHeadersCase(Properties properties) {
852 for (Iterator<Object> i = properties.keySet().iterator(); i.hasNext();) {
853 String header = (String) i.next();
854 for (int j = 0; j < headers.length; j++) {
855 if (!headers[j].equals(header) && headers[j].equalsIgnoreCase(header)) {
856 warning("Using a standard OSGi header with the wrong case (bnd is case sensitive!), using: "
857 + header + " and expecting: " + headers[j]);
858 break;
859 }
860 }
861 }
862 }
863
864 /**
865 * We will add all exports to the imports unless there is a -noimport
866 * directive specified on an export. This directive is skipped for the
867 * manifest.
868 *
869 * We also remove any version parameter so that augmentImports can do the
870 * version policy.
871 *
872 * The following method is really tricky and evolved over time. Coming from
873 * the original background of OSGi, it was a weird idea for me to have a
874 * public package that should not be substitutable. I was so much convinced
875 * that this was the right rule that I rücksichtlos imported them all. Alas,
876 * the real world was more subtle than that. It turns out that it is not a
877 * good idea to always import. First, there must be a need to import, i.e.
878 * there must be a contained package that refers to the exported package for
879 * it to make use importing that package. Second, if an exported package
880 * refers to an internal package than it should not be imported.
881 *
882 * Additionally, it is necessary to treat the exports in groups. If an
883 * exported package refers to another exported packages than it must be in
884 * the same group. A framework can only substitute exports for imports for
885 * the whole of such a group. WHY????? Not clear anymore ...
886 *
887 */
888 /**
889 * I could no longer argue why the groups are needed :-( See what happens
890 * ... The getGroups calculated the groups and then removed the imports from
891 * there. Now we only remove imports that have internal references. Using
892 * internal code for an exported package means that a bundle cannot import
893 * that package from elsewhere because its assumptions might be violated if
894 * it receives a substitution. //
895 */
896 Map<String, Map<String, String>> doExportsToImports(Map<String, Map<String, String>> exports) {
897
898 // private packages = contained - exported.
899 Set<String> privatePackages = new HashSet<String>(contained.keySet());
900 privatePackages.removeAll(exports.keySet());
901
902 // private references = ∀ p : private packages | uses(p)
903 Set<String> privateReferences = newSet();
904 for (String p : privatePackages) {
905 Set<String> uses = this.uses.get(p);
906 if (uses != null)
907 privateReferences.addAll(uses);
908 }
909
910 // Assume we are going to export all exported packages
911 Set<String> toBeImported = new HashSet<String>(exports.keySet());
912
913 // Remove packages that are not referenced privately
914 toBeImported.retainAll(privateReferences);
915
916 // Not necessary to import anything that is already
917 // imported in the Import-Package statement.
918 if (imports != null)
919 toBeImported.removeAll(imports.keySet());
920
921 // Remove exported packages that are referring to
922 // private packages.
923 // Each exported package has a uses clause. We just use
924 // the used packages for each exported package to find out
925 // if it refers to an internal package.
926 //
927
928 for (Iterator<String> i = toBeImported.iterator(); i.hasNext();) {
929 Set<String> usedByExportedPackage = this.uses.get(i.next());
930 for (String privatePackage : privatePackages) {
931 if (usedByExportedPackage.contains(privatePackage)) {
932 i.remove();
933 break;
934 }
935 }
936 }
937
938 // Clean up attributes and generate result map
939 Map<String, Map<String, String>> result = newMap();
940 for (Iterator<String> i = toBeImported.iterator(); i.hasNext();) {
941 String ep = i.next();
942 Map<String, String> parameters = exports.get(ep);
943
944 String noimport = parameters.get(NO_IMPORT_DIRECTIVE);
945 if (noimport != null && noimport.equalsIgnoreCase("true"))
946 continue;
947
948 // // we can't substitute when there is no version
949 // String version = parameters.get(VERSION_ATTRIBUTE);
950 // if (version == null) {
951 // if (isPedantic())
952 // warning(
953 // "Cannot automatically import exported package %s because it has no version defined",
954 // ep);
955 // continue;
956 // }
957
958 parameters = newMap(parameters);
959 parameters.remove(VERSION_ATTRIBUTE);
960 result.put(ep, parameters);
961 }
962 return result;
963 }
964
965 private <T> boolean intersects(Collection<T> aa, Collection<T> bb) {
966 if (aa.equals(bb))
967 return true;
968
969 if (aa.size() > bb.size())
970 return intersects(bb, aa);
971
972 for (T t : aa)
973 if (bb.contains(t))
974 return true;
975 return false;
976 }
977
978 public boolean referred(String packageName) {
979 // return true;
980 for (Map.Entry<String, Set<String>> contained : uses.entrySet()) {
981 if (!contained.getKey().equals(packageName)) {
982 if (contained.getValue().contains(packageName))
983 return true;
984 }
985 }
986 return false;
987 }
988
989 /**
990 * Create the imports/exports by parsing
991 *
992 * @throws IOException
993 */
994 void analyzeClasspath() throws Exception {
995 classpathExports = newHashMap();
996 for (Iterator<Jar> c = getClasspath().iterator(); c.hasNext();) {
997 Jar current = c.next();
998 checkManifest(current);
999 for (Iterator<String> j = current.getDirectories().keySet().iterator(); j.hasNext();) {
1000 String dir = j.next();
1001 Resource resource = current.getResource(dir + "/packageinfo");
1002 if (resource != null) {
1003 InputStream in = resource.openInputStream();
1004 try {
1005 String version = parsePackageInfo(in);
1006 setPackageInfo(dir, VERSION_ATTRIBUTE, version);
1007 } finally {
1008 in.close();
1009 }
1010 }
1011 }
1012 }
1013 }
1014
1015 /**
1016 *
1017 * @param jar
1018 */
1019 void checkManifest(Jar jar) {
1020 try {
1021 Manifest m = jar.getManifest();
1022 if (m != null) {
1023 String exportHeader = m.getMainAttributes().getValue(EXPORT_PACKAGE);
1024 if (exportHeader != null) {
1025 Map<String, Map<String, String>> exported = parseHeader(exportHeader);
1026 if (exported != null) {
1027 for (Map.Entry<String, Map<String, String>> entry : exported.entrySet()) {
1028 if (!classpathExports.containsKey(entry.getKey())) {
1029 classpathExports.put(entry.getKey(), entry.getValue());
1030 }
1031 }
1032 }
1033 }
1034 }
1035 } catch (Exception e) {
1036 warning("Erroneous Manifest for " + jar + " " + e);
1037 }
1038 }
1039
1040 /**
1041 * Find some more information about imports in manifest and other places.
1042 */
1043 void augmentImports() {
1044
1045 for (String packageName : imports.keySet()) {
1046 setProperty(CURRENT_PACKAGE, packageName);
1047 try {
1048 Map<String, String> importAttributes = imports.get(packageName);
1049 Map<String, String> exporterAttributes = classpathExports.get(packageName);
1050 if (exporterAttributes == null)
1051 exporterAttributes = exports.get(packageName);
1052
1053 if (exporterAttributes != null) {
1054 augmentVersion(importAttributes, exporterAttributes);
1055 augmentMandatory(importAttributes, exporterAttributes);
1056 if (exporterAttributes.containsKey(IMPORT_DIRECTIVE))
1057 importAttributes.put(IMPORT_DIRECTIVE,
1058 exporterAttributes.get(IMPORT_DIRECTIVE));
1059 }
1060
1061 fixupAttributes(importAttributes);
1062 removeAttributes(importAttributes);
1063
1064 } finally {
1065 unsetProperty(CURRENT_PACKAGE);
1066 }
1067 }
1068 }
1069
1070 /**
1071 * Provide any macro substitutions and versions for exported packages.
1072 */
1073
1074 void augmentExports() {
1075 for (String packageName : exports.keySet()) {
1076 setProperty(CURRENT_PACKAGE, packageName);
1077 try {
1078 Map<String, String> attributes = exports.get(packageName);
1079 Map<String, String> exporterAttributes = classpathExports.get(packageName);
1080 if (exporterAttributes == null)
1081 continue;
1082
1083 for (Map.Entry<String, String> entry : exporterAttributes.entrySet()) {
1084 String key = entry.getKey();
1085 if (key.equalsIgnoreCase(SPECIFICATION_VERSION))
1086 key = VERSION_ATTRIBUTE;
1087 if (!key.endsWith(":") && !attributes.containsKey(key)) {
1088 attributes.put(key, entry.getValue());
1089 }
1090 }
1091
1092 fixupAttributes(attributes);
1093 removeAttributes(attributes);
1094
1095 } finally {
1096 unsetProperty(CURRENT_PACKAGE);
1097 }
1098 }
1099 }
1100
1101 /**
1102 * Fixup Attributes
1103 *
1104 * Execute any macros on an export and
1105 */
1106
1107 void fixupAttributes(Map<String, String> attributes) {
1108 // Convert any attribute values that have macros.
1109 for (String key : attributes.keySet()) {
1110 String value = attributes.get(key);
1111 if (value.indexOf('$') >= 0) {
1112 value = getReplacer().process(value);
1113 attributes.put(key, value);
1114 }
1115 }
1116
1117 }
1118
1119 /*
1120 * Remove the attributes mentioned in the REMOVE_ATTRIBUTE_DIRECTIVE.
1121 */
1122
1123 void removeAttributes(Map<String, String> attributes) {
1124 // You can add a remove-attribute: directive with a regular
1125 // expression for attributes that need to be removed. We also
1126 // remove all attributes that have a value of !. This allows
1127 // you to use macros with ${if} to remove values.
1128 String remove = attributes.remove(REMOVE_ATTRIBUTE_DIRECTIVE);
1129 Instruction removeInstr = null;
1130
1131 if (remove != null)
1132 removeInstr = Instruction.getPattern(remove);
1133
1134 for (Iterator<Map.Entry<String, String>> i = attributes.entrySet().iterator(); i.hasNext();) {
1135 Map.Entry<String, String> entry = i.next();
1136 if (entry.getValue().equals("!"))
1137 i.remove();
1138 else if (removeInstr != null && removeInstr.matches((String) entry.getKey()))
1139 i.remove();
1140 else {
1141 // Not removed ...
1142 }
1143 }
1144 }
1145
1146 /**
1147 * If we use an import with mandatory attributes we better all use them
1148 *
1149 * @param currentAttributes
1150 * @param exporter
1151 */
1152 private void augmentMandatory(Map<String, String> currentAttributes,
1153 Map<String, String> exporter) {
1154 String mandatory = (String) exporter.get("mandatory:");
1155 if (mandatory != null) {
1156 String[] attrs = mandatory.split("\\s*,\\s*");
1157 for (int i = 0; i < attrs.length; i++) {
1158 if (!currentAttributes.containsKey(attrs[i]))
1159 currentAttributes.put(attrs[i], exporter.get(attrs[i]));
1160 }
1161 }
1162 }
1163
1164 /**
1165 * Check if we can augment the version from the exporter.
1166 *
1167 * We allow the version in the import to specify a @ which is replaced with
1168 * the exporter's version.
1169 *
1170 * @param currentAttributes
1171 * @param exporter
1172 */
1173 private void augmentVersion(Map<String, String> currentAttributes, Map<String, String> exporter) {
1174
1175 String exportVersion = (String) exporter.get(VERSION_ATTRIBUTE);
1176 if (exportVersion == null)
1177 return;
1178
1179 exportVersion = cleanupVersion(exportVersion);
1180 String importRange = currentAttributes.get(VERSION_ATTRIBUTE);
1181 boolean impl = isTrue(currentAttributes.get(PROVIDE_DIRECTIVE));
1182 try {
1183 setProperty("@", exportVersion);
1184
1185 if (importRange != null) {
1186 importRange = cleanupVersion(importRange);
1187 importRange = getReplacer().process(importRange);
1188 } else
1189 importRange = getVersionPolicy(impl);
1190
1191 } finally {
1192 unsetProperty("@");
1193 }
1194 // See if we can borrow the version
1195 // we must replace the ${@} with the version we
1196 // found this can be useful if you want a range to start
1197 // with the found version.
1198 currentAttributes.put(VERSION_ATTRIBUTE, importRange);
1199 }
1200
1201 /**
1202 * Calculate a version from a version policy.
1203 *
1204 * @param version
1205 * The actual exported version
1206 * @param impl
1207 * true for implementations and false for clients
1208 */
1209
1210 String calculateVersionRange(String version, boolean impl) {
1211 setProperty("@", version);
1212 try {
1213 return getVersionPolicy(impl);
1214 } finally {
1215 unsetProperty("@");
1216 }
1217 }
1218
1219 /**
1220 * Add the uses clauses. This method iterates over the exports and cal
1221 *
1222 * @param exports
1223 * @param uses
1224 * @throws MojoExecutionException
1225 */
1226 void doUses(Map<String, Map<String, String>> exports, Map<String, Set<String>> uses,
1227 Map<String, Map<String, String>> imports) {
1228 if ("true".equalsIgnoreCase(getProperty(NOUSES)))
1229 return;
1230
1231 for (Iterator<String> i = exports.keySet().iterator(); i.hasNext();) {
1232 String packageName = i.next();
1233 setProperty(CURRENT_PACKAGE, packageName);
1234 try {
1235 doUses(packageName, exports, uses, imports);
1236 } finally {
1237 unsetProperty(CURRENT_PACKAGE);
1238 }
1239
1240 }
1241 }
1242
1243 /**
1244 * @param packageName
1245 * @param exports
1246 * @param uses
1247 * @param imports
1248 */
1249 protected void doUses(String packageName, Map<String, Map<String, String>> exports,
1250 Map<String, Set<String>> uses, Map<String, Map<String, String>> imports) {
1251 Map<String, String> clause = exports.get(packageName);
1252
1253 // Check if someone already set the uses: directive
1254 String override = clause.get(USES_DIRECTIVE);
1255 if (override == null)
1256 override = USES_USES;
1257
1258 // Get the used packages
1259 Set<String> usedPackages = uses.get(packageName);
1260
1261 if (usedPackages != null) {
1262
1263 // Only do a uses on exported or imported packages
1264 // and uses should also not contain our own package
1265 // name
1266 Set<String> sharedPackages = new HashSet<String>();
1267 sharedPackages.addAll(imports.keySet());
1268 sharedPackages.addAll(exports.keySet());
1269 usedPackages.retainAll(sharedPackages);
1270 usedPackages.remove(packageName);
1271
1272 StringBuffer sb = new StringBuffer();
1273 String del = "";
1274 for (Iterator<String> u = usedPackages.iterator(); u.hasNext();) {
1275 String usedPackage = u.next();
1276 if (!usedPackage.startsWith("java.")) {
1277 sb.append(del);
1278 sb.append(usedPackage);
1279 del = ",";
1280 }
1281 }
1282 if (override.indexOf('$') >= 0) {
1283 setProperty(CURRENT_USES, sb.toString());
1284 override = getReplacer().process(override);
1285 unsetProperty(CURRENT_USES);
1286 } else
1287 // This is for backward compatibility 0.0.287
1288 // can be deprecated over time
1289 override = override.replaceAll(USES_USES, sb.toString()).trim();
1290
1291 if (override.endsWith(","))
1292 override = override.substring(0, override.length() - 1);
1293 if (override.startsWith(","))
1294 override = override.substring(1);
1295 if (override.length() > 0) {
1296 clause.put(USES_DIRECTIVE, override);
1297 }
1298 }
1299 }
1300
1301 /**
1302 * Transitively remove all elemens from unreachable through the uses link.
1303 *
1304 * @param name
1305 * @param unreachable
1306 */
1307 void removeTransitive(String name, Set<String> unreachable) {
1308 if (!unreachable.contains(name))
1309 return;
1310
1311 unreachable.remove(name);
1312
1313 Set<String> ref = uses.get(name);
1314 if (ref != null) {
1315 for (Iterator<String> r = ref.iterator(); r.hasNext();) {
1316 String element = (String) r.next();
1317 removeTransitive(element, unreachable);
1318 }
1319 }
1320 }
1321
1322 /**
1323 * Helper method to set the package info
1324 *
1325 * @param dir
1326 * @param key
1327 * @param value
1328 */
1329 void setPackageInfo(String dir, String key, String value) {
1330 if (value != null) {
1331 String pack = dir.replace('/', '.');
1332 Map<String, String> map = classpathExports.get(pack);
1333 if (map == null) {
1334 map = new HashMap<String, String>();
1335 classpathExports.put(pack, map);
1336 }
1337 if (!map.containsKey(VERSION_ATTRIBUTE))
1338 map.put(key, value);
1339 else if (!map.get(VERSION_ATTRIBUTE).equals(value)) {
1340 // System.out.println("duplicate version info for " + dir + " "
1341 // + value + " and " + map.get(VERSION_ATTRIBUTE));
1342 }
1343 }
1344 }
1345
1346 public void close() {
1347 if (diagnostics) {
1348 PrintStream out = System.out;
1349 out.printf("Current directory : %s\n", new File("").getAbsolutePath());
1350 out.println("Classpath used");
1351 for (Jar jar : getClasspath()) {
1352 out.printf("File : %s\n", jar.getSource());
1353 out.printf("File abs path : %s\n", jar.getSource()
1354 .getAbsolutePath());
1355 out.printf("Name : %s\n", jar.getName());
1356 Map<String, Map<String, Resource>> dirs = jar.getDirectories();
1357 for (Map.Entry<String, Map<String, Resource>> entry : dirs.entrySet()) {
1358 Map<String, Resource> dir = entry.getValue();
1359 String name = entry.getKey().replace('/', '.');
1360 if (dir != null) {
1361 out.printf(" %-30s %d\n", name,
1362 dir.size());
1363 } else {
1364 out.printf(" %-30s <<empty>>\n", name);
1365 }
1366 }
1367 }
1368 }
1369
1370 super.close();
1371 if (dot != null)
1372 dot.close();
1373
1374 if (classpath != null)
1375 for (Iterator<Jar> j = classpath.iterator(); j.hasNext();) {
1376 Jar jar = j.next();
1377 jar.close();
1378 }
1379 }
1380
1381 /**
1382 * Findpath looks through the contents of the JAR and finds paths that end
1383 * with the given regular expression
1384 *
1385 * ${findpath (; reg-expr (; replacement)? )? }
1386 *
1387 * @param args
1388 * @return
1389 */
1390 public String _findpath(String args[]) {
1391 return findPath("findpath", args, true);
1392 }
1393
1394 public String _findname(String args[]) {
1395 return findPath("findname", args, false);
1396 }
1397
1398 String findPath(String name, String[] args, boolean fullPathName) {
1399 if (args.length > 3) {
1400 warning("Invalid nr of arguments to " + name + " " + Arrays.asList(args)
1401 + ", syntax: ${" + name + " (; reg-expr (; replacement)? )? }");
1402 return null;
1403 }
1404
1405 String regexp = ".*";
1406 String replace = null;
1407
1408 switch (args.length) {
1409 case 3:
1410 replace = args[2];
1411 case 2:
1412 regexp = args[1];
1413 }
1414 StringBuffer sb = new StringBuffer();
1415 String del = "";
1416
1417 Pattern expr = Pattern.compile(regexp);
1418 for (Iterator<String> e = dot.getResources().keySet().iterator(); e.hasNext();) {
1419 String path = e.next();
1420 if (!fullPathName) {
1421 int n = path.lastIndexOf('/');
1422 if (n >= 0) {
1423 path = path.substring(n + 1);
1424 }
1425 }
1426
1427 Matcher m = expr.matcher(path);
1428 if (m.matches()) {
1429 if (replace != null)
1430 path = m.replaceAll(replace);
1431
1432 sb.append(del);
1433 sb.append(path);
1434 del = ", ";
1435 }
1436 }
1437 return sb.toString();
1438 }
1439
1440 public void putAll(Map<String, String> additional, boolean force) {
1441 for (Iterator<Map.Entry<String, String>> i = additional.entrySet().iterator(); i.hasNext();) {
1442 Map.Entry<String, String> entry = i.next();
1443 if (force || getProperties().get(entry.getKey()) == null)
1444 setProperty((String) entry.getKey(), (String) entry.getValue());
1445 }
1446 }
1447
1448 boolean firstUse = true;
1449
1450 public List<Jar> getClasspath() {
1451 if (firstUse) {
1452 firstUse = false;
1453 String cp = getProperty(CLASSPATH);
1454 if (cp != null)
1455 for (String s : split(cp)) {
1456 Jar jar = getJarFromName(s, "getting classpath");
1457 if (jar != null)
1458 addClasspath(jar);
1459 else
1460 warning("Cannot find entry on -classpath: %s", s);
1461 }
1462 }
1463 return classpath;
1464 }
1465
1466 public void addClasspath(Jar jar) {
1467 if (isPedantic() && jar.getResources().isEmpty())
1468 warning("There is an empty jar or directory on the classpath: " + jar.getName());
1469
1470 classpath.add(jar);
1471 }
1472
1473 public void addClasspath(File cp) throws IOException {
1474 if (!cp.exists())
1475 warning("File on classpath that does not exist: " + cp);
1476 Jar jar = new Jar(cp);
1477 addClose(jar);
1478 classpath.add(jar);
1479 }
1480
1481 public void clear() {
1482 classpath.clear();
1483 }
1484
1485 public Jar getTarget() {
1486 return dot;
1487 }
1488
1489 protected Map<String, Clazz> analyzeBundleClasspath(Jar dot,
1490 Map<String, Map<String, String>> bundleClasspath,
1491 Map<String, Map<String, String>> contained, Map<String, Map<String, String>> referred,
1492 Map<String, Set<String>> uses) throws Exception {
1493 Map<String, Clazz> classSpace = new HashMap<String, Clazz>();
1494 Set<String> hide = Create.set();
1495 boolean containsDirectory = false;
1496
1497 for (String path : bundleClasspath.keySet()) {
1498 if ( dot.getDirectories().containsKey(path)) {
1499 containsDirectory = true;
1500 break;
1501 }
1502 }
1503
1504 if (bundleClasspath.isEmpty()) {
1505 analyzeJar(dot, "", classSpace, contained, referred, uses, hide, true);
1506 } else {
1507 for (String path : bundleClasspath.keySet()) {
1508 Map<String, String> info = bundleClasspath.get(path);
1509
1510 if (path.equals(".")) {
1511 analyzeJar(dot, "", classSpace, contained, referred, uses, hide, !containsDirectory);
1512 continue;
1513 }
1514 //
1515 // There are 3 cases:
1516 // - embedded JAR file
1517 // - directory
1518 // - error
1519 //
1520
1521 Resource resource = dot.getResource(path);
1522 if (resource != null) {
1523 try {
1524 Jar jar = new Jar(path);
1525 addClose(jar);
1526 EmbeddedResource.build(jar, resource);
1527 analyzeJar(jar, "", classSpace, contained, referred, uses, hide, true);
1528 } catch (Exception e) {
1529 warning("Invalid bundle classpath entry: " + path + " " + e);
1530 }
1531 } else {
1532 if (dot.getDirectories().containsKey(path)) {
1533 // if directories are used, we should not have dot as we
1534 // would have the classes in these directories on the
1535 // class
1536 // path twice.
1537 if (bundleClasspath.containsKey("."))
1538 warning("Bundle-ClassPath uses a directory '%s' as well as '.', this implies the directory is seen \n"
1539 + "twice by the class loader. bnd assumes that the classes are only "
1540 + "loaded from '%s'. It is better to unroll the directory to create a flat bundle.",
1541 path, path);
1542 analyzeJar(dot, Processor.appendPath(path) + "/", classSpace, contained,
1543 referred, uses, hide,true);
1544 } else {
1545 if (!"optional".equals(info.get(RESOLUTION_DIRECTIVE)))
1546 warning("No sub JAR or directory " + path);
1547 }
1548 }
1549 }
1550
1551 for (Clazz c : classSpace.values()) {
1552 formats.add(c.getFormat());
1553 }
1554 }
1555 return classSpace;
1556 }
1557
1558 /**
1559 * We traverse through all the classes that we can find and calculate the
1560 * contained and referred set and uses. This method ignores the Bundle
1561 * classpath.
1562 *
1563 * @param jar
1564 * @param contained
1565 * @param referred
1566 * @param uses
1567 * @throws IOException
1568 */
1569 private void analyzeJar(Jar jar, String prefix, Map<String, Clazz> classSpace,
1570 Map<String, Map<String, String>> contained, Map<String, Map<String, String>> referred,
1571 Map<String, Set<String>> uses, Set<String> hide, boolean reportWrongPath) throws Exception {
1572
1573 next: for (String path : jar.getResources().keySet()) {
1574 if (path.startsWith(prefix) /* && !hide.contains(path) */) {
1575 hide.add(path);
1576 String relativePath = path.substring(prefix.length());
1577
1578 // // TODO this check (and the whole hide) is likely redundant
1579 // // it only protects against repeated checks for non-class
1580 // // bundle resources, but should not affect results otherwise.
1581 // if (!hide.add(relativePath)) {
1582 // continue;
1583 // }
1584
1585 // Check if we'd already had this one.
1586 // Notice that we're flattening the space because
1587 // this is what class loaders do.
1588 if (classSpace.containsKey(relativePath))
1589 continue;
1590
1591 String pack = getPackage(relativePath);
1592
1593 if (pack != null && !contained.containsKey(pack)) {
1594 // For each package we encounter for the first
1595 // time
1596 if (!isMetaData(relativePath)) {
1597
1598 Map<String, String> info = newMap();
1599 contained.put(pack, info);
1600
1601 Resource pinfo = jar.getResource(prefix + pack.replace('.', '/')
1602 + "/packageinfo");
1603 if (pinfo != null) {
1604 InputStream in = pinfo.openInputStream();
1605 String version;
1606 try {
1607 version = parsePackageInfo(in);
1608 } finally {
1609 in.close();
1610 }
1611 if (version != null)
1612 info.put(VERSION_ATTRIBUTE, version);
1613 }
1614 }
1615 }
1616
1617 // Check class resources, we need to analyze them
1618 if (path.endsWith(".class")) {
1619 Resource resource = jar.getResource(path);
1620 Clazz clazz;
1621
1622 try {
1623 InputStream in = resource.openInputStream();
1624 clazz = new Clazz(relativePath, resource);
1625 try {
1626 // Check if we have a package-info
1627 if (relativePath.endsWith("/package-info.class")) {
1628 // package-info can contain an Export annotation
1629 Map<String, String> info = contained.get(pack);
1630 parsePackageInfoClass(clazz, info);
1631 } else {
1632 // Otherwise we just parse it simply
1633 clazz.parseClassFile();
1634 }
1635 } finally {
1636 in.close();
1637 }
1638 } catch (Throwable e) {
1639 error("Invalid class file: " + relativePath, e);
1640 e.printStackTrace();
1641 continue next;
1642 }
1643
1644 String calculatedPath = clazz.getClassName() + ".class";
1645 if (!calculatedPath.equals(relativePath)) {
1646 if (!isNoBundle() && reportWrongPath) {
1647 error("Class in different directory than declared. Path from class name is "
1648 + calculatedPath
1649 + " but the path in the jar is "
1650 + relativePath + " from '" + jar + "'");
1651 }
1652 }
1653
1654 classSpace.put(relativePath, clazz);
1655
1656 // Look at the referred packages
1657 // and copy them to our baseline
1658 for (String p : clazz.getReferred()) {
1659 Map<String, String> attrs = referred.get(p);
1660 if (attrs == null) {
1661 attrs = newMap();
1662 referred.put(p, attrs);
1663 }
1664 }
1665
1666 // Add all the used packages
1667 // to this package
1668 Set<String> t = uses.get(pack);
1669 if (t == null)
1670 uses.put(pack, t = new LinkedHashSet<String>());
1671 t.addAll(clazz.getReferred());
1672 t.remove(pack);
1673 }
1674 }
1675 }
1676 }
1677
1678 static Pattern OBJECT_REFERENCE = Pattern.compile("L([^/]+/)*([^;]+);");
1679
1680 private void parsePackageInfoClass(final Clazz clazz, final Map<String, String> info)
1681 throws Exception {
1682 clazz.parseClassFileWithCollector(new ClassDataCollector() {
1683 @Override public void annotation(Annotation a) {
1684 if (a.name.equals(Clazz.toDescriptor(aQute.bnd.annotation.Version.class))) {
1685
1686 // Check version
1687 String version = a.get("value");
1688 if (!info.containsKey(Constants.VERSION_ATTRIBUTE)) {
1689 if (version != null) {
1690 version = getReplacer().process(version);
1691 if (Verifier.VERSION.matcher(version).matches())
1692 info.put(VERSION_ATTRIBUTE, version);
1693 else
1694 error("Export annotatio in %s has invalid version info: %s", clazz,
1695 version);
1696 }
1697 } else {
1698 // Verify this matches with packageinfo
1699 String presentVersion = info.get(VERSION_ATTRIBUTE);
1700 try {
1701 Version av = new Version(presentVersion);
1702 Version bv = new Version(version);
1703 if (!av.equals(bv)) {
1704 error("Version from annotation for %s differs with packageinfo or Manifest",
1705 Clazz.getPackage(clazz.className));
1706 }
1707 } catch (Exception e) {
1708 // Ignore
1709 }
1710 }
1711 } else if (a.name.equals(Clazz.toDescriptor(Export.class))) {
1712
1713 // Check mandatory attributes
1714 Map<String, String> attrs = doAttrbutes((Object[]) a.get(Export.MANDATORY),
1715 clazz, getReplacer());
1716 if (!attrs.isEmpty()) {
1717 info.putAll(attrs);
1718 info.put(MANDATORY_DIRECTIVE, Processor.join(attrs.keySet()));
1719 }
1720
1721 // Check optional attributes
1722 attrs = doAttrbutes((Object[]) a.get(Export.OPTIONAL), clazz, getReplacer());
1723 if (!attrs.isEmpty()) {
1724 info.putAll(attrs);
1725 }
1726
1727 // Check Included classes
1728 Object[] included = a.get(Export.INCLUDE);
1729 if (included != null && included.length > 0) {
1730 StringBuilder sb = new StringBuilder();
1731 String del = "";
1732 for (Object i : included) {
1733 Matcher m = OBJECT_REFERENCE.matcher((String) i);
1734 if (m.matches()) {
1735 sb.append(del);
1736 sb.append(m.group(2));
1737 del = ",";
1738 }
1739 }
1740 info.put(INCLUDE_DIRECTIVE, sb.toString());
1741 }
1742
1743 // Check Excluded classes
1744 Object[] excluded = a.get(Export.EXCLUDE);
1745 if (excluded != null && excluded.length > 0) {
1746 StringBuilder sb = new StringBuilder();
1747 String del = "";
1748 for (Object i : excluded) {
1749 Matcher m = OBJECT_REFERENCE.matcher((String) i);
1750 if (m.matches()) {
1751 sb.append(del);
1752 sb.append(m.group(2));
1753 del = ",";
1754 }
1755 }
1756 info.put(EXCLUDE_DIRECTIVE, sb.toString());
1757 }
1758
1759 // Check Uses
1760 Object[] uses = a.get(Export.USES);
1761 if (uses != null && uses.length > 0) {
1762 String old = info.get(USES_DIRECTIVE);
1763 if (old == null)
1764 old = "";
1765 StringBuilder sb = new StringBuilder(old);
1766 String del = sb.length() == 0 ? "" : ",";
1767
1768 for (Object use : uses) {
1769 sb.append(del);
1770 sb.append(use);
1771 del = ",";
1772 }
1773 info.put(USES_DIRECTIVE, sb.toString());
1774 }
1775 }
1776 }
1777
1778 });
1779 }
1780
1781 /**
1782 * Clean up version parameters. Other builders use more fuzzy definitions of
1783 * the version syntax. This method cleans up such a version to match an OSGi
1784 * version.
1785 *
1786 * @param VERSION_STRING
1787 * @return
1788 */
1789 static Pattern fuzzyVersion = Pattern
1790 .compile(
1791 "(\\d+)(\\.(\\d+)(\\.(\\d+))?)?([^a-zA-Z0-9](.*))?",
1792 Pattern.DOTALL);
1793 static Pattern fuzzyVersionRange = Pattern
1794 .compile(
1795 "(\\(|\\[)\\s*([-\\da-zA-Z.]+)\\s*,\\s*([-\\da-zA-Z.]+)\\s*(\\]|\\))",
1796 Pattern.DOTALL);
1797 static Pattern fuzzyModifier = Pattern.compile("(\\d+[.-])*(.*)", Pattern.DOTALL);
1798
1799 static Pattern nummeric = Pattern.compile("\\d*");
1800
1801 static public String cleanupVersion(String version) {
1802 Matcher m = Verifier.VERSIONRANGE.matcher(version);
1803
1804 if (m.matches()) {
1805 return version;
1806 }
1807
1808 m = fuzzyVersionRange.matcher(version);
1809 if (m.matches()) {
1810 String prefix = m.group(1);
1811 String first = m.group(2);
1812 String last = m.group(3);
1813 String suffix = m.group(4);
1814 return prefix + cleanupVersion(first) + "," + cleanupVersion(last) + suffix;
1815 } else {
1816 m = fuzzyVersion.matcher(version);
1817 if (m.matches()) {
1818 StringBuffer result = new StringBuffer();
1819 String major = removeLeadingZeroes(m.group(1));
1820 String minor = removeLeadingZeroes(m.group(3));
1821 String micro = removeLeadingZeroes(m.group(5));
1822 String qualifier = m.group(7);
1823
1824 if (major != null) {
1825 result.append(major);
1826 if (minor != null) {
1827 result.append(".");
1828 result.append(minor);
1829 if (micro != null) {
1830 result.append(".");
1831 result.append(micro);
1832 if (qualifier != null) {
1833 result.append(".");
1834 cleanupModifier(result, qualifier);
1835 }
1836 } else if (qualifier != null) {
1837 result.append(".0.");
1838 cleanupModifier(result, qualifier);
1839 }
1840 } else if (qualifier != null) {
1841 result.append(".0.0.");
1842 cleanupModifier(result, qualifier);
1843 }
1844 return result.toString();
1845 }
1846 }
1847 }
1848 return version;
1849 }
1850
1851 private static String removeLeadingZeroes(String group) {
1852 int n = 0;
1853 while (group != null && n < group.length() - 1 && group.charAt(n) == '0')
1854 n++;
1855 if (n == 0)
1856 return group;
1857
1858 return group.substring(n);
1859 }
1860
1861 static void cleanupModifier(StringBuffer result, String modifier) {
1862 Matcher m = fuzzyModifier.matcher(modifier);
1863 if (m.matches())
1864 modifier = m.group(2);
1865
1866 for (int i = 0; i < modifier.length(); i++) {
1867 char c = modifier.charAt(i);
1868 if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
1869 || c == '_' || c == '-')
1870 result.append(c);
1871 }
1872 }
1873
1874 /**
1875 * Decide if the package is a metadata package.
1876 *
1877 * @param pack
1878 * @return
1879 */
1880 boolean isMetaData(String pack) {
1881 for (int i = 0; i < METAPACKAGES.length; i++) {
1882 if (pack.startsWith(METAPACKAGES[i]))
1883 return true;
1884 }
1885 return false;
1886 }
1887
1888 public String getPackage(String clazz) {
1889 int n = clazz.lastIndexOf('/');
1890 if (n < 0)
1891 return ".";
1892 return clazz.substring(0, n).replace('/', '.');
1893 }
1894
1895 //
1896 // We accept more than correct OSGi versions because in a later
1897 // phase we actually cleanup maven versions. But it is a bit yucky
1898 //
1899 static String parsePackageInfo(InputStream jar) throws IOException {
1900 try {
1901 Properties p = new Properties();
1902 p.load(jar);
1903 jar.close();
1904 if (p.containsKey("version")) {
1905 return p.getProperty("version");
1906 }
1907 } catch (Exception e) {
1908 e.printStackTrace();
1909 }
1910 return null;
1911 }
1912
1913 final static String DEFAULT_PROVIDER_POLICY = "${range;[==,=+)}";
1914 final static String DEFAULT_CONSUMER_POLICY = "${range;[==,+)}";
1915
1916 @SuppressWarnings("deprecation") public String getVersionPolicy(boolean implemented) {
1917 if (implemented) {
1918 String s = getProperty(PROVIDER_POLICY);
1919 if (s != null)
1920 return s;
1921
1922 s = getProperty(VERSIONPOLICY_IMPL);
1923 if (s != null)
1924 return s;
1925
1926 return getProperty(VERSIONPOLICY, DEFAULT_PROVIDER_POLICY);
1927 } else {
1928 String s = getProperty(CONSUMER_POLICY);
1929 if (s != null)
1930 return s;
1931
1932 s = getProperty(VERSIONPOLICY_USES);
1933 if (s != null)
1934 return s;
1935
1936 return getProperty(VERSIONPOLICY, DEFAULT_CONSUMER_POLICY);
1937 }
1938 // String vp = implemented ? getProperty(VERSIONPOLICY_IMPL) :
1939 // getProperty(VERSIONPOLICY_USES);
1940 //
1941 // if (vp != null)
1942 // return vp;
1943 //
1944 // if (implemented)
1945 // return getProperty(VERSIONPOLICY_IMPL, "{$range;[==,=+}");
1946 // else
1947 // return getProperty(VERSIONPOLICY, "${range;[==,+)}");
1948 }
1949
1950 /**
1951 * The extends macro traverses all classes and returns a list of class names
1952 * that extend a base class.
1953 */
1954
1955 static String _classesHelp = "${classes;'implementing'|'extending'|'importing'|'named'|'version'|'any';<pattern>}, Return a list of class fully qualified class names that extend/implement/import any of the contained classes matching the pattern\n";
1956
1957 public String _classes(String... args) throws Exception {
1958 // Macro.verifyCommand(args, _classesHelp, new
1959 // Pattern[]{null,Pattern.compile("(implementing|implements|extending|extends|importing|imports|any)"),
1960 // null}, 3,3);
1961
1962 Collection<Clazz> matched = getClasses(args);
1963 if (matched.isEmpty())
1964 return "";
1965
1966 return join(matched);
1967 }
1968
1969 public Collection<Clazz> getClasses(String... args) throws Exception {
1970
1971 Set<Clazz> matched = new HashSet<Clazz>(classspace.values());
1972 for (int i = 1; i < args.length; i++) {
1973 if (args.length < i + 1)
1974 throw new IllegalArgumentException(
1975 "${classes} macro must have odd number of arguments. " + _classesHelp);
1976
1977 String typeName = args[i];
1978 if (typeName.equalsIgnoreCase("extending"))
1979 typeName = "extends";
1980 else if (typeName.equalsIgnoreCase("importing"))
1981 typeName = "imports";
1982 else if (typeName.equalsIgnoreCase("implementing"))
1983 typeName = "implements";
1984
1985 Clazz.QUERY type = Clazz.QUERY.valueOf(typeName.toUpperCase());
1986
1987 if (type == null)
1988 throw new IllegalArgumentException("${classes} has invalid type: " + typeName
1989 + ". " + _classesHelp);
1990
1991 Instruction instr = null;
1992 if (Clazz.HAS_ARGUMENT.contains(type)) {
1993 StringBuilder sb = new StringBuilder();
1994 String s = args[++i];
1995 if (type == QUERY.ANNOTATION) {
1996 // Annotations use the descriptor format ...
1997 // But at least they're always an object
1998 sb.append("L");
1999 for (int ci = 0; ci < s.length(); ci++) {
2000 char c = s.charAt(ci);
2001 if (c == '.')
2002 sb.append("/");
2003 else
2004 sb.append(c);
2005 }
2006 sb.append(';');
2007 } else {
2008 // The argument is declared as a dotted name but the classes
2009 // use a slashed named. So convert the name before we make
2010 // it a instruction. We also have to take into account
2011 // that some classes are nested and use $ for separator
2012 for (int ci = 0; ci < s.length(); ci++) {
2013 char c = s.charAt(ci);
2014 if (c == '.')
2015 sb.append("(/|\\$)");
2016 else
2017 sb.append(c);
2018 }
2019 }
2020 instr = Instruction.getPattern(sb.toString());
2021 }
2022 for (Iterator<Clazz> c = matched.iterator(); c.hasNext();) {
2023 Clazz clazz = c.next();
2024 if (!clazz.is(type, instr, this)) {
2025 c.remove();
2026 }
2027 }
2028 }
2029 return matched;
2030 }
2031
2032 /**
2033 * Get the exporter of a package ...
2034 */
2035
2036 public String _exporters(String args[]) throws Exception {
2037 Macro.verifyCommand(
2038 args,
2039 "${exporters;<packagename>}, returns the list of jars that export the given package",
2040 null, 2, 2);
2041 StringBuilder sb = new StringBuilder();
2042 String del = "";
2043 String pack = args[1].replace('.', '/');
2044 for (Jar jar : classpath) {
2045 if (jar.getDirectories().containsKey(pack)) {
2046 sb.append(del);
2047 sb.append(jar.getName());
2048 }
2049 }
2050 return sb.toString();
2051 }
2052
2053 public Map<String, Clazz> getClassspace() {
2054 return classspace;
2055 }
2056
2057 /**
2058 * Locate a resource on the class path.
2059 *
2060 * @param path
2061 * Path of the reosurce
2062 * @return A resource or <code>null</code>
2063 */
2064 public Resource findResource(String path) {
2065 for (Jar entry : getClasspath()) {
2066 Resource r = entry.getResource(path);
2067 if (r != null)
2068 return r;
2069 }
2070 return null;
2071 }
2072
2073 /**
2074 * Find a clazz on the class path. This class has been parsed.
2075 *
2076 * @param path
2077 * @return
2078 */
2079 public Clazz findClass(String path) throws Exception {
2080 Clazz c = classspace.get(path);
2081 if (c != null)
2082 return c;
2083
2084 c = importedClassesCache.get(path);
2085 if (c != null)
2086 return c;
2087
2088 Resource r = findResource(path);
2089 if (r != null) {
2090 c = new Clazz(path, r);
2091 c.parseClassFile();
2092 importedClassesCache.put(path, c);
2093 }
2094 return c;
2095 }
2096
2097 /**
2098 * Answer the bundle version.
2099 *
2100 * @return
2101 */
2102 public String getVersion() {
2103 String version = getProperty(BUNDLE_VERSION);
2104 if (version == null)
2105 version = "0.0.0";
2106 return version;
2107 }
2108
2109 public boolean isNoBundle() {
2110 return isTrue(getProperty(RESOURCEONLY)) || isTrue(getProperty(NOMANIFEST));
2111 }
2112
2113 public void referTo(String impl) {
2114 String pack = Clazz.getPackage(impl);
2115 if (!referred.containsKey(pack))
2116 referred.put(pack, new LinkedHashMap<String, String>());
2117 }
2118
2119 /**
2120 * Calculate the groups inside the bundle. A group consists of packages that
2121 * have a reference to each other.
2122 */
2123
2124 public MultiMap<Set<String>, String> getGroups() {
2125 MultiMap<String, String> map = new MultiMap<String, String>();
2126 Set<String> keys = uses.keySet();
2127
2128 for (Map.Entry<String, Set<String>> entry : uses.entrySet()) {
2129 Set<String> newSet = new HashSet<String>(entry.getValue());
2130 newSet.retainAll(keys);
2131 map.put(entry.getKey(), newSet);
2132 }
2133
2134 // Calculate strongly connected packages
2135 Set<Set<String>> scc = Tarjan.tarjan(map);
2136
2137 MultiMap<Set<String>, String> grouped = new MultiMap<Set<String>, String>();
2138 for (Set<String> group : scc) {
2139 for (String p : group) {
2140 grouped.addAll(group, uses.get(p));
2141 }
2142 }
2143 return grouped;
2144 }
2145
2146 /**
2147 * Ensure that we are running on the correct bnd.
2148 */
2149 void doRequireBnd() {
2150 Map<String, String> require = OSGiHeader.parseProperties(getProperty(REQUIRE_BND));
2151 if (require == null || require.isEmpty())
2152 return;
2153
2154 Hashtable<String, String> map = new Hashtable<String, String>();
2155 map.put(Constants.VERSION_FILTER, getBndVersion());
2156
2157 for (String filter : require.keySet()) {
2158 try {
2159 Filter f = new Filter(filter);
2160 if (f.match(map))
2161 continue;
2162 error("%s fails %s", REQUIRE_BND, require.get(filter));
2163 } catch (Exception t) {
2164 error("%s with value %s throws exception", t, REQUIRE_BND, require);
2165 }
2166 }
2167 }
2168
2169}