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