blob: 1a5c61b4a0c9fd510d94c30dbd3a9fdc597cc323 [file] [log] [blame]
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00001package aQute.bnd.osgi;
Stuart McCullochbb014372012-06-07 21:57:32 +00002
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 static aQute.libg.generics.Create.*;
23
24import java.io.*;
25import java.net.*;
26import java.util.*;
27import java.util.Map.Entry;
28import java.util.jar.*;
29import java.util.jar.Attributes.Name;
30import java.util.regex.*;
31
32import aQute.bnd.annotation.*;
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +000033import aQute.bnd.header.*;
34import aQute.bnd.osgi.Descriptors.Descriptor;
35import aQute.bnd.osgi.Descriptors.PackageRef;
36import aQute.bnd.osgi.Descriptors.TypeRef;
Stuart McCullochbb014372012-06-07 21:57:32 +000037import aQute.bnd.service.*;
38import aQute.lib.base64.*;
39import aQute.lib.collections.*;
40import aQute.lib.filter.*;
41import aQute.lib.hex.*;
42import aQute.lib.io.*;
Stuart McCullochbb014372012-06-07 21:57:32 +000043import aQute.libg.cryptography.*;
44import aQute.libg.generics.*;
Stuart McCulloch2286f232012-06-15 13:27:53 +000045import aQute.libg.reporter.*;
Stuart McCullochbb014372012-06-07 21:57:32 +000046
47public class Analyzer extends Processor {
48 private final SortedSet<Clazz.JAVA> ees = new TreeSet<Clazz.JAVA>();
Stuart McCullochd4826102012-06-26 16:34:24 +000049 static Properties bndInfo;
Stuart McCullochbb014372012-06-07 21:57:32 +000050
51 // Bundle parameters
52 private Jar dot;
53 private final Packages contained = new Packages();
54 private final Packages referred = new Packages();
55 private Packages exports;
56 private Packages imports;
57 private TypeRef activator;
58
59 // Global parameters
Stuart McCulloch2286f232012-06-15 13:27:53 +000060 private final MultiMap<PackageRef,PackageRef> uses = new MultiMap<PackageRef,PackageRef>(
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +000061 PackageRef.class, PackageRef.class,
62 true);
63 private final MultiMap<PackageRef,PackageRef> apiUses = new MultiMap<PackageRef,PackageRef>(
64 PackageRef.class, PackageRef.class,
65 true);
Stuart McCullochbb014372012-06-07 21:57:32 +000066 private final Packages classpathExports = new Packages();
67 private final Descriptors descriptors = new Descriptors();
68 private final List<Jar> classpath = list();
Stuart McCulloch2286f232012-06-15 13:27:53 +000069 private final Map<TypeRef,Clazz> classspace = map();
70 private final Map<TypeRef,Clazz> importedClassesCache = map();
Stuart McCullochbb014372012-06-07 21:57:32 +000071 private boolean analyzed = false;
72 private boolean diagnostics = false;
73 private boolean inited = false;
Stuart McCulloch2286f232012-06-15 13:27:53 +000074 final protected AnalyzerMessages msgs = ReporterMessages.base(this,
75 AnalyzerMessages.class);
Stuart McCullochbb014372012-06-07 21:57:32 +000076
77 public Analyzer(Processor parent) {
78 super(parent);
79 }
80
Stuart McCulloch2286f232012-06-15 13:27:53 +000081 public Analyzer() {}
Stuart McCullochbb014372012-06-07 21:57:32 +000082
83 /**
84 * Specifically for Maven
85 *
Stuart McCulloch2286f232012-06-15 13:27:53 +000086 * @param properties
87 * the properties
Stuart McCullochbb014372012-06-07 21:57:32 +000088 */
89
90 public static Properties getManifest(File dirOrJar) throws Exception {
91 Analyzer analyzer = new Analyzer();
92 try {
93 analyzer.setJar(dirOrJar);
94 Properties properties = new Properties();
95 properties.put(IMPORT_PACKAGE, "*");
96 properties.put(EXPORT_PACKAGE, "*");
97 analyzer.setProperties(properties);
98 Manifest m = analyzer.calcManifest();
99 Properties result = new Properties();
100 for (Iterator<Object> i = m.getMainAttributes().keySet().iterator(); i.hasNext();) {
101 Attributes.Name name = (Attributes.Name) i.next();
102 result.put(name.toString(), m.getMainAttributes().getValue(name));
103 }
104 return result;
105 }
106 finally {
107 analyzer.close();
108 }
109 }
110
111 /**
112 * Calculates the data structures for generating a manifest.
113 *
114 * @throws IOException
115 */
116 public void analyze() throws Exception {
117 if (!analyzed) {
118 analyzed = true;
119 uses.clear();
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +0000120 apiUses.clear();
Stuart McCullochbb014372012-06-07 21:57:32 +0000121 classspace.clear();
122 classpathExports.clear();
123
124 // Parse all the class in the
125 // the jar according to the OSGi bcp
126 analyzeBundleClasspath();
127
128 //
129 // calculate class versions in use
130 //
131 for (Clazz c : classspace.values()) {
132 ees.add(c.getFormat());
133 }
134
135 //
136 // Get exported packages from the
137 // entries on the classpath
138 //
139
140 for (Jar current : getClasspath()) {
141 getExternalExports(current, classpathExports);
142 for (String dir : current.getDirectories().keySet()) {
143 PackageRef packageRef = getPackageRef(dir);
144 Resource resource = current.getResource(dir + "/packageinfo");
Stuart McCullochd4826102012-06-26 16:34:24 +0000145 getExportVersionsFromPackageInfo(packageRef, resource, classpathExports);
Stuart McCullochbb014372012-06-07 21:57:32 +0000146 }
147 }
148
149 // Handle the bundle activator
150
151 String s = getProperty(BUNDLE_ACTIVATOR);
152 if (s != null) {
153 activator = getTypeRefFromFQN(s);
154 referTo(activator);
Stuart McCullochd4826102012-06-26 16:34:24 +0000155 trace("activator %s %s", s, activator);
Stuart McCullochbb014372012-06-07 21:57:32 +0000156 }
157
158 // Execute any plugins
159 // TODO handle better reanalyze
160 doPlugins();
161
162 Jar extra = getExtra();
163 while (extra != null) {
164 dot.addAll(extra);
165 analyzeJar(extra, "", true);
166 extra = getExtra();
167 }
168
169 referred.keySet().removeAll(contained.keySet());
170
171 //
172 // EXPORTS
173 //
174 {
175 Set<Instruction> unused = Create.set();
176
177 Instructions filter = new Instructions(getExportPackage());
178 filter.append(getExportContents());
179
180 exports = filter(filter, contained, unused);
181
182 if (!unused.isEmpty()) {
183 warning("Unused Export-Package instructions: %s ", unused);
184 }
185
186 // See what information we can find to augment the
187 // exports. I.e. look on the classpath
188 augmentExports(exports);
189 }
190
191 //
192 // IMPORTS
193 // Imports MUST come after exports because we use information from
194 // the exports
195 //
196 {
197 // Add all exports that do not have an -noimport: directive
198 // to the imports.
199 Packages referredAndExported = new Packages(referred);
200 referredAndExported.putAll(doExportsToImports(exports));
201
202 removeDynamicImports(referredAndExported);
203
204 // Remove any Java references ... where are the closures???
205 for (Iterator<PackageRef> i = referredAndExported.keySet().iterator(); i.hasNext();) {
206 if (i.next().isJava())
207 i.remove();
208 }
209
210 Set<Instruction> unused = Create.set();
211 String h = getProperty(IMPORT_PACKAGE);
212 if (h == null) // If not set use a default
213 h = "*";
214
215 if (isPedantic() && h.trim().length() == 0)
216 warning("Empty Import-Package header");
217
218 Instructions filter = new Instructions(h);
219 imports = filter(filter, referredAndExported, unused);
220 if (!unused.isEmpty()) {
221 // We ignore the end wildcard catch
222 if (!(unused.size() == 1 && unused.iterator().next().toString().equals("*")))
223 warning("Unused Import-Package instructions: %s ", unused);
224 }
225
226 // See what information we can find to augment the
227 // imports. I.e. look in the exports
228 augmentImports(imports, exports);
229 }
230
231 //
232 // USES
233 //
234 // Add the uses clause to the exports
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +0000235
236 boolean api = isTrue(getProperty(EXPERIMENTS)) || true; // brave, lets see
237
238 doUses(exports, api ? apiUses : uses, imports);
239
240 //
241 // Verify that no exported package has a reference to a private
242 // package
243 // This can cause a lot of harm.
244 // TODO restrict the check to public API only, but even then
245 // exported packages
246 // should preferably not refer to private packages.
247 //
248 Set<PackageRef> privatePackages = new HashSet<PackageRef>(contained.keySet());
249 privatePackages.removeAll(exports.keySet());
250 privatePackages.removeAll(imports.keySet());
251
252 // References to java are not imported so they would show up as
253 // private
254 // packages, lets kill them as well.
255
256 for (Iterator<PackageRef> p = privatePackages.iterator(); p.hasNext();)
257 if (p.next().isJava())
258 p.remove();
259
260 for (PackageRef exported : exports.keySet()) {
261 List<PackageRef> used = uses.get(exported);
262 if (used != null) {
263 Set<PackageRef> privateReferences = new HashSet<PackageRef>(apiUses.get(exported));
264 privateReferences.retainAll(privatePackages);
265 if (!privateReferences.isEmpty())
266 msgs.Export_Has_PrivateReferences_(exported, privateReferences.size(), privateReferences);
267 }
268 }
Stuart McCullochbb014372012-06-07 21:57:32 +0000269
270 //
271 // Checks
272 //
273 if (referred.containsKey(Descriptors.DEFAULT_PACKAGE)) {
274 error("The default package '.' is not permitted by the Import-Package syntax. \n"
275 + " This can be caused by compile errors in Eclipse because Eclipse creates \n"
276 + "valid class files regardless of compile errors.\n"
277 + "The following package(s) import from the default package "
278 + uses.transpose().get(Descriptors.DEFAULT_PACKAGE));
279 }
280
281 }
282 }
283
284 /**
285 * Discussed with BJ and decided to kill the .
286 *
287 * @param referredAndExported
288 */
289 void removeDynamicImports(Packages referredAndExported) {
290
291 // // Remove any matching a dynamic import package instruction
292 // Instructions dynamicImports = new
293 // Instructions(getDynamicImportPackage());
294 // Collection<PackageRef> dynamic = dynamicImports.select(
295 // referredAndExported.keySet(), false);
296 // referredAndExported.keySet().removeAll(dynamic);
297 }
298
299 protected Jar getExtra() throws Exception {
300 return null;
301 }
302
303 /**
304 *
305 */
306 void doPlugins() {
307 for (AnalyzerPlugin plugin : getPlugins(AnalyzerPlugin.class)) {
308 try {
309 boolean reanalyze = plugin.analyzeJar(this);
310 if (reanalyze) {
311 classspace.clear();
312 analyzeBundleClasspath();
313 }
314 }
315 catch (Exception e) {
316 error("Analyzer Plugin %s failed %s", plugin, e);
317 }
318 }
319 }
320
321 /**
Stuart McCullochbb014372012-06-07 21:57:32 +0000322 * @return
323 */
324 boolean isResourceOnly() {
325 return isTrue(getProperty(RESOURCEONLY));
326 }
327
328 /**
329 * One of the main workhorses of this class. This will analyze the current
330 * setp and calculate a new manifest according to this setup. This method
331 * will also set the manifest on the main jar dot
332 *
333 * @return
334 * @throws IOException
335 */
336 public Manifest calcManifest() throws Exception {
337 analyze();
338 Manifest manifest = new Manifest();
339 Attributes main = manifest.getMainAttributes();
340
341 main.put(Attributes.Name.MANIFEST_VERSION, "1.0");
342 main.putValue(BUNDLE_MANIFESTVERSION, "2");
343
344 boolean noExtraHeaders = "true".equalsIgnoreCase(getProperty(NOEXTRAHEADERS));
345
346 if (!noExtraHeaders) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000347 main.putValue(CREATED_BY, System.getProperty("java.version") + " (" + System.getProperty("java.vendor")
348 + ")");
Stuart McCullochbb014372012-06-07 21:57:32 +0000349 main.putValue(TOOL, "Bnd-" + getBndVersion());
350 main.putValue(BND_LASTMODIFIED, "" + System.currentTimeMillis());
351 }
352
353 String exportHeader = printClauses(exports, true);
354
355 if (exportHeader.length() > 0)
356 main.putValue(EXPORT_PACKAGE, exportHeader);
357 else
358 main.remove(EXPORT_PACKAGE);
359
360 // Remove all the Java packages from the imports
361 if (!imports.isEmpty()) {
362 main.putValue(IMPORT_PACKAGE, printClauses(imports));
Stuart McCulloch2286f232012-06-15 13:27:53 +0000363 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +0000364 main.remove(IMPORT_PACKAGE);
365 }
366
367 Packages temp = new Packages(contained);
368 temp.keySet().removeAll(exports.keySet());
369
370 if (!temp.isEmpty())
371 main.putValue(PRIVATE_PACKAGE, printClauses(temp));
372 else
373 main.remove(PRIVATE_PACKAGE);
374
375 Parameters bcp = getBundleClasspath();
376 if (bcp.isEmpty() || (bcp.containsKey(".") && bcp.size() == 1))
377 main.remove(BUNDLE_CLASSPATH);
378 else
379 main.putValue(BUNDLE_CLASSPATH, printClauses(bcp));
380
381 doNamesection(dot, manifest);
382
383 for (Enumeration< ? > h = getProperties().propertyNames(); h.hasMoreElements();) {
384 String header = (String) h.nextElement();
385 if (header.trim().length() == 0) {
386 warning("Empty property set with value: " + getProperties().getProperty(header));
387 continue;
388 }
389
390 if (isMissingPlugin(header.trim())) {
391 error("Missing plugin for command %s", header);
392 }
393 if (!Character.isUpperCase(header.charAt(0))) {
394 if (header.charAt(0) == '@')
395 doNameSection(manifest, header);
396 continue;
397 }
398
Stuart McCulloch2286f232012-06-15 13:27:53 +0000399 if (header.equals(BUNDLE_CLASSPATH) || header.equals(EXPORT_PACKAGE) || header.equals(IMPORT_PACKAGE))
Stuart McCullochbb014372012-06-07 21:57:32 +0000400 continue;
401
402 if (header.equalsIgnoreCase("Name")) {
403 error("Your bnd file contains a header called 'Name'. This interferes with the manifest name section.");
404 continue;
405 }
406
407 if (Verifier.HEADER_PATTERN.matcher(header).matches()) {
408 String value = getProperty(header);
409 if (value != null && main.getValue(header) == null) {
410 if (value.trim().length() == 0)
411 main.remove(header);
Stuart McCulloch2286f232012-06-15 13:27:53 +0000412 else if (value.trim().equals(EMPTY_HEADER))
413 main.putValue(header, "");
Stuart McCullochbb014372012-06-07 21:57:32 +0000414 else
Stuart McCulloch2286f232012-06-15 13:27:53 +0000415 main.putValue(header, value);
Stuart McCullochbb014372012-06-07 21:57:32 +0000416 }
Stuart McCulloch2286f232012-06-15 13:27:53 +0000417 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +0000418 // TODO should we report?
419 }
420 }
421
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +0000422 // Copy old values into new manifest, when they
423 // exist in the old one, but not in the new one
424 merge(manifest, dot.getManifest());
425
Stuart McCullochbb014372012-06-07 21:57:32 +0000426 //
427 // Calculate the bundle symbolic name if it is
428 // not set.
429 // 1. set
430 // 2. name of properties file (must be != bnd.bnd)
431 // 3. name of directory, which is usualy project name
432 //
433 String bsn = getBsn();
434 if (main.getValue(BUNDLE_SYMBOLICNAME) == null) {
435 main.putValue(BUNDLE_SYMBOLICNAME, bsn);
436 }
437
438 //
439 // Use the same name for the bundle name as BSN when
440 // the bundle name is not set
441 //
442 if (main.getValue(BUNDLE_NAME) == null) {
443 main.putValue(BUNDLE_NAME, bsn);
444 }
445
446 if (main.getValue(BUNDLE_VERSION) == null)
447 main.putValue(BUNDLE_VERSION, "0");
448
Stuart McCullochbb014372012-06-07 21:57:32 +0000449 // Remove all the headers mentioned in -removeheaders
450 Instructions instructions = new Instructions(getProperty(REMOVEHEADERS));
451 Collection<Object> result = instructions.select(main.keySet(), false);
452 main.keySet().removeAll(result);
453
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +0000454 // We should not set the manifest here, this is in general done
455 // by the caller.
456 // dot.setManifest(manifest);
Stuart McCullochbb014372012-06-07 21:57:32 +0000457 return manifest;
458 }
459
460 /**
461 * Parse the namesection as instructions and then match them against the
Stuart McCulloch2286f232012-06-15 13:27:53 +0000462 * current set of resources For example:
Stuart McCullochbb014372012-06-07 21:57:32 +0000463 *
464 * <pre>
465 * -namesection: *;baz=true, abc/def/bar/X.class=3
466 * </pre>
467 *
468 * The raw value of {@link Constants#NAMESECTION} is used but the values of
469 * the attributes are replaced where @ is set to the resource name. This
470 * allows macro to operate on the resource
Stuart McCullochbb014372012-06-07 21:57:32 +0000471 */
472
473 private void doNamesection(Jar dot, Manifest manifest) {
474
475 Parameters namesection = parseHeader(getProperties().getProperty(NAMESECTION));
476 Instructions instructions = new Instructions(namesection);
477 Set<String> resources = new HashSet<String>(dot.getResources().keySet());
478
479 //
480 // For each instruction, iterator over the resources and filter
481 // them. If a resource matches, it must be removed even if the
482 // instruction is negative. If positive, add a name section
483 // to the manifest for the given resource name. Then add all
484 // attributes from the instruction to that name section.
485 //
Stuart McCulloch2286f232012-06-15 13:27:53 +0000486 for (Map.Entry<Instruction,Attrs> instr : instructions.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000487 boolean matched = false;
488
489 // For each instruction
490
491 for (Iterator<String> i = resources.iterator(); i.hasNext();) {
492 String path = i.next();
493 // For each resource
494
495 if (instr.getKey().matches(path)) {
496
497 // Instruction matches the resource
498
499 matched = true;
500 if (!instr.getKey().isNegated()) {
501
502 // Positive match, add the attributes
503
504 Attributes attrs = manifest.getAttributes(path);
505 if (attrs == null) {
506 attrs = new Attributes();
507 manifest.getEntries().put(path, attrs);
508 }
509
510 //
511 // Add all the properties from the instruction to the
512 // name section
513 //
514
Stuart McCulloch2286f232012-06-15 13:27:53 +0000515 for (Map.Entry<String,String> property : instr.getValue().entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000516 setProperty("@", path);
517 try {
518 String processed = getReplacer().process(property.getValue());
519 attrs.putValue(property.getKey(), processed);
520 }
521 finally {
522 unsetProperty("@");
523 }
524 }
525 }
526 i.remove();
527 }
528 }
529
530 if (!matched && resources.size() > 0)
Stuart McCulloch2286f232012-06-15 13:27:53 +0000531 warning("The instruction %s in %s did not match any resources", instr.getKey(), NAMESECTION);
Stuart McCullochbb014372012-06-07 21:57:32 +0000532 }
533
534 }
535
536 /**
537 * This method is called when the header starts with a @, signifying a name
538 * section header. The name part is defined by replacing all the @ signs to
539 * a /, removing the first and the last, and using the last part as header
540 * name:
541 *
542 * <pre>
543 * &#064;org@osgi@service@event@Implementation-Title
544 * </pre>
545 *
546 * This will be the header Implementation-Title in the
547 * org/osgi/service/event named section.
548 *
549 * @param manifest
550 * @param header
551 */
552 private void doNameSection(Manifest manifest, String header) {
553 String path = header.replace('@', '/');
554 int n = path.lastIndexOf('/');
555 // Must succeed because we start with @
556 String name = path.substring(n + 1);
557 // Skip first /
558 path = path.substring(1, n);
559 if (name.length() != 0 && path.length() != 0) {
560 Attributes attrs = manifest.getAttributes(path);
561 if (attrs == null) {
562 attrs = new Attributes();
563 manifest.getEntries().put(path, attrs);
564 }
565 attrs.putValue(name, getProperty(header));
Stuart McCulloch2286f232012-06-15 13:27:53 +0000566 } else {
567 warning("Invalid header (starts with @ but does not seem to be for the Name section): %s", header);
Stuart McCullochbb014372012-06-07 21:57:32 +0000568 }
569 }
570
571 /**
572 * Clear the key part of a header. I.e. remove everything from the first ';'
573 *
574 * @param value
575 * @return
576 */
577 public String getBsn() {
578 String value = getProperty(BUNDLE_SYMBOLICNAME);
579 if (value == null) {
580 if (getPropertiesFile() != null)
581 value = getPropertiesFile().getName();
582
583 String projectName = getBase().getName();
584 if (value == null || value.equals("bnd.bnd")) {
585 value = projectName;
Stuart McCulloch2286f232012-06-15 13:27:53 +0000586 } else if (value.endsWith(".bnd")) {
587 value = value.substring(0, value.length() - 4);
588 if (!value.startsWith(getBase().getName()))
589 value = projectName + "." + value;
Stuart McCullochbb014372012-06-07 21:57:32 +0000590 }
Stuart McCullochbb014372012-06-07 21:57:32 +0000591 }
592
593 if (value == null)
594 return "untitled";
595
596 int n = value.indexOf(';');
597 if (n > 0)
598 value = value.substring(0, n);
599 return value.trim();
600 }
601
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +0000602 public String _bsn(@SuppressWarnings("unused")
603 String args[]) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000604 return getBsn();
605 }
606
607 /**
608 * Calculate an export header solely based on the contents of a JAR file
609 *
Stuart McCulloch2286f232012-06-15 13:27:53 +0000610 * @param bundle
611 * The jar file to analyze
Stuart McCullochbb014372012-06-07 21:57:32 +0000612 * @return
613 */
614 public String calculateExportsFromContents(Jar bundle) {
615 String ddel = "";
616 StringBuilder sb = new StringBuilder();
Stuart McCulloch2286f232012-06-15 13:27:53 +0000617 Map<String,Map<String,Resource>> map = bundle.getDirectories();
Stuart McCullochbb014372012-06-07 21:57:32 +0000618 for (Iterator<String> i = map.keySet().iterator(); i.hasNext();) {
619 String directory = i.next();
620 if (directory.equals("META-INF") || directory.startsWith("META-INF/"))
621 continue;
622 if (directory.equals("OSGI-OPT") || directory.startsWith("OSGI-OPT/"))
623 continue;
624 if (directory.equals("/"))
625 continue;
626
627 if (directory.endsWith("/"))
628 directory = directory.substring(0, directory.length() - 1);
629
630 directory = directory.replace('/', '.');
631 sb.append(ddel);
632 sb.append(directory);
633 ddel = ",";
634 }
635 return sb.toString();
636 }
637
638 public Packages getContained() {
639 return contained;
640 }
641
642 public Packages getExports() {
643 return exports;
644 }
645
646 public Packages getImports() {
647 return imports;
648 }
649
650 public Jar getJar() {
651 return dot;
652 }
653
654 public Packages getReferred() {
655 return referred;
656 }
657
658 /**
659 * Return the set of unreachable code depending on exports and the bundle
660 * activator.
661 *
662 * @return
663 */
664 public Set<PackageRef> getUnreachable() {
665 Set<PackageRef> unreachable = new HashSet<PackageRef>(uses.keySet()); // all
666 for (Iterator<PackageRef> r = exports.keySet().iterator(); r.hasNext();) {
667 PackageRef packageRef = r.next();
668 removeTransitive(packageRef, unreachable);
669 }
670 if (activator != null) {
671 removeTransitive(activator.getPackageRef(), unreachable);
672 }
673 return unreachable;
674 }
675
Stuart McCulloch2286f232012-06-15 13:27:53 +0000676 public MultiMap<PackageRef,PackageRef> getUses() {
Stuart McCullochbb014372012-06-07 21:57:32 +0000677 return uses;
678 }
679
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +0000680 public MultiMap<PackageRef,PackageRef> getAPIUses() {
681 return apiUses;
682 }
683
Stuart McCullochbb014372012-06-07 21:57:32 +0000684 /**
685 * Get the version for this bnd
686 *
687 * @return version or unknown.
688 */
689 public String getBndVersion() {
Stuart McCullochd4826102012-06-26 16:34:24 +0000690 return getBndInfo("version", "<unknown>");
Stuart McCullochbb014372012-06-07 21:57:32 +0000691 }
692
693 public long getBndLastModified() {
Stuart McCullochd4826102012-06-26 16:34:24 +0000694 String time = getBndInfo("lastmodified", "0");
Stuart McCullochbb014372012-06-07 21:57:32 +0000695 try {
696 return Long.parseLong(time);
697 }
698 catch (Exception e) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000699 // Ignore
Stuart McCullochbb014372012-06-07 21:57:32 +0000700 }
701 return 0;
702 }
703
704 public String getBndInfo(String key, String defaultValue) {
Stuart McCullochffa8aaf2012-06-17 20:38:35 +0000705 if (bndInfo == null) {
706 try {
Stuart McCullochd4826102012-06-26 16:34:24 +0000707 Properties bndInfoLocal = new Properties();
708 URL url = Analyzer.class.getResource("bnd.info");
709 if (url != null) {
710 InputStream in = url.openStream();
711 try {
712 bndInfoLocal.load(in);
713 }
714 finally {
715 in.close();
716 }
717 }
718 bndInfo = bndInfoLocal;
Stuart McCullochffa8aaf2012-06-17 20:38:35 +0000719 }
720 catch (Exception e) {
Stuart McCullochd4826102012-06-26 16:34:24 +0000721 e.printStackTrace();
Stuart McCullochffa8aaf2012-06-17 20:38:35 +0000722 return defaultValue;
Stuart McCullochbb014372012-06-07 21:57:32 +0000723 }
724 }
Stuart McCullochd4826102012-06-26 16:34:24 +0000725 String value = bndInfo.getProperty(key);
Stuart McCullochffa8aaf2012-06-17 20:38:35 +0000726 if (value == null)
727 return defaultValue;
728 return value;
Stuart McCullochbb014372012-06-07 21:57:32 +0000729 }
730
731 /**
732 * Merge the existing manifest with the instructions but do not override
733 * existing properties.
734 *
Stuart McCulloch2286f232012-06-15 13:27:53 +0000735 * @param manifest
736 * The manifest to merge with
Stuart McCullochbb014372012-06-07 21:57:32 +0000737 * @throws IOException
738 */
739 public void mergeManifest(Manifest manifest) throws IOException {
740 if (manifest != null) {
741 Attributes attributes = manifest.getMainAttributes();
742 for (Iterator<Object> i = attributes.keySet().iterator(); i.hasNext();) {
743 Name name = (Name) i.next();
744 String key = name.toString();
745 // Dont want instructions
746 if (key.startsWith("-"))
747 continue;
748
749 if (getProperty(key) == null)
750 setProperty(key, attributes.getValue(name));
751 }
752 }
753 }
754
755 public void setBase(File file) {
756 super.setBase(file);
757 getProperties().put("project.dir", getBase().getAbsolutePath());
758 }
759
760 /**
761 * Set the classpath for this analyzer by file.
762 *
763 * @param classpath
764 * @throws IOException
765 */
766 public void setClasspath(File[] classpath) throws IOException {
767 List<Jar> list = new ArrayList<Jar>();
768 for (int i = 0; i < classpath.length; i++) {
769 if (classpath[i].exists()) {
770 Jar current = new Jar(classpath[i]);
771 list.add(current);
Stuart McCulloch2286f232012-06-15 13:27:53 +0000772 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +0000773 error("Missing file on classpath: %s", classpath[i]);
774 }
775 }
776 for (Iterator<Jar> i = list.iterator(); i.hasNext();) {
777 addClasspath(i.next());
778 }
779 }
780
781 public void setClasspath(Jar[] classpath) {
782 for (int i = 0; i < classpath.length; i++) {
783 addClasspath(classpath[i]);
784 }
785 }
786
787 public void setClasspath(String[] classpath) {
788 for (int i = 0; i < classpath.length; i++) {
789 Jar jar = getJarFromName(classpath[i], " setting classpath");
790 if (jar != null)
791 addClasspath(jar);
792 }
793 }
794
795 /**
796 * Set the JAR file we are going to work in. This will read the JAR in
797 * memory.
798 *
799 * @param jar
800 * @return
801 * @throws IOException
802 */
803 public Jar setJar(File jar) throws IOException {
804 Jar jarx = new Jar(jar);
805 addClose(jarx);
806 return setJar(jarx);
807 }
808
809 /**
810 * Set the JAR directly we are going to work on.
811 *
812 * @param jar
813 * @return
814 */
815 public Jar setJar(Jar jar) {
816 if (dot != null)
817 removeClose(dot);
818
819 this.dot = jar;
820 if (dot != null)
821 addClose(dot);
822
823 return jar;
824 }
825
826 protected void begin() {
827 if (inited == false) {
828 inited = true;
829 super.begin();
830
831 updateModified(getBndLastModified(), "bnd last modified");
832 verifyManifestHeadersCase(getProperties());
833
834 }
835 }
836
837 /**
838 * Try to get a Jar from a file name/path or a url, or in last resort from
839 * the classpath name part of their files.
840 *
Stuart McCulloch2286f232012-06-15 13:27:53 +0000841 * @param name
842 * URL or filename relative to the base
843 * @param from
844 * Message identifying the caller for errors
Stuart McCullochbb014372012-06-07 21:57:32 +0000845 * @return null or a Jar with the contents for the name
846 */
847 Jar getJarFromName(String name, String from) {
848 File file = new File(name);
849 if (!file.isAbsolute())
850 file = new File(getBase(), name);
851
852 if (file.exists())
853 try {
854 Jar jar = new Jar(file);
855 addClose(jar);
856 return jar;
857 }
858 catch (Exception e) {
859 error("Exception in parsing jar file for " + from + ": " + name + " " + e);
860 }
861 // It is not a file ...
862 try {
863 // Lets try a URL
864 URL url = new URL(name);
865 Jar jar = new Jar(fileName(url.getPath()));
866 addClose(jar);
867 URLConnection connection = url.openConnection();
868 InputStream in = connection.getInputStream();
869 long lastModified = connection.getLastModified();
870 if (lastModified == 0)
871 // We assume the worst :-(
872 lastModified = System.currentTimeMillis();
873 EmbeddedResource.build(jar, in, lastModified);
874 in.close();
875 return jar;
876 }
877 catch (IOException ee) {
878 // Check if we have files on the classpath
879 // that have the right name, allows us to specify those
880 // names instead of the full path.
881 for (Iterator<Jar> cp = getClasspath().iterator(); cp.hasNext();) {
882 Jar entry = cp.next();
883 if (entry.getSource() != null && entry.getSource().getName().equals(name)) {
884 return entry;
885 }
886 }
887 // error("Can not find jar file for " + from + ": " + name);
888 }
889 return null;
890 }
891
892 private String fileName(String path) {
893 int n = path.lastIndexOf('/');
894 if (n > 0)
895 return path.substring(n + 1);
896 return path;
897 }
898
899 /**
Stuart McCullochbb014372012-06-07 21:57:32 +0000900 * @param manifests
901 * @throws Exception
902 */
Stuart McCulloch2286f232012-06-15 13:27:53 +0000903 private void merge(Manifest result, Manifest old) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000904 if (old != null) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000905 for (Iterator<Map.Entry<Object,Object>> e = old.getMainAttributes().entrySet().iterator(); e.hasNext();) {
906 Map.Entry<Object,Object> entry = e.next();
Stuart McCullochbb014372012-06-07 21:57:32 +0000907 Attributes.Name name = (Attributes.Name) entry.getKey();
908 String value = (String) entry.getValue();
909 if (name.toString().equalsIgnoreCase("Created-By"))
910 name = new Attributes.Name("Originally-Created-By");
911 if (!result.getMainAttributes().containsKey(name))
912 result.getMainAttributes().put(name, value);
913 }
914
915 // do not overwrite existing entries
Stuart McCulloch2286f232012-06-15 13:27:53 +0000916 Map<String,Attributes> oldEntries = old.getEntries();
917 Map<String,Attributes> newEntries = result.getEntries();
918 for (Iterator<Map.Entry<String,Attributes>> e = oldEntries.entrySet().iterator(); e.hasNext();) {
919 Map.Entry<String,Attributes> entry = e.next();
Stuart McCullochbb014372012-06-07 21:57:32 +0000920 if (!newEntries.containsKey(entry.getKey())) {
921 newEntries.put(entry.getKey(), entry.getValue());
922 }
923 }
924 }
925 }
926
927 /**
928 * Bnd is case sensitive for the instructions so we better check people are
929 * not using an invalid case. We do allow this to set headers that should
930 * not be processed by us but should be used by the framework.
931 *
Stuart McCulloch2286f232012-06-15 13:27:53 +0000932 * @param properties
933 * Properties to verify.
Stuart McCullochbb014372012-06-07 21:57:32 +0000934 */
935
936 void verifyManifestHeadersCase(Properties properties) {
937 for (Iterator<Object> i = properties.keySet().iterator(); i.hasNext();) {
938 String header = (String) i.next();
939 for (int j = 0; j < headers.length; j++) {
940 if (!headers[j].equals(header) && headers[j].equalsIgnoreCase(header)) {
941 warning("Using a standard OSGi header with the wrong case (bnd is case sensitive!), using: "
942 + header + " and expecting: " + headers[j]);
943 break;
944 }
945 }
946 }
947 }
948
949 /**
950 * We will add all exports to the imports unless there is a -noimport
951 * directive specified on an export. This directive is skipped for the
Stuart McCulloch2286f232012-06-15 13:27:53 +0000952 * manifest. We also remove any version parameter so that augmentImports can
953 * do the version policy. The following method is really tricky and evolved
954 * over time. Coming from the original background of OSGi, it was a weird
955 * idea for me to have a public package that should not be substitutable. I
956 * was so much convinced that this was the right rule that I rücksichtlos
957 * imported them all. Alas, the real world was more subtle than that. It
958 * turns out that it is not a good idea to always import. First, there must
959 * be a need to import, i.e. there must be a contained package that refers
960 * to the exported package for it to make use importing that package.
961 * Second, if an exported package refers to an internal package than it
962 * should not be imported. Additionally, it is necessary to treat the
963 * exports in groups. If an exported package refers to another exported
964 * packages than it must be in the same group. A framework can only
965 * substitute exports for imports for the whole of such a group. WHY?????
966 * Not clear anymore ...
Stuart McCullochbb014372012-06-07 21:57:32 +0000967 */
968 Packages doExportsToImports(Packages exports) {
969
970 // private packages = contained - exported.
971 Set<PackageRef> privatePackages = new HashSet<PackageRef>(contained.keySet());
972 privatePackages.removeAll(exports.keySet());
973
974 // private references = ∀ p : private packages | uses(p)
975 Set<PackageRef> privateReferences = newSet();
976 for (PackageRef p : privatePackages) {
977 Collection<PackageRef> uses = this.uses.get(p);
978 if (uses != null)
979 privateReferences.addAll(uses);
980 }
981
982 // Assume we are going to export all exported packages
983 Set<PackageRef> toBeImported = new HashSet<PackageRef>(exports.keySet());
984
985 // Remove packages that are not referenced privately
986 toBeImported.retainAll(privateReferences);
987
988 // Not necessary to import anything that is already
989 // imported in the Import-Package statement.
990 // TODO toBeImported.removeAll(imports.keySet());
991
992 // Remove exported packages that are referring to
993 // private packages.
994 // Each exported package has a uses clause. We just use
995 // the used packages for each exported package to find out
996 // if it refers to an internal package.
997 //
998
999 for (Iterator<PackageRef> i = toBeImported.iterator(); i.hasNext();) {
1000 PackageRef next = i.next();
1001 Collection<PackageRef> usedByExportedPackage = this.uses.get(next);
1002
1003 for (PackageRef privatePackage : privatePackages) {
1004 if (usedByExportedPackage.contains(privatePackage)) {
1005 i.remove();
1006 break;
1007 }
1008 }
1009 }
1010
1011 // Clean up attributes and generate result map
1012 Packages result = new Packages();
1013 for (Iterator<PackageRef> i = toBeImported.iterator(); i.hasNext();) {
1014 PackageRef ep = i.next();
1015 Attrs parameters = exports.get(ep);
1016
Stuart McCullochffa8aaf2012-06-17 20:38:35 +00001017 String noimport = parameters == null ? null : parameters.get(NO_IMPORT_DIRECTIVE);
Stuart McCullochbb014372012-06-07 21:57:32 +00001018 if (noimport != null && noimport.equalsIgnoreCase("true"))
1019 continue;
1020
1021 // // we can't substitute when there is no version
1022 // String version = parameters.get(VERSION_ATTRIBUTE);
1023 // if (version == null) {
1024 // if (isPedantic())
1025 // warning(
1026 // "Cannot automatically import exported package %s because it has no version defined",
1027 // ep);
1028 // continue;
1029 // }
1030
1031 parameters = new Attrs();
1032 parameters.remove(VERSION_ATTRIBUTE);
1033 result.put(ep, parameters);
1034 }
1035 return result;
1036 }
1037
1038 public boolean referred(PackageRef packageName) {
1039 // return true;
Stuart McCulloch2286f232012-06-15 13:27:53 +00001040 for (Map.Entry<PackageRef,List<PackageRef>> contained : uses.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001041 if (!contained.getKey().equals(packageName)) {
1042 if (contained.getValue().contains(packageName))
1043 return true;
1044 }
1045 }
1046 return false;
1047 }
1048
1049 /**
Stuart McCullochbb014372012-06-07 21:57:32 +00001050 * @param jar
1051 */
1052 private void getExternalExports(Jar jar, Packages classpathExports) {
1053 try {
1054 Manifest m = jar.getManifest();
1055 if (m != null) {
1056 Domain domain = Domain.domain(m);
1057 Parameters exported = domain.getExportPackage();
Stuart McCulloch2286f232012-06-15 13:27:53 +00001058 for (Entry<String,Attrs> e : exported.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001059 PackageRef ref = getPackageRef(e.getKey());
1060 if (!classpathExports.containsKey(ref)) {
1061 // TODO e.getValue().put(SOURCE_DIRECTIVE,
1062 // jar.getBsn()+"-"+jar.getVersion());
1063
1064 classpathExports.put(ref, e.getValue());
1065 }
1066 }
1067 }
1068 }
1069 catch (Exception e) {
1070 warning("Erroneous Manifest for " + jar + " " + e);
1071 }
1072 }
1073
1074 /**
1075 * Find some more information about imports in manifest and other places. It
1076 * is assumed that the augmentsExports has already copied external attrs
1077 * from the classpathExports.
1078 *
1079 * @throws Exception
1080 */
1081 void augmentImports(Packages imports, Packages exports) throws Exception {
1082 List<PackageRef> noimports = Create.list();
1083 Set<PackageRef> provided = findProvidedPackages();
1084
1085 for (PackageRef packageRef : imports.keySet()) {
1086 String packageName = packageRef.getFQN();
1087
1088 setProperty(CURRENT_PACKAGE, packageName);
1089 try {
1090 Attrs importAttributes = imports.get(packageRef);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001091 Attrs exportAttributes = exports.get(packageRef, classpathExports.get(packageRef, new Attrs()));
Stuart McCullochbb014372012-06-07 21:57:32 +00001092
1093 String exportVersion = exportAttributes.getVersion();
1094 String importRange = importAttributes.getVersion();
1095
1096 if (exportVersion == null) {
1097 // TODO Should check if the source is from a bundle.
1098
Stuart McCulloch2286f232012-06-15 13:27:53 +00001099 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001100
1101 //
1102 // Version Policy - Import version substitution. We
1103 // calculate the export version and then allow the
1104 // import version attribute to use it in a substitution
1105 // by using a ${@} macro. The export version can
1106 // be defined externally or locally
1107 //
1108
1109 boolean provider = isTrue(importAttributes.get(PROVIDE_DIRECTIVE))
Stuart McCulloch2286f232012-06-15 13:27:53 +00001110 || isTrue(exportAttributes.get(PROVIDE_DIRECTIVE)) || provided.contains(packageRef);
Stuart McCullochbb014372012-06-07 21:57:32 +00001111
1112 exportVersion = cleanupVersion(exportVersion);
1113
1114 try {
1115 setProperty("@", exportVersion);
1116
1117 if (importRange != null) {
1118 importRange = cleanupVersion(importRange);
1119 importRange = getReplacer().process(importRange);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001120 } else
Stuart McCullochbb014372012-06-07 21:57:32 +00001121 importRange = getVersionPolicy(provider);
1122
1123 }
1124 finally {
1125 unsetProperty("@");
1126 }
1127 importAttributes.put(VERSION_ATTRIBUTE, importRange);
1128 }
1129
1130 //
1131 // Check if exporter has mandatory attributes
1132 //
1133 String mandatory = exportAttributes.get(MANDATORY_DIRECTIVE);
1134 if (mandatory != null) {
1135 String[] attrs = mandatory.split("\\s*,\\s*");
1136 for (int i = 0; i < attrs.length; i++) {
1137 if (!importAttributes.containsKey(attrs[i]))
1138 importAttributes.put(attrs[i], exportAttributes.get(attrs[i]));
1139 }
1140 }
1141
1142 if (exportAttributes.containsKey(IMPORT_DIRECTIVE))
1143 importAttributes.put(IMPORT_DIRECTIVE, exportAttributes.get(IMPORT_DIRECTIVE));
1144
1145 fixupAttributes(importAttributes);
1146 removeAttributes(importAttributes);
1147
1148 String result = importAttributes.get(Constants.VERSION_ATTRIBUTE);
1149 if (result == null)
1150 noimports.add(packageRef);
1151 }
1152 finally {
1153 unsetProperty(CURRENT_PACKAGE);
1154 }
1155 }
1156
1157 if (isPedantic() && noimports.size() != 0) {
1158 warning("Imports that lack version ranges: %s", noimports);
1159 }
1160 }
1161
1162 /**
1163 * Find the packages we depend on, where we implement an interface that is a
1164 * Provider Type. These packages, when we import them, must use the provider
1165 * policy.
1166 *
1167 * @throws Exception
1168 */
1169 Set<PackageRef> findProvidedPackages() throws Exception {
1170 Set<PackageRef> providers = Create.set();
1171 Set<TypeRef> cached = Create.set();
1172
1173 for (Clazz c : classspace.values()) {
1174 TypeRef[] interfaces = c.getInterfaces();
1175 if (interfaces != null)
1176 for (TypeRef t : interfaces)
1177 if (cached.contains(t) || isProvider(t)) {
1178 cached.add(t);
1179 providers.add(t.getPackageRef());
1180 }
1181 }
1182 return providers;
1183 }
1184
1185 private boolean isProvider(TypeRef t) throws Exception {
1186 Clazz c = findClass(t);
1187 if (c == null)
1188 return false;
1189
1190 if (c.annotations == null)
1191 return false;
1192
1193 TypeRef pt = getTypeRefFromFQN(ProviderType.class.getName());
1194 boolean result = c.annotations.contains(pt);
1195 return result;
1196 }
1197
1198 /**
1199 * Provide any macro substitutions and versions for exported packages.
1200 */
1201
1202 void augmentExports(Packages exports) {
1203 for (PackageRef packageRef : exports.keySet()) {
1204 String packageName = packageRef.getFQN();
1205 setProperty(CURRENT_PACKAGE, packageName);
1206 try {
1207 Attrs attributes = exports.get(packageRef);
1208 Attrs exporterAttributes = classpathExports.get(packageRef);
1209 if (exporterAttributes == null)
1210 continue;
1211
Stuart McCulloch2286f232012-06-15 13:27:53 +00001212 for (Map.Entry<String,String> entry : exporterAttributes.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001213 String key = entry.getKey();
1214 if (key.equalsIgnoreCase(SPECIFICATION_VERSION))
1215 key = VERSION_ATTRIBUTE;
1216
1217 // dont overwrite and no directives
1218 if (!key.endsWith(":") && !attributes.containsKey(key)) {
1219 attributes.put(key, entry.getValue());
1220 }
1221 }
1222
1223 fixupAttributes(attributes);
1224 removeAttributes(attributes);
1225
1226 }
1227 finally {
1228 unsetProperty(CURRENT_PACKAGE);
1229 }
1230 }
1231 }
1232
1233 /**
Stuart McCulloch2286f232012-06-15 13:27:53 +00001234 * Fixup Attributes Execute any macros on an export and
Stuart McCullochbb014372012-06-07 21:57:32 +00001235 */
1236
1237 void fixupAttributes(Attrs attributes) {
1238 // Convert any attribute values that have macros.
1239 for (String key : attributes.keySet()) {
1240 String value = attributes.get(key);
1241 if (value.indexOf('$') >= 0) {
1242 value = getReplacer().process(value);
1243 attributes.put(key, value);
1244 }
1245 }
1246
1247 }
1248
1249 /**
1250 * Remove the attributes mentioned in the REMOVE_ATTRIBUTE_DIRECTIVE. You
1251 * can add a remove-attribute: directive with a regular expression for
1252 * attributes that need to be removed. We also remove all attributes that
1253 * have a value of !. This allows you to use macros with ${if} to remove
1254 * values.
1255 */
1256
1257 void removeAttributes(Attrs attributes) {
1258 String remove = attributes.remove(REMOVE_ATTRIBUTE_DIRECTIVE);
1259
1260 if (remove != null) {
1261 Instructions removeInstr = new Instructions(remove);
1262 attributes.keySet().removeAll(removeInstr.select(attributes.keySet(), false));
1263 }
1264
1265 // Remove any ! valued attributes
Stuart McCulloch2286f232012-06-15 13:27:53 +00001266 for (Iterator<Entry<String,String>> i = attributes.entrySet().iterator(); i.hasNext();) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001267 String v = i.next().getValue();
1268 if (v.equals("!"))
1269 i.remove();
1270 }
1271 }
1272
1273 /**
1274 * Calculate a version from a version policy.
1275 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00001276 * @param version
1277 * The actual exported version
1278 * @param impl
1279 * true for implementations and false for clients
Stuart McCullochbb014372012-06-07 21:57:32 +00001280 */
1281
1282 String calculateVersionRange(String version, boolean impl) {
1283 setProperty("@", version);
1284 try {
1285 return getVersionPolicy(impl);
1286 }
1287 finally {
1288 unsetProperty("@");
1289 }
1290 }
1291
1292 /**
1293 * Add the uses clauses. This method iterates over the exports and cal
1294 *
1295 * @param exports
1296 * @param uses
1297 * @throws MojoExecutionException
1298 */
Stuart McCulloch2286f232012-06-15 13:27:53 +00001299 void doUses(Packages exports, MultiMap<PackageRef,PackageRef> uses, Packages imports) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001300 if ("true".equalsIgnoreCase(getProperty(NOUSES)))
1301 return;
1302
1303 for (Iterator<PackageRef> i = exports.keySet().iterator(); i.hasNext();) {
1304 PackageRef packageRef = i.next();
1305 String packageName = packageRef.getFQN();
1306 setProperty(CURRENT_PACKAGE, packageName);
1307 try {
1308 doUses(packageRef, exports, uses, imports);
1309 }
1310 finally {
1311 unsetProperty(CURRENT_PACKAGE);
1312 }
1313
1314 }
1315 }
1316
1317 /**
1318 * @param packageName
1319 * @param exports
1320 * @param uses
1321 * @param imports
1322 */
Stuart McCulloch2286f232012-06-15 13:27:53 +00001323 protected void doUses(PackageRef packageRef, Packages exports, MultiMap<PackageRef,PackageRef> uses,
1324 Packages imports) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001325 Attrs clause = exports.get(packageRef);
1326
1327 // Check if someone already set the uses: directive
1328 String override = clause.get(USES_DIRECTIVE);
1329 if (override == null)
1330 override = USES_USES;
1331
1332 // Get the used packages
1333 Collection<PackageRef> usedPackages = uses.get(packageRef);
1334
1335 if (usedPackages != null) {
1336
1337 // Only do a uses on exported or imported packages
1338 // and uses should also not contain our own package
1339 // name
1340 Set<PackageRef> sharedPackages = new HashSet<PackageRef>();
1341 sharedPackages.addAll(imports.keySet());
1342 sharedPackages.addAll(exports.keySet());
1343 sharedPackages.retainAll(usedPackages);
1344 sharedPackages.remove(packageRef);
1345
1346 StringBuilder sb = new StringBuilder();
1347 String del = "";
1348 for (Iterator<PackageRef> u = sharedPackages.iterator(); u.hasNext();) {
1349 PackageRef usedPackage = u.next();
1350 if (!usedPackage.isJava()) {
1351 sb.append(del);
1352 sb.append(usedPackage.getFQN());
1353 del = ",";
1354 }
1355 }
1356 if (override.indexOf('$') >= 0) {
1357 setProperty(CURRENT_USES, sb.toString());
1358 override = getReplacer().process(override);
1359 unsetProperty(CURRENT_USES);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001360 } else
Stuart McCullochbb014372012-06-07 21:57:32 +00001361 // This is for backward compatibility 0.0.287
1362 // can be deprecated over time
Stuart McCulloch2286f232012-06-15 13:27:53 +00001363 override = override.replaceAll(USES_USES, Matcher.quoteReplacement(sb.toString())).trim();
Stuart McCullochbb014372012-06-07 21:57:32 +00001364
1365 if (override.endsWith(","))
1366 override = override.substring(0, override.length() - 1);
1367 if (override.startsWith(","))
1368 override = override.substring(1);
1369 if (override.length() > 0) {
1370 clause.put(USES_DIRECTIVE, override);
1371 }
1372 }
1373 }
1374
1375 /**
1376 * Transitively remove all elemens from unreachable through the uses link.
1377 *
1378 * @param name
1379 * @param unreachable
1380 */
1381 void removeTransitive(PackageRef name, Set<PackageRef> unreachable) {
1382 if (!unreachable.contains(name))
1383 return;
1384
1385 unreachable.remove(name);
1386
1387 List<PackageRef> ref = uses.get(name);
1388 if (ref != null) {
1389 for (Iterator<PackageRef> r = ref.iterator(); r.hasNext();) {
1390 PackageRef element = r.next();
1391 removeTransitive(element, unreachable);
1392 }
1393 }
1394 }
1395
1396 /**
1397 * Helper method to set the package info resource
1398 *
1399 * @param dir
1400 * @param key
1401 * @param value
1402 * @throws Exception
1403 */
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00001404 void getExportVersionsFromPackageInfo(PackageRef packageRef, Resource r, Packages classpathExports)
1405 throws Exception {
Stuart McCullochbb014372012-06-07 21:57:32 +00001406 if (r == null)
1407 return;
1408
1409 Properties p = new Properties();
Stuart McCullochbb014372012-06-07 21:57:32 +00001410 try {
Stuart McCullochd4826102012-06-26 16:34:24 +00001411 InputStream in = r.openInputStream();
1412 try {
1413 p.load(in);
Stuart McCullochbb014372012-06-07 21:57:32 +00001414 }
Stuart McCullochd4826102012-06-26 16:34:24 +00001415 finally {
1416 in.close();
1417 }
1418 Attrs map = classpathExports.get(packageRef);
1419 if (map == null) {
1420 classpathExports.put(packageRef, map = new Attrs());
1421 }
1422 for (Enumeration<String> t = (Enumeration<String>) p.propertyNames(); t.hasMoreElements();) {
1423 String key = t.nextElement();
1424 String value = map.get(key);
1425 if (value == null) {
1426 value = p.getProperty(key);
1427
1428 // Messy, to allow directives we need to
1429 // allow the value to start with a ':' since we cannot
1430 // encode this in a property name
1431
1432 if (value.startsWith(":")) {
1433 key = key + ":";
1434 value = value.substring(1);
1435 }
1436 map.put(key, value);
1437 }
1438 }
1439 }
1440 catch (Exception e) {
1441 msgs.NoSuchFile_(r);
Stuart McCullochbb014372012-06-07 21:57:32 +00001442 }
1443 }
1444
1445 public void close() {
1446 if (diagnostics) {
1447 PrintStream out = System.err;
1448 out.printf("Current directory : %s%n", new File("").getAbsolutePath());
1449 out.println("Classpath used");
1450 for (Jar jar : getClasspath()) {
1451 out.printf("File : %s%n", jar.getSource());
Stuart McCulloch2286f232012-06-15 13:27:53 +00001452 out.printf("File abs path : %s%n", jar.getSource().getAbsolutePath());
Stuart McCullochbb014372012-06-07 21:57:32 +00001453 out.printf("Name : %s%n", jar.getName());
Stuart McCulloch2286f232012-06-15 13:27:53 +00001454 Map<String,Map<String,Resource>> dirs = jar.getDirectories();
1455 for (Map.Entry<String,Map<String,Resource>> entry : dirs.entrySet()) {
1456 Map<String,Resource> dir = entry.getValue();
Stuart McCullochbb014372012-06-07 21:57:32 +00001457 String name = entry.getKey().replace('/', '.');
1458 if (dir != null) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001459 out.printf(" %-30s %d%n", name, dir.size());
1460 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001461 out.printf(" %-30s <<empty>>%n", name);
1462 }
1463 }
1464 }
1465 }
1466
1467 super.close();
1468 if (dot != null)
1469 dot.close();
1470
1471 if (classpath != null)
1472 for (Iterator<Jar> j = classpath.iterator(); j.hasNext();) {
1473 Jar jar = j.next();
1474 jar.close();
1475 }
1476 }
1477
1478 /**
1479 * Findpath looks through the contents of the JAR and finds paths that end
Stuart McCulloch2286f232012-06-15 13:27:53 +00001480 * with the given regular expression ${findpath (; reg-expr (; replacement)?
1481 * )? }
Stuart McCullochbb014372012-06-07 21:57:32 +00001482 *
1483 * @param args
1484 * @return
1485 */
1486 public String _findpath(String args[]) {
1487 return findPath("findpath", args, true);
1488 }
1489
1490 public String _findname(String args[]) {
1491 return findPath("findname", args, false);
1492 }
1493
1494 String findPath(String name, String[] args, boolean fullPathName) {
1495 if (args.length > 3) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001496 warning("Invalid nr of arguments to " + name + " " + Arrays.asList(args) + ", syntax: ${" + name
1497 + " (; reg-expr (; replacement)? )? }");
Stuart McCullochbb014372012-06-07 21:57:32 +00001498 return null;
1499 }
1500
1501 String regexp = ".*";
1502 String replace = null;
1503
1504 switch (args.length) {
1505 case 3 :
1506 replace = args[2];
1507 //$FALL-THROUGH$
1508 case 2 :
1509 regexp = args[1];
1510 }
1511 StringBuilder sb = new StringBuilder();
1512 String del = "";
1513
1514 Pattern expr = Pattern.compile(regexp);
1515 for (Iterator<String> e = dot.getResources().keySet().iterator(); e.hasNext();) {
1516 String path = e.next();
1517 if (!fullPathName) {
1518 int n = path.lastIndexOf('/');
1519 if (n >= 0) {
1520 path = path.substring(n + 1);
1521 }
1522 }
1523
1524 Matcher m = expr.matcher(path);
1525 if (m.matches()) {
1526 if (replace != null)
1527 path = m.replaceAll(replace);
1528
1529 sb.append(del);
1530 sb.append(path);
1531 del = ", ";
1532 }
1533 }
1534 return sb.toString();
1535 }
1536
Stuart McCulloch2286f232012-06-15 13:27:53 +00001537 public void putAll(Map<String,String> additional, boolean force) {
1538 for (Iterator<Map.Entry<String,String>> i = additional.entrySet().iterator(); i.hasNext();) {
1539 Map.Entry<String,String> entry = i.next();
Stuart McCullochbb014372012-06-07 21:57:32 +00001540 if (force || getProperties().get(entry.getKey()) == null)
1541 setProperty(entry.getKey(), entry.getValue());
1542 }
1543 }
1544
1545 boolean firstUse = true;
1546
1547 public List<Jar> getClasspath() {
1548 if (firstUse) {
1549 firstUse = false;
1550 String cp = getProperty(CLASSPATH);
1551 if (cp != null)
1552 for (String s : split(cp)) {
1553 Jar jar = getJarFromName(s, "getting classpath");
1554 if (jar != null)
1555 addClasspath(jar);
1556 else
1557 warning("Cannot find entry on -classpath: %s", s);
1558 }
1559 }
1560 return classpath;
1561 }
1562
1563 public void addClasspath(Jar jar) {
1564 if (isPedantic() && jar.getResources().isEmpty())
1565 warning("There is an empty jar or directory on the classpath: " + jar.getName());
1566
1567 classpath.add(jar);
1568 }
1569
1570 public void addClasspath(Collection< ? > jars) throws IOException {
1571 for (Object jar : jars) {
1572 if (jar instanceof Jar)
1573 addClasspath((Jar) jar);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001574 else if (jar instanceof File)
1575 addClasspath((File) jar);
1576 else if (jar instanceof String)
1577 addClasspath(getFile((String) jar));
Stuart McCullochbb014372012-06-07 21:57:32 +00001578 else
Stuart McCulloch2286f232012-06-15 13:27:53 +00001579 error("Cannot convert to JAR to add to classpath %s. Not a File, Jar, or String", jar);
Stuart McCullochbb014372012-06-07 21:57:32 +00001580 }
1581 }
1582
1583 public void addClasspath(File cp) throws IOException {
1584 if (!cp.exists())
1585 warning("File on classpath that does not exist: " + cp);
1586 Jar jar = new Jar(cp);
1587 addClose(jar);
1588 classpath.add(jar);
1589 }
1590
1591 public void clear() {
1592 classpath.clear();
1593 }
1594
1595 public Jar getTarget() {
1596 return dot;
1597 }
1598
1599 private void analyzeBundleClasspath() throws Exception {
1600 Parameters bcp = getBundleClasspath();
1601
1602 if (bcp.isEmpty()) {
1603 analyzeJar(dot, "", true);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001604 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001605 boolean okToIncludeDirs = true;
1606
1607 for (String path : bcp.keySet()) {
1608 if (dot.getDirectories().containsKey(path)) {
1609 okToIncludeDirs = false;
1610 break;
1611 }
1612 }
1613
1614 for (String path : bcp.keySet()) {
1615 Attrs info = bcp.get(path);
1616
1617 if (path.equals(".")) {
1618 analyzeJar(dot, "", okToIncludeDirs);
1619 continue;
1620 }
1621 //
1622 // There are 3 cases:
1623 // - embedded JAR file
1624 // - directory
1625 // - error
1626 //
1627
1628 Resource resource = dot.getResource(path);
1629 if (resource != null) {
1630 try {
1631 Jar jar = new Jar(path);
1632 addClose(jar);
1633 EmbeddedResource.build(jar, resource);
1634 analyzeJar(jar, "", true);
1635 }
1636 catch (Exception e) {
1637 warning("Invalid bundle classpath entry: " + path + " " + e);
1638 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001639 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001640 if (dot.getDirectories().containsKey(path)) {
1641 // if directories are used, we should not have dot as we
1642 // would have the classes in these directories on the
1643 // class path twice.
1644 if (bcp.containsKey("."))
1645 warning("Bundle-ClassPath uses a directory '%s' as well as '.'. This means bnd does not know if a directory is a package.",
1646 path, path);
1647 analyzeJar(dot, Processor.appendPath(path) + "/", true);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001648 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001649 if (!"optional".equals(info.get(RESOLUTION_DIRECTIVE)))
1650 warning("No sub JAR or directory " + path);
1651 }
1652 }
1653 }
1654
1655 }
1656 }
1657
1658 /**
1659 * We traverse through all the classes that we can find and calculate the
1660 * contained and referred set and uses. This method ignores the Bundle
1661 * classpath.
1662 *
1663 * @param jar
1664 * @param contained
1665 * @param referred
1666 * @param uses
1667 * @throws IOException
1668 */
1669 private boolean analyzeJar(Jar jar, String prefix, boolean okToIncludeDirs) throws Exception {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001670 Map<String,Clazz> mismatched = new HashMap<String,Clazz>();
Stuart McCullochbb014372012-06-07 21:57:32 +00001671
1672 next: for (String path : jar.getResources().keySet()) {
1673 if (path.startsWith(prefix)) {
1674
1675 String relativePath = path.substring(prefix.length());
1676
1677 if (okToIncludeDirs) {
1678 int n = relativePath.lastIndexOf('/');
1679 if (n < 0)
1680 n = relativePath.length();
1681 String relativeDir = relativePath.substring(0, n);
1682
1683 PackageRef packageRef = getPackageRef(relativeDir);
1684 if (!packageRef.isMetaData() && !contained.containsKey(packageRef)) {
1685 contained.put(packageRef);
1686
1687 // For each package we encounter for the first
1688 // time. Unfortunately we can only do this once
1689 // we found a class since the bcp has a tendency
1690 // to overlap
1691 if (!packageRef.isMetaData()) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001692 Resource pinfo = jar.getResource(prefix + packageRef.getPath() + "/packageinfo");
Stuart McCullochd4826102012-06-26 16:34:24 +00001693 getExportVersionsFromPackageInfo(packageRef, pinfo, classpathExports);
Stuart McCullochbb014372012-06-07 21:57:32 +00001694 }
1695 }
1696 }
1697
1698 // Check class resources, we need to analyze them
1699 if (path.endsWith(".class")) {
1700 Resource resource = jar.getResource(path);
1701 Clazz clazz;
1702 Attrs info = null;
1703
1704 try {
1705 InputStream in = resource.openInputStream();
1706 clazz = new Clazz(this, path, resource);
1707 try {
1708 // Check if we have a package-info
1709 if (relativePath.endsWith("/package-info.class")) {
1710 // package-info can contain an Export annotation
1711 info = new Attrs();
1712 parsePackageInfoClass(clazz, info);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001713 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001714 // Otherwise we just parse it simply
1715 clazz.parseClassFile();
1716 }
1717 }
1718 finally {
1719 in.close();
1720 }
1721 }
1722 catch (Throwable e) {
1723 error("Invalid class file %s (%s)", e, relativePath, e);
1724 e.printStackTrace();
1725 continue next;
1726 }
1727
1728 String calculatedPath = clazz.getClassName().getPath();
1729 if (!calculatedPath.equals(relativePath)) {
1730 // If there is a mismatch we
1731 // warning
1732 if (okToIncludeDirs) // assume already reported
1733 mismatched.put(clazz.getAbsolutePath(), clazz);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001734 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001735 classspace.put(clazz.getClassName(), clazz);
1736 PackageRef packageRef = clazz.getClassName().getPackageRef();
1737
1738 if (!contained.containsKey(packageRef)) {
1739 contained.put(packageRef);
1740 if (!packageRef.isMetaData()) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001741 Resource pinfo = jar.getResource(prefix + packageRef.getPath() + "/packageinfo");
Stuart McCullochd4826102012-06-26 16:34:24 +00001742 getExportVersionsFromPackageInfo(packageRef, pinfo, classpathExports);
Stuart McCullochbb014372012-06-07 21:57:32 +00001743 }
1744 }
1745 if (info != null)
1746 contained.merge(packageRef, false, info);
1747
Stuart McCullochbb014372012-06-07 21:57:32 +00001748
1749 // Look at the referred packages
1750 // and copy them to our baseline
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00001751 Set<PackageRef> refs = Create.set();
Stuart McCullochbb014372012-06-07 21:57:32 +00001752 for (PackageRef p : clazz.getReferred()) {
1753 referred.put(p);
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00001754 refs.add(p);
Stuart McCullochbb014372012-06-07 21:57:32 +00001755 }
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00001756 refs.remove(packageRef);
1757 uses.addAll(packageRef, refs);
1758
1759 // Collect the API
1760 apiUses.addAll(packageRef, clazz.getAPIUses());
Stuart McCullochbb014372012-06-07 21:57:32 +00001761 }
1762 }
1763 }
1764 }
1765
1766 if (mismatched.size() > 0) {
1767 error("Classes found in the wrong directory: %s", mismatched);
1768 return false;
1769 }
1770 return true;
1771 }
1772
1773 static Pattern OBJECT_REFERENCE = Pattern.compile("L([^/]+/)*([^;]+);");
1774
1775 private void parsePackageInfoClass(final Clazz clazz, final Attrs info) throws Exception {
1776 clazz.parseClassFileWithCollector(new ClassDataCollector() {
1777 @Override
1778 public void annotation(Annotation a) {
1779 String name = a.name.getFQN();
1780 if (aQute.bnd.annotation.Version.class.getName().equals(name)) {
1781
1782 // Check version
1783 String version = a.get("value");
1784 if (!info.containsKey(Constants.VERSION_ATTRIBUTE)) {
1785 if (version != null) {
1786 version = getReplacer().process(version);
1787 if (Verifier.VERSION.matcher(version).matches())
1788 info.put(VERSION_ATTRIBUTE, version);
1789 else
Stuart McCulloch2286f232012-06-15 13:27:53 +00001790 error("Export annotation in %s has invalid version info: %s", clazz, version);
Stuart McCullochbb014372012-06-07 21:57:32 +00001791 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001792 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001793 // Verify this matches with packageinfo
1794 String presentVersion = info.get(VERSION_ATTRIBUTE);
1795 try {
1796 Version av = new Version(presentVersion);
1797 Version bv = new Version(version);
1798 if (!av.equals(bv)) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001799 error("Version from annotation for %s differs with packageinfo or Manifest", clazz
1800 .getClassName().getFQN());
Stuart McCullochbb014372012-06-07 21:57:32 +00001801 }
1802 }
1803 catch (Exception e) {
1804 // Ignore
1805 }
1806 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001807 } else if (name.equals(Export.class.getName())) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001808
Stuart McCulloch2286f232012-06-15 13:27:53 +00001809 // Check mandatory attributes
1810 Attrs attrs = doAttrbutes((Object[]) a.get(Export.MANDATORY), clazz, getReplacer());
1811 if (!attrs.isEmpty()) {
1812 info.putAll(attrs);
1813 info.put(MANDATORY_DIRECTIVE, Processor.join(attrs.keySet()));
1814 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001815
Stuart McCulloch2286f232012-06-15 13:27:53 +00001816 // Check optional attributes
1817 attrs = doAttrbutes((Object[]) a.get(Export.OPTIONAL), clazz, getReplacer());
1818 if (!attrs.isEmpty()) {
1819 info.putAll(attrs);
1820 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001821
Stuart McCulloch2286f232012-06-15 13:27:53 +00001822 // Check Included classes
1823 Object[] included = a.get(Export.INCLUDE);
1824 if (included != null && included.length > 0) {
1825 StringBuilder sb = new StringBuilder();
1826 String del = "";
1827 for (Object i : included) {
1828 Matcher m = OBJECT_REFERENCE.matcher((String) i);
1829 if (m.matches()) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001830 sb.append(del);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001831 sb.append(m.group(2));
Stuart McCullochbb014372012-06-07 21:57:32 +00001832 del = ",";
1833 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001834 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001835 info.put(INCLUDE_DIRECTIVE, sb.toString());
Stuart McCullochbb014372012-06-07 21:57:32 +00001836 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001837
1838 // Check Excluded classes
1839 Object[] excluded = a.get(Export.EXCLUDE);
1840 if (excluded != null && excluded.length > 0) {
1841 StringBuilder sb = new StringBuilder();
1842 String del = "";
1843 for (Object i : excluded) {
1844 Matcher m = OBJECT_REFERENCE.matcher((String) i);
1845 if (m.matches()) {
1846 sb.append(del);
1847 sb.append(m.group(2));
1848 del = ",";
1849 }
1850 }
1851 info.put(EXCLUDE_DIRECTIVE, sb.toString());
1852 }
1853
1854 // Check Uses
1855 Object[] uses = a.get(Export.USES);
1856 if (uses != null && uses.length > 0) {
1857 String old = info.get(USES_DIRECTIVE);
1858 if (old == null)
1859 old = "";
1860 StringBuilder sb = new StringBuilder(old);
1861 String del = sb.length() == 0 ? "" : ",";
1862
1863 for (Object use : uses) {
1864 sb.append(del);
1865 sb.append(use);
1866 del = ",";
1867 }
1868 info.put(USES_DIRECTIVE, sb.toString());
1869 }
1870 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001871 }
1872
1873 });
1874 }
1875
1876 /**
1877 * Clean up version parameters. Other builders use more fuzzy definitions of
1878 * the version syntax. This method cleans up such a version to match an OSGi
1879 * version.
1880 *
1881 * @param VERSION_STRING
1882 * @return
1883 */
Stuart McCulloch2286f232012-06-15 13:27:53 +00001884 static Pattern fuzzyVersion = Pattern.compile("(\\d+)(\\.(\\d+)(\\.(\\d+))?)?([^a-zA-Z0-9](.*))?",
1885 Pattern.DOTALL);
1886 static Pattern fuzzyVersionRange = Pattern.compile(
1887 "(\\(|\\[)\\s*([-\\da-zA-Z.]+)\\s*,\\s*([-\\da-zA-Z.]+)\\s*(\\]|\\))",
1888 Pattern.DOTALL);
Stuart McCullochbb014372012-06-07 21:57:32 +00001889 static Pattern fuzzyModifier = Pattern.compile("(\\d+[.-])*(.*)", Pattern.DOTALL);
1890
1891 static Pattern nummeric = Pattern.compile("\\d*");
1892
1893 static public String cleanupVersion(String version) {
1894 Matcher m = Verifier.VERSIONRANGE.matcher(version);
1895
1896 if (m.matches()) {
1897 return version;
1898 }
1899
1900 m = fuzzyVersionRange.matcher(version);
1901 if (m.matches()) {
1902 String prefix = m.group(1);
1903 String first = m.group(2);
1904 String last = m.group(3);
1905 String suffix = m.group(4);
1906 return prefix + cleanupVersion(first) + "," + cleanupVersion(last) + suffix;
1907 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001908
Stuart McCulloch2286f232012-06-15 13:27:53 +00001909 m = fuzzyVersion.matcher(version);
1910 if (m.matches()) {
1911 StringBuilder result = new StringBuilder();
1912 String major = removeLeadingZeroes(m.group(1));
1913 String minor = removeLeadingZeroes(m.group(3));
1914 String micro = removeLeadingZeroes(m.group(5));
1915 String qualifier = m.group(7);
1916
1917 if (major != null) {
1918 result.append(major);
1919 if (minor != null) {
1920 result.append(".");
1921 result.append(minor);
1922 if (micro != null) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001923 result.append(".");
Stuart McCulloch2286f232012-06-15 13:27:53 +00001924 result.append(micro);
Stuart McCullochbb014372012-06-07 21:57:32 +00001925 if (qualifier != null) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001926 result.append(".");
Stuart McCullochbb014372012-06-07 21:57:32 +00001927 cleanupModifier(result, qualifier);
1928 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001929 } else if (qualifier != null) {
1930 result.append(".0.");
1931 cleanupModifier(result, qualifier);
1932 }
1933 } else if (qualifier != null) {
1934 result.append(".0.0.");
1935 cleanupModifier(result, qualifier);
Stuart McCullochbb014372012-06-07 21:57:32 +00001936 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001937 return result.toString();
Stuart McCullochbb014372012-06-07 21:57:32 +00001938 }
1939 }
1940 return version;
1941 }
1942
1943 private static String removeLeadingZeroes(String group) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001944 if (group == null)
1945 return null;
1946
Stuart McCullochbb014372012-06-07 21:57:32 +00001947 int n = 0;
Stuart McCulloch2286f232012-06-15 13:27:53 +00001948 while (n < group.length() - 1 && group.charAt(n) == '0')
Stuart McCullochbb014372012-06-07 21:57:32 +00001949 n++;
1950 if (n == 0)
1951 return group;
1952
1953 return group.substring(n);
1954 }
1955
1956 static void cleanupModifier(StringBuilder result, String modifier) {
1957 Matcher m = fuzzyModifier.matcher(modifier);
1958 if (m.matches())
1959 modifier = m.group(2);
1960
1961 for (int i = 0; i < modifier.length(); i++) {
1962 char c = modifier.charAt(i);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001963 if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '-')
Stuart McCullochbb014372012-06-07 21:57:32 +00001964 result.append(c);
1965 }
1966 }
1967
1968 final static String DEFAULT_PROVIDER_POLICY = "${range;[==,=+)}";
1969 final static String DEFAULT_CONSUMER_POLICY = "${range;[==,+)}";
1970
Stuart McCullochbb014372012-06-07 21:57:32 +00001971 public String getVersionPolicy(boolean implemented) {
1972 if (implemented) {
Stuart McCullochd4826102012-06-26 16:34:24 +00001973 return getProperty(PROVIDER_POLICY, DEFAULT_PROVIDER_POLICY);
Stuart McCullochbb014372012-06-07 21:57:32 +00001974 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001975
Stuart McCullochd4826102012-06-26 16:34:24 +00001976 return getProperty(CONSUMER_POLICY, DEFAULT_CONSUMER_POLICY);
Stuart McCullochbb014372012-06-07 21:57:32 +00001977 }
1978
1979 /**
1980 * The extends macro traverses all classes and returns a list of class names
1981 * that extend a base class.
1982 */
1983
1984 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";
1985
1986 public String _classes(String... args) throws Exception {
1987 // Macro.verifyCommand(args, _classesHelp, new
1988 // Pattern[]{null,Pattern.compile("(implementing|implements|extending|extends|importing|imports|any)"),
1989 // null}, 3,3);
1990
1991 Collection<Clazz> matched = getClasses(args);
1992 if (matched.isEmpty())
1993 return "";
1994
1995 return join(matched);
1996 }
1997
1998 public Collection<Clazz> getClasses(String... args) throws Exception {
1999
2000 Set<Clazz> matched = new HashSet<Clazz>(classspace.values());
2001 for (int i = 1; i < args.length; i++) {
2002 if (args.length < i + 1)
Stuart McCulloch2286f232012-06-15 13:27:53 +00002003 throw new IllegalArgumentException("${classes} macro must have odd number of arguments. "
2004 + _classesHelp);
Stuart McCullochbb014372012-06-07 21:57:32 +00002005
2006 String typeName = args[i];
2007 if (typeName.equalsIgnoreCase("extending"))
2008 typeName = "extends";
Stuart McCulloch2286f232012-06-15 13:27:53 +00002009 else if (typeName.equalsIgnoreCase("importing"))
2010 typeName = "imports";
2011 else if (typeName.equalsIgnoreCase("implementing"))
2012 typeName = "implements";
Stuart McCullochbb014372012-06-07 21:57:32 +00002013
2014 Clazz.QUERY type = Clazz.QUERY.valueOf(typeName.toUpperCase());
2015
2016 if (type == null)
Stuart McCulloch2286f232012-06-15 13:27:53 +00002017 throw new IllegalArgumentException("${classes} has invalid type: " + typeName + ". " + _classesHelp);
Stuart McCullochbb014372012-06-07 21:57:32 +00002018
2019 Instruction instr = null;
2020 if (Clazz.HAS_ARGUMENT.contains(type)) {
2021 String s = args[++i];
2022 instr = new Instruction(s);
2023 }
2024 for (Iterator<Clazz> c = matched.iterator(); c.hasNext();) {
2025 Clazz clazz = c.next();
2026 if (!clazz.is(type, instr, this)) {
2027 c.remove();
2028 }
2029 }
2030 }
2031 return matched;
2032 }
2033
2034 /**
2035 * Get the exporter of a package ...
2036 */
2037
2038 public String _exporters(String args[]) throws Exception {
Stuart McCulloch2286f232012-06-15 13:27:53 +00002039 Macro.verifyCommand(args, "${exporters;<packagename>}, returns the list of jars that export the given package",
Stuart McCullochbb014372012-06-07 21:57:32 +00002040 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
Stuart McCulloch2286f232012-06-15 13:27:53 +00002053 public Map<TypeRef,Clazz> getClassspace() {
Stuart McCullochbb014372012-06-07 21:57:32 +00002054 return classspace;
2055 }
2056
2057 /**
2058 * Locate a resource on the class path.
2059 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00002060 * @param path
2061 * Path of the reosurce
Stuart McCullochbb014372012-06-07 21:57:32 +00002062 * @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(TypeRef typeRef) throws Exception {
2080 Clazz c = classspace.get(typeRef);
2081 if (c != null)
2082 return c;
2083
2084 c = importedClassesCache.get(typeRef);
2085 if (c != null)
2086 return c;
2087
2088 Resource r = findResource(typeRef.getPath());
2089 if (r == null) {
2090 getClass().getClassLoader();
2091 URL url = ClassLoader.getSystemResource(typeRef.getPath());
2092 if (url != null)
2093 r = new URLResource(url);
2094 }
2095 if (r != null) {
2096 c = new Clazz(this, typeRef.getPath(), r);
2097 c.parseClassFile();
2098 importedClassesCache.put(typeRef, c);
2099 }
2100 return c;
2101 }
2102
2103 /**
2104 * Answer the bundle version.
2105 *
2106 * @return
2107 */
2108 public String getVersion() {
2109 String version = getProperty(BUNDLE_VERSION);
2110 if (version == null)
2111 version = "0.0.0";
2112 return version;
2113 }
2114
2115 public boolean isNoBundle() {
2116 return isTrue(getProperty(RESOURCEONLY)) || isTrue(getProperty(NOMANIFEST));
2117 }
2118
2119 public void referTo(TypeRef ref) {
2120 PackageRef pack = ref.getPackageRef();
2121 if (!referred.containsKey(pack))
2122 referred.put(pack, new Attrs());
2123 }
2124
2125 public void referToByBinaryName(String binaryClassName) {
2126 TypeRef ref = descriptors.getTypeRef(binaryClassName);
2127 referTo(ref);
2128 }
2129
2130 /**
2131 * Ensure that we are running on the correct bnd.
2132 */
2133 void doRequireBnd() {
2134 Attrs require = OSGiHeader.parseProperties(getProperty(REQUIRE_BND));
2135 if (require == null || require.isEmpty())
2136 return;
2137
Stuart McCulloch2286f232012-06-15 13:27:53 +00002138 Hashtable<String,String> map = new Hashtable<String,String>();
Stuart McCullochbb014372012-06-07 21:57:32 +00002139 map.put(Constants.VERSION_FILTER, getBndVersion());
2140
2141 for (String filter : require.keySet()) {
2142 try {
2143 Filter f = new Filter(filter);
2144 if (f.match(map))
2145 continue;
2146 error("%s fails %s", REQUIRE_BND, require.get(filter));
2147 }
2148 catch (Exception t) {
2149 error("%s with value %s throws exception", t, REQUIRE_BND, require);
2150 }
2151 }
2152 }
2153
2154 /**
2155 * md5 macro
2156 */
2157
2158 static String _md5Help = "${md5;path}";
2159
2160 public String _md5(String args[]) throws Exception {
Stuart McCulloch2286f232012-06-15 13:27:53 +00002161 Macro.verifyCommand(args, _md5Help, new Pattern[] {
2162 null, null, Pattern.compile("base64|hex")
2163 }, 2, 3);
Stuart McCullochbb014372012-06-07 21:57:32 +00002164
2165 Digester<MD5> digester = MD5.getDigester();
2166 Resource r = dot.getResource(args[1]);
2167 if (r == null)
2168 throw new FileNotFoundException("From " + digester + ", not found " + args[1]);
2169
2170 IO.copy(r.openInputStream(), digester);
2171 boolean hex = args.length > 2 && args[2].equals("hex");
2172 if (hex)
2173 return Hex.toHexString(digester.digest().digest());
Stuart McCulloch2286f232012-06-15 13:27:53 +00002174
2175 return Base64.encodeBase64(digester.digest().digest());
Stuart McCullochbb014372012-06-07 21:57:32 +00002176 }
2177
2178 /**
2179 * SHA1 macro
2180 */
2181
2182 static String _sha1Help = "${sha1;path}";
2183
2184 public String _sha1(String args[]) throws Exception {
Stuart McCulloch2286f232012-06-15 13:27:53 +00002185 Macro.verifyCommand(args, _sha1Help, new Pattern[] {
2186 null, null, Pattern.compile("base64|hex")
2187 }, 2, 3);
Stuart McCullochbb014372012-06-07 21:57:32 +00002188 Digester<SHA1> digester = SHA1.getDigester();
2189 Resource r = dot.getResource(args[1]);
2190 if (r == null)
2191 throw new FileNotFoundException("From sha1, not found " + args[1]);
2192
2193 IO.copy(r.openInputStream(), digester);
2194 return Base64.encodeBase64(digester.digest().digest());
2195 }
2196
2197 public Descriptor getDescriptor(String descriptor) {
2198 return descriptors.getDescriptor(descriptor);
2199 }
2200
2201 public TypeRef getTypeRef(String binaryClassName) {
2202 return descriptors.getTypeRef(binaryClassName);
2203 }
2204
2205 public PackageRef getPackageRef(String binaryName) {
2206 return descriptors.getPackageRef(binaryName);
2207 }
2208
2209 public TypeRef getTypeRefFromFQN(String fqn) {
2210 return descriptors.getTypeRefFromFQN(fqn);
2211 }
2212
2213 public TypeRef getTypeRefFromPath(String path) {
2214 return descriptors.getTypeRefFromPath(path);
2215 }
2216
2217 public boolean isImported(PackageRef packageRef) {
2218 return imports.containsKey(packageRef);
2219 }
2220
2221 /**
2222 * Merge the attributes of two maps, where the first map can contain
2223 * wildcarded names. The idea is that the first map contains instructions
2224 * (for example *) with a set of attributes. These patterns are matched
2225 * against the found packages in actual. If they match, the result is set
2226 * with the merged set of attributes. It is expected that the instructions
2227 * are ordered so that the instructor can define which pattern matches
2228 * first. Attributes in the instructions override any attributes from the
2229 * actual.<br/>
Stuart McCullochbb014372012-06-07 21:57:32 +00002230 * A pattern is a modified regexp so it looks like globbing. The * becomes a
2231 * .* just like the ? becomes a .?. '.' are replaced with \\. Additionally,
2232 * if the pattern starts with an exclamation mark, it will remove that
2233 * matches for that pattern (- the !) from the working set. So the following
2234 * patterns should work:
2235 * <ul>
2236 * <li>com.foo.bar</li>
2237 * <li>com.foo.*</li>
2238 * <li>com.foo.???</li>
2239 * <li>com.*.[^b][^a][^r]</li>
2240 * <li>!com.foo.* (throws away any match for com.foo.*)</li>
2241 * </ul>
2242 * Enough rope to hang the average developer I would say.
2243 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00002244 * @param instructions
2245 * the instructions with patterns.
2246 * @param source
2247 * the actual found packages, contains no duplicates
Stuart McCullochbb014372012-06-07 21:57:32 +00002248 * @return Only the packages that were filtered by the given instructions
2249 */
2250
2251 Packages filter(Instructions instructions, Packages source, Set<Instruction> nomatch) {
2252 Packages result = new Packages();
2253 List<PackageRef> refs = new ArrayList<PackageRef>(source.keySet());
2254 Collections.sort(refs);
2255
2256 List<Instruction> filters = new ArrayList<Instruction>(instructions.keySet());
2257 if (nomatch == null)
2258 nomatch = Create.set();
2259
2260 for (Instruction instruction : filters) {
2261 boolean match = false;
2262
2263 for (Iterator<PackageRef> i = refs.iterator(); i.hasNext();) {
2264 PackageRef packageRef = i.next();
2265
2266 if (packageRef.isMetaData()) {
2267 i.remove(); // no use checking it again
2268 continue;
2269 }
2270
2271 String packageName = packageRef.getFQN();
2272
2273 if (instruction.matches(packageName)) {
2274 match = true;
2275 if (!instruction.isNegated()) {
2276 result.merge(packageRef, instruction.isDuplicate(), source.get(packageRef),
2277 instructions.get(instruction));
2278 }
2279 i.remove(); // Can never match again for another pattern
2280 }
2281 }
2282 if (!match && !instruction.isAny())
2283 nomatch.add(instruction);
2284 }
2285
2286 /*
2287 * Tricky. If we have umatched instructions they might indicate that we
2288 * want to have multiple decorators for the same package. So we check
2289 * the unmatched against the result list. If then then match and have
2290 * actually interesting properties then we merge them
2291 */
2292
2293 for (Iterator<Instruction> i = nomatch.iterator(); i.hasNext();) {
2294 Instruction instruction = i.next();
2295
2296 // We assume the user knows what he is
2297 // doing and inserted a literal. So
2298 // we ignore any not matched literals
2299 if (instruction.isLiteral()) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00002300 result.merge(getPackageRef(instruction.getLiteral()), true, instructions.get(instruction));
Stuart McCullochbb014372012-06-07 21:57:32 +00002301 i.remove();
2302 continue;
2303 }
2304
2305 // Not matching a negated instruction looks
2306 // like an error ...
2307 if (instruction.isNegated()) {
2308 continue;
2309 }
2310
2311 // An optional instruction should not generate
2312 // an error
2313 if (instruction.isOptional()) {
2314 i.remove();
2315 continue;
2316 }
2317
2318 // boolean matched = false;
2319 // Set<PackageRef> prefs = new HashSet<PackageRef>(result.keySet());
2320 // for (PackageRef ref : prefs) {
2321 // if (instruction.matches(ref.getFQN())) {
2322 // result.merge(ref, true, source.get(ref),
2323 // instructions.get(instruction));
2324 // matched = true;
2325 // }
2326 // }
2327 // if (matched)
2328 // i.remove();
2329 }
2330 return result;
2331 }
2332
2333 public void setDiagnostics(boolean b) {
2334 diagnostics = b;
2335 }
2336
2337 public Clazz.JAVA getLowestEE() {
2338 if (ees.isEmpty())
2339 return Clazz.JAVA.JDK1_4;
2340
2341 return ees.first();
2342 }
2343
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00002344 public String _ee(@SuppressWarnings("unused")
2345 String args[]) {
Stuart McCullochbb014372012-06-07 21:57:32 +00002346 return getLowestEE().getEE();
2347 }
2348
2349 /**
2350 * Calculate the output file for the given target. The strategy is:
2351 *
2352 * <pre>
2353 * parameter given if not null and not directory
2354 * if directory, this will be the output directory
2355 * based on bsn-version.jar
2356 * name of the source file if exists
2357 * Untitled-[n]
2358 * </pre>
2359 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00002360 * @param output
2361 * may be null, otherwise a file path relative to base
Stuart McCullochbb014372012-06-07 21:57:32 +00002362 */
2363 public File getOutputFile(String output) {
2364
2365 if (output == null)
2366 output = get(Constants.OUTPUT);
2367
2368 File outputDir;
2369
2370 if (output != null) {
2371 File outputFile = getFile(output);
2372 if (outputFile.isDirectory())
2373 outputDir = outputFile;
2374 else
2375 return outputFile;
Stuart McCulloch2286f232012-06-15 13:27:53 +00002376 } else
Stuart McCullochbb014372012-06-07 21:57:32 +00002377 outputDir = getBase();
2378
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00002379 Entry<String,Attrs> name = getBundleSymbolicName();
2380 if (name != null) {
2381 String bsn = name.getKey();
Stuart McCullochbb014372012-06-07 21:57:32 +00002382 String version = getBundleVersion();
2383 Version v = Version.parseVersion(version);
Stuart McCulloch2286f232012-06-15 13:27:53 +00002384 String outputName = bsn + "-" + v.getWithoutQualifier() + Constants.DEFAULT_JAR_EXTENSION;
Stuart McCullochbb014372012-06-07 21:57:32 +00002385 return new File(outputDir, outputName);
2386 }
2387
2388 File source = getJar().getSource();
2389 if (source != null) {
2390 String outputName = source.getName();
2391 return new File(outputDir, outputName);
2392 }
2393
Stuart McCulloch2286f232012-06-15 13:27:53 +00002394 error("Cannot establish an output name from %s, nor bsn, nor source file name, using Untitled", output);
Stuart McCullochbb014372012-06-07 21:57:32 +00002395 int n = 0;
2396 File f = getFile(outputDir, "Untitled");
2397 while (f.isFile()) {
2398 f = getFile(outputDir, "Untitled-" + n++);
2399 }
2400 return f;
2401 }
2402
2403 /**
2404 * Utility function to carefully save the file. Will create a backup if the
2405 * source file has the same path as the output. It will also only save if
2406 * the file was modified or the force flag is true
2407 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00002408 * @param output
2409 * the output file, if null {@link #getOutputFile(String)} is
2410 * used.
2411 * @param force
2412 * if it needs to be overwritten
Stuart McCullochbb014372012-06-07 21:57:32 +00002413 * @throws Exception
2414 */
2415
2416 public boolean save(File output, boolean force) throws Exception {
2417 if (output == null)
2418 output = getOutputFile(null);
2419
2420 Jar jar = getJar();
2421 File source = jar.getSource();
2422
Stuart McCulloch2286f232012-06-15 13:27:53 +00002423 trace("check for modified build=%s file=%s, diff=%s", jar.lastModified(), output.lastModified(),
2424 jar.lastModified() - output.lastModified());
Stuart McCullochbb014372012-06-07 21:57:32 +00002425
2426 if (!output.exists() || output.lastModified() <= jar.lastModified() || force) {
2427 output.getParentFile().mkdirs();
2428 if (source != null && output.getCanonicalPath().equals(source.getCanonicalPath())) {
2429 File bak = new File(source.getParentFile(), source.getName() + ".bak");
2430 if (!source.renameTo(bak)) {
2431 error("Could not create backup file %s", bak);
Stuart McCulloch2286f232012-06-15 13:27:53 +00002432 } else
Stuart McCullochbb014372012-06-07 21:57:32 +00002433 source.delete();
2434 }
2435 try {
2436 trace("Saving jar to %s", output);
2437 getJar().write(output);
2438 }
2439 catch (Exception e) {
2440 output.delete();
2441 error("Cannot write JAR file to %s due to %s", e, output, e.getMessage());
2442 }
2443 return true;
2444 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00002445 trace("Not modified %s", output);
2446 return false;
2447
Stuart McCullochbb014372012-06-07 21:57:32 +00002448 }
2449
2450 /**
2451 * Set default import and export instructions if none are set
2452 */
2453 public void setDefaults(String bsn, Version version) {
2454 if (getExportPackage() == null)
2455 setExportPackage("*");
2456 if (getImportPackage() == null)
2457 setExportPackage("*");
2458 if (bsn != null && getBundleSymbolicName() == null)
2459 setBundleSymbolicName(bsn);
2460 if (version != null && getBundleVersion() == null)
2461 setBundleVersion(version);
2462 }
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00002463
2464 /**
2465 * Remove the own references and optionall java references from the uses lib
2466 * @param apiUses
2467 * @param removeJava
2468 * @return
2469 */
2470 public MultiMap<PackageRef,PackageRef> cleanupUses(MultiMap<PackageRef,PackageRef> apiUses, boolean removeJava) {
2471 MultiMap<PackageRef,PackageRef> map = new MultiMap<PackageRef,PackageRef>(apiUses);
2472 for ( Entry<PackageRef,List<PackageRef>> e : map.entrySet()) {
2473 e.getValue().remove(e.getKey());
2474 if (!removeJava)
2475 continue;
2476
2477 for (Iterator<PackageRef> i = e.getValue().iterator(); i.hasNext(); ) {
2478 if ( i.next().isJava())
2479 i.remove();
2480 }
2481 }
2482 return map;
2483 }
Stuart McCullochbb014372012-06-07 21:57:32 +00002484}