blob: d6be831c90b18deb1132e1b0460fa66dd83afe3f [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.*;
Stuart McCullochec47fe72012-09-19 12:56:05 +000038import aQute.bnd.version.*;
Stuart McCulloch6a046662012-07-19 13:11:20 +000039import aQute.bnd.version.Version;
Stuart McCullochbb014372012-06-07 21:57:32 +000040import aQute.lib.base64.*;
41import aQute.lib.collections.*;
42import aQute.lib.filter.*;
43import aQute.lib.hex.*;
44import aQute.lib.io.*;
Stuart McCullochbb014372012-06-07 21:57:32 +000045import aQute.libg.cryptography.*;
46import aQute.libg.generics.*;
Stuart McCulloch2286f232012-06-15 13:27:53 +000047import aQute.libg.reporter.*;
Stuart McCullochbb014372012-06-07 21:57:32 +000048
49public class Analyzer extends Processor {
50 private final SortedSet<Clazz.JAVA> ees = new TreeSet<Clazz.JAVA>();
Stuart McCullochd4826102012-06-26 16:34:24 +000051 static Properties bndInfo;
Stuart McCullochbb014372012-06-07 21:57:32 +000052
53 // Bundle parameters
54 private Jar dot;
55 private final Packages contained = new Packages();
56 private final Packages referred = new Packages();
57 private Packages exports;
58 private Packages imports;
59 private TypeRef activator;
60
61 // Global parameters
Stuart McCulloch2286f232012-06-15 13:27:53 +000062 private final MultiMap<PackageRef,PackageRef> uses = new MultiMap<PackageRef,PackageRef>(
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +000063 PackageRef.class, PackageRef.class,
64 true);
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +000065 private final MultiMap<PackageRef,PackageRef> apiUses = new MultiMap<PackageRef,PackageRef>(
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +000066 PackageRef.class, PackageRef.class,
67 true);
Stuart McCullochbb014372012-06-07 21:57:32 +000068 private final Packages classpathExports = new Packages();
69 private final Descriptors descriptors = new Descriptors();
70 private final List<Jar> classpath = list();
Stuart McCulloch2286f232012-06-15 13:27:53 +000071 private final Map<TypeRef,Clazz> classspace = map();
72 private final Map<TypeRef,Clazz> importedClassesCache = map();
Stuart McCullochbb014372012-06-07 21:57:32 +000073 private boolean analyzed = false;
74 private boolean diagnostics = false;
75 private boolean inited = false;
Stuart McCulloch2286f232012-06-15 13:27:53 +000076 final protected AnalyzerMessages msgs = ReporterMessages.base(this,
77 AnalyzerMessages.class);
Stuart McCullochbb014372012-06-07 21:57:32 +000078
79 public Analyzer(Processor parent) {
80 super(parent);
81 }
82
Stuart McCulloch2286f232012-06-15 13:27:53 +000083 public Analyzer() {}
Stuart McCullochbb014372012-06-07 21:57:32 +000084
85 /**
86 * Specifically for Maven
87 *
Stuart McCulloch2286f232012-06-15 13:27:53 +000088 * @param properties
89 * the properties
Stuart McCullochbb014372012-06-07 21:57:32 +000090 */
91
92 public static Properties getManifest(File dirOrJar) throws Exception {
93 Analyzer analyzer = new Analyzer();
94 try {
95 analyzer.setJar(dirOrJar);
96 Properties properties = new Properties();
97 properties.put(IMPORT_PACKAGE, "*");
98 properties.put(EXPORT_PACKAGE, "*");
99 analyzer.setProperties(properties);
100 Manifest m = analyzer.calcManifest();
101 Properties result = new Properties();
102 for (Iterator<Object> i = m.getMainAttributes().keySet().iterator(); i.hasNext();) {
103 Attributes.Name name = (Attributes.Name) i.next();
104 result.put(name.toString(), m.getMainAttributes().getValue(name));
105 }
106 return result;
107 }
108 finally {
109 analyzer.close();
110 }
111 }
112
113 /**
114 * Calculates the data structures for generating a manifest.
115 *
116 * @throws IOException
117 */
118 public void analyze() throws Exception {
119 if (!analyzed) {
120 analyzed = true;
121 uses.clear();
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +0000122 apiUses.clear();
Stuart McCullochbb014372012-06-07 21:57:32 +0000123 classspace.clear();
124 classpathExports.clear();
125
126 // Parse all the class in the
127 // the jar according to the OSGi bcp
128 analyzeBundleClasspath();
129
130 //
131 // calculate class versions in use
132 //
133 for (Clazz c : classspace.values()) {
134 ees.add(c.getFormat());
135 }
136
137 //
138 // Get exported packages from the
139 // entries on the classpath
140 //
141
142 for (Jar current : getClasspath()) {
143 getExternalExports(current, classpathExports);
144 for (String dir : current.getDirectories().keySet()) {
145 PackageRef packageRef = getPackageRef(dir);
146 Resource resource = current.getResource(dir + "/packageinfo");
Stuart McCullochd4826102012-06-26 16:34:24 +0000147 getExportVersionsFromPackageInfo(packageRef, resource, classpathExports);
Stuart McCullochbb014372012-06-07 21:57:32 +0000148 }
149 }
150
151 // Handle the bundle activator
152
153 String s = getProperty(BUNDLE_ACTIVATOR);
154 if (s != null) {
155 activator = getTypeRefFromFQN(s);
156 referTo(activator);
Stuart McCullochd4826102012-06-26 16:34:24 +0000157 trace("activator %s %s", s, activator);
Stuart McCullochbb014372012-06-07 21:57:32 +0000158 }
159
160 // Execute any plugins
161 // TODO handle better reanalyze
162 doPlugins();
163
164 Jar extra = getExtra();
165 while (extra != null) {
166 dot.addAll(extra);
167 analyzeJar(extra, "", true);
168 extra = getExtra();
169 }
170
171 referred.keySet().removeAll(contained.keySet());
172
173 //
174 // EXPORTS
175 //
176 {
177 Set<Instruction> unused = Create.set();
178
179 Instructions filter = new Instructions(getExportPackage());
180 filter.append(getExportContents());
181
182 exports = filter(filter, contained, unused);
183
184 if (!unused.isEmpty()) {
185 warning("Unused Export-Package instructions: %s ", unused);
186 }
187
188 // See what information we can find to augment the
189 // exports. I.e. look on the classpath
190 augmentExports(exports);
191 }
192
193 //
194 // IMPORTS
195 // Imports MUST come after exports because we use information from
196 // the exports
197 //
198 {
199 // Add all exports that do not have an -noimport: directive
200 // to the imports.
201 Packages referredAndExported = new Packages(referred);
202 referredAndExported.putAll(doExportsToImports(exports));
203
204 removeDynamicImports(referredAndExported);
205
206 // Remove any Java references ... where are the closures???
207 for (Iterator<PackageRef> i = referredAndExported.keySet().iterator(); i.hasNext();) {
208 if (i.next().isJava())
209 i.remove();
210 }
211
212 Set<Instruction> unused = Create.set();
213 String h = getProperty(IMPORT_PACKAGE);
214 if (h == null) // If not set use a default
215 h = "*";
216
217 if (isPedantic() && h.trim().length() == 0)
218 warning("Empty Import-Package header");
219
220 Instructions filter = new Instructions(h);
221 imports = filter(filter, referredAndExported, unused);
222 if (!unused.isEmpty()) {
223 // We ignore the end wildcard catch
224 if (!(unused.size() == 1 && unused.iterator().next().toString().equals("*")))
225 warning("Unused Import-Package instructions: %s ", unused);
226 }
227
228 // See what information we can find to augment the
229 // imports. I.e. look in the exports
230 augmentImports(imports, exports);
231 }
232
233 //
234 // USES
235 //
236 // Add the uses clause to the exports
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +0000237
238 boolean api = isTrue(getProperty(EXPERIMENTS)) || true; // brave,
239 // lets see
240
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +0000241 doUses(exports, api ? apiUses : uses, imports);
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +0000242
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +0000243 //
244 // Verify that no exported package has a reference to a private
245 // package
246 // This can cause a lot of harm.
247 // TODO restrict the check to public API only, but even then
248 // exported packages
249 // should preferably not refer to private packages.
250 //
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +0000251 Set<PackageRef> privatePackages = getPrivates();
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +0000252
253 // References to java are not imported so they would show up as
254 // private
255 // packages, lets kill them as well.
256
257 for (Iterator<PackageRef> p = privatePackages.iterator(); p.hasNext();)
258 if (p.next().isJava())
259 p.remove();
260
261 for (PackageRef exported : exports.keySet()) {
262 List<PackageRef> used = uses.get(exported);
263 if (used != null) {
264 Set<PackageRef> privateReferences = new HashSet<PackageRef>(apiUses.get(exported));
265 privateReferences.retainAll(privatePackages);
266 if (!privateReferences.isEmpty())
267 msgs.Export_Has_PrivateReferences_(exported, privateReferences.size(), privateReferences);
268 }
269 }
Stuart McCullochbb014372012-06-07 21:57:32 +0000270
271 //
272 // Checks
273 //
274 if (referred.containsKey(Descriptors.DEFAULT_PACKAGE)) {
275 error("The default package '.' is not permitted by the Import-Package syntax. \n"
276 + " This can be caused by compile errors in Eclipse because Eclipse creates \n"
277 + "valid class files regardless of compile errors.\n"
278 + "The following package(s) import from the default package "
279 + uses.transpose().get(Descriptors.DEFAULT_PACKAGE));
280 }
281
282 }
283 }
284
285 /**
286 * Discussed with BJ and decided to kill the .
287 *
288 * @param referredAndExported
289 */
290 void removeDynamicImports(Packages referredAndExported) {
291
292 // // Remove any matching a dynamic import package instruction
293 // Instructions dynamicImports = new
294 // Instructions(getDynamicImportPackage());
295 // Collection<PackageRef> dynamic = dynamicImports.select(
296 // referredAndExported.keySet(), false);
297 // referredAndExported.keySet().removeAll(dynamic);
298 }
299
300 protected Jar getExtra() throws Exception {
301 return null;
302 }
303
304 /**
305 *
306 */
307 void doPlugins() {
308 for (AnalyzerPlugin plugin : getPlugins(AnalyzerPlugin.class)) {
309 try {
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000310 Processor previous = beginHandleErrors(plugin.toString());
Stuart McCullochbb014372012-06-07 21:57:32 +0000311 boolean reanalyze = plugin.analyzeJar(this);
Stuart McCulloch4b9de8e2012-07-22 00:19:13 +0000312 endHandleErrors(previous);
Stuart McCullochbb014372012-06-07 21:57:32 +0000313 if (reanalyze) {
314 classspace.clear();
315 analyzeBundleClasspath();
316 }
317 }
318 catch (Exception e) {
319 error("Analyzer Plugin %s failed %s", plugin, e);
320 }
321 }
322 }
323
324 /**
Stuart McCullochbb014372012-06-07 21:57:32 +0000325 * @return
326 */
327 boolean isResourceOnly() {
328 return isTrue(getProperty(RESOURCEONLY));
329 }
330
331 /**
332 * One of the main workhorses of this class. This will analyze the current
333 * setp and calculate a new manifest according to this setup. This method
334 * will also set the manifest on the main jar dot
335 *
336 * @return
337 * @throws IOException
338 */
339 public Manifest calcManifest() throws Exception {
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000340 try {
341 analyze();
342 Manifest manifest = new Manifest();
343 Attributes main = manifest.getMainAttributes();
Stuart McCullochbb014372012-06-07 21:57:32 +0000344
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000345 main.put(Attributes.Name.MANIFEST_VERSION, "1.0");
346 main.putValue(BUNDLE_MANIFESTVERSION, "2");
Stuart McCullochbb014372012-06-07 21:57:32 +0000347
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000348 boolean noExtraHeaders = "true".equalsIgnoreCase(getProperty(NOEXTRAHEADERS));
Stuart McCullochbb014372012-06-07 21:57:32 +0000349
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000350 if (!noExtraHeaders) {
351 main.putValue(CREATED_BY, System.getProperty("java.version") + " (" + System.getProperty("java.vendor")
352 + ")");
353 main.putValue(TOOL, "Bnd-" + getBndVersion());
354 main.putValue(BND_LASTMODIFIED, "" + System.currentTimeMillis());
Stuart McCullochbb014372012-06-07 21:57:32 +0000355 }
356
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000357 String exportHeader = printClauses(exports, true);
Stuart McCullochbb014372012-06-07 21:57:32 +0000358
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000359 if (exportHeader.length() > 0)
360 main.putValue(EXPORT_PACKAGE, exportHeader);
361 else
362 main.remove(EXPORT_PACKAGE);
Stuart McCullochbb014372012-06-07 21:57:32 +0000363
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000364 // Remove all the Java packages from the imports
365 if (!imports.isEmpty()) {
366 main.putValue(IMPORT_PACKAGE, printClauses(imports));
Stuart McCulloch2286f232012-06-15 13:27:53 +0000367 } else {
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000368 main.remove(IMPORT_PACKAGE);
Stuart McCullochbb014372012-06-07 21:57:32 +0000369 }
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000370
371 Packages temp = new Packages(contained);
372 temp.keySet().removeAll(exports.keySet());
373
374 if (!temp.isEmpty())
375 main.putValue(PRIVATE_PACKAGE, printClauses(temp));
376 else
377 main.remove(PRIVATE_PACKAGE);
378
379 Parameters bcp = getBundleClasspath();
380 if (bcp.isEmpty() || (bcp.containsKey(".") && bcp.size() == 1))
381 main.remove(BUNDLE_CLASSPATH);
382 else
383 main.putValue(BUNDLE_CLASSPATH, printClauses(bcp));
384
385 doNamesection(dot, manifest);
386
387 for (Enumeration< ? > h = getProperties().propertyNames(); h.hasMoreElements();) {
388 String header = (String) h.nextElement();
389 if (header.trim().length() == 0) {
390 warning("Empty property set with value: " + getProperties().getProperty(header));
391 continue;
392 }
393
394 if (isMissingPlugin(header.trim())) {
395 error("Missing plugin for command %s", header);
396 }
397 if (!Character.isUpperCase(header.charAt(0))) {
398 if (header.charAt(0) == '@')
399 doNameSection(manifest, header);
400 continue;
401 }
402
403 if (header.equals(BUNDLE_CLASSPATH) || header.equals(EXPORT_PACKAGE) || header.equals(IMPORT_PACKAGE))
404 continue;
405
406 if (header.equalsIgnoreCase("Name")) {
407 error("Your bnd file contains a header called 'Name'. This interferes with the manifest name section.");
408 continue;
409 }
410
411 if (Verifier.HEADER_PATTERN.matcher(header).matches()) {
412 String value = getProperty(header);
413 if (value != null && main.getValue(header) == null) {
414 if (value.trim().length() == 0)
415 main.remove(header);
416 else if (value.trim().equals(EMPTY_HEADER))
417 main.putValue(header, "");
418 else
419 main.putValue(header, value);
420 }
421 } else {
422 // TODO should we report?
423 }
424 }
425
426 // Copy old values into new manifest, when they
427 // exist in the old one, but not in the new one
428 merge(manifest, dot.getManifest());
429
430 //
431 // Calculate the bundle symbolic name if it is
432 // not set.
433 // 1. set
434 // 2. name of properties file (must be != bnd.bnd)
435 // 3. name of directory, which is usualy project name
436 //
437 String bsn = getBsn();
438 if (main.getValue(BUNDLE_SYMBOLICNAME) == null) {
439 main.putValue(BUNDLE_SYMBOLICNAME, bsn);
440 }
441
442 //
443 // Use the same name for the bundle name as BSN when
444 // the bundle name is not set
445 //
446 if (main.getValue(BUNDLE_NAME) == null) {
447 main.putValue(BUNDLE_NAME, bsn);
448 }
449
450 if (main.getValue(BUNDLE_VERSION) == null)
451 main.putValue(BUNDLE_VERSION, "0");
452
453 // Remove all the headers mentioned in -removeheaders
454 Instructions instructions = new Instructions(getProperty(REMOVEHEADERS));
455 Collection<Object> result = instructions.select(main.keySet(), false);
456 main.keySet().removeAll(result);
457
458 // We should not set the manifest here, this is in general done
459 // by the caller.
460 // dot.setManifest(manifest);
461 return manifest;
Stuart McCullochbb014372012-06-07 21:57:32 +0000462 }
Stuart McCulloch61c61eb2012-07-24 21:37:47 +0000463 catch (Exception e) {
464 // This should not really happen. The code should never throw
465 // exceptions in normal situations. So if it happens we need more
466 // information. So to help diagnostics. We do a full property dump
Stuart McCullochec47fe72012-09-19 12:56:05 +0000467 throw new IllegalStateException("Calc manifest failed, state=\n" + getFlattenedProperties(), e);
Stuart McCullochbb014372012-06-07 21:57:32 +0000468 }
Stuart McCullochbb014372012-06-07 21:57:32 +0000469 }
470
471 /**
472 * Parse the namesection as instructions and then match them against the
Stuart McCulloch2286f232012-06-15 13:27:53 +0000473 * current set of resources For example:
Stuart McCullochbb014372012-06-07 21:57:32 +0000474 *
475 * <pre>
476 * -namesection: *;baz=true, abc/def/bar/X.class=3
477 * </pre>
478 *
479 * The raw value of {@link Constants#NAMESECTION} is used but the values of
480 * the attributes are replaced where @ is set to the resource name. This
481 * allows macro to operate on the resource
Stuart McCullochbb014372012-06-07 21:57:32 +0000482 */
483
484 private void doNamesection(Jar dot, Manifest manifest) {
485
486 Parameters namesection = parseHeader(getProperties().getProperty(NAMESECTION));
487 Instructions instructions = new Instructions(namesection);
488 Set<String> resources = new HashSet<String>(dot.getResources().keySet());
489
490 //
491 // For each instruction, iterator over the resources and filter
492 // them. If a resource matches, it must be removed even if the
493 // instruction is negative. If positive, add a name section
494 // to the manifest for the given resource name. Then add all
495 // attributes from the instruction to that name section.
496 //
Stuart McCulloch2286f232012-06-15 13:27:53 +0000497 for (Map.Entry<Instruction,Attrs> instr : instructions.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000498 boolean matched = false;
499
500 // For each instruction
501
502 for (Iterator<String> i = resources.iterator(); i.hasNext();) {
503 String path = i.next();
504 // For each resource
505
506 if (instr.getKey().matches(path)) {
507
508 // Instruction matches the resource
509
510 matched = true;
511 if (!instr.getKey().isNegated()) {
512
513 // Positive match, add the attributes
514
515 Attributes attrs = manifest.getAttributes(path);
516 if (attrs == null) {
517 attrs = new Attributes();
518 manifest.getEntries().put(path, attrs);
519 }
520
521 //
522 // Add all the properties from the instruction to the
523 // name section
524 //
525
Stuart McCulloch2286f232012-06-15 13:27:53 +0000526 for (Map.Entry<String,String> property : instr.getValue().entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000527 setProperty("@", path);
528 try {
529 String processed = getReplacer().process(property.getValue());
530 attrs.putValue(property.getKey(), processed);
531 }
532 finally {
533 unsetProperty("@");
534 }
535 }
536 }
537 i.remove();
538 }
539 }
540
541 if (!matched && resources.size() > 0)
Stuart McCulloch2286f232012-06-15 13:27:53 +0000542 warning("The instruction %s in %s did not match any resources", instr.getKey(), NAMESECTION);
Stuart McCullochbb014372012-06-07 21:57:32 +0000543 }
544
545 }
546
547 /**
548 * This method is called when the header starts with a @, signifying a name
549 * section header. The name part is defined by replacing all the @ signs to
550 * a /, removing the first and the last, and using the last part as header
551 * name:
552 *
553 * <pre>
554 * &#064;org@osgi@service@event@Implementation-Title
555 * </pre>
556 *
557 * This will be the header Implementation-Title in the
558 * org/osgi/service/event named section.
559 *
560 * @param manifest
561 * @param header
562 */
563 private void doNameSection(Manifest manifest, String header) {
564 String path = header.replace('@', '/');
565 int n = path.lastIndexOf('/');
566 // Must succeed because we start with @
567 String name = path.substring(n + 1);
568 // Skip first /
569 path = path.substring(1, n);
570 if (name.length() != 0 && path.length() != 0) {
571 Attributes attrs = manifest.getAttributes(path);
572 if (attrs == null) {
573 attrs = new Attributes();
574 manifest.getEntries().put(path, attrs);
575 }
576 attrs.putValue(name, getProperty(header));
Stuart McCulloch2286f232012-06-15 13:27:53 +0000577 } else {
578 warning("Invalid header (starts with @ but does not seem to be for the Name section): %s", header);
Stuart McCullochbb014372012-06-07 21:57:32 +0000579 }
580 }
581
582 /**
583 * Clear the key part of a header. I.e. remove everything from the first ';'
584 *
585 * @param value
586 * @return
587 */
588 public String getBsn() {
589 String value = getProperty(BUNDLE_SYMBOLICNAME);
590 if (value == null) {
591 if (getPropertiesFile() != null)
592 value = getPropertiesFile().getName();
593
594 String projectName = getBase().getName();
595 if (value == null || value.equals("bnd.bnd")) {
596 value = projectName;
Stuart McCulloch2286f232012-06-15 13:27:53 +0000597 } else if (value.endsWith(".bnd")) {
598 value = value.substring(0, value.length() - 4);
599 if (!value.startsWith(getBase().getName()))
600 value = projectName + "." + value;
Stuart McCullochbb014372012-06-07 21:57:32 +0000601 }
Stuart McCullochbb014372012-06-07 21:57:32 +0000602 }
603
604 if (value == null)
605 return "untitled";
606
607 int n = value.indexOf(';');
608 if (n > 0)
609 value = value.substring(0, n);
610 return value.trim();
611 }
612
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +0000613 public String _bsn(@SuppressWarnings("unused")
614 String args[]) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000615 return getBsn();
616 }
617
618 /**
619 * Calculate an export header solely based on the contents of a JAR file
620 *
Stuart McCulloch2286f232012-06-15 13:27:53 +0000621 * @param bundle
622 * The jar file to analyze
Stuart McCullochbb014372012-06-07 21:57:32 +0000623 * @return
624 */
625 public String calculateExportsFromContents(Jar bundle) {
626 String ddel = "";
627 StringBuilder sb = new StringBuilder();
Stuart McCulloch2286f232012-06-15 13:27:53 +0000628 Map<String,Map<String,Resource>> map = bundle.getDirectories();
Stuart McCullochbb014372012-06-07 21:57:32 +0000629 for (Iterator<String> i = map.keySet().iterator(); i.hasNext();) {
630 String directory = i.next();
631 if (directory.equals("META-INF") || directory.startsWith("META-INF/"))
632 continue;
633 if (directory.equals("OSGI-OPT") || directory.startsWith("OSGI-OPT/"))
634 continue;
635 if (directory.equals("/"))
636 continue;
637
638 if (directory.endsWith("/"))
639 directory = directory.substring(0, directory.length() - 1);
640
641 directory = directory.replace('/', '.');
642 sb.append(ddel);
643 sb.append(directory);
644 ddel = ",";
645 }
646 return sb.toString();
647 }
648
649 public Packages getContained() {
650 return contained;
651 }
652
653 public Packages getExports() {
654 return exports;
655 }
656
657 public Packages getImports() {
658 return imports;
659 }
660
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +0000661 public Set<PackageRef> getPrivates() {
662 HashSet<PackageRef> privates = new HashSet<PackageRef>(contained.keySet());
663 privates.removeAll(exports.keySet());
664 privates.removeAll(imports.keySet());
665 return privates;
666 }
667
Stuart McCullochbb014372012-06-07 21:57:32 +0000668 public Jar getJar() {
669 return dot;
670 }
671
672 public Packages getReferred() {
673 return referred;
674 }
675
676 /**
677 * Return the set of unreachable code depending on exports and the bundle
678 * activator.
679 *
680 * @return
681 */
682 public Set<PackageRef> getUnreachable() {
683 Set<PackageRef> unreachable = new HashSet<PackageRef>(uses.keySet()); // all
684 for (Iterator<PackageRef> r = exports.keySet().iterator(); r.hasNext();) {
685 PackageRef packageRef = r.next();
686 removeTransitive(packageRef, unreachable);
687 }
688 if (activator != null) {
689 removeTransitive(activator.getPackageRef(), unreachable);
690 }
691 return unreachable;
692 }
693
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +0000694 public Map<PackageRef,List<PackageRef>> getUses() {
Stuart McCullochbb014372012-06-07 21:57:32 +0000695 return uses;
696 }
697
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +0000698 public Map<PackageRef,List<PackageRef>> getAPIUses() {
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +0000699 return apiUses;
700 }
701
Stuart McCullochb215bfd2012-09-06 18:28:06 +0000702 public Packages getClasspathExports() {
703 return classpathExports;
704 }
705
Stuart McCullochbb014372012-06-07 21:57:32 +0000706 /**
707 * Get the version for this bnd
708 *
709 * @return version or unknown.
710 */
711 public String getBndVersion() {
Stuart McCullochd4826102012-06-26 16:34:24 +0000712 return getBndInfo("version", "<unknown>");
Stuart McCullochbb014372012-06-07 21:57:32 +0000713 }
714
715 public long getBndLastModified() {
Stuart McCullochd4826102012-06-26 16:34:24 +0000716 String time = getBndInfo("lastmodified", "0");
Stuart McCullochbb014372012-06-07 21:57:32 +0000717 try {
718 return Long.parseLong(time);
719 }
720 catch (Exception e) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000721 // Ignore
Stuart McCullochbb014372012-06-07 21:57:32 +0000722 }
723 return 0;
724 }
725
726 public String getBndInfo(String key, String defaultValue) {
Stuart McCullochffa8aaf2012-06-17 20:38:35 +0000727 if (bndInfo == null) {
728 try {
Stuart McCullochd4826102012-06-26 16:34:24 +0000729 Properties bndInfoLocal = new Properties();
730 URL url = Analyzer.class.getResource("bnd.info");
731 if (url != null) {
732 InputStream in = url.openStream();
733 try {
734 bndInfoLocal.load(in);
735 }
736 finally {
737 in.close();
738 }
739 }
740 bndInfo = bndInfoLocal;
Stuart McCullochffa8aaf2012-06-17 20:38:35 +0000741 }
742 catch (Exception e) {
Stuart McCullochd4826102012-06-26 16:34:24 +0000743 e.printStackTrace();
Stuart McCullochffa8aaf2012-06-17 20:38:35 +0000744 return defaultValue;
Stuart McCullochbb014372012-06-07 21:57:32 +0000745 }
746 }
Stuart McCullochd4826102012-06-26 16:34:24 +0000747 String value = bndInfo.getProperty(key);
Stuart McCullochffa8aaf2012-06-17 20:38:35 +0000748 if (value == null)
749 return defaultValue;
750 return value;
Stuart McCullochbb014372012-06-07 21:57:32 +0000751 }
752
753 /**
754 * Merge the existing manifest with the instructions but do not override
755 * existing properties.
756 *
Stuart McCulloch2286f232012-06-15 13:27:53 +0000757 * @param manifest
758 * The manifest to merge with
Stuart McCullochbb014372012-06-07 21:57:32 +0000759 * @throws IOException
760 */
761 public void mergeManifest(Manifest manifest) throws IOException {
762 if (manifest != null) {
763 Attributes attributes = manifest.getMainAttributes();
764 for (Iterator<Object> i = attributes.keySet().iterator(); i.hasNext();) {
765 Name name = (Name) i.next();
766 String key = name.toString();
767 // Dont want instructions
768 if (key.startsWith("-"))
769 continue;
770
771 if (getProperty(key) == null)
772 setProperty(key, attributes.getValue(name));
773 }
774 }
775 }
776
Stuart McCulloch55d4dfe2012-08-07 10:57:21 +0000777 @Override
Stuart McCullochbb014372012-06-07 21:57:32 +0000778 public void setBase(File file) {
779 super.setBase(file);
780 getProperties().put("project.dir", getBase().getAbsolutePath());
781 }
782
783 /**
784 * Set the classpath for this analyzer by file.
785 *
786 * @param classpath
787 * @throws IOException
788 */
789 public void setClasspath(File[] classpath) throws IOException {
790 List<Jar> list = new ArrayList<Jar>();
791 for (int i = 0; i < classpath.length; i++) {
792 if (classpath[i].exists()) {
793 Jar current = new Jar(classpath[i]);
794 list.add(current);
Stuart McCulloch2286f232012-06-15 13:27:53 +0000795 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +0000796 error("Missing file on classpath: %s", classpath[i]);
797 }
798 }
799 for (Iterator<Jar> i = list.iterator(); i.hasNext();) {
800 addClasspath(i.next());
801 }
802 }
803
804 public void setClasspath(Jar[] classpath) {
805 for (int i = 0; i < classpath.length; i++) {
806 addClasspath(classpath[i]);
807 }
808 }
809
810 public void setClasspath(String[] classpath) {
811 for (int i = 0; i < classpath.length; i++) {
812 Jar jar = getJarFromName(classpath[i], " setting classpath");
813 if (jar != null)
814 addClasspath(jar);
815 }
816 }
817
818 /**
819 * Set the JAR file we are going to work in. This will read the JAR in
820 * memory.
821 *
822 * @param jar
823 * @return
824 * @throws IOException
825 */
826 public Jar setJar(File jar) throws IOException {
827 Jar jarx = new Jar(jar);
828 addClose(jarx);
829 return setJar(jarx);
830 }
831
832 /**
833 * Set the JAR directly we are going to work on.
834 *
835 * @param jar
836 * @return
837 */
838 public Jar setJar(Jar jar) {
839 if (dot != null)
840 removeClose(dot);
841
842 this.dot = jar;
843 if (dot != null)
844 addClose(dot);
845
846 return jar;
847 }
848
Stuart McCulloch55d4dfe2012-08-07 10:57:21 +0000849 @Override
Stuart McCullochbb014372012-06-07 21:57:32 +0000850 protected void begin() {
851 if (inited == false) {
852 inited = true;
853 super.begin();
854
855 updateModified(getBndLastModified(), "bnd last modified");
856 verifyManifestHeadersCase(getProperties());
857
858 }
859 }
860
861 /**
862 * Try to get a Jar from a file name/path or a url, or in last resort from
863 * the classpath name part of their files.
864 *
Stuart McCulloch2286f232012-06-15 13:27:53 +0000865 * @param name
866 * URL or filename relative to the base
867 * @param from
868 * Message identifying the caller for errors
Stuart McCullochbb014372012-06-07 21:57:32 +0000869 * @return null or a Jar with the contents for the name
870 */
871 Jar getJarFromName(String name, String from) {
872 File file = new File(name);
873 if (!file.isAbsolute())
874 file = new File(getBase(), name);
875
876 if (file.exists())
877 try {
878 Jar jar = new Jar(file);
879 addClose(jar);
880 return jar;
881 }
882 catch (Exception e) {
883 error("Exception in parsing jar file for " + from + ": " + name + " " + e);
884 }
885 // It is not a file ...
886 try {
887 // Lets try a URL
888 URL url = new URL(name);
889 Jar jar = new Jar(fileName(url.getPath()));
890 addClose(jar);
891 URLConnection connection = url.openConnection();
892 InputStream in = connection.getInputStream();
893 long lastModified = connection.getLastModified();
894 if (lastModified == 0)
895 // We assume the worst :-(
896 lastModified = System.currentTimeMillis();
897 EmbeddedResource.build(jar, in, lastModified);
898 in.close();
899 return jar;
900 }
901 catch (IOException ee) {
902 // Check if we have files on the classpath
903 // that have the right name, allows us to specify those
904 // names instead of the full path.
905 for (Iterator<Jar> cp = getClasspath().iterator(); cp.hasNext();) {
906 Jar entry = cp.next();
907 if (entry.getSource() != null && entry.getSource().getName().equals(name)) {
908 return entry;
909 }
910 }
911 // error("Can not find jar file for " + from + ": " + name);
912 }
913 return null;
914 }
915
916 private String fileName(String path) {
917 int n = path.lastIndexOf('/');
918 if (n > 0)
919 return path.substring(n + 1);
920 return path;
921 }
922
923 /**
Stuart McCullochbb014372012-06-07 21:57:32 +0000924 * @param manifests
925 * @throws Exception
926 */
Stuart McCulloch2286f232012-06-15 13:27:53 +0000927 private void merge(Manifest result, Manifest old) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000928 if (old != null) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000929 for (Iterator<Map.Entry<Object,Object>> e = old.getMainAttributes().entrySet().iterator(); e.hasNext();) {
930 Map.Entry<Object,Object> entry = e.next();
Stuart McCullochbb014372012-06-07 21:57:32 +0000931 Attributes.Name name = (Attributes.Name) entry.getKey();
932 String value = (String) entry.getValue();
933 if (name.toString().equalsIgnoreCase("Created-By"))
934 name = new Attributes.Name("Originally-Created-By");
935 if (!result.getMainAttributes().containsKey(name))
936 result.getMainAttributes().put(name, value);
937 }
938
939 // do not overwrite existing entries
Stuart McCulloch2286f232012-06-15 13:27:53 +0000940 Map<String,Attributes> oldEntries = old.getEntries();
941 Map<String,Attributes> newEntries = result.getEntries();
942 for (Iterator<Map.Entry<String,Attributes>> e = oldEntries.entrySet().iterator(); e.hasNext();) {
943 Map.Entry<String,Attributes> entry = e.next();
Stuart McCullochbb014372012-06-07 21:57:32 +0000944 if (!newEntries.containsKey(entry.getKey())) {
945 newEntries.put(entry.getKey(), entry.getValue());
946 }
947 }
948 }
949 }
950
951 /**
952 * Bnd is case sensitive for the instructions so we better check people are
953 * not using an invalid case. We do allow this to set headers that should
954 * not be processed by us but should be used by the framework.
955 *
Stuart McCulloch2286f232012-06-15 13:27:53 +0000956 * @param properties
957 * Properties to verify.
Stuart McCullochbb014372012-06-07 21:57:32 +0000958 */
959
960 void verifyManifestHeadersCase(Properties properties) {
961 for (Iterator<Object> i = properties.keySet().iterator(); i.hasNext();) {
962 String header = (String) i.next();
963 for (int j = 0; j < headers.length; j++) {
964 if (!headers[j].equals(header) && headers[j].equalsIgnoreCase(header)) {
965 warning("Using a standard OSGi header with the wrong case (bnd is case sensitive!), using: "
966 + header + " and expecting: " + headers[j]);
967 break;
968 }
969 }
970 }
971 }
972
973 /**
974 * We will add all exports to the imports unless there is a -noimport
975 * directive specified on an export. This directive is skipped for the
Stuart McCulloch2286f232012-06-15 13:27:53 +0000976 * manifest. We also remove any version parameter so that augmentImports can
977 * do the version policy. The following method is really tricky and evolved
978 * over time. Coming from the original background of OSGi, it was a weird
979 * idea for me to have a public package that should not be substitutable. I
980 * was so much convinced that this was the right rule that I rücksichtlos
981 * imported them all. Alas, the real world was more subtle than that. It
982 * turns out that it is not a good idea to always import. First, there must
983 * be a need to import, i.e. there must be a contained package that refers
984 * to the exported package for it to make use importing that package.
985 * Second, if an exported package refers to an internal package than it
986 * should not be imported. Additionally, it is necessary to treat the
987 * exports in groups. If an exported package refers to another exported
988 * packages than it must be in the same group. A framework can only
989 * substitute exports for imports for the whole of such a group. WHY?????
990 * Not clear anymore ...
Stuart McCullochbb014372012-06-07 21:57:32 +0000991 */
992 Packages doExportsToImports(Packages exports) {
993
994 // private packages = contained - exported.
995 Set<PackageRef> privatePackages = new HashSet<PackageRef>(contained.keySet());
996 privatePackages.removeAll(exports.keySet());
997
998 // private references = ∀ p : private packages | uses(p)
999 Set<PackageRef> privateReferences = newSet();
1000 for (PackageRef p : privatePackages) {
1001 Collection<PackageRef> uses = this.uses.get(p);
1002 if (uses != null)
1003 privateReferences.addAll(uses);
1004 }
1005
1006 // Assume we are going to export all exported packages
1007 Set<PackageRef> toBeImported = new HashSet<PackageRef>(exports.keySet());
1008
1009 // Remove packages that are not referenced privately
1010 toBeImported.retainAll(privateReferences);
1011
1012 // Not necessary to import anything that is already
1013 // imported in the Import-Package statement.
1014 // TODO toBeImported.removeAll(imports.keySet());
1015
1016 // Remove exported packages that are referring to
1017 // private packages.
1018 // Each exported package has a uses clause. We just use
1019 // the used packages for each exported package to find out
1020 // if it refers to an internal package.
1021 //
1022
1023 for (Iterator<PackageRef> i = toBeImported.iterator(); i.hasNext();) {
1024 PackageRef next = i.next();
1025 Collection<PackageRef> usedByExportedPackage = this.uses.get(next);
1026
Stuart McCulloch61c61eb2012-07-24 21:37:47 +00001027 // We had an NPE on usedByExportedPackage in GF.
1028 // I guess this can happen with hard coded
1029 // imports that do not match reality ...
1030 if (usedByExportedPackage == null || usedByExportedPackage.isEmpty()) {
1031 continue;
1032 }
1033
Stuart McCullochbb014372012-06-07 21:57:32 +00001034 for (PackageRef privatePackage : privatePackages) {
1035 if (usedByExportedPackage.contains(privatePackage)) {
1036 i.remove();
1037 break;
1038 }
1039 }
1040 }
1041
1042 // Clean up attributes and generate result map
1043 Packages result = new Packages();
1044 for (Iterator<PackageRef> i = toBeImported.iterator(); i.hasNext();) {
1045 PackageRef ep = i.next();
1046 Attrs parameters = exports.get(ep);
1047
Stuart McCullochffa8aaf2012-06-17 20:38:35 +00001048 String noimport = parameters == null ? null : parameters.get(NO_IMPORT_DIRECTIVE);
Stuart McCullochbb014372012-06-07 21:57:32 +00001049 if (noimport != null && noimport.equalsIgnoreCase("true"))
1050 continue;
1051
1052 // // we can't substitute when there is no version
1053 // String version = parameters.get(VERSION_ATTRIBUTE);
1054 // if (version == null) {
1055 // if (isPedantic())
1056 // warning(
1057 // "Cannot automatically import exported package %s because it has no version defined",
1058 // ep);
1059 // continue;
1060 // }
1061
1062 parameters = new Attrs();
1063 parameters.remove(VERSION_ATTRIBUTE);
1064 result.put(ep, parameters);
1065 }
1066 return result;
1067 }
1068
1069 public boolean referred(PackageRef packageName) {
1070 // return true;
Stuart McCulloch2286f232012-06-15 13:27:53 +00001071 for (Map.Entry<PackageRef,List<PackageRef>> contained : uses.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001072 if (!contained.getKey().equals(packageName)) {
1073 if (contained.getValue().contains(packageName))
1074 return true;
1075 }
1076 }
1077 return false;
1078 }
1079
1080 /**
Stuart McCullochbb014372012-06-07 21:57:32 +00001081 * @param jar
1082 */
1083 private void getExternalExports(Jar jar, Packages classpathExports) {
1084 try {
1085 Manifest m = jar.getManifest();
1086 if (m != null) {
1087 Domain domain = Domain.domain(m);
1088 Parameters exported = domain.getExportPackage();
Stuart McCulloch2286f232012-06-15 13:27:53 +00001089 for (Entry<String,Attrs> e : exported.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001090 PackageRef ref = getPackageRef(e.getKey());
1091 if (!classpathExports.containsKey(ref)) {
1092 // TODO e.getValue().put(SOURCE_DIRECTIVE,
1093 // jar.getBsn()+"-"+jar.getVersion());
1094
1095 classpathExports.put(ref, e.getValue());
1096 }
1097 }
1098 }
1099 }
1100 catch (Exception e) {
1101 warning("Erroneous Manifest for " + jar + " " + e);
1102 }
1103 }
1104
1105 /**
1106 * Find some more information about imports in manifest and other places. It
1107 * is assumed that the augmentsExports has already copied external attrs
1108 * from the classpathExports.
1109 *
1110 * @throws Exception
1111 */
1112 void augmentImports(Packages imports, Packages exports) throws Exception {
1113 List<PackageRef> noimports = Create.list();
1114 Set<PackageRef> provided = findProvidedPackages();
1115
1116 for (PackageRef packageRef : imports.keySet()) {
1117 String packageName = packageRef.getFQN();
1118
1119 setProperty(CURRENT_PACKAGE, packageName);
1120 try {
1121 Attrs importAttributes = imports.get(packageRef);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001122 Attrs exportAttributes = exports.get(packageRef, classpathExports.get(packageRef, new Attrs()));
Stuart McCullochbb014372012-06-07 21:57:32 +00001123
1124 String exportVersion = exportAttributes.getVersion();
1125 String importRange = importAttributes.getVersion();
1126
1127 if (exportVersion == null) {
1128 // TODO Should check if the source is from a bundle.
1129
Stuart McCulloch2286f232012-06-15 13:27:53 +00001130 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001131
1132 //
1133 // Version Policy - Import version substitution. We
1134 // calculate the export version and then allow the
1135 // import version attribute to use it in a substitution
1136 // by using a ${@} macro. The export version can
1137 // be defined externally or locally
1138 //
1139
1140 boolean provider = isTrue(importAttributes.get(PROVIDE_DIRECTIVE))
Stuart McCulloch2286f232012-06-15 13:27:53 +00001141 || isTrue(exportAttributes.get(PROVIDE_DIRECTIVE)) || provided.contains(packageRef);
Stuart McCullochbb014372012-06-07 21:57:32 +00001142
1143 exportVersion = cleanupVersion(exportVersion);
1144
1145 try {
1146 setProperty("@", exportVersion);
1147
1148 if (importRange != null) {
1149 importRange = cleanupVersion(importRange);
1150 importRange = getReplacer().process(importRange);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001151 } else
Stuart McCullochbb014372012-06-07 21:57:32 +00001152 importRange = getVersionPolicy(provider);
1153
1154 }
1155 finally {
1156 unsetProperty("@");
1157 }
1158 importAttributes.put(VERSION_ATTRIBUTE, importRange);
1159 }
1160
1161 //
1162 // Check if exporter has mandatory attributes
1163 //
1164 String mandatory = exportAttributes.get(MANDATORY_DIRECTIVE);
1165 if (mandatory != null) {
1166 String[] attrs = mandatory.split("\\s*,\\s*");
1167 for (int i = 0; i < attrs.length; i++) {
1168 if (!importAttributes.containsKey(attrs[i]))
1169 importAttributes.put(attrs[i], exportAttributes.get(attrs[i]));
1170 }
1171 }
1172
1173 if (exportAttributes.containsKey(IMPORT_DIRECTIVE))
1174 importAttributes.put(IMPORT_DIRECTIVE, exportAttributes.get(IMPORT_DIRECTIVE));
1175
1176 fixupAttributes(importAttributes);
1177 removeAttributes(importAttributes);
1178
1179 String result = importAttributes.get(Constants.VERSION_ATTRIBUTE);
1180 if (result == null)
1181 noimports.add(packageRef);
1182 }
1183 finally {
1184 unsetProperty(CURRENT_PACKAGE);
1185 }
1186 }
1187
1188 if (isPedantic() && noimports.size() != 0) {
1189 warning("Imports that lack version ranges: %s", noimports);
1190 }
1191 }
1192
1193 /**
1194 * Find the packages we depend on, where we implement an interface that is a
1195 * Provider Type. These packages, when we import them, must use the provider
1196 * policy.
1197 *
1198 * @throws Exception
1199 */
1200 Set<PackageRef> findProvidedPackages() throws Exception {
1201 Set<PackageRef> providers = Create.set();
1202 Set<TypeRef> cached = Create.set();
1203
1204 for (Clazz c : classspace.values()) {
1205 TypeRef[] interfaces = c.getInterfaces();
1206 if (interfaces != null)
1207 for (TypeRef t : interfaces)
1208 if (cached.contains(t) || isProvider(t)) {
1209 cached.add(t);
1210 providers.add(t.getPackageRef());
1211 }
1212 }
1213 return providers;
1214 }
1215
1216 private boolean isProvider(TypeRef t) throws Exception {
1217 Clazz c = findClass(t);
1218 if (c == null)
1219 return false;
1220
1221 if (c.annotations == null)
1222 return false;
1223
1224 TypeRef pt = getTypeRefFromFQN(ProviderType.class.getName());
1225 boolean result = c.annotations.contains(pt);
1226 return result;
1227 }
1228
1229 /**
1230 * Provide any macro substitutions and versions for exported packages.
1231 */
1232
1233 void augmentExports(Packages exports) {
1234 for (PackageRef packageRef : exports.keySet()) {
1235 String packageName = packageRef.getFQN();
1236 setProperty(CURRENT_PACKAGE, packageName);
1237 try {
1238 Attrs attributes = exports.get(packageRef);
1239 Attrs exporterAttributes = classpathExports.get(packageRef);
1240 if (exporterAttributes == null)
1241 continue;
1242
Stuart McCulloch2286f232012-06-15 13:27:53 +00001243 for (Map.Entry<String,String> entry : exporterAttributes.entrySet()) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001244 String key = entry.getKey();
1245 if (key.equalsIgnoreCase(SPECIFICATION_VERSION))
1246 key = VERSION_ATTRIBUTE;
1247
1248 // dont overwrite and no directives
1249 if (!key.endsWith(":") && !attributes.containsKey(key)) {
1250 attributes.put(key, entry.getValue());
1251 }
1252 }
1253
1254 fixupAttributes(attributes);
1255 removeAttributes(attributes);
1256
1257 }
1258 finally {
1259 unsetProperty(CURRENT_PACKAGE);
1260 }
1261 }
1262 }
1263
1264 /**
Stuart McCulloch2286f232012-06-15 13:27:53 +00001265 * Fixup Attributes Execute any macros on an export and
Stuart McCullochbb014372012-06-07 21:57:32 +00001266 */
1267
1268 void fixupAttributes(Attrs attributes) {
1269 // Convert any attribute values that have macros.
1270 for (String key : attributes.keySet()) {
1271 String value = attributes.get(key);
1272 if (value.indexOf('$') >= 0) {
1273 value = getReplacer().process(value);
1274 attributes.put(key, value);
1275 }
1276 }
1277
1278 }
1279
1280 /**
1281 * Remove the attributes mentioned in the REMOVE_ATTRIBUTE_DIRECTIVE. You
1282 * can add a remove-attribute: directive with a regular expression for
1283 * attributes that need to be removed. We also remove all attributes that
1284 * have a value of !. This allows you to use macros with ${if} to remove
1285 * values.
1286 */
1287
1288 void removeAttributes(Attrs attributes) {
1289 String remove = attributes.remove(REMOVE_ATTRIBUTE_DIRECTIVE);
1290
1291 if (remove != null) {
1292 Instructions removeInstr = new Instructions(remove);
1293 attributes.keySet().removeAll(removeInstr.select(attributes.keySet(), false));
1294 }
1295
1296 // Remove any ! valued attributes
Stuart McCulloch2286f232012-06-15 13:27:53 +00001297 for (Iterator<Entry<String,String>> i = attributes.entrySet().iterator(); i.hasNext();) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001298 String v = i.next().getValue();
1299 if (v.equals("!"))
1300 i.remove();
1301 }
1302 }
1303
1304 /**
1305 * Calculate a version from a version policy.
1306 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00001307 * @param version
1308 * The actual exported version
1309 * @param impl
1310 * true for implementations and false for clients
Stuart McCullochbb014372012-06-07 21:57:32 +00001311 */
1312
1313 String calculateVersionRange(String version, boolean impl) {
1314 setProperty("@", version);
1315 try {
1316 return getVersionPolicy(impl);
1317 }
1318 finally {
1319 unsetProperty("@");
1320 }
1321 }
1322
1323 /**
1324 * Add the uses clauses. This method iterates over the exports and cal
1325 *
1326 * @param exports
1327 * @param uses
1328 * @throws MojoExecutionException
1329 */
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00001330 void doUses(Packages exports, Map<PackageRef,List<PackageRef>> uses, Packages imports) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001331 if ("true".equalsIgnoreCase(getProperty(NOUSES)))
1332 return;
1333
1334 for (Iterator<PackageRef> i = exports.keySet().iterator(); i.hasNext();) {
1335 PackageRef packageRef = i.next();
1336 String packageName = packageRef.getFQN();
1337 setProperty(CURRENT_PACKAGE, packageName);
1338 try {
1339 doUses(packageRef, exports, uses, imports);
1340 }
1341 finally {
1342 unsetProperty(CURRENT_PACKAGE);
1343 }
1344
1345 }
1346 }
1347
1348 /**
1349 * @param packageName
1350 * @param exports
1351 * @param uses
1352 * @param imports
1353 */
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00001354 protected void doUses(PackageRef packageRef, Packages exports, Map<PackageRef,List<PackageRef>> uses,
Stuart McCulloch2286f232012-06-15 13:27:53 +00001355 Packages imports) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001356 Attrs clause = exports.get(packageRef);
1357
1358 // Check if someone already set the uses: directive
1359 String override = clause.get(USES_DIRECTIVE);
1360 if (override == null)
1361 override = USES_USES;
1362
1363 // Get the used packages
1364 Collection<PackageRef> usedPackages = uses.get(packageRef);
1365
1366 if (usedPackages != null) {
1367
1368 // Only do a uses on exported or imported packages
1369 // and uses should also not contain our own package
1370 // name
1371 Set<PackageRef> sharedPackages = new HashSet<PackageRef>();
1372 sharedPackages.addAll(imports.keySet());
1373 sharedPackages.addAll(exports.keySet());
1374 sharedPackages.retainAll(usedPackages);
1375 sharedPackages.remove(packageRef);
1376
1377 StringBuilder sb = new StringBuilder();
1378 String del = "";
1379 for (Iterator<PackageRef> u = sharedPackages.iterator(); u.hasNext();) {
1380 PackageRef usedPackage = u.next();
1381 if (!usedPackage.isJava()) {
1382 sb.append(del);
1383 sb.append(usedPackage.getFQN());
1384 del = ",";
1385 }
1386 }
1387 if (override.indexOf('$') >= 0) {
1388 setProperty(CURRENT_USES, sb.toString());
1389 override = getReplacer().process(override);
1390 unsetProperty(CURRENT_USES);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001391 } else
Stuart McCullochbb014372012-06-07 21:57:32 +00001392 // This is for backward compatibility 0.0.287
1393 // can be deprecated over time
Stuart McCulloch2286f232012-06-15 13:27:53 +00001394 override = override.replaceAll(USES_USES, Matcher.quoteReplacement(sb.toString())).trim();
Stuart McCullochbb014372012-06-07 21:57:32 +00001395
1396 if (override.endsWith(","))
1397 override = override.substring(0, override.length() - 1);
1398 if (override.startsWith(","))
1399 override = override.substring(1);
1400 if (override.length() > 0) {
1401 clause.put(USES_DIRECTIVE, override);
1402 }
1403 }
1404 }
1405
1406 /**
1407 * Transitively remove all elemens from unreachable through the uses link.
1408 *
1409 * @param name
1410 * @param unreachable
1411 */
1412 void removeTransitive(PackageRef name, Set<PackageRef> unreachable) {
1413 if (!unreachable.contains(name))
1414 return;
1415
1416 unreachable.remove(name);
1417
1418 List<PackageRef> ref = uses.get(name);
1419 if (ref != null) {
1420 for (Iterator<PackageRef> r = ref.iterator(); r.hasNext();) {
1421 PackageRef element = r.next();
1422 removeTransitive(element, unreachable);
1423 }
1424 }
1425 }
1426
1427 /**
1428 * Helper method to set the package info resource
1429 *
1430 * @param dir
1431 * @param key
1432 * @param value
1433 * @throws Exception
1434 */
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00001435 void getExportVersionsFromPackageInfo(PackageRef packageRef, Resource r, Packages classpathExports)
1436 throws Exception {
Stuart McCullochbb014372012-06-07 21:57:32 +00001437 if (r == null)
1438 return;
1439
1440 Properties p = new Properties();
Stuart McCullochbb014372012-06-07 21:57:32 +00001441 try {
Stuart McCullochd4826102012-06-26 16:34:24 +00001442 InputStream in = r.openInputStream();
1443 try {
1444 p.load(in);
Stuart McCullochbb014372012-06-07 21:57:32 +00001445 }
Stuart McCullochd4826102012-06-26 16:34:24 +00001446 finally {
1447 in.close();
1448 }
1449 Attrs map = classpathExports.get(packageRef);
1450 if (map == null) {
1451 classpathExports.put(packageRef, map = new Attrs());
1452 }
1453 for (Enumeration<String> t = (Enumeration<String>) p.propertyNames(); t.hasMoreElements();) {
1454 String key = t.nextElement();
1455 String value = map.get(key);
1456 if (value == null) {
1457 value = p.getProperty(key);
1458
1459 // Messy, to allow directives we need to
1460 // allow the value to start with a ':' since we cannot
1461 // encode this in a property name
1462
1463 if (value.startsWith(":")) {
1464 key = key + ":";
1465 value = value.substring(1);
1466 }
1467 map.put(key, value);
1468 }
1469 }
1470 }
1471 catch (Exception e) {
1472 msgs.NoSuchFile_(r);
Stuart McCullochbb014372012-06-07 21:57:32 +00001473 }
1474 }
1475
Stuart McCulloch55d4dfe2012-08-07 10:57:21 +00001476 @Override
Stuart McCullochbb014372012-06-07 21:57:32 +00001477 public void close() {
1478 if (diagnostics) {
1479 PrintStream out = System.err;
1480 out.printf("Current directory : %s%n", new File("").getAbsolutePath());
1481 out.println("Classpath used");
1482 for (Jar jar : getClasspath()) {
1483 out.printf("File : %s%n", jar.getSource());
Stuart McCulloch2286f232012-06-15 13:27:53 +00001484 out.printf("File abs path : %s%n", jar.getSource().getAbsolutePath());
Stuart McCullochbb014372012-06-07 21:57:32 +00001485 out.printf("Name : %s%n", jar.getName());
Stuart McCulloch2286f232012-06-15 13:27:53 +00001486 Map<String,Map<String,Resource>> dirs = jar.getDirectories();
1487 for (Map.Entry<String,Map<String,Resource>> entry : dirs.entrySet()) {
1488 Map<String,Resource> dir = entry.getValue();
Stuart McCullochbb014372012-06-07 21:57:32 +00001489 String name = entry.getKey().replace('/', '.');
1490 if (dir != null) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001491 out.printf(" %-30s %d%n", name, dir.size());
1492 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001493 out.printf(" %-30s <<empty>>%n", name);
1494 }
1495 }
1496 }
1497 }
1498
1499 super.close();
1500 if (dot != null)
1501 dot.close();
1502
1503 if (classpath != null)
1504 for (Iterator<Jar> j = classpath.iterator(); j.hasNext();) {
1505 Jar jar = j.next();
1506 jar.close();
1507 }
1508 }
1509
1510 /**
1511 * Findpath looks through the contents of the JAR and finds paths that end
Stuart McCulloch2286f232012-06-15 13:27:53 +00001512 * with the given regular expression ${findpath (; reg-expr (; replacement)?
1513 * )? }
Stuart McCullochbb014372012-06-07 21:57:32 +00001514 *
1515 * @param args
1516 * @return
1517 */
1518 public String _findpath(String args[]) {
1519 return findPath("findpath", args, true);
1520 }
1521
1522 public String _findname(String args[]) {
1523 return findPath("findname", args, false);
1524 }
1525
1526 String findPath(String name, String[] args, boolean fullPathName) {
1527 if (args.length > 3) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001528 warning("Invalid nr of arguments to " + name + " " + Arrays.asList(args) + ", syntax: ${" + name
1529 + " (; reg-expr (; replacement)? )? }");
Stuart McCullochbb014372012-06-07 21:57:32 +00001530 return null;
1531 }
1532
1533 String regexp = ".*";
1534 String replace = null;
1535
1536 switch (args.length) {
1537 case 3 :
1538 replace = args[2];
1539 //$FALL-THROUGH$
1540 case 2 :
1541 regexp = args[1];
1542 }
1543 StringBuilder sb = new StringBuilder();
1544 String del = "";
1545
1546 Pattern expr = Pattern.compile(regexp);
1547 for (Iterator<String> e = dot.getResources().keySet().iterator(); e.hasNext();) {
1548 String path = e.next();
1549 if (!fullPathName) {
1550 int n = path.lastIndexOf('/');
1551 if (n >= 0) {
1552 path = path.substring(n + 1);
1553 }
1554 }
1555
1556 Matcher m = expr.matcher(path);
1557 if (m.matches()) {
1558 if (replace != null)
1559 path = m.replaceAll(replace);
1560
1561 sb.append(del);
1562 sb.append(path);
1563 del = ", ";
1564 }
1565 }
1566 return sb.toString();
1567 }
1568
Stuart McCulloch2286f232012-06-15 13:27:53 +00001569 public void putAll(Map<String,String> additional, boolean force) {
1570 for (Iterator<Map.Entry<String,String>> i = additional.entrySet().iterator(); i.hasNext();) {
1571 Map.Entry<String,String> entry = i.next();
Stuart McCullochbb014372012-06-07 21:57:32 +00001572 if (force || getProperties().get(entry.getKey()) == null)
1573 setProperty(entry.getKey(), entry.getValue());
1574 }
1575 }
1576
1577 boolean firstUse = true;
1578
1579 public List<Jar> getClasspath() {
1580 if (firstUse) {
1581 firstUse = false;
1582 String cp = getProperty(CLASSPATH);
1583 if (cp != null)
1584 for (String s : split(cp)) {
1585 Jar jar = getJarFromName(s, "getting classpath");
1586 if (jar != null)
1587 addClasspath(jar);
1588 else
1589 warning("Cannot find entry on -classpath: %s", s);
1590 }
1591 }
1592 return classpath;
1593 }
1594
1595 public void addClasspath(Jar jar) {
1596 if (isPedantic() && jar.getResources().isEmpty())
1597 warning("There is an empty jar or directory on the classpath: " + jar.getName());
1598
1599 classpath.add(jar);
1600 }
1601
1602 public void addClasspath(Collection< ? > jars) throws IOException {
1603 for (Object jar : jars) {
1604 if (jar instanceof Jar)
1605 addClasspath((Jar) jar);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001606 else if (jar instanceof File)
1607 addClasspath((File) jar);
1608 else if (jar instanceof String)
1609 addClasspath(getFile((String) jar));
Stuart McCullochbb014372012-06-07 21:57:32 +00001610 else
Stuart McCulloch2286f232012-06-15 13:27:53 +00001611 error("Cannot convert to JAR to add to classpath %s. Not a File, Jar, or String", jar);
Stuart McCullochbb014372012-06-07 21:57:32 +00001612 }
1613 }
1614
1615 public void addClasspath(File cp) throws IOException {
1616 if (!cp.exists())
1617 warning("File on classpath that does not exist: " + cp);
1618 Jar jar = new Jar(cp);
1619 addClose(jar);
1620 classpath.add(jar);
1621 }
1622
Stuart McCulloch55d4dfe2012-08-07 10:57:21 +00001623 @Override
Stuart McCullochbb014372012-06-07 21:57:32 +00001624 public void clear() {
1625 classpath.clear();
1626 }
1627
1628 public Jar getTarget() {
1629 return dot;
1630 }
1631
1632 private void analyzeBundleClasspath() throws Exception {
1633 Parameters bcp = getBundleClasspath();
1634
1635 if (bcp.isEmpty()) {
1636 analyzeJar(dot, "", true);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001637 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001638 boolean okToIncludeDirs = true;
1639
1640 for (String path : bcp.keySet()) {
1641 if (dot.getDirectories().containsKey(path)) {
1642 okToIncludeDirs = false;
1643 break;
1644 }
1645 }
1646
1647 for (String path : bcp.keySet()) {
1648 Attrs info = bcp.get(path);
1649
1650 if (path.equals(".")) {
1651 analyzeJar(dot, "", okToIncludeDirs);
1652 continue;
1653 }
1654 //
1655 // There are 3 cases:
1656 // - embedded JAR file
1657 // - directory
1658 // - error
1659 //
1660
1661 Resource resource = dot.getResource(path);
1662 if (resource != null) {
1663 try {
1664 Jar jar = new Jar(path);
1665 addClose(jar);
1666 EmbeddedResource.build(jar, resource);
1667 analyzeJar(jar, "", true);
1668 }
1669 catch (Exception e) {
1670 warning("Invalid bundle classpath entry: " + path + " " + e);
1671 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001672 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001673 if (dot.getDirectories().containsKey(path)) {
1674 // if directories are used, we should not have dot as we
1675 // would have the classes in these directories on the
1676 // class path twice.
1677 if (bcp.containsKey("."))
1678 warning("Bundle-ClassPath uses a directory '%s' as well as '.'. This means bnd does not know if a directory is a package.",
1679 path, path);
1680 analyzeJar(dot, Processor.appendPath(path) + "/", true);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001681 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001682 if (!"optional".equals(info.get(RESOLUTION_DIRECTIVE)))
1683 warning("No sub JAR or directory " + path);
1684 }
1685 }
1686 }
1687
1688 }
1689 }
1690
1691 /**
1692 * We traverse through all the classes that we can find and calculate the
1693 * contained and referred set and uses. This method ignores the Bundle
1694 * classpath.
1695 *
1696 * @param jar
1697 * @param contained
1698 * @param referred
1699 * @param uses
1700 * @throws IOException
1701 */
1702 private boolean analyzeJar(Jar jar, String prefix, boolean okToIncludeDirs) throws Exception {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001703 Map<String,Clazz> mismatched = new HashMap<String,Clazz>();
Stuart McCullochbb014372012-06-07 21:57:32 +00001704
1705 next: for (String path : jar.getResources().keySet()) {
1706 if (path.startsWith(prefix)) {
1707
1708 String relativePath = path.substring(prefix.length());
1709
1710 if (okToIncludeDirs) {
1711 int n = relativePath.lastIndexOf('/');
1712 if (n < 0)
1713 n = relativePath.length();
1714 String relativeDir = relativePath.substring(0, n);
1715
1716 PackageRef packageRef = getPackageRef(relativeDir);
1717 if (!packageRef.isMetaData() && !contained.containsKey(packageRef)) {
1718 contained.put(packageRef);
1719
1720 // For each package we encounter for the first
1721 // time. Unfortunately we can only do this once
1722 // we found a class since the bcp has a tendency
1723 // to overlap
1724 if (!packageRef.isMetaData()) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001725 Resource pinfo = jar.getResource(prefix + packageRef.getPath() + "/packageinfo");
Stuart McCullochd4826102012-06-26 16:34:24 +00001726 getExportVersionsFromPackageInfo(packageRef, pinfo, classpathExports);
Stuart McCullochbb014372012-06-07 21:57:32 +00001727 }
1728 }
1729 }
1730
1731 // Check class resources, we need to analyze them
1732 if (path.endsWith(".class")) {
1733 Resource resource = jar.getResource(path);
1734 Clazz clazz;
1735 Attrs info = null;
1736
1737 try {
1738 InputStream in = resource.openInputStream();
1739 clazz = new Clazz(this, path, resource);
1740 try {
1741 // Check if we have a package-info
1742 if (relativePath.endsWith("/package-info.class")) {
1743 // package-info can contain an Export annotation
1744 info = new Attrs();
1745 parsePackageInfoClass(clazz, info);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001746 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001747 // Otherwise we just parse it simply
1748 clazz.parseClassFile();
1749 }
1750 }
1751 finally {
1752 in.close();
1753 }
1754 }
1755 catch (Throwable e) {
1756 error("Invalid class file %s (%s)", e, relativePath, e);
1757 e.printStackTrace();
1758 continue next;
1759 }
1760
1761 String calculatedPath = clazz.getClassName().getPath();
1762 if (!calculatedPath.equals(relativePath)) {
1763 // If there is a mismatch we
1764 // warning
1765 if (okToIncludeDirs) // assume already reported
1766 mismatched.put(clazz.getAbsolutePath(), clazz);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001767 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001768 classspace.put(clazz.getClassName(), clazz);
1769 PackageRef packageRef = clazz.getClassName().getPackageRef();
1770
1771 if (!contained.containsKey(packageRef)) {
1772 contained.put(packageRef);
1773 if (!packageRef.isMetaData()) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001774 Resource pinfo = jar.getResource(prefix + packageRef.getPath() + "/packageinfo");
Stuart McCullochd4826102012-06-26 16:34:24 +00001775 getExportVersionsFromPackageInfo(packageRef, pinfo, classpathExports);
Stuart McCullochbb014372012-06-07 21:57:32 +00001776 }
1777 }
1778 if (info != null)
1779 contained.merge(packageRef, false, info);
1780
Stuart McCullochbb014372012-06-07 21:57:32 +00001781 // Look at the referred packages
1782 // and copy them to our baseline
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00001783 Set<PackageRef> refs = Create.set();
Stuart McCullochbb014372012-06-07 21:57:32 +00001784 for (PackageRef p : clazz.getReferred()) {
1785 referred.put(p);
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00001786 refs.add(p);
Stuart McCullochbb014372012-06-07 21:57:32 +00001787 }
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00001788 refs.remove(packageRef);
1789 uses.addAll(packageRef, refs);
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00001790
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00001791 // Collect the API
1792 apiUses.addAll(packageRef, clazz.getAPIUses());
Stuart McCullochbb014372012-06-07 21:57:32 +00001793 }
1794 }
1795 }
1796 }
1797
1798 if (mismatched.size() > 0) {
1799 error("Classes found in the wrong directory: %s", mismatched);
1800 return false;
1801 }
1802 return true;
1803 }
1804
1805 static Pattern OBJECT_REFERENCE = Pattern.compile("L([^/]+/)*([^;]+);");
1806
1807 private void parsePackageInfoClass(final Clazz clazz, final Attrs info) throws Exception {
1808 clazz.parseClassFileWithCollector(new ClassDataCollector() {
1809 @Override
1810 public void annotation(Annotation a) {
1811 String name = a.name.getFQN();
1812 if (aQute.bnd.annotation.Version.class.getName().equals(name)) {
1813
1814 // Check version
1815 String version = a.get("value");
1816 if (!info.containsKey(Constants.VERSION_ATTRIBUTE)) {
1817 if (version != null) {
1818 version = getReplacer().process(version);
1819 if (Verifier.VERSION.matcher(version).matches())
1820 info.put(VERSION_ATTRIBUTE, version);
1821 else
Stuart McCulloch2286f232012-06-15 13:27:53 +00001822 error("Export annotation in %s has invalid version info: %s", clazz, version);
Stuart McCullochbb014372012-06-07 21:57:32 +00001823 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001824 } else {
Stuart McCullochbb014372012-06-07 21:57:32 +00001825 // Verify this matches with packageinfo
1826 String presentVersion = info.get(VERSION_ATTRIBUTE);
1827 try {
1828 Version av = new Version(presentVersion);
1829 Version bv = new Version(version);
1830 if (!av.equals(bv)) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001831 error("Version from annotation for %s differs with packageinfo or Manifest", clazz
1832 .getClassName().getFQN());
Stuart McCullochbb014372012-06-07 21:57:32 +00001833 }
1834 }
1835 catch (Exception e) {
1836 // Ignore
1837 }
1838 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001839 } else if (name.equals(Export.class.getName())) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001840
Stuart McCulloch2286f232012-06-15 13:27:53 +00001841 // Check mandatory attributes
1842 Attrs attrs = doAttrbutes((Object[]) a.get(Export.MANDATORY), clazz, getReplacer());
1843 if (!attrs.isEmpty()) {
1844 info.putAll(attrs);
1845 info.put(MANDATORY_DIRECTIVE, Processor.join(attrs.keySet()));
1846 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001847
Stuart McCulloch2286f232012-06-15 13:27:53 +00001848 // Check optional attributes
1849 attrs = doAttrbutes((Object[]) a.get(Export.OPTIONAL), clazz, getReplacer());
1850 if (!attrs.isEmpty()) {
1851 info.putAll(attrs);
1852 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001853
Stuart McCulloch2286f232012-06-15 13:27:53 +00001854 // Check Included classes
1855 Object[] included = a.get(Export.INCLUDE);
1856 if (included != null && included.length > 0) {
1857 StringBuilder sb = new StringBuilder();
1858 String del = "";
1859 for (Object i : included) {
1860 Matcher m = OBJECT_REFERENCE.matcher((String) i);
1861 if (m.matches()) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001862 sb.append(del);
Stuart McCulloch2286f232012-06-15 13:27:53 +00001863 sb.append(m.group(2));
Stuart McCullochbb014372012-06-07 21:57:32 +00001864 del = ",";
1865 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001866 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001867 info.put(INCLUDE_DIRECTIVE, sb.toString());
Stuart McCullochbb014372012-06-07 21:57:32 +00001868 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001869
1870 // Check Excluded classes
1871 Object[] excluded = a.get(Export.EXCLUDE);
1872 if (excluded != null && excluded.length > 0) {
1873 StringBuilder sb = new StringBuilder();
1874 String del = "";
1875 for (Object i : excluded) {
1876 Matcher m = OBJECT_REFERENCE.matcher((String) i);
1877 if (m.matches()) {
1878 sb.append(del);
1879 sb.append(m.group(2));
1880 del = ",";
1881 }
1882 }
1883 info.put(EXCLUDE_DIRECTIVE, sb.toString());
1884 }
1885
1886 // Check Uses
1887 Object[] uses = a.get(Export.USES);
1888 if (uses != null && uses.length > 0) {
1889 String old = info.get(USES_DIRECTIVE);
1890 if (old == null)
1891 old = "";
1892 StringBuilder sb = new StringBuilder(old);
1893 String del = sb.length() == 0 ? "" : ",";
1894
1895 for (Object use : uses) {
1896 sb.append(del);
1897 sb.append(use);
1898 del = ",";
1899 }
1900 info.put(USES_DIRECTIVE, sb.toString());
1901 }
1902 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001903 }
1904
1905 });
1906 }
1907
1908 /**
1909 * Clean up version parameters. Other builders use more fuzzy definitions of
1910 * the version syntax. This method cleans up such a version to match an OSGi
1911 * version.
1912 *
1913 * @param VERSION_STRING
1914 * @return
1915 */
Stuart McCulloch2286f232012-06-15 13:27:53 +00001916 static Pattern fuzzyVersion = Pattern.compile("(\\d+)(\\.(\\d+)(\\.(\\d+))?)?([^a-zA-Z0-9](.*))?",
1917 Pattern.DOTALL);
1918 static Pattern fuzzyVersionRange = Pattern.compile(
1919 "(\\(|\\[)\\s*([-\\da-zA-Z.]+)\\s*,\\s*([-\\da-zA-Z.]+)\\s*(\\]|\\))",
1920 Pattern.DOTALL);
Stuart McCullochbb014372012-06-07 21:57:32 +00001921 static Pattern fuzzyModifier = Pattern.compile("(\\d+[.-])*(.*)", Pattern.DOTALL);
1922
1923 static Pattern nummeric = Pattern.compile("\\d*");
1924
1925 static public String cleanupVersion(String version) {
1926 Matcher m = Verifier.VERSIONRANGE.matcher(version);
1927
1928 if (m.matches()) {
Stuart McCullochec47fe72012-09-19 12:56:05 +00001929 try {
1930 VersionRange vr = new VersionRange(version);
1931 return version;
1932 } catch( Exception e) {
1933 // ignore
1934 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001935 }
1936
1937 m = fuzzyVersionRange.matcher(version);
1938 if (m.matches()) {
1939 String prefix = m.group(1);
1940 String first = m.group(2);
1941 String last = m.group(3);
1942 String suffix = m.group(4);
1943 return prefix + cleanupVersion(first) + "," + cleanupVersion(last) + suffix;
1944 }
Stuart McCullochbb014372012-06-07 21:57:32 +00001945
Stuart McCulloch2286f232012-06-15 13:27:53 +00001946 m = fuzzyVersion.matcher(version);
1947 if (m.matches()) {
1948 StringBuilder result = new StringBuilder();
1949 String major = removeLeadingZeroes(m.group(1));
1950 String minor = removeLeadingZeroes(m.group(3));
1951 String micro = removeLeadingZeroes(m.group(5));
1952 String qualifier = m.group(7);
1953
Stuart McCullochec47fe72012-09-19 12:56:05 +00001954 if (qualifier == null) {
1955 if (!isInteger(minor)) {
1956 qualifier = minor;
1957 minor = "0";
1958 } else if (!isInteger(micro)) {
1959 qualifier = micro;
1960 micro = "0";
1961 }
1962 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001963 if (major != null) {
1964 result.append(major);
1965 if (minor != null) {
1966 result.append(".");
1967 result.append(minor);
1968 if (micro != null) {
Stuart McCullochbb014372012-06-07 21:57:32 +00001969 result.append(".");
Stuart McCulloch2286f232012-06-15 13:27:53 +00001970 result.append(micro);
Stuart McCullochbb014372012-06-07 21:57:32 +00001971 if (qualifier != null) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00001972 result.append(".");
Stuart McCullochbb014372012-06-07 21:57:32 +00001973 cleanupModifier(result, qualifier);
1974 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001975 } else if (qualifier != null) {
1976 result.append(".0.");
1977 cleanupModifier(result, qualifier);
1978 }
1979 } else if (qualifier != null) {
1980 result.append(".0.0.");
1981 cleanupModifier(result, qualifier);
Stuart McCullochbb014372012-06-07 21:57:32 +00001982 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00001983 return result.toString();
Stuart McCullochbb014372012-06-07 21:57:32 +00001984 }
1985 }
1986 return version;
1987 }
1988
Stuart McCullochec47fe72012-09-19 12:56:05 +00001989 /**
1990 * TRhe cleanup version got confused when people used numeric dates like
1991 * 201209091230120 as qualifiers. These are too large for Integers. This
1992 * method checks if the all digit string fits in an integer.
1993 * <pre>
1994 * maxint = 2,147,483,647 = 10 digits
1995 * </pre>
1996 * @param integer
1997 * @return if this fits in an integer
1998 */
1999 private static boolean isInteger(String minor) {
2000 return minor.length() < 10 || (minor.length() == 10 && minor.compareTo("2147483647") < 0);
2001 }
2002
Stuart McCullochbb014372012-06-07 21:57:32 +00002003 private static String removeLeadingZeroes(String group) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00002004 if (group == null)
2005 return null;
2006
Stuart McCullochbb014372012-06-07 21:57:32 +00002007 int n = 0;
Stuart McCulloch2286f232012-06-15 13:27:53 +00002008 while (n < group.length() - 1 && group.charAt(n) == '0')
Stuart McCullochbb014372012-06-07 21:57:32 +00002009 n++;
2010 if (n == 0)
2011 return group;
2012
2013 return group.substring(n);
2014 }
2015
2016 static void cleanupModifier(StringBuilder result, String modifier) {
2017 Matcher m = fuzzyModifier.matcher(modifier);
2018 if (m.matches())
2019 modifier = m.group(2);
2020
2021 for (int i = 0; i < modifier.length(); i++) {
2022 char c = modifier.charAt(i);
Stuart McCulloch2286f232012-06-15 13:27:53 +00002023 if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '-')
Stuart McCullochbb014372012-06-07 21:57:32 +00002024 result.append(c);
2025 }
2026 }
2027
2028 final static String DEFAULT_PROVIDER_POLICY = "${range;[==,=+)}";
2029 final static String DEFAULT_CONSUMER_POLICY = "${range;[==,+)}";
2030
Stuart McCullochbb014372012-06-07 21:57:32 +00002031 public String getVersionPolicy(boolean implemented) {
2032 if (implemented) {
Stuart McCullochd4826102012-06-26 16:34:24 +00002033 return getProperty(PROVIDER_POLICY, DEFAULT_PROVIDER_POLICY);
Stuart McCullochbb014372012-06-07 21:57:32 +00002034 }
Stuart McCullochbb014372012-06-07 21:57:32 +00002035
Stuart McCullochd4826102012-06-26 16:34:24 +00002036 return getProperty(CONSUMER_POLICY, DEFAULT_CONSUMER_POLICY);
Stuart McCullochbb014372012-06-07 21:57:32 +00002037 }
2038
2039 /**
2040 * The extends macro traverses all classes and returns a list of class names
2041 * that extend a base class.
2042 */
2043
2044 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";
2045
2046 public String _classes(String... args) throws Exception {
2047 // Macro.verifyCommand(args, _classesHelp, new
2048 // Pattern[]{null,Pattern.compile("(implementing|implements|extending|extends|importing|imports|any)"),
2049 // null}, 3,3);
2050
2051 Collection<Clazz> matched = getClasses(args);
2052 if (matched.isEmpty())
2053 return "";
2054
2055 return join(matched);
2056 }
2057
2058 public Collection<Clazz> getClasses(String... args) throws Exception {
2059
2060 Set<Clazz> matched = new HashSet<Clazz>(classspace.values());
2061 for (int i = 1; i < args.length; i++) {
2062 if (args.length < i + 1)
Stuart McCulloch2286f232012-06-15 13:27:53 +00002063 throw new IllegalArgumentException("${classes} macro must have odd number of arguments. "
2064 + _classesHelp);
Stuart McCullochbb014372012-06-07 21:57:32 +00002065
2066 String typeName = args[i];
2067 if (typeName.equalsIgnoreCase("extending"))
2068 typeName = "extends";
Stuart McCulloch2286f232012-06-15 13:27:53 +00002069 else if (typeName.equalsIgnoreCase("importing"))
2070 typeName = "imports";
Stuart McCullochb215bfd2012-09-06 18:28:06 +00002071 else if (typeName.equalsIgnoreCase("annotation"))
2072 typeName = "annotated";
Stuart McCulloch2286f232012-06-15 13:27:53 +00002073 else if (typeName.equalsIgnoreCase("implementing"))
2074 typeName = "implements";
Stuart McCullochbb014372012-06-07 21:57:32 +00002075
2076 Clazz.QUERY type = Clazz.QUERY.valueOf(typeName.toUpperCase());
2077
2078 if (type == null)
Stuart McCulloch2286f232012-06-15 13:27:53 +00002079 throw new IllegalArgumentException("${classes} has invalid type: " + typeName + ". " + _classesHelp);
Stuart McCullochbb014372012-06-07 21:57:32 +00002080
2081 Instruction instr = null;
2082 if (Clazz.HAS_ARGUMENT.contains(type)) {
2083 String s = args[++i];
2084 instr = new Instruction(s);
2085 }
2086 for (Iterator<Clazz> c = matched.iterator(); c.hasNext();) {
2087 Clazz clazz = c.next();
2088 if (!clazz.is(type, instr, this)) {
2089 c.remove();
2090 }
2091 }
2092 }
2093 return matched;
2094 }
2095
2096 /**
2097 * Get the exporter of a package ...
2098 */
2099
2100 public String _exporters(String args[]) throws Exception {
Stuart McCulloch2286f232012-06-15 13:27:53 +00002101 Macro.verifyCommand(args, "${exporters;<packagename>}, returns the list of jars that export the given package",
Stuart McCullochbb014372012-06-07 21:57:32 +00002102 null, 2, 2);
2103 StringBuilder sb = new StringBuilder();
2104 String del = "";
2105 String pack = args[1].replace('.', '/');
2106 for (Jar jar : classpath) {
2107 if (jar.getDirectories().containsKey(pack)) {
2108 sb.append(del);
2109 sb.append(jar.getName());
2110 }
2111 }
2112 return sb.toString();
2113 }
2114
Stuart McCulloch2286f232012-06-15 13:27:53 +00002115 public Map<TypeRef,Clazz> getClassspace() {
Stuart McCullochbb014372012-06-07 21:57:32 +00002116 return classspace;
2117 }
2118
2119 /**
2120 * Locate a resource on the class path.
2121 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00002122 * @param path
2123 * Path of the reosurce
Stuart McCullochbb014372012-06-07 21:57:32 +00002124 * @return A resource or <code>null</code>
2125 */
2126 public Resource findResource(String path) {
2127 for (Jar entry : getClasspath()) {
2128 Resource r = entry.getResource(path);
2129 if (r != null)
2130 return r;
2131 }
2132 return null;
2133 }
2134
2135 /**
2136 * Find a clazz on the class path. This class has been parsed.
2137 *
2138 * @param path
2139 * @return
2140 */
2141 public Clazz findClass(TypeRef typeRef) throws Exception {
2142 Clazz c = classspace.get(typeRef);
2143 if (c != null)
2144 return c;
2145
2146 c = importedClassesCache.get(typeRef);
2147 if (c != null)
2148 return c;
2149
2150 Resource r = findResource(typeRef.getPath());
2151 if (r == null) {
2152 getClass().getClassLoader();
2153 URL url = ClassLoader.getSystemResource(typeRef.getPath());
2154 if (url != null)
2155 r = new URLResource(url);
2156 }
2157 if (r != null) {
2158 c = new Clazz(this, typeRef.getPath(), r);
2159 c.parseClassFile();
2160 importedClassesCache.put(typeRef, c);
2161 }
2162 return c;
2163 }
2164
2165 /**
2166 * Answer the bundle version.
2167 *
2168 * @return
2169 */
2170 public String getVersion() {
2171 String version = getProperty(BUNDLE_VERSION);
2172 if (version == null)
2173 version = "0.0.0";
2174 return version;
2175 }
2176
2177 public boolean isNoBundle() {
2178 return isTrue(getProperty(RESOURCEONLY)) || isTrue(getProperty(NOMANIFEST));
2179 }
2180
2181 public void referTo(TypeRef ref) {
2182 PackageRef pack = ref.getPackageRef();
2183 if (!referred.containsKey(pack))
2184 referred.put(pack, new Attrs());
2185 }
2186
2187 public void referToByBinaryName(String binaryClassName) {
2188 TypeRef ref = descriptors.getTypeRef(binaryClassName);
2189 referTo(ref);
2190 }
2191
2192 /**
2193 * Ensure that we are running on the correct bnd.
2194 */
2195 void doRequireBnd() {
2196 Attrs require = OSGiHeader.parseProperties(getProperty(REQUIRE_BND));
2197 if (require == null || require.isEmpty())
2198 return;
2199
Stuart McCulloch2286f232012-06-15 13:27:53 +00002200 Hashtable<String,String> map = new Hashtable<String,String>();
Stuart McCullochbb014372012-06-07 21:57:32 +00002201 map.put(Constants.VERSION_FILTER, getBndVersion());
2202
2203 for (String filter : require.keySet()) {
2204 try {
2205 Filter f = new Filter(filter);
2206 if (f.match(map))
2207 continue;
2208 error("%s fails %s", REQUIRE_BND, require.get(filter));
2209 }
2210 catch (Exception t) {
2211 error("%s with value %s throws exception", t, REQUIRE_BND, require);
2212 }
2213 }
2214 }
2215
2216 /**
2217 * md5 macro
2218 */
2219
2220 static String _md5Help = "${md5;path}";
2221
2222 public String _md5(String args[]) throws Exception {
Stuart McCulloch2286f232012-06-15 13:27:53 +00002223 Macro.verifyCommand(args, _md5Help, new Pattern[] {
2224 null, null, Pattern.compile("base64|hex")
2225 }, 2, 3);
Stuart McCullochbb014372012-06-07 21:57:32 +00002226
2227 Digester<MD5> digester = MD5.getDigester();
2228 Resource r = dot.getResource(args[1]);
2229 if (r == null)
2230 throw new FileNotFoundException("From " + digester + ", not found " + args[1]);
2231
2232 IO.copy(r.openInputStream(), digester);
2233 boolean hex = args.length > 2 && args[2].equals("hex");
2234 if (hex)
2235 return Hex.toHexString(digester.digest().digest());
Stuart McCulloch2286f232012-06-15 13:27:53 +00002236
2237 return Base64.encodeBase64(digester.digest().digest());
Stuart McCullochbb014372012-06-07 21:57:32 +00002238 }
2239
2240 /**
2241 * SHA1 macro
2242 */
2243
2244 static String _sha1Help = "${sha1;path}";
2245
2246 public String _sha1(String args[]) throws Exception {
Stuart McCulloch2286f232012-06-15 13:27:53 +00002247 Macro.verifyCommand(args, _sha1Help, new Pattern[] {
2248 null, null, Pattern.compile("base64|hex")
2249 }, 2, 3);
Stuart McCullochbb014372012-06-07 21:57:32 +00002250 Digester<SHA1> digester = SHA1.getDigester();
2251 Resource r = dot.getResource(args[1]);
2252 if (r == null)
2253 throw new FileNotFoundException("From sha1, not found " + args[1]);
2254
2255 IO.copy(r.openInputStream(), digester);
2256 return Base64.encodeBase64(digester.digest().digest());
2257 }
2258
2259 public Descriptor getDescriptor(String descriptor) {
2260 return descriptors.getDescriptor(descriptor);
2261 }
2262
2263 public TypeRef getTypeRef(String binaryClassName) {
2264 return descriptors.getTypeRef(binaryClassName);
2265 }
2266
2267 public PackageRef getPackageRef(String binaryName) {
2268 return descriptors.getPackageRef(binaryName);
2269 }
2270
2271 public TypeRef getTypeRefFromFQN(String fqn) {
2272 return descriptors.getTypeRefFromFQN(fqn);
2273 }
2274
2275 public TypeRef getTypeRefFromPath(String path) {
2276 return descriptors.getTypeRefFromPath(path);
2277 }
2278
2279 public boolean isImported(PackageRef packageRef) {
2280 return imports.containsKey(packageRef);
2281 }
2282
2283 /**
2284 * Merge the attributes of two maps, where the first map can contain
2285 * wildcarded names. The idea is that the first map contains instructions
2286 * (for example *) with a set of attributes. These patterns are matched
2287 * against the found packages in actual. If they match, the result is set
2288 * with the merged set of attributes. It is expected that the instructions
2289 * are ordered so that the instructor can define which pattern matches
2290 * first. Attributes in the instructions override any attributes from the
2291 * actual.<br/>
Stuart McCullochbb014372012-06-07 21:57:32 +00002292 * A pattern is a modified regexp so it looks like globbing. The * becomes a
2293 * .* just like the ? becomes a .?. '.' are replaced with \\. Additionally,
2294 * if the pattern starts with an exclamation mark, it will remove that
2295 * matches for that pattern (- the !) from the working set. So the following
2296 * patterns should work:
2297 * <ul>
2298 * <li>com.foo.bar</li>
2299 * <li>com.foo.*</li>
2300 * <li>com.foo.???</li>
2301 * <li>com.*.[^b][^a][^r]</li>
2302 * <li>!com.foo.* (throws away any match for com.foo.*)</li>
2303 * </ul>
2304 * Enough rope to hang the average developer I would say.
2305 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00002306 * @param instructions
2307 * the instructions with patterns.
2308 * @param source
2309 * the actual found packages, contains no duplicates
Stuart McCullochbb014372012-06-07 21:57:32 +00002310 * @return Only the packages that were filtered by the given instructions
2311 */
2312
2313 Packages filter(Instructions instructions, Packages source, Set<Instruction> nomatch) {
2314 Packages result = new Packages();
2315 List<PackageRef> refs = new ArrayList<PackageRef>(source.keySet());
2316 Collections.sort(refs);
2317
2318 List<Instruction> filters = new ArrayList<Instruction>(instructions.keySet());
2319 if (nomatch == null)
2320 nomatch = Create.set();
2321
2322 for (Instruction instruction : filters) {
2323 boolean match = false;
2324
2325 for (Iterator<PackageRef> i = refs.iterator(); i.hasNext();) {
2326 PackageRef packageRef = i.next();
2327
2328 if (packageRef.isMetaData()) {
2329 i.remove(); // no use checking it again
2330 continue;
2331 }
2332
2333 String packageName = packageRef.getFQN();
2334
2335 if (instruction.matches(packageName)) {
2336 match = true;
2337 if (!instruction.isNegated()) {
2338 result.merge(packageRef, instruction.isDuplicate(), source.get(packageRef),
2339 instructions.get(instruction));
2340 }
2341 i.remove(); // Can never match again for another pattern
2342 }
2343 }
2344 if (!match && !instruction.isAny())
2345 nomatch.add(instruction);
2346 }
2347
2348 /*
2349 * Tricky. If we have umatched instructions they might indicate that we
2350 * want to have multiple decorators for the same package. So we check
2351 * the unmatched against the result list. If then then match and have
2352 * actually interesting properties then we merge them
2353 */
2354
2355 for (Iterator<Instruction> i = nomatch.iterator(); i.hasNext();) {
2356 Instruction instruction = i.next();
2357
2358 // We assume the user knows what he is
2359 // doing and inserted a literal. So
2360 // we ignore any not matched literals
Stuart McCullochec47fe72012-09-19 12:56:05 +00002361 // #252, we should not be negated to make it a constant
2362 if (instruction.isLiteral() && !instruction.isNegated()) {
Stuart McCulloch2286f232012-06-15 13:27:53 +00002363 result.merge(getPackageRef(instruction.getLiteral()), true, instructions.get(instruction));
Stuart McCullochbb014372012-06-07 21:57:32 +00002364 i.remove();
2365 continue;
2366 }
2367
2368 // Not matching a negated instruction looks
Stuart McCullochec47fe72012-09-19 12:56:05 +00002369 // like an error ... Though so, but
2370 // in the second phase of Export-Package
2371 // the !package will never match anymore.
Stuart McCullochbb014372012-06-07 21:57:32 +00002372 if (instruction.isNegated()) {
Stuart McCullochec47fe72012-09-19 12:56:05 +00002373 i.remove();
Stuart McCullochbb014372012-06-07 21:57:32 +00002374 continue;
2375 }
2376
2377 // An optional instruction should not generate
2378 // an error
2379 if (instruction.isOptional()) {
2380 i.remove();
2381 continue;
2382 }
2383
2384 // boolean matched = false;
2385 // Set<PackageRef> prefs = new HashSet<PackageRef>(result.keySet());
2386 // for (PackageRef ref : prefs) {
2387 // if (instruction.matches(ref.getFQN())) {
2388 // result.merge(ref, true, source.get(ref),
2389 // instructions.get(instruction));
2390 // matched = true;
2391 // }
2392 // }
2393 // if (matched)
2394 // i.remove();
2395 }
2396 return result;
2397 }
2398
2399 public void setDiagnostics(boolean b) {
2400 diagnostics = b;
2401 }
2402
2403 public Clazz.JAVA getLowestEE() {
2404 if (ees.isEmpty())
2405 return Clazz.JAVA.JDK1_4;
2406
2407 return ees.first();
2408 }
2409
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00002410 public String _ee(@SuppressWarnings("unused")
2411 String args[]) {
Stuart McCullochbb014372012-06-07 21:57:32 +00002412 return getLowestEE().getEE();
2413 }
2414
2415 /**
2416 * Calculate the output file for the given target. The strategy is:
2417 *
2418 * <pre>
2419 * parameter given if not null and not directory
2420 * if directory, this will be the output directory
2421 * based on bsn-version.jar
2422 * name of the source file if exists
2423 * Untitled-[n]
2424 * </pre>
2425 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00002426 * @param output
2427 * may be null, otherwise a file path relative to base
Stuart McCullochbb014372012-06-07 21:57:32 +00002428 */
2429 public File getOutputFile(String output) {
2430
2431 if (output == null)
2432 output = get(Constants.OUTPUT);
2433
2434 File outputDir;
2435
2436 if (output != null) {
2437 File outputFile = getFile(output);
2438 if (outputFile.isDirectory())
2439 outputDir = outputFile;
2440 else
2441 return outputFile;
Stuart McCulloch2286f232012-06-15 13:27:53 +00002442 } else
Stuart McCullochbb014372012-06-07 21:57:32 +00002443 outputDir = getBase();
2444
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00002445 Entry<String,Attrs> name = getBundleSymbolicName();
2446 if (name != null) {
2447 String bsn = name.getKey();
Stuart McCullochbb014372012-06-07 21:57:32 +00002448 String version = getBundleVersion();
2449 Version v = Version.parseVersion(version);
Stuart McCulloch2286f232012-06-15 13:27:53 +00002450 String outputName = bsn + "-" + v.getWithoutQualifier() + Constants.DEFAULT_JAR_EXTENSION;
Stuart McCullochbb014372012-06-07 21:57:32 +00002451 return new File(outputDir, outputName);
2452 }
2453
2454 File source = getJar().getSource();
2455 if (source != null) {
2456 String outputName = source.getName();
2457 return new File(outputDir, outputName);
2458 }
2459
Stuart McCulloch2286f232012-06-15 13:27:53 +00002460 error("Cannot establish an output name from %s, nor bsn, nor source file name, using Untitled", output);
Stuart McCullochbb014372012-06-07 21:57:32 +00002461 int n = 0;
2462 File f = getFile(outputDir, "Untitled");
2463 while (f.isFile()) {
2464 f = getFile(outputDir, "Untitled-" + n++);
2465 }
2466 return f;
2467 }
2468
2469 /**
2470 * Utility function to carefully save the file. Will create a backup if the
2471 * source file has the same path as the output. It will also only save if
2472 * the file was modified or the force flag is true
2473 *
Stuart McCulloch2286f232012-06-15 13:27:53 +00002474 * @param output
2475 * the output file, if null {@link #getOutputFile(String)} is
2476 * used.
2477 * @param force
2478 * if it needs to be overwritten
Stuart McCullochbb014372012-06-07 21:57:32 +00002479 * @throws Exception
2480 */
2481
2482 public boolean save(File output, boolean force) throws Exception {
2483 if (output == null)
2484 output = getOutputFile(null);
2485
2486 Jar jar = getJar();
2487 File source = jar.getSource();
2488
Stuart McCulloch2286f232012-06-15 13:27:53 +00002489 trace("check for modified build=%s file=%s, diff=%s", jar.lastModified(), output.lastModified(),
2490 jar.lastModified() - output.lastModified());
Stuart McCullochbb014372012-06-07 21:57:32 +00002491
2492 if (!output.exists() || output.lastModified() <= jar.lastModified() || force) {
Stuart McCulloch55d4dfe2012-08-07 10:57:21 +00002493 File op = output.getParentFile();
2494 if (!op.exists() && !op.mkdirs()) {
2495 throw new IOException("Could not create directory " + op);
2496 }
Stuart McCullochbb014372012-06-07 21:57:32 +00002497 if (source != null && output.getCanonicalPath().equals(source.getCanonicalPath())) {
2498 File bak = new File(source.getParentFile(), source.getName() + ".bak");
2499 if (!source.renameTo(bak)) {
2500 error("Could not create backup file %s", bak);
Stuart McCulloch2286f232012-06-15 13:27:53 +00002501 } else
Stuart McCullochbb014372012-06-07 21:57:32 +00002502 source.delete();
2503 }
2504 try {
2505 trace("Saving jar to %s", output);
2506 getJar().write(output);
2507 }
2508 catch (Exception e) {
2509 output.delete();
2510 error("Cannot write JAR file to %s due to %s", e, output, e.getMessage());
2511 }
2512 return true;
2513 }
Stuart McCulloch2286f232012-06-15 13:27:53 +00002514 trace("Not modified %s", output);
2515 return false;
2516
Stuart McCullochbb014372012-06-07 21:57:32 +00002517 }
2518
2519 /**
2520 * Set default import and export instructions if none are set
2521 */
2522 public void setDefaults(String bsn, Version version) {
2523 if (getExportPackage() == null)
2524 setExportPackage("*");
2525 if (getImportPackage() == null)
2526 setExportPackage("*");
2527 if (bsn != null && getBundleSymbolicName() == null)
2528 setBundleSymbolicName(bsn);
2529 if (version != null && getBundleVersion() == null)
2530 setBundleVersion(version);
2531 }
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00002532
2533 /**
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002534 * Remove the own references and optional java references from the uses lib
2535 *
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00002536 * @param apiUses
2537 * @param removeJava
2538 * @return
2539 */
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002540 public Map<PackageRef,List<PackageRef>> cleanupUses(Map<PackageRef,List<PackageRef>> apiUses, boolean removeJava) {
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00002541 MultiMap<PackageRef,PackageRef> map = new MultiMap<PackageRef,PackageRef>(apiUses);
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002542 for (Entry<PackageRef,List<PackageRef>> e : map.entrySet()) {
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00002543 e.getValue().remove(e.getKey());
2544 if (!removeJava)
2545 continue;
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002546
2547 for (Iterator<PackageRef> i = e.getValue().iterator(); i.hasNext();) {
2548 if (i.next().isJava())
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00002549 i.remove();
2550 }
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002551 }
Stuart McCulloch39cc9ac2012-07-16 13:43:38 +00002552 return map;
2553 }
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002554
2555 /**
2556 * Return the classes for a given source package.
2557 *
2558 * @param source
2559 * the source package
2560 * @return a set of classes for the requested package.
2561 */
2562 public Set<Clazz> getClassspace(PackageRef source) {
2563 Set<Clazz> result = new HashSet<Clazz>();
2564 for (Clazz c : getClassspace().values()) {
2565 if (c.getClassName().getPackageRef() == source)
2566 result.add(c);
2567 }
2568 return result;
2569 }
2570
2571 /**
2572 * Create a cross reference from package source, to packages in dest
Stuart McCulloch61c61eb2012-07-24 21:37:47 +00002573 *
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002574 * @param source
2575 * @param dest
2576 * @param sourceModifiers
2577 * @return
2578 * @throws Exception
2579 */
Stuart McCulloch61c61eb2012-07-24 21:37:47 +00002580 public Map<Clazz.Def,List<TypeRef>> getXRef(final PackageRef source, final Collection<PackageRef> dest,
2581 final int sourceModifiers) throws Exception {
2582 final MultiMap<Clazz.Def,TypeRef> xref = new MultiMap<Clazz.Def,TypeRef>(Clazz.Def.class, TypeRef.class, true);
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002583
2584 for (final Clazz clazz : getClassspace().values()) {
2585 if ((clazz.accessx & sourceModifiers) == 0)
2586 continue;
2587
Stuart McCulloch61c61eb2012-07-24 21:37:47 +00002588 if (source != null && source != clazz.getClassName().getPackageRef())
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002589 continue;
2590
2591 clazz.parseClassFileWithCollector(new ClassDataCollector() {
2592 Clazz.Def member;
2593
Stuart McCulloch55d4dfe2012-08-07 10:57:21 +00002594 @Override
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002595 public void extendsClass(TypeRef zuper) throws Exception {
2596 if (dest.contains(zuper.getPackageRef()))
2597 xref.add(clazz.getExtends(zuper), zuper);
2598 }
2599
Stuart McCulloch55d4dfe2012-08-07 10:57:21 +00002600 @Override
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002601 public void implementsInterfaces(TypeRef[] interfaces) throws Exception {
2602 for (TypeRef i : interfaces) {
2603 if (dest.contains(i.getPackageRef()))
2604 xref.add(clazz.getImplements(i), i);
2605 }
2606 }
2607
Stuart McCulloch55d4dfe2012-08-07 10:57:21 +00002608 @Override
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002609 public void referTo(TypeRef to, int modifiers) {
2610 if (to.isJava())
2611 return;
2612
2613 if (!dest.contains(to.getPackageRef()))
2614 return;
2615
2616 if (member != null && ((modifiers & sourceModifiers) != 0)) {
2617 xref.add(member, to);
2618 }
2619
2620 }
2621
Stuart McCulloch55d4dfe2012-08-07 10:57:21 +00002622 @Override
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002623 public void method(Clazz.MethodDef defined) {
2624 member = defined;
2625 }
2626
Stuart McCulloch55d4dfe2012-08-07 10:57:21 +00002627 @Override
Stuart McCulloch3b7e6af2012-07-16 14:10:57 +00002628 public void field(Clazz.FieldDef defined) {
2629 member = defined;
2630 }
2631
2632 });
2633
2634 }
2635 return xref;
2636 }
2637
Stuart McCullochbb014372012-06-07 21:57:32 +00002638}