blob: 3c0c9b27dd02b56c59597d9870126fec6f437a14 [file] [log] [blame]
Stuart McCullochbb014372012-06-07 21:57:32 +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 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.*;
33import aQute.bnd.service.*;
34import aQute.lib.base64.*;
35import aQute.lib.collections.*;
36import aQute.lib.filter.*;
37import aQute.lib.hex.*;
38import aQute.lib.io.*;
39import aQute.lib.osgi.Descriptors.Descriptor;
40import aQute.lib.osgi.Descriptors.PackageRef;
41import aQute.lib.osgi.Descriptors.TypeRef;
42import aQute.libg.cryptography.*;
43import aQute.libg.generics.*;
44import aQute.libg.header.*;
Stuart McCulloch2286f232012-06-15 13:27:53 +000045import aQute.libg.reporter.*;
Stuart McCullochbb014372012-06-07 21:57:32 +000046import aQute.libg.version.Version;
47
48public class Analyzer extends Processor {
49 private final SortedSet<Clazz.JAVA> ees = new TreeSet<Clazz.JAVA>();
Stuart McCullochffa8aaf2012-06-17 20:38:35 +000050 static Manifest bndInfo;
Stuart McCullochbb014372012-06-07 21:57:32 +000051
52 // Bundle parameters
53 private Jar dot;
54 private final Packages contained = new Packages();
55 private final Packages referred = new Packages();
56 private Packages exports;
57 private Packages imports;
58 private TypeRef activator;
59
60 // Global parameters
Stuart McCulloch2286f232012-06-15 13:27:53 +000061 private final MultiMap<PackageRef,PackageRef> uses = new MultiMap<PackageRef,PackageRef>(
62 PackageRef.class, PackageRef.class,
Stuart McCullochbb014372012-06-07 21:57:32 +000063 true);
64 private final Packages classpathExports = new Packages();
65 private final Descriptors descriptors = new Descriptors();
66 private final List<Jar> classpath = list();
Stuart McCulloch2286f232012-06-15 13:27:53 +000067 private final Map<TypeRef,Clazz> classspace = map();
68 private final Map<TypeRef,Clazz> importedClassesCache = map();
Stuart McCullochbb014372012-06-07 21:57:32 +000069 private boolean analyzed = false;
70 private boolean diagnostics = false;
71 private boolean inited = false;
Stuart McCulloch2286f232012-06-15 13:27:53 +000072 final protected AnalyzerMessages msgs = ReporterMessages.base(this,
73 AnalyzerMessages.class);
Stuart McCullochbb014372012-06-07 21:57:32 +000074
75 public Analyzer(Processor parent) {
76 super(parent);
77 }
78
Stuart McCulloch2286f232012-06-15 13:27:53 +000079 public Analyzer() {}
Stuart McCullochbb014372012-06-07 21:57:32 +000080
81 /**
82 * Specifically for Maven
83 *
Stuart McCulloch2286f232012-06-15 13:27:53 +000084 * @param properties
85 * the properties
Stuart McCullochbb014372012-06-07 21:57:32 +000086 */
87
88 public static Properties getManifest(File dirOrJar) throws Exception {
89 Analyzer analyzer = new Analyzer();
90 try {
91 analyzer.setJar(dirOrJar);
92 Properties properties = new Properties();
93 properties.put(IMPORT_PACKAGE, "*");
94 properties.put(EXPORT_PACKAGE, "*");
95 analyzer.setProperties(properties);
96 Manifest m = analyzer.calcManifest();
97 Properties result = new Properties();
98 for (Iterator<Object> i = m.getMainAttributes().keySet().iterator(); i.hasNext();) {
99 Attributes.Name name = (Attributes.Name) i.next();
100 result.put(name.toString(), m.getMainAttributes().getValue(name));
101 }
102 return result;
103 }
104 finally {
105 analyzer.close();
106 }
107 }
108
109 /**
110 * Calculates the data structures for generating a manifest.
111 *
112 * @throws IOException
113 */
114 public void analyze() throws Exception {
115 if (!analyzed) {
116 analyzed = true;
117 uses.clear();
118 classspace.clear();
119 classpathExports.clear();
120
121 // Parse all the class in the
122 // the jar according to the OSGi bcp
123 analyzeBundleClasspath();
124
125 //
126 // calculate class versions in use
127 //
128 for (Clazz c : classspace.values()) {
129 ees.add(c.getFormat());
130 }
131
132 //
133 // Get exported packages from the
134 // entries on the classpath
135 //
136
137 for (Jar current : getClasspath()) {
138 getExternalExports(current, classpathExports);
139 for (String dir : current.getDirectories().keySet()) {
140 PackageRef packageRef = getPackageRef(dir);
141 Resource resource = current.getResource(dir + "/packageinfo");
142 setPackageInfo(packageRef, resource, classpathExports);
143 }
144 }
145
146 // Handle the bundle activator
147
148 String s = getProperty(BUNDLE_ACTIVATOR);
149 if (s != null) {
150 activator = getTypeRefFromFQN(s);
151 referTo(activator);
152 }
153
154 // Execute any plugins
155 // TODO handle better reanalyze
156 doPlugins();
157
158 Jar extra = getExtra();
159 while (extra != null) {
160 dot.addAll(extra);
161 analyzeJar(extra, "", true);
162 extra = getExtra();
163 }
164
165 referred.keySet().removeAll(contained.keySet());
166
167 //
168 // EXPORTS
169 //
170 {
171 Set<Instruction> unused = Create.set();
172
173 Instructions filter = new Instructions(getExportPackage());
174 filter.append(getExportContents());
175
176 exports = filter(filter, contained, unused);
177
178 if (!unused.isEmpty()) {
179 warning("Unused Export-Package instructions: %s ", unused);
180 }
181
182 // See what information we can find to augment the
183 // exports. I.e. look on the classpath
184 augmentExports(exports);
185 }
186
187 //
188 // IMPORTS
189 // Imports MUST come after exports because we use information from
190 // the exports
191 //
192 {
193 // Add all exports that do not have an -noimport: directive
194 // to the imports.
195 Packages referredAndExported = new Packages(referred);
196 referredAndExported.putAll(doExportsToImports(exports));
197
198 removeDynamicImports(referredAndExported);
199
200 // Remove any Java references ... where are the closures???
201 for (Iterator<PackageRef> i = referredAndExported.keySet().iterator(); i.hasNext();) {
202 if (i.next().isJava())
203 i.remove();
204 }
205
206 Set<Instruction> unused = Create.set();
207 String h = getProperty(IMPORT_PACKAGE);
208 if (h == null) // If not set use a default
209 h = "*";
210
211 if (isPedantic() && h.trim().length() == 0)
212 warning("Empty Import-Package header");
213
214 Instructions filter = new Instructions(h);
215 imports = filter(filter, referredAndExported, unused);
216 if (!unused.isEmpty()) {
217 // We ignore the end wildcard catch
218 if (!(unused.size() == 1 && unused.iterator().next().toString().equals("*")))
219 warning("Unused Import-Package instructions: %s ", unused);
220 }
221
222 // See what information we can find to augment the
223 // imports. I.e. look in the exports
224 augmentImports(imports, exports);
225 }
226
227 //
228 // USES
229 //
230 // Add the uses clause to the exports
231 doUses(exports, uses, imports);
232
233 //
234 // Checks
235 //
236 if (referred.containsKey(Descriptors.DEFAULT_PACKAGE)) {
237 error("The default package '.' is not permitted by the Import-Package syntax. \n"
238 + " This can be caused by compile errors in Eclipse because Eclipse creates \n"
239 + "valid class files regardless of compile errors.\n"
240 + "The following package(s) import from the default package "
241 + uses.transpose().get(Descriptors.DEFAULT_PACKAGE));
242 }
243
244 }
245 }
246
247 /**
248 * Discussed with BJ and decided to kill the .
249 *
250 * @param referredAndExported
251 */
252 void removeDynamicImports(Packages referredAndExported) {
253
254 // // Remove any matching a dynamic import package instruction
255 // Instructions dynamicImports = new
256 // Instructions(getDynamicImportPackage());
257 // Collection<PackageRef> dynamic = dynamicImports.select(
258 // referredAndExported.keySet(), false);
259 // referredAndExported.keySet().removeAll(dynamic);
260 }
261
262 protected Jar getExtra() throws Exception {
263 return null;
264 }
265
266 /**
267 *
268 */
269 void doPlugins() {
270 for (AnalyzerPlugin plugin : getPlugins(AnalyzerPlugin.class)) {
271 try {
272 boolean reanalyze = plugin.analyzeJar(this);
273 if (reanalyze) {
274 classspace.clear();
275 analyzeBundleClasspath();
276 }
277 }
278 catch (Exception e) {
279 error("Analyzer Plugin %s failed %s", plugin, e);
280 }
281 }
282 }
283
284 /**
Stuart McCullochbb014372012-06-07 21:57:32 +0000285 * @return
286 */
287 boolean isResourceOnly() {
288 return isTrue(getProperty(RESOURCEONLY));
289 }
290
291 /**
292 * One of the main workhorses of this class. This will analyze the current
293 * setp and calculate a new manifest according to this setup. This method
294 * will also set the manifest on the main jar dot
295 *
296 * @return
297 * @throws IOException
298 */
299 public Manifest calcManifest() throws Exception {
300 analyze();
301 Manifest manifest = new Manifest();
302 Attributes main = manifest.getMainAttributes();
303
304 main.put(Attributes.Name.MANIFEST_VERSION, "1.0");
305 main.putValue(BUNDLE_MANIFESTVERSION, "2");
306
307 boolean noExtraHeaders = "true".equalsIgnoreCase(getProperty(NOEXTRAHEADERS));
308
309 if (!noExtraHeaders) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000310 main.putValue(CREATED_BY, System.getProperty("java.version") + " (" + System.getProperty("java.vendor")
311 + ")");
Stuart McCullochbb014372012-06-07 21:57:32 +0000312 main.putValue(TOOL, "Bnd-" + getBndVersion());
313 main.putValue(BND_LASTMODIFIED, "" + System.currentTimeMillis());
314 }
315
316 String exportHeader = printClauses(exports, true);
317
318 if (exportHeader.length() > 0)
319 main.putValue(EXPORT_PACKAGE, exportHeader);
320 else
321 main.remove(EXPORT_PACKAGE);
322
323 // Remove all the Java packages from the imports
324 if (!imports.isEmpty()) {
325 main.putValue(IMPORT_PACKAGE, printClauses(imports));
Stuart McCulloch2286f232012-06-15 13:27:53 +0000326 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +0000327 main.remove(IMPORT_PACKAGE);
328 }
329
330 Packages temp = new Packages(contained);
331 temp.keySet().removeAll(exports.keySet());
332
333 if (!temp.isEmpty())
334 main.putValue(PRIVATE_PACKAGE, printClauses(temp));
335 else
336 main.remove(PRIVATE_PACKAGE);
337
338 Parameters bcp = getBundleClasspath();
339 if (bcp.isEmpty() || (bcp.containsKey(".") && bcp.size() == 1))
340 main.remove(BUNDLE_CLASSPATH);
341 else
342 main.putValue(BUNDLE_CLASSPATH, printClauses(bcp));
343
344 doNamesection(dot, manifest);
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
Stuart McCulloch2286f232012-06-15 13:27:53 +0000362 if (header.equals(BUNDLE_CLASSPATH) || header.equals(EXPORT_PACKAGE) || header.equals(IMPORT_PACKAGE))
Stuart McCullochbb014372012-06-07 21:57:32 +0000363 continue;
364
365 if (header.equalsIgnoreCase("Name")) {
366 error("Your bnd file contains a header called 'Name'. This interferes with the manifest name section.");
367 continue;
368 }
369
370 if (Verifier.HEADER_PATTERN.matcher(header).matches()) {
371 String value = getProperty(header);
372 if (value != null && main.getValue(header) == null) {
373 if (value.trim().length() == 0)
374 main.remove(header);
Stuart McCulloch2286f232012-06-15 13:27:53 +0000375 else if (value.trim().equals(EMPTY_HEADER))
376 main.putValue(header, "");
Stuart McCullochbb014372012-06-07 21:57:32 +0000377 else
Stuart McCulloch2286f232012-06-15 13:27:53 +0000378 main.putValue(header, value);
Stuart McCullochbb014372012-06-07 21:57:32 +0000379 }
Stuart McCulloch2286f232012-06-15 13:27:53 +0000380 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +0000381 // TODO should we report?
382 }
383 }
384
385 //
386 // Calculate the bundle symbolic name if it is
387 // not set.
388 // 1. set
389 // 2. name of properties file (must be != bnd.bnd)
390 // 3. name of directory, which is usualy project name
391 //
392 String bsn = getBsn();
393 if (main.getValue(BUNDLE_SYMBOLICNAME) == null) {
394 main.putValue(BUNDLE_SYMBOLICNAME, bsn);
395 }
396
397 //
398 // Use the same name for the bundle name as BSN when
399 // the bundle name is not set
400 //
401 if (main.getValue(BUNDLE_NAME) == null) {
402 main.putValue(BUNDLE_NAME, bsn);
403 }
404
405 if (main.getValue(BUNDLE_VERSION) == null)
406 main.putValue(BUNDLE_VERSION, "0");
407
408 // Copy old values into new manifest, when they
409 // exist in the old one, but not in the new one
410 merge(manifest, dot.getManifest());
411
412 // Remove all the headers mentioned in -removeheaders
413 Instructions instructions = new Instructions(getProperty(REMOVEHEADERS));
414 Collection<Object> result = instructions.select(main.keySet(), false);
415 main.keySet().removeAll(result);
416
417 dot.setManifest(manifest);
418 return manifest;
419 }
420
421 /**
422 * Parse the namesection as instructions and then match them against the
Stuart McCulloch2286f232012-06-15 13:27:53 +0000423 * current set of resources For example:
Stuart McCullochbb014372012-06-07 21:57:32 +0000424 *
425 * <pre>
426 * -namesection: *;baz=true, abc/def/bar/X.class=3
427 * </pre>
428 *
429 * The raw value of {@link Constants#NAMESECTION} is used but the values of
430 * the attributes are replaced where @ is set to the resource name. This
431 * allows macro to operate on the resource
Stuart McCullochbb014372012-06-07 21:57:32 +0000432 */
433
434 private void doNamesection(Jar dot, Manifest manifest) {
435
436 Parameters namesection = parseHeader(getProperties().getProperty(NAMESECTION));
437 Instructions instructions = new Instructions(namesection);
438 Set<String> resources = new HashSet<String>(dot.getResources().keySet());
439
440 //
441 // For each instruction, iterator over the resources and filter
442 // them. If a resource matches, it must be removed even if the
443 // instruction is negative. If positive, add a name section
444 // to the manifest for the given resource name. Then add all
445 // attributes from the instruction to that name section.
446 //
Stuart McCulloch2286f232012-06-15 13:27:53 +0000447 for (Map.Entry<Instruction,Attrs> instr : instructions.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000448 boolean matched = false;
449
450 // For each instruction
451
452 for (Iterator<String> i = resources.iterator(); i.hasNext();) {
453 String path = i.next();
454 // For each resource
455
456 if (instr.getKey().matches(path)) {
457
458 // Instruction matches the resource
459
460 matched = true;
461 if (!instr.getKey().isNegated()) {
462
463 // Positive match, add the attributes
464
465 Attributes attrs = manifest.getAttributes(path);
466 if (attrs == null) {
467 attrs = new Attributes();
468 manifest.getEntries().put(path, attrs);
469 }
470
471 //
472 // Add all the properties from the instruction to the
473 // name section
474 //
475
Stuart McCulloch2286f232012-06-15 13:27:53 +0000476 for (Map.Entry<String,String> property : instr.getValue().entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000477 setProperty("@", path);
478 try {
479 String processed = getReplacer().process(property.getValue());
480 attrs.putValue(property.getKey(), processed);
481 }
482 finally {
483 unsetProperty("@");
484 }
485 }
486 }
487 i.remove();
488 }
489 }
490
491 if (!matched && resources.size() > 0)
Stuart McCulloch2286f232012-06-15 13:27:53 +0000492 warning("The instruction %s in %s did not match any resources", instr.getKey(), NAMESECTION);
Stuart McCullochbb014372012-06-07 21:57:32 +0000493 }
494
495 }
496
497 /**
498 * This method is called when the header starts with a @, signifying a name
499 * section header. The name part is defined by replacing all the @ signs to
500 * a /, removing the first and the last, and using the last part as header
501 * name:
502 *
503 * <pre>
504 * &#064;org@osgi@service@event@Implementation-Title
505 * </pre>
506 *
507 * This will be the header Implementation-Title in the
508 * org/osgi/service/event named section.
509 *
510 * @param manifest
511 * @param header
512 */
513 private void doNameSection(Manifest manifest, String header) {
514 String path = header.replace('@', '/');
515 int n = path.lastIndexOf('/');
516 // Must succeed because we start with @
517 String name = path.substring(n + 1);
518 // Skip first /
519 path = path.substring(1, n);
520 if (name.length() != 0 && path.length() != 0) {
521 Attributes attrs = manifest.getAttributes(path);
522 if (attrs == null) {
523 attrs = new Attributes();
524 manifest.getEntries().put(path, attrs);
525 }
526 attrs.putValue(name, getProperty(header));
Stuart McCulloch2286f232012-06-15 13:27:53 +0000527 } else {
528 warning("Invalid header (starts with @ but does not seem to be for the Name section): %s", header);
Stuart McCullochbb014372012-06-07 21:57:32 +0000529 }
530 }
531
532 /**
533 * Clear the key part of a header. I.e. remove everything from the first ';'
534 *
535 * @param value
536 * @return
537 */
538 public String getBsn() {
539 String value = getProperty(BUNDLE_SYMBOLICNAME);
540 if (value == null) {
541 if (getPropertiesFile() != null)
542 value = getPropertiesFile().getName();
543
544 String projectName = getBase().getName();
545 if (value == null || value.equals("bnd.bnd")) {
546 value = projectName;
Stuart McCulloch2286f232012-06-15 13:27:53 +0000547 } else if (value.endsWith(".bnd")) {
548 value = value.substring(0, value.length() - 4);
549 if (!value.startsWith(getBase().getName()))
550 value = projectName + "." + value;
Stuart McCullochbb014372012-06-07 21:57:32 +0000551 }
Stuart McCullochbb014372012-06-07 21:57:32 +0000552 }
553
554 if (value == null)
555 return "untitled";
556
557 int n = value.indexOf(';');
558 if (n > 0)
559 value = value.substring(0, n);
560 return value.trim();
561 }
562
563 public String _bsn(String args[]) {
564 return getBsn();
565 }
566
567 /**
568 * Calculate an export header solely based on the contents of a JAR file
569 *
Stuart McCulloch2286f232012-06-15 13:27:53 +0000570 * @param bundle
571 * The jar file to analyze
Stuart McCullochbb014372012-06-07 21:57:32 +0000572 * @return
573 */
574 public String calculateExportsFromContents(Jar bundle) {
575 String ddel = "";
576 StringBuilder sb = new StringBuilder();
Stuart McCulloch2286f232012-06-15 13:27:53 +0000577 Map<String,Map<String,Resource>> map = bundle.getDirectories();
Stuart McCullochbb014372012-06-07 21:57:32 +0000578 for (Iterator<String> i = map.keySet().iterator(); i.hasNext();) {
579 String directory = i.next();
580 if (directory.equals("META-INF") || directory.startsWith("META-INF/"))
581 continue;
582 if (directory.equals("OSGI-OPT") || directory.startsWith("OSGI-OPT/"))
583 continue;
584 if (directory.equals("/"))
585 continue;
586
587 if (directory.endsWith("/"))
588 directory = directory.substring(0, directory.length() - 1);
589
590 directory = directory.replace('/', '.');
591 sb.append(ddel);
592 sb.append(directory);
593 ddel = ",";
594 }
595 return sb.toString();
596 }
597
598 public Packages getContained() {
599 return contained;
600 }
601
602 public Packages getExports() {
603 return exports;
604 }
605
606 public Packages getImports() {
607 return imports;
608 }
609
610 public Jar getJar() {
611 return dot;
612 }
613
614 public Packages getReferred() {
615 return referred;
616 }
617
618 /**
619 * Return the set of unreachable code depending on exports and the bundle
620 * activator.
621 *
622 * @return
623 */
624 public Set<PackageRef> getUnreachable() {
625 Set<PackageRef> unreachable = new HashSet<PackageRef>(uses.keySet()); // all
626 for (Iterator<PackageRef> r = exports.keySet().iterator(); r.hasNext();) {
627 PackageRef packageRef = r.next();
628 removeTransitive(packageRef, unreachable);
629 }
630 if (activator != null) {
631 removeTransitive(activator.getPackageRef(), unreachable);
632 }
633 return unreachable;
634 }
635
Stuart McCulloch2286f232012-06-15 13:27:53 +0000636 public MultiMap<PackageRef,PackageRef> getUses() {
Stuart McCullochbb014372012-06-07 21:57:32 +0000637 return uses;
638 }
639
640 /**
641 * Get the version for this bnd
642 *
643 * @return version or unknown.
644 */
645 public String getBndVersion() {
Stuart McCullochffa8aaf2012-06-17 20:38:35 +0000646 return getBndInfo("Bundle-Version", "<unknown>");
Stuart McCullochbb014372012-06-07 21:57:32 +0000647 }
648
649 public long getBndLastModified() {
Stuart McCullochffa8aaf2012-06-17 20:38:35 +0000650 String time = getBndInfo("Bnd-LastModified", "0");
Stuart McCullochbb014372012-06-07 21:57:32 +0000651 try {
652 return Long.parseLong(time);
653 }
654 catch (Exception e) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000655 // Ignore
Stuart McCullochbb014372012-06-07 21:57:32 +0000656 }
657 return 0;
658 }
659
660 public String getBndInfo(String key, String defaultValue) {
Stuart McCullochffa8aaf2012-06-17 20:38:35 +0000661 if (bndInfo == null) {
662 try {
663 bndInfo = new Manifest(getClass().getResourceAsStream("META-INF/MANIFEST.MF"));
664 }
665 catch (Exception e) {
666 return defaultValue;
Stuart McCullochbb014372012-06-07 21:57:32 +0000667 }
668 }
Stuart McCullochffa8aaf2012-06-17 20:38:35 +0000669 String value = bndInfo.getMainAttributes().getValue(key);
670 if (value == null)
671 return defaultValue;
672 return value;
Stuart McCullochbb014372012-06-07 21:57:32 +0000673 }
674
675 /**
676 * Merge the existing manifest with the instructions but do not override
677 * existing properties.
678 *
Stuart McCulloch2286f232012-06-15 13:27:53 +0000679 * @param manifest
680 * The manifest to merge with
Stuart McCullochbb014372012-06-07 21:57:32 +0000681 * @throws IOException
682 */
683 public void mergeManifest(Manifest manifest) throws IOException {
684 if (manifest != null) {
685 Attributes attributes = manifest.getMainAttributes();
686 for (Iterator<Object> i = attributes.keySet().iterator(); i.hasNext();) {
687 Name name = (Name) i.next();
688 String key = name.toString();
689 // Dont want instructions
690 if (key.startsWith("-"))
691 continue;
692
693 if (getProperty(key) == null)
694 setProperty(key, attributes.getValue(name));
695 }
696 }
697 }
698
699 public void setBase(File file) {
700 super.setBase(file);
701 getProperties().put("project.dir", getBase().getAbsolutePath());
702 }
703
704 /**
705 * Set the classpath for this analyzer by file.
706 *
707 * @param classpath
708 * @throws IOException
709 */
710 public void setClasspath(File[] classpath) throws IOException {
711 List<Jar> list = new ArrayList<Jar>();
712 for (int i = 0; i < classpath.length; i++) {
713 if (classpath[i].exists()) {
714 Jar current = new Jar(classpath[i]);
715 list.add(current);
Stuart McCulloch2286f232012-06-15 13:27:53 +0000716 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +0000717 error("Missing file on classpath: %s", classpath[i]);
718 }
719 }
720 for (Iterator<Jar> i = list.iterator(); i.hasNext();) {
721 addClasspath(i.next());
722 }
723 }
724
725 public void setClasspath(Jar[] classpath) {
726 for (int i = 0; i < classpath.length; i++) {
727 addClasspath(classpath[i]);
728 }
729 }
730
731 public void setClasspath(String[] classpath) {
732 for (int i = 0; i < classpath.length; i++) {
733 Jar jar = getJarFromName(classpath[i], " setting classpath");
734 if (jar != null)
735 addClasspath(jar);
736 }
737 }
738
739 /**
740 * Set the JAR file we are going to work in. This will read the JAR in
741 * memory.
742 *
743 * @param jar
744 * @return
745 * @throws IOException
746 */
747 public Jar setJar(File jar) throws IOException {
748 Jar jarx = new Jar(jar);
749 addClose(jarx);
750 return setJar(jarx);
751 }
752
753 /**
754 * Set the JAR directly we are going to work on.
755 *
756 * @param jar
757 * @return
758 */
759 public Jar setJar(Jar jar) {
760 if (dot != null)
761 removeClose(dot);
762
763 this.dot = jar;
764 if (dot != null)
765 addClose(dot);
766
767 return jar;
768 }
769
770 protected void begin() {
771 if (inited == false) {
772 inited = true;
773 super.begin();
774
775 updateModified(getBndLastModified(), "bnd last modified");
776 verifyManifestHeadersCase(getProperties());
777
778 }
779 }
780
781 /**
782 * Try to get a Jar from a file name/path or a url, or in last resort from
783 * the classpath name part of their files.
784 *
Stuart McCulloch2286f232012-06-15 13:27:53 +0000785 * @param name
786 * URL or filename relative to the base
787 * @param from
788 * Message identifying the caller for errors
Stuart McCullochbb014372012-06-07 21:57:32 +0000789 * @return null or a Jar with the contents for the name
790 */
791 Jar getJarFromName(String name, String from) {
792 File file = new File(name);
793 if (!file.isAbsolute())
794 file = new File(getBase(), name);
795
796 if (file.exists())
797 try {
798 Jar jar = new Jar(file);
799 addClose(jar);
800 return jar;
801 }
802 catch (Exception e) {
803 error("Exception in parsing jar file for " + from + ": " + name + " " + e);
804 }
805 // It is not a file ...
806 try {
807 // Lets try a URL
808 URL url = new URL(name);
809 Jar jar = new Jar(fileName(url.getPath()));
810 addClose(jar);
811 URLConnection connection = url.openConnection();
812 InputStream in = connection.getInputStream();
813 long lastModified = connection.getLastModified();
814 if (lastModified == 0)
815 // We assume the worst :-(
816 lastModified = System.currentTimeMillis();
817 EmbeddedResource.build(jar, in, lastModified);
818 in.close();
819 return jar;
820 }
821 catch (IOException ee) {
822 // Check if we have files on the classpath
823 // that have the right name, allows us to specify those
824 // names instead of the full path.
825 for (Iterator<Jar> cp = getClasspath().iterator(); cp.hasNext();) {
826 Jar entry = cp.next();
827 if (entry.getSource() != null && entry.getSource().getName().equals(name)) {
828 return entry;
829 }
830 }
831 // error("Can not find jar file for " + from + ": " + name);
832 }
833 return null;
834 }
835
836 private String fileName(String path) {
837 int n = path.lastIndexOf('/');
838 if (n > 0)
839 return path.substring(n + 1);
840 return path;
841 }
842
843 /**
Stuart McCullochbb014372012-06-07 21:57:32 +0000844 * @param manifests
845 * @throws Exception
846 */
Stuart McCulloch2286f232012-06-15 13:27:53 +0000847 private void merge(Manifest result, Manifest old) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000848 if (old != null) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000849 for (Iterator<Map.Entry<Object,Object>> e = old.getMainAttributes().entrySet().iterator(); e.hasNext();) {
850 Map.Entry<Object,Object> entry = e.next();
Stuart McCullochbb014372012-06-07 21:57:32 +0000851 Attributes.Name name = (Attributes.Name) entry.getKey();
852 String value = (String) entry.getValue();
853 if (name.toString().equalsIgnoreCase("Created-By"))
854 name = new Attributes.Name("Originally-Created-By");
855 if (!result.getMainAttributes().containsKey(name))
856 result.getMainAttributes().put(name, value);
857 }
858
859 // do not overwrite existing entries
Stuart McCulloch2286f232012-06-15 13:27:53 +0000860 Map<String,Attributes> oldEntries = old.getEntries();
861 Map<String,Attributes> newEntries = result.getEntries();
862 for (Iterator<Map.Entry<String,Attributes>> e = oldEntries.entrySet().iterator(); e.hasNext();) {
863 Map.Entry<String,Attributes> entry = e.next();
Stuart McCullochbb014372012-06-07 21:57:32 +0000864 if (!newEntries.containsKey(entry.getKey())) {
865 newEntries.put(entry.getKey(), entry.getValue());
866 }
867 }
868 }
869 }
870
871 /**
872 * Bnd is case sensitive for the instructions so we better check people are
873 * not using an invalid case. We do allow this to set headers that should
874 * not be processed by us but should be used by the framework.
875 *
Stuart McCulloch2286f232012-06-15 13:27:53 +0000876 * @param properties
877 * Properties to verify.
Stuart McCullochbb014372012-06-07 21:57:32 +0000878 */
879
880 void verifyManifestHeadersCase(Properties properties) {
881 for (Iterator<Object> i = properties.keySet().iterator(); i.hasNext();) {
882 String header = (String) i.next();
883 for (int j = 0; j < headers.length; j++) {
884 if (!headers[j].equals(header) && headers[j].equalsIgnoreCase(header)) {
885 warning("Using a standard OSGi header with the wrong case (bnd is case sensitive!), using: "
886 + header + " and expecting: " + headers[j]);
887 break;
888 }
889 }
890 }
891 }
892
893 /**
894 * We will add all exports to the imports unless there is a -noimport
895 * directive specified on an export. This directive is skipped for the
Stuart McCulloch2286f232012-06-15 13:27:53 +0000896 * manifest. We also remove any version parameter so that augmentImports can
897 * do the version policy. The following method is really tricky and evolved
898 * over time. Coming from the original background of OSGi, it was a weird
899 * idea for me to have a public package that should not be substitutable. I
900 * was so much convinced that this was the right rule that I rücksichtlos
901 * imported them all. Alas, the real world was more subtle than that. It
902 * turns out that it is not a good idea to always import. First, there must
903 * be a need to import, i.e. there must be a contained package that refers
904 * to the exported package for it to make use importing that package.
905 * Second, if an exported package refers to an internal package than it
906 * should not be imported. Additionally, it is necessary to treat the
907 * exports in groups. If an exported package refers to another exported
908 * packages than it must be in the same group. A framework can only
909 * substitute exports for imports for the whole of such a group. WHY?????
910 * Not clear anymore ...
Stuart McCullochbb014372012-06-07 21:57:32 +0000911 */
912 Packages doExportsToImports(Packages exports) {
913
914 // private packages = contained - exported.
915 Set<PackageRef> privatePackages = new HashSet<PackageRef>(contained.keySet());
916 privatePackages.removeAll(exports.keySet());
917
918 // private references = ∀ p : private packages | uses(p)
919 Set<PackageRef> privateReferences = newSet();
920 for (PackageRef p : privatePackages) {
921 Collection<PackageRef> uses = this.uses.get(p);
922 if (uses != null)
923 privateReferences.addAll(uses);
924 }
925
926 // Assume we are going to export all exported packages
927 Set<PackageRef> toBeImported = new HashSet<PackageRef>(exports.keySet());
928
929 // Remove packages that are not referenced privately
930 toBeImported.retainAll(privateReferences);
931
932 // Not necessary to import anything that is already
933 // imported in the Import-Package statement.
934 // TODO toBeImported.removeAll(imports.keySet());
935
936 // Remove exported packages that are referring to
937 // private packages.
938 // Each exported package has a uses clause. We just use
939 // the used packages for each exported package to find out
940 // if it refers to an internal package.
941 //
942
943 for (Iterator<PackageRef> i = toBeImported.iterator(); i.hasNext();) {
944 PackageRef next = i.next();
945 Collection<PackageRef> usedByExportedPackage = this.uses.get(next);
946
947 for (PackageRef privatePackage : privatePackages) {
948 if (usedByExportedPackage.contains(privatePackage)) {
949 i.remove();
950 break;
951 }
952 }
953 }
954
955 // Clean up attributes and generate result map
956 Packages result = new Packages();
957 for (Iterator<PackageRef> i = toBeImported.iterator(); i.hasNext();) {
958 PackageRef ep = i.next();
959 Attrs parameters = exports.get(ep);
960
Stuart McCullochffa8aaf2012-06-17 20:38:35 +0000961 String noimport = parameters == null ? null : parameters.get(NO_IMPORT_DIRECTIVE);
Stuart McCullochbb014372012-06-07 21:57:32 +0000962 if (noimport != null && noimport.equalsIgnoreCase("true"))
963 continue;
964
965 // // we can't substitute when there is no version
966 // String version = parameters.get(VERSION_ATTRIBUTE);
967 // if (version == null) {
968 // if (isPedantic())
969 // warning(
970 // "Cannot automatically import exported package %s because it has no version defined",
971 // ep);
972 // continue;
973 // }
974
975 parameters = new Attrs();
976 parameters.remove(VERSION_ATTRIBUTE);
977 result.put(ep, parameters);
978 }
979 return result;
980 }
981
982 public boolean referred(PackageRef packageName) {
983 // return true;
Stuart McCulloch2286f232012-06-15 13:27:53 +0000984 for (Map.Entry<PackageRef,List<PackageRef>> contained : uses.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000985 if (!contained.getKey().equals(packageName)) {
986 if (contained.getValue().contains(packageName))
987 return true;
988 }
989 }
990 return false;
991 }
992
993 /**
Stuart McCullochbb014372012-06-07 21:57:32 +0000994 * @param jar
995 */
996 private void getExternalExports(Jar jar, Packages classpathExports) {
997 try {
998 Manifest m = jar.getManifest();
999 if (m != null) {
1000 Domain domain = Domain.domain(m);
1001 Parameters exported = domain.getExportPackage();
Stuart McCulloch2286f232012-06-15 13:27:53 +00001002 for (Entry<String,Attrs> e : exported.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001003 PackageRef ref = getPackageRef(e.getKey());
1004 if (!classpathExports.containsKey(ref)) {
1005 // TODO e.getValue().put(SOURCE_DIRECTIVE,
1006 // jar.getBsn()+"-"+jar.getVersion());
1007
1008 classpathExports.put(ref, e.getValue());
1009 }
1010 }
1011 }
1012 }
1013 catch (Exception e) {
1014 warning("Erroneous Manifest for " + jar + " " + e);
1015 }
1016 }
1017
1018 /**
1019 * Find some more information about imports in manifest and other places. It
1020 * is assumed that the augmentsExports has already copied external attrs
1021 * from the classpathExports.
1022 *
1023 * @throws Exception
1024 */
1025 void augmentImports(Packages imports, Packages exports) throws Exception {
1026 List<PackageRef> noimports = Create.list();
1027 Set<PackageRef> provided = findProvidedPackages();
1028
1029 for (PackageRef packageRef : imports.keySet()) {
1030 String packageName = packageRef.getFQN();
1031
1032 setProperty(CURRENT_PACKAGE, packageName);
1033 try {
1034 Attrs importAttributes = imports.get(packageRef);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001035 Attrs exportAttributes = exports.get(packageRef, classpathExports.get(packageRef, new Attrs()));
Stuart McCullochbb014372012-06-07 21:57:32 +00001036
1037 String exportVersion = exportAttributes.getVersion();
1038 String importRange = importAttributes.getVersion();
1039
1040 if (exportVersion == null) {
1041 // TODO Should check if the source is from a bundle.
1042
Stuart McCulloch2286f232012-06-15 13:27:53 +00001043 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001044
1045 //
1046 // Version Policy - Import version substitution. We
1047 // calculate the export version and then allow the
1048 // import version attribute to use it in a substitution
1049 // by using a ${@} macro. The export version can
1050 // be defined externally or locally
1051 //
1052
1053 boolean provider = isTrue(importAttributes.get(PROVIDE_DIRECTIVE))
Stuart McCulloch2286f232012-06-15 13:27:53 +00001054 || isTrue(exportAttributes.get(PROVIDE_DIRECTIVE)) || provided.contains(packageRef);
Stuart McCullochbb014372012-06-07 21:57:32 +00001055
1056 exportVersion = cleanupVersion(exportVersion);
1057
1058 try {
1059 setProperty("@", exportVersion);
1060
1061 if (importRange != null) {
1062 importRange = cleanupVersion(importRange);
1063 importRange = getReplacer().process(importRange);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001064 } else
Stuart McCullochbb014372012-06-07 21:57:32 +00001065 importRange = getVersionPolicy(provider);
1066
1067 }
1068 finally {
1069 unsetProperty("@");
1070 }
1071 importAttributes.put(VERSION_ATTRIBUTE, importRange);
1072 }
1073
1074 //
1075 // Check if exporter has mandatory attributes
1076 //
1077 String mandatory = exportAttributes.get(MANDATORY_DIRECTIVE);
1078 if (mandatory != null) {
1079 String[] attrs = mandatory.split("\\s*,\\s*");
1080 for (int i = 0; i < attrs.length; i++) {
1081 if (!importAttributes.containsKey(attrs[i]))
1082 importAttributes.put(attrs[i], exportAttributes.get(attrs[i]));
1083 }
1084 }
1085
1086 if (exportAttributes.containsKey(IMPORT_DIRECTIVE))
1087 importAttributes.put(IMPORT_DIRECTIVE, exportAttributes.get(IMPORT_DIRECTIVE));
1088
1089 fixupAttributes(importAttributes);
1090 removeAttributes(importAttributes);
1091
1092 String result = importAttributes.get(Constants.VERSION_ATTRIBUTE);
1093 if (result == null)
1094 noimports.add(packageRef);
1095 }
1096 finally {
1097 unsetProperty(CURRENT_PACKAGE);
1098 }
1099 }
1100
1101 if (isPedantic() && noimports.size() != 0) {
1102 warning("Imports that lack version ranges: %s", noimports);
1103 }
1104 }
1105
1106 /**
1107 * Find the packages we depend on, where we implement an interface that is a
1108 * Provider Type. These packages, when we import them, must use the provider
1109 * policy.
1110 *
1111 * @throws Exception
1112 */
1113 Set<PackageRef> findProvidedPackages() throws Exception {
1114 Set<PackageRef> providers = Create.set();
1115 Set<TypeRef> cached = Create.set();
1116
1117 for (Clazz c : classspace.values()) {
1118 TypeRef[] interfaces = c.getInterfaces();
1119 if (interfaces != null)
1120 for (TypeRef t : interfaces)
1121 if (cached.contains(t) || isProvider(t)) {
1122 cached.add(t);
1123 providers.add(t.getPackageRef());
1124 }
1125 }
1126 return providers;
1127 }
1128
1129 private boolean isProvider(TypeRef t) throws Exception {
1130 Clazz c = findClass(t);
1131 if (c == null)
1132 return false;
1133
1134 if (c.annotations == null)
1135 return false;
1136
1137 TypeRef pt = getTypeRefFromFQN(ProviderType.class.getName());
1138 boolean result = c.annotations.contains(pt);
1139 return result;
1140 }
1141
1142 /**
1143 * Provide any macro substitutions and versions for exported packages.
1144 */
1145
1146 void augmentExports(Packages exports) {
1147 for (PackageRef packageRef : exports.keySet()) {
1148 String packageName = packageRef.getFQN();
1149 setProperty(CURRENT_PACKAGE, packageName);
1150 try {
1151 Attrs attributes = exports.get(packageRef);
1152 Attrs exporterAttributes = classpathExports.get(packageRef);
1153 if (exporterAttributes == null)
1154 continue;
1155
Stuart McCulloch2286f232012-06-15 13:27:53 +00001156 for (Map.Entry<String,String> entry : exporterAttributes.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001157 String key = entry.getKey();
1158 if (key.equalsIgnoreCase(SPECIFICATION_VERSION))
1159 key = VERSION_ATTRIBUTE;
1160
1161 // dont overwrite and no directives
1162 if (!key.endsWith(":") && !attributes.containsKey(key)) {
1163 attributes.put(key, entry.getValue());
1164 }
1165 }
1166
1167 fixupAttributes(attributes);
1168 removeAttributes(attributes);
1169
1170 }
1171 finally {
1172 unsetProperty(CURRENT_PACKAGE);
1173 }
1174 }
1175 }
1176
1177 /**
Stuart McCulloch2286f232012-06-15 13:27:53 +00001178 * Fixup Attributes Execute any macros on an export and
Stuart McCullochbb014372012-06-07 21:57:32 +00001179 */
1180
1181 void fixupAttributes(Attrs attributes) {
1182 // Convert any attribute values that have macros.
1183 for (String key : attributes.keySet()) {
1184 String value = attributes.get(key);
1185 if (value.indexOf('$') >= 0) {
1186 value = getReplacer().process(value);
1187 attributes.put(key, value);
1188 }
1189 }
1190
1191 }
1192
1193 /**
1194 * Remove the attributes mentioned in the REMOVE_ATTRIBUTE_DIRECTIVE. You
1195 * can add a remove-attribute: directive with a regular expression for
1196 * attributes that need to be removed. We also remove all attributes that
1197 * have a value of !. This allows you to use macros with ${if} to remove
1198 * values.
1199 */
1200
1201 void removeAttributes(Attrs attributes) {
1202 String remove = attributes.remove(REMOVE_ATTRIBUTE_DIRECTIVE);
1203
1204 if (remove != null) {
1205 Instructions removeInstr = new Instructions(remove);
1206 attributes.keySet().removeAll(removeInstr.select(attributes.keySet(), false));
1207 }
1208
1209 // Remove any ! valued attributes
Stuart McCulloch2286f232012-06-15 13:27:53 +00001210 for (Iterator<Entry<String,String>> i = attributes.entrySet().iterator(); i.hasNext();) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001211 String v = i.next().getValue();
1212 if (v.equals("!"))
1213 i.remove();
1214 }
1215 }
1216
1217 /**
1218 * Calculate a version from a version policy.
1219 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00001220 * @param version
1221 * The actual exported version
1222 * @param impl
1223 * true for implementations and false for clients
Stuart McCullochbb014372012-06-07 21:57:32 +00001224 */
1225
1226 String calculateVersionRange(String version, boolean impl) {
1227 setProperty("@", version);
1228 try {
1229 return getVersionPolicy(impl);
1230 }
1231 finally {
1232 unsetProperty("@");
1233 }
1234 }
1235
1236 /**
1237 * Add the uses clauses. This method iterates over the exports and cal
1238 *
1239 * @param exports
1240 * @param uses
1241 * @throws MojoExecutionException
1242 */
Stuart McCulloch2286f232012-06-15 13:27:53 +00001243 void doUses(Packages exports, MultiMap<PackageRef,PackageRef> uses, Packages imports) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001244 if ("true".equalsIgnoreCase(getProperty(NOUSES)))
1245 return;
1246
1247 for (Iterator<PackageRef> i = exports.keySet().iterator(); i.hasNext();) {
1248 PackageRef packageRef = i.next();
1249 String packageName = packageRef.getFQN();
1250 setProperty(CURRENT_PACKAGE, packageName);
1251 try {
1252 doUses(packageRef, exports, uses, imports);
1253 }
1254 finally {
1255 unsetProperty(CURRENT_PACKAGE);
1256 }
1257
1258 }
1259 }
1260
1261 /**
1262 * @param packageName
1263 * @param exports
1264 * @param uses
1265 * @param imports
1266 */
Stuart McCulloch2286f232012-06-15 13:27:53 +00001267 protected void doUses(PackageRef packageRef, Packages exports, MultiMap<PackageRef,PackageRef> uses,
1268 Packages imports) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001269 Attrs clause = exports.get(packageRef);
1270
1271 // Check if someone already set the uses: directive
1272 String override = clause.get(USES_DIRECTIVE);
1273 if (override == null)
1274 override = USES_USES;
1275
1276 // Get the used packages
1277 Collection<PackageRef> usedPackages = uses.get(packageRef);
1278
1279 if (usedPackages != null) {
1280
1281 // Only do a uses on exported or imported packages
1282 // and uses should also not contain our own package
1283 // name
1284 Set<PackageRef> sharedPackages = new HashSet<PackageRef>();
1285 sharedPackages.addAll(imports.keySet());
1286 sharedPackages.addAll(exports.keySet());
1287 sharedPackages.retainAll(usedPackages);
1288 sharedPackages.remove(packageRef);
1289
1290 StringBuilder sb = new StringBuilder();
1291 String del = "";
1292 for (Iterator<PackageRef> u = sharedPackages.iterator(); u.hasNext();) {
1293 PackageRef usedPackage = u.next();
1294 if (!usedPackage.isJava()) {
1295 sb.append(del);
1296 sb.append(usedPackage.getFQN());
1297 del = ",";
1298 }
1299 }
1300 if (override.indexOf('$') >= 0) {
1301 setProperty(CURRENT_USES, sb.toString());
1302 override = getReplacer().process(override);
1303 unsetProperty(CURRENT_USES);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001304 } else
Stuart McCullochbb014372012-06-07 21:57:32 +00001305 // This is for backward compatibility 0.0.287
1306 // can be deprecated over time
Stuart McCulloch2286f232012-06-15 13:27:53 +00001307 override = override.replaceAll(USES_USES, Matcher.quoteReplacement(sb.toString())).trim();
Stuart McCullochbb014372012-06-07 21:57:32 +00001308
1309 if (override.endsWith(","))
1310 override = override.substring(0, override.length() - 1);
1311 if (override.startsWith(","))
1312 override = override.substring(1);
1313 if (override.length() > 0) {
1314 clause.put(USES_DIRECTIVE, override);
1315 }
1316 }
1317 }
1318
1319 /**
1320 * Transitively remove all elemens from unreachable through the uses link.
1321 *
1322 * @param name
1323 * @param unreachable
1324 */
1325 void removeTransitive(PackageRef name, Set<PackageRef> unreachable) {
1326 if (!unreachable.contains(name))
1327 return;
1328
1329 unreachable.remove(name);
1330
1331 List<PackageRef> ref = uses.get(name);
1332 if (ref != null) {
1333 for (Iterator<PackageRef> r = ref.iterator(); r.hasNext();) {
1334 PackageRef element = r.next();
1335 removeTransitive(element, unreachable);
1336 }
1337 }
1338 }
1339
1340 /**
1341 * Helper method to set the package info resource
1342 *
1343 * @param dir
1344 * @param key
1345 * @param value
1346 * @throws Exception
1347 */
Stuart McCulloch2286f232012-06-15 13:27:53 +00001348 void setPackageInfo(PackageRef packageRef, Resource r, Packages classpathExports) throws Exception {
Stuart McCullochbb014372012-06-07 21:57:32 +00001349 if (r == null)
1350 return;
1351
1352 Properties p = new Properties();
1353 InputStream in = r.openInputStream();
1354 try {
1355 p.load(in);
1356 }
1357 finally {
1358 in.close();
1359 }
1360 Attrs map = classpathExports.get(packageRef);
1361 if (map == null) {
1362 classpathExports.put(packageRef, map = new Attrs());
1363 }
Stuart McCullochffa8aaf2012-06-17 20:38:35 +00001364 for (Enumeration<String> t = (Enumeration<String>) p.propertyNames(); t.hasMoreElements();) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001365 String key = t.nextElement();
1366 String value = map.get(key);
1367 if (value == null) {
1368 value = p.getProperty(key);
1369
1370 // Messy, to allow directives we need to
1371 // allow the value to start with a ':' since we cannot
1372 // encode this in a property name
1373
1374 if (value.startsWith(":")) {
1375 key = key + ":";
1376 value = value.substring(1);
1377 }
1378 map.put(key, value);
1379 }
1380 }
1381 }
1382
1383 public void close() {
1384 if (diagnostics) {
1385 PrintStream out = System.err;
1386 out.printf("Current directory : %s%n", new File("").getAbsolutePath());
1387 out.println("Classpath used");
1388 for (Jar jar : getClasspath()) {
1389 out.printf("File : %s%n", jar.getSource());
Stuart McCulloch2286f232012-06-15 13:27:53 +00001390 out.printf("File abs path : %s%n", jar.getSource().getAbsolutePath());
Stuart McCullochbb014372012-06-07 21:57:32 +00001391 out.printf("Name : %s%n", jar.getName());
Stuart McCulloch2286f232012-06-15 13:27:53 +00001392 Map<String,Map<String,Resource>> dirs = jar.getDirectories();
1393 for (Map.Entry<String,Map<String,Resource>> entry : dirs.entrySet()) {
1394 Map<String,Resource> dir = entry.getValue();
Stuart McCullochbb014372012-06-07 21:57:32 +00001395 String name = entry.getKey().replace('/', '.');
1396 if (dir != null) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001397 out.printf(" %-30s %d%n", name, dir.size());
1398 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001399 out.printf(" %-30s <<empty>>%n", name);
1400 }
1401 }
1402 }
1403 }
1404
1405 super.close();
1406 if (dot != null)
1407 dot.close();
1408
1409 if (classpath != null)
1410 for (Iterator<Jar> j = classpath.iterator(); j.hasNext();) {
1411 Jar jar = j.next();
1412 jar.close();
1413 }
1414 }
1415
1416 /**
1417 * Findpath looks through the contents of the JAR and finds paths that end
Stuart McCulloch2286f232012-06-15 13:27:53 +00001418 * with the given regular expression ${findpath (; reg-expr (; replacement)?
1419 * )? }
Stuart McCullochbb014372012-06-07 21:57:32 +00001420 *
1421 * @param args
1422 * @return
1423 */
1424 public String _findpath(String args[]) {
1425 return findPath("findpath", args, true);
1426 }
1427
1428 public String _findname(String args[]) {
1429 return findPath("findname", args, false);
1430 }
1431
1432 String findPath(String name, String[] args, boolean fullPathName) {
1433 if (args.length > 3) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001434 warning("Invalid nr of arguments to " + name + " " + Arrays.asList(args) + ", syntax: ${" + name
1435 + " (; reg-expr (; replacement)? )? }");
Stuart McCullochbb014372012-06-07 21:57:32 +00001436 return null;
1437 }
1438
1439 String regexp = ".*";
1440 String replace = null;
1441
1442 switch (args.length) {
1443 case 3 :
1444 replace = args[2];
1445 //$FALL-THROUGH$
1446 case 2 :
1447 regexp = args[1];
1448 }
1449 StringBuilder sb = new StringBuilder();
1450 String del = "";
1451
1452 Pattern expr = Pattern.compile(regexp);
1453 for (Iterator<String> e = dot.getResources().keySet().iterator(); e.hasNext();) {
1454 String path = e.next();
1455 if (!fullPathName) {
1456 int n = path.lastIndexOf('/');
1457 if (n >= 0) {
1458 path = path.substring(n + 1);
1459 }
1460 }
1461
1462 Matcher m = expr.matcher(path);
1463 if (m.matches()) {
1464 if (replace != null)
1465 path = m.replaceAll(replace);
1466
1467 sb.append(del);
1468 sb.append(path);
1469 del = ", ";
1470 }
1471 }
1472 return sb.toString();
1473 }
1474
Stuart McCulloch2286f232012-06-15 13:27:53 +00001475 public void putAll(Map<String,String> additional, boolean force) {
1476 for (Iterator<Map.Entry<String,String>> i = additional.entrySet().iterator(); i.hasNext();) {
1477 Map.Entry<String,String> entry = i.next();
Stuart McCullochbb014372012-06-07 21:57:32 +00001478 if (force || getProperties().get(entry.getKey()) == null)
1479 setProperty(entry.getKey(), entry.getValue());
1480 }
1481 }
1482
1483 boolean firstUse = true;
1484
1485 public List<Jar> getClasspath() {
1486 if (firstUse) {
1487 firstUse = false;
1488 String cp = getProperty(CLASSPATH);
1489 if (cp != null)
1490 for (String s : split(cp)) {
1491 Jar jar = getJarFromName(s, "getting classpath");
1492 if (jar != null)
1493 addClasspath(jar);
1494 else
1495 warning("Cannot find entry on -classpath: %s", s);
1496 }
1497 }
1498 return classpath;
1499 }
1500
1501 public void addClasspath(Jar jar) {
1502 if (isPedantic() && jar.getResources().isEmpty())
1503 warning("There is an empty jar or directory on the classpath: " + jar.getName());
1504
1505 classpath.add(jar);
1506 }
1507
1508 public void addClasspath(Collection< ? > jars) throws IOException {
1509 for (Object jar : jars) {
1510 if (jar instanceof Jar)
1511 addClasspath((Jar) jar);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001512 else if (jar instanceof File)
1513 addClasspath((File) jar);
1514 else if (jar instanceof String)
1515 addClasspath(getFile((String) jar));
Stuart McCullochbb014372012-06-07 21:57:32 +00001516 else
Stuart McCulloch2286f232012-06-15 13:27:53 +00001517 error("Cannot convert to JAR to add to classpath %s. Not a File, Jar, or String", jar);
Stuart McCullochbb014372012-06-07 21:57:32 +00001518 }
1519 }
1520
1521 public void addClasspath(File cp) throws IOException {
1522 if (!cp.exists())
1523 warning("File on classpath that does not exist: " + cp);
1524 Jar jar = new Jar(cp);
1525 addClose(jar);
1526 classpath.add(jar);
1527 }
1528
1529 public void clear() {
1530 classpath.clear();
1531 }
1532
1533 public Jar getTarget() {
1534 return dot;
1535 }
1536
1537 private void analyzeBundleClasspath() throws Exception {
1538 Parameters bcp = getBundleClasspath();
1539
1540 if (bcp.isEmpty()) {
1541 analyzeJar(dot, "", true);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001542 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001543 boolean okToIncludeDirs = true;
1544
1545 for (String path : bcp.keySet()) {
1546 if (dot.getDirectories().containsKey(path)) {
1547 okToIncludeDirs = false;
1548 break;
1549 }
1550 }
1551
1552 for (String path : bcp.keySet()) {
1553 Attrs info = bcp.get(path);
1554
1555 if (path.equals(".")) {
1556 analyzeJar(dot, "", okToIncludeDirs);
1557 continue;
1558 }
1559 //
1560 // There are 3 cases:
1561 // - embedded JAR file
1562 // - directory
1563 // - error
1564 //
1565
1566 Resource resource = dot.getResource(path);
1567 if (resource != null) {
1568 try {
1569 Jar jar = new Jar(path);
1570 addClose(jar);
1571 EmbeddedResource.build(jar, resource);
1572 analyzeJar(jar, "", true);
1573 }
1574 catch (Exception e) {
1575 warning("Invalid bundle classpath entry: " + path + " " + e);
1576 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001577 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001578 if (dot.getDirectories().containsKey(path)) {
1579 // if directories are used, we should not have dot as we
1580 // would have the classes in these directories on the
1581 // class path twice.
1582 if (bcp.containsKey("."))
1583 warning("Bundle-ClassPath uses a directory '%s' as well as '.'. This means bnd does not know if a directory is a package.",
1584 path, path);
1585 analyzeJar(dot, Processor.appendPath(path) + "/", true);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001586 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001587 if (!"optional".equals(info.get(RESOLUTION_DIRECTIVE)))
1588 warning("No sub JAR or directory " + path);
1589 }
1590 }
1591 }
1592
1593 }
1594 }
1595
1596 /**
1597 * We traverse through all the classes that we can find and calculate the
1598 * contained and referred set and uses. This method ignores the Bundle
1599 * classpath.
1600 *
1601 * @param jar
1602 * @param contained
1603 * @param referred
1604 * @param uses
1605 * @throws IOException
1606 */
1607 private boolean analyzeJar(Jar jar, String prefix, boolean okToIncludeDirs) throws Exception {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001608 Map<String,Clazz> mismatched = new HashMap<String,Clazz>();
Stuart McCullochbb014372012-06-07 21:57:32 +00001609
1610 next: for (String path : jar.getResources().keySet()) {
1611 if (path.startsWith(prefix)) {
1612
1613 String relativePath = path.substring(prefix.length());
1614
1615 if (okToIncludeDirs) {
1616 int n = relativePath.lastIndexOf('/');
1617 if (n < 0)
1618 n = relativePath.length();
1619 String relativeDir = relativePath.substring(0, n);
1620
1621 PackageRef packageRef = getPackageRef(relativeDir);
1622 if (!packageRef.isMetaData() && !contained.containsKey(packageRef)) {
1623 contained.put(packageRef);
1624
1625 // For each package we encounter for the first
1626 // time. Unfortunately we can only do this once
1627 // we found a class since the bcp has a tendency
1628 // to overlap
1629 if (!packageRef.isMetaData()) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001630 Resource pinfo = jar.getResource(prefix + packageRef.getPath() + "/packageinfo");
Stuart McCullochbb014372012-06-07 21:57:32 +00001631 setPackageInfo(packageRef, pinfo, classpathExports);
1632 }
1633 }
1634 }
1635
1636 // Check class resources, we need to analyze them
1637 if (path.endsWith(".class")) {
1638 Resource resource = jar.getResource(path);
1639 Clazz clazz;
1640 Attrs info = null;
1641
1642 try {
1643 InputStream in = resource.openInputStream();
1644 clazz = new Clazz(this, path, resource);
1645 try {
1646 // Check if we have a package-info
1647 if (relativePath.endsWith("/package-info.class")) {
1648 // package-info can contain an Export annotation
1649 info = new Attrs();
1650 parsePackageInfoClass(clazz, info);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001651 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001652 // Otherwise we just parse it simply
1653 clazz.parseClassFile();
1654 }
1655 }
1656 finally {
1657 in.close();
1658 }
1659 }
1660 catch (Throwable e) {
1661 error("Invalid class file %s (%s)", e, relativePath, e);
1662 e.printStackTrace();
1663 continue next;
1664 }
1665
1666 String calculatedPath = clazz.getClassName().getPath();
1667 if (!calculatedPath.equals(relativePath)) {
1668 // If there is a mismatch we
1669 // warning
1670 if (okToIncludeDirs) // assume already reported
1671 mismatched.put(clazz.getAbsolutePath(), clazz);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001672 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001673 classspace.put(clazz.getClassName(), clazz);
1674 PackageRef packageRef = clazz.getClassName().getPackageRef();
1675
1676 if (!contained.containsKey(packageRef)) {
1677 contained.put(packageRef);
1678 if (!packageRef.isMetaData()) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001679 Resource pinfo = jar.getResource(prefix + packageRef.getPath() + "/packageinfo");
Stuart McCullochbb014372012-06-07 21:57:32 +00001680 setPackageInfo(packageRef, pinfo, classpathExports);
1681 }
1682 }
1683 if (info != null)
1684 contained.merge(packageRef, false, info);
1685
1686 Set<PackageRef> set = Create.set();
1687
1688 // Look at the referred packages
1689 // and copy them to our baseline
1690 for (PackageRef p : clazz.getReferred()) {
1691 referred.put(p);
1692 set.add(p);
1693 }
1694 set.remove(packageRef);
1695 uses.addAll(packageRef, set);
1696 }
1697 }
1698 }
1699 }
1700
1701 if (mismatched.size() > 0) {
1702 error("Classes found in the wrong directory: %s", mismatched);
1703 return false;
1704 }
1705 return true;
1706 }
1707
1708 static Pattern OBJECT_REFERENCE = Pattern.compile("L([^/]+/)*([^;]+);");
1709
1710 private void parsePackageInfoClass(final Clazz clazz, final Attrs info) throws Exception {
1711 clazz.parseClassFileWithCollector(new ClassDataCollector() {
1712 @Override
1713 public void annotation(Annotation a) {
1714 String name = a.name.getFQN();
1715 if (aQute.bnd.annotation.Version.class.getName().equals(name)) {
1716
1717 // Check version
1718 String version = a.get("value");
1719 if (!info.containsKey(Constants.VERSION_ATTRIBUTE)) {
1720 if (version != null) {
1721 version = getReplacer().process(version);
1722 if (Verifier.VERSION.matcher(version).matches())
1723 info.put(VERSION_ATTRIBUTE, version);
1724 else
Stuart McCulloch2286f232012-06-15 13:27:53 +00001725 error("Export annotation in %s has invalid version info: %s", clazz, version);
Stuart McCullochbb014372012-06-07 21:57:32 +00001726 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001727 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001728 // Verify this matches with packageinfo
1729 String presentVersion = info.get(VERSION_ATTRIBUTE);
1730 try {
1731 Version av = new Version(presentVersion);
1732 Version bv = new Version(version);
1733 if (!av.equals(bv)) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001734 error("Version from annotation for %s differs with packageinfo or Manifest", clazz
1735 .getClassName().getFQN());
Stuart McCullochbb014372012-06-07 21:57:32 +00001736 }
1737 }
1738 catch (Exception e) {
1739 // Ignore
1740 }
1741 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001742 } else if (name.equals(Export.class.getName())) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001743
Stuart McCulloch2286f232012-06-15 13:27:53 +00001744 // Check mandatory attributes
1745 Attrs attrs = doAttrbutes((Object[]) a.get(Export.MANDATORY), clazz, getReplacer());
1746 if (!attrs.isEmpty()) {
1747 info.putAll(attrs);
1748 info.put(MANDATORY_DIRECTIVE, Processor.join(attrs.keySet()));
1749 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001750
Stuart McCulloch2286f232012-06-15 13:27:53 +00001751 // Check optional attributes
1752 attrs = doAttrbutes((Object[]) a.get(Export.OPTIONAL), clazz, getReplacer());
1753 if (!attrs.isEmpty()) {
1754 info.putAll(attrs);
1755 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001756
Stuart McCulloch2286f232012-06-15 13:27:53 +00001757 // Check Included classes
1758 Object[] included = a.get(Export.INCLUDE);
1759 if (included != null && included.length > 0) {
1760 StringBuilder sb = new StringBuilder();
1761 String del = "";
1762 for (Object i : included) {
1763 Matcher m = OBJECT_REFERENCE.matcher((String) i);
1764 if (m.matches()) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001765 sb.append(del);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001766 sb.append(m.group(2));
Stuart McCullochbb014372012-06-07 21:57:32 +00001767 del = ",";
1768 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001769 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001770 info.put(INCLUDE_DIRECTIVE, sb.toString());
Stuart McCullochbb014372012-06-07 21:57:32 +00001771 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001772
1773 // Check Excluded classes
1774 Object[] excluded = a.get(Export.EXCLUDE);
1775 if (excluded != null && excluded.length > 0) {
1776 StringBuilder sb = new StringBuilder();
1777 String del = "";
1778 for (Object i : excluded) {
1779 Matcher m = OBJECT_REFERENCE.matcher((String) i);
1780 if (m.matches()) {
1781 sb.append(del);
1782 sb.append(m.group(2));
1783 del = ",";
1784 }
1785 }
1786 info.put(EXCLUDE_DIRECTIVE, sb.toString());
1787 }
1788
1789 // Check Uses
1790 Object[] uses = a.get(Export.USES);
1791 if (uses != null && uses.length > 0) {
1792 String old = info.get(USES_DIRECTIVE);
1793 if (old == null)
1794 old = "";
1795 StringBuilder sb = new StringBuilder(old);
1796 String del = sb.length() == 0 ? "" : ",";
1797
1798 for (Object use : uses) {
1799 sb.append(del);
1800 sb.append(use);
1801 del = ",";
1802 }
1803 info.put(USES_DIRECTIVE, sb.toString());
1804 }
1805 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001806 }
1807
1808 });
1809 }
1810
1811 /**
1812 * Clean up version parameters. Other builders use more fuzzy definitions of
1813 * the version syntax. This method cleans up such a version to match an OSGi
1814 * version.
1815 *
1816 * @param VERSION_STRING
1817 * @return
1818 */
Stuart McCulloch2286f232012-06-15 13:27:53 +00001819 static Pattern fuzzyVersion = Pattern.compile("(\\d+)(\\.(\\d+)(\\.(\\d+))?)?([^a-zA-Z0-9](.*))?",
1820 Pattern.DOTALL);
1821 static Pattern fuzzyVersionRange = Pattern.compile(
1822 "(\\(|\\[)\\s*([-\\da-zA-Z.]+)\\s*,\\s*([-\\da-zA-Z.]+)\\s*(\\]|\\))",
1823 Pattern.DOTALL);
Stuart McCullochbb014372012-06-07 21:57:32 +00001824 static Pattern fuzzyModifier = Pattern.compile("(\\d+[.-])*(.*)", Pattern.DOTALL);
1825
1826 static Pattern nummeric = Pattern.compile("\\d*");
1827
1828 static public String cleanupVersion(String version) {
1829 Matcher m = Verifier.VERSIONRANGE.matcher(version);
1830
1831 if (m.matches()) {
1832 return version;
1833 }
1834
1835 m = fuzzyVersionRange.matcher(version);
1836 if (m.matches()) {
1837 String prefix = m.group(1);
1838 String first = m.group(2);
1839 String last = m.group(3);
1840 String suffix = m.group(4);
1841 return prefix + cleanupVersion(first) + "," + cleanupVersion(last) + suffix;
1842 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001843
Stuart McCulloch2286f232012-06-15 13:27:53 +00001844 m = fuzzyVersion.matcher(version);
1845 if (m.matches()) {
1846 StringBuilder result = new StringBuilder();
1847 String major = removeLeadingZeroes(m.group(1));
1848 String minor = removeLeadingZeroes(m.group(3));
1849 String micro = removeLeadingZeroes(m.group(5));
1850 String qualifier = m.group(7);
1851
1852 if (major != null) {
1853 result.append(major);
1854 if (minor != null) {
1855 result.append(".");
1856 result.append(minor);
1857 if (micro != null) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001858 result.append(".");
Stuart McCulloch2286f232012-06-15 13:27:53 +00001859 result.append(micro);
Stuart McCullochbb014372012-06-07 21:57:32 +00001860 if (qualifier != null) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001861 result.append(".");
Stuart McCullochbb014372012-06-07 21:57:32 +00001862 cleanupModifier(result, qualifier);
1863 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001864 } else if (qualifier != null) {
1865 result.append(".0.");
1866 cleanupModifier(result, qualifier);
1867 }
1868 } else if (qualifier != null) {
1869 result.append(".0.0.");
1870 cleanupModifier(result, qualifier);
Stuart McCullochbb014372012-06-07 21:57:32 +00001871 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001872 return result.toString();
Stuart McCullochbb014372012-06-07 21:57:32 +00001873 }
1874 }
1875 return version;
1876 }
1877
1878 private static String removeLeadingZeroes(String group) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001879 if (group == null)
1880 return null;
1881
Stuart McCullochbb014372012-06-07 21:57:32 +00001882 int n = 0;
Stuart McCulloch2286f232012-06-15 13:27:53 +00001883 while (n < group.length() - 1 && group.charAt(n) == '0')
Stuart McCullochbb014372012-06-07 21:57:32 +00001884 n++;
1885 if (n == 0)
1886 return group;
1887
1888 return group.substring(n);
1889 }
1890
1891 static void cleanupModifier(StringBuilder result, String modifier) {
1892 Matcher m = fuzzyModifier.matcher(modifier);
1893 if (m.matches())
1894 modifier = m.group(2);
1895
1896 for (int i = 0; i < modifier.length(); i++) {
1897 char c = modifier.charAt(i);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001898 if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '-')
Stuart McCullochbb014372012-06-07 21:57:32 +00001899 result.append(c);
1900 }
1901 }
1902
1903 final static String DEFAULT_PROVIDER_POLICY = "${range;[==,=+)}";
1904 final static String DEFAULT_CONSUMER_POLICY = "${range;[==,+)}";
1905
1906 @SuppressWarnings("deprecation")
1907 public String getVersionPolicy(boolean implemented) {
1908 if (implemented) {
1909 String s = getProperty(PROVIDER_POLICY);
1910 if (s != null)
1911 return s;
1912
1913 s = getProperty(VERSIONPOLICY_IMPL);
1914 if (s != null)
1915 return s;
1916
1917 return getProperty(VERSIONPOLICY, DEFAULT_PROVIDER_POLICY);
1918 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001919 String s = getProperty(CONSUMER_POLICY);
1920 if (s != null)
1921 return s;
Stuart McCullochbb014372012-06-07 21:57:32 +00001922
Stuart McCulloch2286f232012-06-15 13:27:53 +00001923 s = getProperty(VERSIONPOLICY_USES);
1924 if (s != null)
1925 return s;
Stuart McCullochbb014372012-06-07 21:57:32 +00001926
Stuart McCulloch2286f232012-06-15 13:27:53 +00001927 return getProperty(VERSIONPOLICY, DEFAULT_CONSUMER_POLICY);
1928
Stuart McCullochbb014372012-06-07 21:57:32 +00001929 // String vp = implemented ? getProperty(VERSIONPOLICY_IMPL) :
1930 // getProperty(VERSIONPOLICY_USES);
1931 //
1932 // if (vp != null)
1933 // return vp;
1934 //
1935 // if (implemented)
1936 // return getProperty(VERSIONPOLICY_IMPL, "{$range;[==,=+}");
1937 // else
1938 // return getProperty(VERSIONPOLICY, "${range;[==,+)}");
1939 }
1940
1941 /**
1942 * The extends macro traverses all classes and returns a list of class names
1943 * that extend a base class.
1944 */
1945
1946 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";
1947
1948 public String _classes(String... args) throws Exception {
1949 // Macro.verifyCommand(args, _classesHelp, new
1950 // Pattern[]{null,Pattern.compile("(implementing|implements|extending|extends|importing|imports|any)"),
1951 // null}, 3,3);
1952
1953 Collection<Clazz> matched = getClasses(args);
1954 if (matched.isEmpty())
1955 return "";
1956
1957 return join(matched);
1958 }
1959
1960 public Collection<Clazz> getClasses(String... args) throws Exception {
1961
1962 Set<Clazz> matched = new HashSet<Clazz>(classspace.values());
1963 for (int i = 1; i < args.length; i++) {
1964 if (args.length < i + 1)
Stuart McCulloch2286f232012-06-15 13:27:53 +00001965 throw new IllegalArgumentException("${classes} macro must have odd number of arguments. "
1966 + _classesHelp);
Stuart McCullochbb014372012-06-07 21:57:32 +00001967
1968 String typeName = args[i];
1969 if (typeName.equalsIgnoreCase("extending"))
1970 typeName = "extends";
Stuart McCulloch2286f232012-06-15 13:27:53 +00001971 else if (typeName.equalsIgnoreCase("importing"))
1972 typeName = "imports";
1973 else if (typeName.equalsIgnoreCase("implementing"))
1974 typeName = "implements";
Stuart McCullochbb014372012-06-07 21:57:32 +00001975
1976 Clazz.QUERY type = Clazz.QUERY.valueOf(typeName.toUpperCase());
1977
1978 if (type == null)
Stuart McCulloch2286f232012-06-15 13:27:53 +00001979 throw new IllegalArgumentException("${classes} has invalid type: " + typeName + ". " + _classesHelp);
Stuart McCullochbb014372012-06-07 21:57:32 +00001980
1981 Instruction instr = null;
1982 if (Clazz.HAS_ARGUMENT.contains(type)) {
1983 String s = args[++i];
1984 instr = new Instruction(s);
1985 }
1986 for (Iterator<Clazz> c = matched.iterator(); c.hasNext();) {
1987 Clazz clazz = c.next();
1988 if (!clazz.is(type, instr, this)) {
1989 c.remove();
1990 }
1991 }
1992 }
1993 return matched;
1994 }
1995
1996 /**
1997 * Get the exporter of a package ...
1998 */
1999
2000 public String _exporters(String args[]) throws Exception {
Stuart McCulloch2286f232012-06-15 13:27:53 +00002001 Macro.verifyCommand(args, "${exporters;<packagename>}, returns the list of jars that export the given package",
Stuart McCullochbb014372012-06-07 21:57:32 +00002002 null, 2, 2);
2003 StringBuilder sb = new StringBuilder();
2004 String del = "";
2005 String pack = args[1].replace('.', '/');
2006 for (Jar jar : classpath) {
2007 if (jar.getDirectories().containsKey(pack)) {
2008 sb.append(del);
2009 sb.append(jar.getName());
2010 }
2011 }
2012 return sb.toString();
2013 }
2014
Stuart McCulloch2286f232012-06-15 13:27:53 +00002015 public Map<TypeRef,Clazz> getClassspace() {
Stuart McCullochbb014372012-06-07 21:57:32 +00002016 return classspace;
2017 }
2018
2019 /**
2020 * Locate a resource on the class path.
2021 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00002022 * @param path
2023 * Path of the reosurce
Stuart McCullochbb014372012-06-07 21:57:32 +00002024 * @return A resource or <code>null</code>
2025 */
2026 public Resource findResource(String path) {
2027 for (Jar entry : getClasspath()) {
2028 Resource r = entry.getResource(path);
2029 if (r != null)
2030 return r;
2031 }
2032 return null;
2033 }
2034
2035 /**
2036 * Find a clazz on the class path. This class has been parsed.
2037 *
2038 * @param path
2039 * @return
2040 */
2041 public Clazz findClass(TypeRef typeRef) throws Exception {
2042 Clazz c = classspace.get(typeRef);
2043 if (c != null)
2044 return c;
2045
2046 c = importedClassesCache.get(typeRef);
2047 if (c != null)
2048 return c;
2049
2050 Resource r = findResource(typeRef.getPath());
2051 if (r == null) {
2052 getClass().getClassLoader();
2053 URL url = ClassLoader.getSystemResource(typeRef.getPath());
2054 if (url != null)
2055 r = new URLResource(url);
2056 }
2057 if (r != null) {
2058 c = new Clazz(this, typeRef.getPath(), r);
2059 c.parseClassFile();
2060 importedClassesCache.put(typeRef, c);
2061 }
2062 return c;
2063 }
2064
2065 /**
2066 * Answer the bundle version.
2067 *
2068 * @return
2069 */
2070 public String getVersion() {
2071 String version = getProperty(BUNDLE_VERSION);
2072 if (version == null)
2073 version = "0.0.0";
2074 return version;
2075 }
2076
2077 public boolean isNoBundle() {
2078 return isTrue(getProperty(RESOURCEONLY)) || isTrue(getProperty(NOMANIFEST));
2079 }
2080
2081 public void referTo(TypeRef ref) {
2082 PackageRef pack = ref.getPackageRef();
2083 if (!referred.containsKey(pack))
2084 referred.put(pack, new Attrs());
2085 }
2086
2087 public void referToByBinaryName(String binaryClassName) {
2088 TypeRef ref = descriptors.getTypeRef(binaryClassName);
2089 referTo(ref);
2090 }
2091
2092 /**
2093 * Ensure that we are running on the correct bnd.
2094 */
2095 void doRequireBnd() {
2096 Attrs require = OSGiHeader.parseProperties(getProperty(REQUIRE_BND));
2097 if (require == null || require.isEmpty())
2098 return;
2099
Stuart McCulloch2286f232012-06-15 13:27:53 +00002100 Hashtable<String,String> map = new Hashtable<String,String>();
Stuart McCullochbb014372012-06-07 21:57:32 +00002101 map.put(Constants.VERSION_FILTER, getBndVersion());
2102
2103 for (String filter : require.keySet()) {
2104 try {
2105 Filter f = new Filter(filter);
2106 if (f.match(map))
2107 continue;
2108 error("%s fails %s", REQUIRE_BND, require.get(filter));
2109 }
2110 catch (Exception t) {
2111 error("%s with value %s throws exception", t, REQUIRE_BND, require);
2112 }
2113 }
2114 }
2115
2116 /**
2117 * md5 macro
2118 */
2119
2120 static String _md5Help = "${md5;path}";
2121
2122 public String _md5(String args[]) throws Exception {
Stuart McCulloch2286f232012-06-15 13:27:53 +00002123 Macro.verifyCommand(args, _md5Help, new Pattern[] {
2124 null, null, Pattern.compile("base64|hex")
2125 }, 2, 3);
Stuart McCullochbb014372012-06-07 21:57:32 +00002126
2127 Digester<MD5> digester = MD5.getDigester();
2128 Resource r = dot.getResource(args[1]);
2129 if (r == null)
2130 throw new FileNotFoundException("From " + digester + ", not found " + args[1]);
2131
2132 IO.copy(r.openInputStream(), digester);
2133 boolean hex = args.length > 2 && args[2].equals("hex");
2134 if (hex)
2135 return Hex.toHexString(digester.digest().digest());
Stuart McCulloch2286f232012-06-15 13:27:53 +00002136
2137 return Base64.encodeBase64(digester.digest().digest());
Stuart McCullochbb014372012-06-07 21:57:32 +00002138 }
2139
2140 /**
2141 * SHA1 macro
2142 */
2143
2144 static String _sha1Help = "${sha1;path}";
2145
2146 public String _sha1(String args[]) throws Exception {
Stuart McCulloch2286f232012-06-15 13:27:53 +00002147 Macro.verifyCommand(args, _sha1Help, new Pattern[] {
2148 null, null, Pattern.compile("base64|hex")
2149 }, 2, 3);
Stuart McCullochbb014372012-06-07 21:57:32 +00002150 Digester<SHA1> digester = SHA1.getDigester();
2151 Resource r = dot.getResource(args[1]);
2152 if (r == null)
2153 throw new FileNotFoundException("From sha1, not found " + args[1]);
2154
2155 IO.copy(r.openInputStream(), digester);
2156 return Base64.encodeBase64(digester.digest().digest());
2157 }
2158
2159 public Descriptor getDescriptor(String descriptor) {
2160 return descriptors.getDescriptor(descriptor);
2161 }
2162
2163 public TypeRef getTypeRef(String binaryClassName) {
2164 return descriptors.getTypeRef(binaryClassName);
2165 }
2166
2167 public PackageRef getPackageRef(String binaryName) {
2168 return descriptors.getPackageRef(binaryName);
2169 }
2170
2171 public TypeRef getTypeRefFromFQN(String fqn) {
2172 return descriptors.getTypeRefFromFQN(fqn);
2173 }
2174
2175 public TypeRef getTypeRefFromPath(String path) {
2176 return descriptors.getTypeRefFromPath(path);
2177 }
2178
2179 public boolean isImported(PackageRef packageRef) {
2180 return imports.containsKey(packageRef);
2181 }
2182
2183 /**
2184 * Merge the attributes of two maps, where the first map can contain
2185 * wildcarded names. The idea is that the first map contains instructions
2186 * (for example *) with a set of attributes. These patterns are matched
2187 * against the found packages in actual. If they match, the result is set
2188 * with the merged set of attributes. It is expected that the instructions
2189 * are ordered so that the instructor can define which pattern matches
2190 * first. Attributes in the instructions override any attributes from the
2191 * actual.<br/>
Stuart McCullochbb014372012-06-07 21:57:32 +00002192 * A pattern is a modified regexp so it looks like globbing. The * becomes a
2193 * .* just like the ? becomes a .?. '.' are replaced with \\. Additionally,
2194 * if the pattern starts with an exclamation mark, it will remove that
2195 * matches for that pattern (- the !) from the working set. So the following
2196 * patterns should work:
2197 * <ul>
2198 * <li>com.foo.bar</li>
2199 * <li>com.foo.*</li>
2200 * <li>com.foo.???</li>
2201 * <li>com.*.[^b][^a][^r]</li>
2202 * <li>!com.foo.* (throws away any match for com.foo.*)</li>
2203 * </ul>
2204 * Enough rope to hang the average developer I would say.
2205 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00002206 * @param instructions
2207 * the instructions with patterns.
2208 * @param source
2209 * the actual found packages, contains no duplicates
Stuart McCullochbb014372012-06-07 21:57:32 +00002210 * @return Only the packages that were filtered by the given instructions
2211 */
2212
2213 Packages filter(Instructions instructions, Packages source, Set<Instruction> nomatch) {
2214 Packages result = new Packages();
2215 List<PackageRef> refs = new ArrayList<PackageRef>(source.keySet());
2216 Collections.sort(refs);
2217
2218 List<Instruction> filters = new ArrayList<Instruction>(instructions.keySet());
2219 if (nomatch == null)
2220 nomatch = Create.set();
2221
2222 for (Instruction instruction : filters) {
2223 boolean match = false;
2224
2225 for (Iterator<PackageRef> i = refs.iterator(); i.hasNext();) {
2226 PackageRef packageRef = i.next();
2227
2228 if (packageRef.isMetaData()) {
2229 i.remove(); // no use checking it again
2230 continue;
2231 }
2232
2233 String packageName = packageRef.getFQN();
2234
2235 if (instruction.matches(packageName)) {
2236 match = true;
2237 if (!instruction.isNegated()) {
2238 result.merge(packageRef, instruction.isDuplicate(), source.get(packageRef),
2239 instructions.get(instruction));
2240 }
2241 i.remove(); // Can never match again for another pattern
2242 }
2243 }
2244 if (!match && !instruction.isAny())
2245 nomatch.add(instruction);
2246 }
2247
2248 /*
2249 * Tricky. If we have umatched instructions they might indicate that we
2250 * want to have multiple decorators for the same package. So we check
2251 * the unmatched against the result list. If then then match and have
2252 * actually interesting properties then we merge them
2253 */
2254
2255 for (Iterator<Instruction> i = nomatch.iterator(); i.hasNext();) {
2256 Instruction instruction = i.next();
2257
2258 // We assume the user knows what he is
2259 // doing and inserted a literal. So
2260 // we ignore any not matched literals
2261 if (instruction.isLiteral()) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00002262 result.merge(getPackageRef(instruction.getLiteral()), true, instructions.get(instruction));
Stuart McCullochbb014372012-06-07 21:57:32 +00002263 i.remove();
2264 continue;
2265 }
2266
2267 // Not matching a negated instruction looks
2268 // like an error ...
2269 if (instruction.isNegated()) {
2270 continue;
2271 }
2272
2273 // An optional instruction should not generate
2274 // an error
2275 if (instruction.isOptional()) {
2276 i.remove();
2277 continue;
2278 }
2279
2280 // boolean matched = false;
2281 // Set<PackageRef> prefs = new HashSet<PackageRef>(result.keySet());
2282 // for (PackageRef ref : prefs) {
2283 // if (instruction.matches(ref.getFQN())) {
2284 // result.merge(ref, true, source.get(ref),
2285 // instructions.get(instruction));
2286 // matched = true;
2287 // }
2288 // }
2289 // if (matched)
2290 // i.remove();
2291 }
2292 return result;
2293 }
2294
2295 public void setDiagnostics(boolean b) {
2296 diagnostics = b;
2297 }
2298
2299 public Clazz.JAVA getLowestEE() {
2300 if (ees.isEmpty())
2301 return Clazz.JAVA.JDK1_4;
2302
2303 return ees.first();
2304 }
2305
2306 public String _ee(String args[]) {
2307 return getLowestEE().getEE();
2308 }
2309
2310 /**
2311 * Calculate the output file for the given target. The strategy is:
2312 *
2313 * <pre>
2314 * parameter given if not null and not directory
2315 * if directory, this will be the output directory
2316 * based on bsn-version.jar
2317 * name of the source file if exists
2318 * Untitled-[n]
2319 * </pre>
2320 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00002321 * @param output
2322 * may be null, otherwise a file path relative to base
Stuart McCullochbb014372012-06-07 21:57:32 +00002323 */
2324 public File getOutputFile(String output) {
2325
2326 if (output == null)
2327 output = get(Constants.OUTPUT);
2328
2329 File outputDir;
2330
2331 if (output != null) {
2332 File outputFile = getFile(output);
2333 if (outputFile.isDirectory())
2334 outputDir = outputFile;
2335 else
2336 return outputFile;
Stuart McCulloch2286f232012-06-15 13:27:53 +00002337 } else
Stuart McCullochbb014372012-06-07 21:57:32 +00002338 outputDir = getBase();
2339
2340 if (getBundleSymbolicName() != null) {
2341 String bsn = getBundleSymbolicName();
2342 String version = getBundleVersion();
2343 Version v = Version.parseVersion(version);
Stuart McCulloch2286f232012-06-15 13:27:53 +00002344 String outputName = bsn + "-" + v.getWithoutQualifier() + Constants.DEFAULT_JAR_EXTENSION;
Stuart McCullochbb014372012-06-07 21:57:32 +00002345 return new File(outputDir, outputName);
2346 }
2347
2348 File source = getJar().getSource();
2349 if (source != null) {
2350 String outputName = source.getName();
2351 return new File(outputDir, outputName);
2352 }
2353
Stuart McCulloch2286f232012-06-15 13:27:53 +00002354 error("Cannot establish an output name from %s, nor bsn, nor source file name, using Untitled", output);
Stuart McCullochbb014372012-06-07 21:57:32 +00002355 int n = 0;
2356 File f = getFile(outputDir, "Untitled");
2357 while (f.isFile()) {
2358 f = getFile(outputDir, "Untitled-" + n++);
2359 }
2360 return f;
2361 }
2362
2363 /**
2364 * Utility function to carefully save the file. Will create a backup if the
2365 * source file has the same path as the output. It will also only save if
2366 * the file was modified or the force flag is true
2367 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00002368 * @param output
2369 * the output file, if null {@link #getOutputFile(String)} is
2370 * used.
2371 * @param force
2372 * if it needs to be overwritten
Stuart McCullochbb014372012-06-07 21:57:32 +00002373 * @throws Exception
2374 */
2375
2376 public boolean save(File output, boolean force) throws Exception {
2377 if (output == null)
2378 output = getOutputFile(null);
2379
2380 Jar jar = getJar();
2381 File source = jar.getSource();
2382
Stuart McCulloch2286f232012-06-15 13:27:53 +00002383 trace("check for modified build=%s file=%s, diff=%s", jar.lastModified(), output.lastModified(),
2384 jar.lastModified() - output.lastModified());
Stuart McCullochbb014372012-06-07 21:57:32 +00002385
2386 if (!output.exists() || output.lastModified() <= jar.lastModified() || force) {
2387 output.getParentFile().mkdirs();
2388 if (source != null && output.getCanonicalPath().equals(source.getCanonicalPath())) {
2389 File bak = new File(source.getParentFile(), source.getName() + ".bak");
2390 if (!source.renameTo(bak)) {
2391 error("Could not create backup file %s", bak);
Stuart McCulloch2286f232012-06-15 13:27:53 +00002392 } else
Stuart McCullochbb014372012-06-07 21:57:32 +00002393 source.delete();
2394 }
2395 try {
2396 trace("Saving jar to %s", output);
2397 getJar().write(output);
2398 }
2399 catch (Exception e) {
2400 output.delete();
2401 error("Cannot write JAR file to %s due to %s", e, output, e.getMessage());
2402 }
2403 return true;
2404 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00002405 trace("Not modified %s", output);
2406 return false;
2407
Stuart McCullochbb014372012-06-07 21:57:32 +00002408 }
2409
2410 /**
2411 * Set default import and export instructions if none are set
2412 */
2413 public void setDefaults(String bsn, Version version) {
2414 if (getExportPackage() == null)
2415 setExportPackage("*");
2416 if (getImportPackage() == null)
2417 setExportPackage("*");
2418 if (bsn != null && getBundleSymbolicName() == null)
2419 setBundleSymbolicName(bsn);
2420 if (version != null && getBundleVersion() == null)
2421 setBundleVersion(version);
2422 }
2423}