blob: c348cec91341733b4575c1eaf2cc00c6d8a17d14 [file] [log] [blame]
Stuart McCulloch5ec302d2008-12-04 07:58:07 +00001/* Copyright 2006 aQute SARL
2 * Licensed under the Apache License, Version 2.0, see http://www.apache.org/licenses/LICENSE-2.0 */
3package aQute.lib.osgi;
4
5/**
6 * This class can calculate the required headers for a (potential) JAR file. It
7 * analyzes a directory or JAR for the packages that are contained and that are
8 * referred to by the bytecodes. The user can the use regular expressions to
9 * define the attributes and directives. The matching is not fully regex for
10 * convenience. A * and ? get a . prefixed and dots are escaped.
11 *
12 * <pre>
13 * *;auto=true any
14 * org.acme.*;auto=true org.acme.xyz
15 * org.[abc]*;auto=true org.acme.xyz
16 * </pre>
17 *
18 * Additional, the package instruction can start with a '=' or a '!'. The '!'
19 * indicates negation. Any matching package is removed. The '=' is literal, the
20 * expression will be copied verbatim and no matching will take place.
21 *
22 * Any headers in the given properties are used in the output properties.
23 */
24import java.io.*;
25import java.net.*;
26import java.util.*;
27import java.util.jar.*;
28import java.util.jar.Attributes.*;
29import java.util.regex.*;
30
31import aQute.bnd.service.*;
32import aQute.lib.filter.*;
33
34public class Analyzer extends Processor {
35
36 static Pattern doNotCopy = Pattern
37 .compile("CVS|.svn");
38 static String version;
39 static Pattern versionPattern = Pattern
40 .compile("(\\d+\\.\\d+)\\.\\d+.*");
41 final Map<String, Map<String, String>> contained = newHashMap(); // package
42 final Map<String, Map<String, String>> referred = newHashMap(); // package
43 final Map<String, Set<String>> uses = newHashMap(); // package
44 Map<String, Clazz> classspace;
45 Map<String, Map<String, String>> exports;
46 Map<String, Map<String, String>> imports;
47 Map<String, Map<String, String>> bundleClasspath; // Bundle
48 final Map<String, Map<String, String>> ignored = newHashMap(); // Ignored
49 // packages
50 Jar dot;
51 Map<String, Map<String, String>> classpathExports;
52
53 String activator;
54
55 final List<Jar> classpath = newList();
56
57 static Properties bndInfo;
58
59 boolean analyzed;
60 String bsn;
61
62 public Analyzer(Processor parent) {
63 super(parent);
64 }
65
66 public Analyzer() {
67 }
68
69 /**
70 * Specifically for Maven
71 *
72 * @param properties
73 * the properties
74 */
75
76 public static Properties getManifest(File dirOrJar) throws IOException {
77 Analyzer analyzer = new Analyzer();
78 analyzer.setJar(dirOrJar);
79 Properties properties = new Properties();
80 properties.put(IMPORT_PACKAGE, "*");
81 properties.put(EXPORT_PACKAGE, "*");
82 analyzer.setProperties(properties);
83 Manifest m = analyzer.calcManifest();
84 Properties result = new Properties();
85 for (Iterator<Object> i = m.getMainAttributes().keySet().iterator(); i
86 .hasNext();) {
87 Attributes.Name name = (Attributes.Name) i.next();
88 result.put(name.toString(), m.getMainAttributes().getValue(name));
89 }
90 return result;
91 }
92
93 /**
94 * Calcualtes the data structures for generating a manifest.
95 *
96 * @throws IOException
97 */
98 public void analyze() throws IOException {
99 if (!analyzed) {
100 analyzed = true;
101 classpathExports = newHashMap();
102 activator = getProperty(BUNDLE_ACTIVATOR);
103 bundleClasspath = parseHeader(getProperty(BUNDLE_CLASSPATH));
104
105 analyzeClasspath();
106
107 classspace = analyzeBundleClasspath(dot, bundleClasspath,
108 contained, referred, uses);
109
110 for (AnalyzerPlugin plugin : getPlugins(AnalyzerPlugin.class)) {
111 if (plugin instanceof AnalyzerPlugin) {
112 AnalyzerPlugin analyzer = (AnalyzerPlugin) plugin;
113 try {
114 boolean reanalyze = analyzer.analyzeJar(this);
115 if (reanalyze)
116 classspace = analyzeBundleClasspath(dot,
117 bundleClasspath, contained, referred, uses);
118 } catch (Exception e) {
119 error("Plugin Analyzer " + analyzer
120 + " throws exception " + e);
121 e.printStackTrace();
122 }
123 }
124 }
125
126 if (activator != null) {
127 // Add the package of the activator to the set
128 // of referred classes. This must be done before we remove
129 // contained set.
130 int n = activator.lastIndexOf('.');
131 if (n > 0) {
132 referred.put(activator.substring(0, n),
133 new LinkedHashMap<String, String>());
134 }
135 }
136
137 referred.keySet().removeAll(contained.keySet());
138 if (referred.containsKey(".")) {
139 error("The default package '.' is not permitted by the Import-Package syntax. \n"
140 + " This can be caused by compile errors in Eclipse because Eclipse creates \n"
141 + "valid class files regardless of compile errors.\n"
142 + "The following package(s) import from the default package "
143 + getUsedBy("."));
144 }
145
146 Map<String, Map<String, String>> exportInstructions = parseHeader(getProperty(EXPORT_PACKAGE));
147 Map<String, Map<String, String>> additionalExportInstructions = parseHeader(getProperty(EXPORT_CONTENTS));
148 exportInstructions.putAll(additionalExportInstructions);
149 Map<String, Map<String, String>> importInstructions = parseHeader(getImportPackages());
150 Map<String, Map<String, String>> dynamicImports = parseHeader(getProperty(DYNAMICIMPORT_PACKAGE));
151
152 if (dynamicImports != null) {
153 // Remove any dynamic imports from the referred set.
154 referred.keySet().removeAll(dynamicImports.keySet());
155 }
156
157 Map<String, Map<String, String>> superfluous = newHashMap();
158 // Tricky!
159 for (Iterator<String> i = exportInstructions.keySet().iterator(); i
160 .hasNext();) {
161 String instr = i.next();
162 if (!instr.startsWith("!"))
163 superfluous.put(instr, exportInstructions.get(instr));
164 }
165
166 exports = merge("export-package", exportInstructions, contained,
167 superfluous.keySet());
168
169 for (Iterator<Map.Entry<String, Map<String, String>>> i = superfluous
170 .entrySet().iterator(); i.hasNext();) {
171 // It is possible to mention metadata directories in the export
172 // explicitly, they are then exported and removed from the
173 // warnings. Note that normally metadata directories are not
174 // exported.
175 Map.Entry<String, Map<String, String>> entry = i.next();
176 String pack = entry.getKey();
177 if (isDuplicate(pack))
178 i.remove();
179 else if (isMetaData(pack)) {
180 exports.put(pack, entry.getValue());
181 i.remove();
182 }
183 }
184
185 if (!superfluous.isEmpty()) {
186 warning("Superfluous export-package instructions: "
187 + superfluous.keySet());
188 }
189
190 // Add all exports that do not have an -noimport: directive
191 // to the imports.
192 Map<String, Map<String, String>> referredAndExported = newMap(referred);
193 referredAndExported.putAll(addExportsToImports(exports));
194
195 // match the imports to the referred and exported packages,
196 // merge the info for matching packages
197 Set<String> extra = new TreeSet<String>(importInstructions.keySet());
198 imports = merge("import-package", importInstructions,
199 referredAndExported, extra);
200
201 // Instructions that have not been used could be superfluous
202 // or if they do not contain wildcards, should be added
203 // as extra imports, the user knows best.
204 for (Iterator<String> i = extra.iterator(); i.hasNext();) {
205 String p = i.next();
206 if (p.startsWith("!") || p.indexOf('*') >= 0
207 || p.indexOf('?') >= 0 || p.indexOf('[') >= 0) {
208 if (!isResourceOnly())
209 warning("Did not find matching referal for " + p);
210 } else {
211 Map<String, String> map = importInstructions.get(p);
212 imports.put(p, map);
213 }
214 }
215
216 // See what information we can find to augment the
217 // imports. I.e. look on the classpath
218 augmentImports();
219
220 // Add the uses clause to the exports
221 doUses(exports, uses, imports);
222 }
223 }
224
225 /**
226 * Copy the input collection into an output set but skip names that have
227 * been marked as duplicates or are optional.
228 *
229 * @param superfluous
230 * @return
231 */
232 Set<Instruction> removeMarkedDuplicates(Collection<Instruction> superfluous) {
233 Set<Instruction> result = new HashSet<Instruction>();
234 for (Iterator<Instruction> i = superfluous.iterator(); i.hasNext();) {
235 Instruction instr = (Instruction) i.next();
236 if (!isDuplicate(instr.getPattern()) && !instr.isOptional())
237 result.add(instr);
238 }
239 return result;
240 }
241
242 /**
243 * Analyzer has an empty default but the builder has a * as default.
244 *
245 * @return
246 */
247 protected String getImportPackages() {
248 return getProperty(IMPORT_PACKAGE);
249 }
250
251 /**
252 *
253 * @return
254 */
255 boolean isResourceOnly() {
256 return getProperty(RESOURCEONLY, "false").equalsIgnoreCase("true");
257 }
258
259 /**
260 * Answer the list of packages that use the given package.
261 */
262 Set<String> getUsedBy(String pack) {
263 Set<String> set = newSet();
264 for (Iterator<Map.Entry<String, Set<String>>> i = uses.entrySet()
265 .iterator(); i.hasNext();) {
266 Map.Entry<String, Set<String>> entry = i.next();
267 Set<String> used = entry.getValue();
268 if (used.contains(pack))
269 set.add(entry.getKey());
270 }
271 return set;
272 }
273
274 /**
275 * One of the main workhorses of this class. This will analyze the current
276 * setp and calculate a new manifest according to this setup. This method
277 * will also set the manifest on the main jar dot
278 *
279 * @return
280 * @throws IOException
281 */
282 public Manifest calcManifest() throws IOException {
283 analyze();
284 Manifest manifest = new Manifest();
285 Attributes main = manifest.getMainAttributes();
286
287 main.put(Attributes.Name.MANIFEST_VERSION, "1.0");
288 main.putValue(BUNDLE_MANIFESTVERSION, "2");
289
290 boolean noExtraHeaders = "true"
291 .equalsIgnoreCase(getProperty(NOEXTRAHEADERS));
292
293 if (!noExtraHeaders) {
294 main.putValue(CREATED_BY, System.getProperty("java.version") + " ("
295 + System.getProperty("java.vendor") + ")");
296 main.putValue(TOOL, "Bnd-" + getVersion());
297 main.putValue(BND_LASTMODIFIED, "" + System.currentTimeMillis());
298 }
299 String exportHeader = printClauses(exports,
300 "uses:|include:|exclude:|mandatory:|" + IMPORT_DIRECTIVE, true);
301
302 if (exportHeader.length() > 0)
303 main.putValue(EXPORT_PACKAGE, exportHeader);
304 else
305 main.remove(EXPORT_PACKAGE);
306
307 Map<String, Map<String, String>> temp = removeKeys(imports, "java.");
308 if (!temp.isEmpty()) {
309 main.putValue(IMPORT_PACKAGE, printClauses(temp, "resolution:"));
310 } else {
311 main.remove(IMPORT_PACKAGE);
312 }
313
314 temp = newMap(contained);
315 temp.keySet().removeAll(exports.keySet());
316
317 if (!temp.isEmpty())
318 main.putValue(PRIVATE_PACKAGE, printClauses(temp, ""));
319 else
320 main.remove(PRIVATE_PACKAGE);
321
322 if (!ignored.isEmpty()) {
323 main.putValue(IGNORE_PACKAGE, printClauses(ignored, ""));
324 } else {
325 main.remove(IGNORE_PACKAGE);
326 }
327
328 if (bundleClasspath != null && !bundleClasspath.isEmpty())
329 main.putValue(BUNDLE_CLASSPATH, printClauses(bundleClasspath, ""));
330 else
331 main.remove(BUNDLE_CLASSPATH);
332
333 Map<String, Map<String, String>> l = doServiceComponent(getProperty(SERVICE_COMPONENT));
334 if (!l.isEmpty())
335 main.putValue(SERVICE_COMPONENT, printClauses(l, ""));
336 else
337 main.remove(SERVICE_COMPONENT);
338
339 for (Enumeration<?> h = getProperties().propertyNames(); h
340 .hasMoreElements();) {
341 String header = (String) h.nextElement();
342 if (header.trim().length() == 0) {
343 warning("Empty property set with value: "
344 + getProperties().getProperty(header));
345 continue;
346 }
347 if (!Character.isUpperCase(header.charAt(0))) {
348 if (header.charAt(0) == '@')
349 doNameSection(manifest, header);
350 continue;
351 }
352
353 if (header.equals(BUNDLE_CLASSPATH)
354 || header.equals(EXPORT_PACKAGE)
355 || header.equals(IMPORT_PACKAGE))
356 continue;
357
358 if (Verifier.HEADER_PATTERN.matcher(header).matches()) {
359 String value = getProperty(header);
360 if (value != null && main.getValue(header) == null) {
361 if (value.trim().length() == 0)
362 main.remove(header);
363 else
364 main.putValue(header, value);
365 }
366 } else {
367 // TODO should we report?
368 }
369 }
370
371 //
372 // Calculate the bundle symbolic name if it is
373 // not set.
374 // 1. set
375 // 2. name of properties file (must be != bnd.bnd)
376 // 3. name of directory, which is usualy project name
377 //
378 String bsn = getBsn();
379 if (main.getValue(BUNDLE_SYMBOLICNAME) == null) {
380 main.putValue(BUNDLE_SYMBOLICNAME, bsn);
381 }
382
383 //
384 // Use the same name for the bundle name as BSN when
385 // the bundle name is not set
386 //
387 if (main.getValue(BUNDLE_NAME) == null) {
388 main.putValue(BUNDLE_NAME, bsn);
389 }
390
391 if (main.getValue(BUNDLE_VERSION) == null)
392 main.putValue(BUNDLE_VERSION, "0");
393
394 // Copy old values into new manifest, when they
395 // exist in the old one, but not in the new one
396 merge(manifest, dot.getManifest());
397
398 // Remove all the headers mentioned in -removeheaders
399 Map<String, Map<String, String>> removes = parseHeader(getProperty(REMOVE_HEADERS));
400 for (Iterator<String> i = removes.keySet().iterator(); i.hasNext();) {
401 String header = i.next();
402 for (Iterator<Object> j = main.keySet().iterator(); j.hasNext();) {
403 Attributes.Name attr = (Attributes.Name) j.next();
404 if (attr.toString().matches(header)) {
405 j.remove();
406 progress("Removing header: " + header);
407 }
408 }
409 }
410
411 dot.setManifest(manifest);
412 return manifest;
413 }
414
415 /**
416 * This method is called when the header starts with a @, signifying
417 * a name section header. The name part is defined by replacing all the @
418 * signs to a /, removing the first and the last, and using the last
419 * part as header name:
420 * <pre>
421 * @org@osgi@service@event@Implementation-Title
422 * </pre>
423 * This will be the header Implementation-Title in the org/osgi/service/event
424 * named section.
425 *
426 * @param manifest
427 * @param header
428 */
429 private void doNameSection(Manifest manifest, String header) {
430 String path = header.replace('@', '/');
431 int n = path.lastIndexOf('/');
432 // Must succeed because we start with @
433 String name = path.substring(n + 1);
434 // Skip first /
435 path = path.substring(1, n);
436 if (name.length() != 0 && path.length() != 0) {
437 Attributes attrs = manifest.getAttributes(path);
438 if (attrs == null) {
439 attrs = new Attributes();
440 manifest.getEntries().put(path, attrs);
441 }
442 attrs.putValue(name, getProperty(header));
443 } else {
444 warning(
445 "Invalid header (starts with @ but does not seem to be for the Name section): %s",
446 header);
447 }
448 }
449
450 /**
451 * Clear the key part of a header. I.e. remove everything from the first ';'
452 *
453 * @param value
454 * @return
455 */
456 public String getBsn() {
457 String value = getProperty(BUNDLE_SYMBOLICNAME);
458 if (value == null) {
459 if (getPropertiesFile() != null)
460 value = getPropertiesFile().getName();
461
462 if (value == null || value.equals("bnd.bnd"))
463 value = getBase().getName();
464 else if (value.endsWith(".bnd"))
465 value = value.substring(0, value.length() - 4);
466 }
467
468 if (value == null)
469 return "untitled";
470
471 int n = value.indexOf(';');
472 if (n > 0)
473 value = value.substring(0, n);
474 return value.trim();
475 }
476
477 /**
478 * Calculate an export header solely based on the contents of a JAR file
479 *
480 * @param bundle
481 * The jar file to analyze
482 * @return
483 */
484 public String calculateExportsFromContents(Jar bundle) {
485 String ddel = "";
486 StringBuffer sb = new StringBuffer();
487 Map<String, Map<String, Resource>> map = bundle.getDirectories();
488 for (Iterator<String> i = map.keySet().iterator(); i.hasNext();) {
489 String directory = (String) i.next();
490 if (directory.equals("META-INF")
491 || directory.startsWith("META-INF/"))
492 continue;
493 if (directory.equals("OSGI-OPT")
494 || directory.startsWith("OSGI-OPT/"))
495 continue;
496 if (directory.equals("/"))
497 continue;
498
499 if (directory.endsWith("/"))
500 directory = directory.substring(0, directory.length() - 1);
501
502 directory = directory.replace('/', '.');
503 sb.append(ddel);
504 sb.append(directory);
505 ddel = ",";
506 }
507 return sb.toString();
508 }
509
510 /**
511 * Check if a service component header is actually referring to a class. If
512 * so, replace the reference with an XML file reference. This makes it
513 * easier to create and use components.
514 *
515 * @throws UnsupportedEncodingException
516 *
517 */
518 public Map<String, Map<String, String>> doServiceComponent(
519 String serviceComponent) throws IOException {
520 Map<String, Map<String, String>> list = newMap();
521 Map<String, Map<String, String>> sc = parseHeader(serviceComponent);
522 if (!sc.isEmpty()) {
523 for (Iterator<Map.Entry<String, Map<String, String>>> i = sc
524 .entrySet().iterator(); i.hasNext();) {
525 Map.Entry<String, Map<String, String>> entry = i.next();
526 String name = entry.getKey();
527 Map<String, String> info = entry.getValue();
528 if (name == null) {
529 error("No name in Service-Component header: " + info);
530 continue;
531 }
532 if (dot.exists(name)) {
533 // Normal service component
534 list.put(name, info);
535 } else {
536 String impl = name;
537 if (info.containsKey(COMPONENT_IMPLEMENTATION))
538 impl = info.get(COMPONENT_IMPLEMENTATION);
539
540 if (!checkClass(impl))
541 error("Not found Service-Component header: " + name);
542 else {
543 // We have a definition, so make an XML resources
544 Resource resource = createComponentResource(name, info);
545 dot.putResource("OSGI-INF/" + name + ".xml", resource);
546 Map<String, String> empty = Collections.emptyMap();
547 list.put("OSGI-INF/" + name + ".xml", empty);
548 }
549 }
550 }
551 }
552 return list;
553 }
554
555 public Map<String, Map<String, String>> getBundleClasspath() {
556 return bundleClasspath;
557 }
558
559 public Map<String, Map<String, String>> getContained() {
560 return contained;
561 }
562
563 public Map<String, Map<String, String>> getExports() {
564 return exports;
565 }
566
567 public Map<String, Map<String, String>> getImports() {
568 return imports;
569 }
570
571 public Jar getJar() {
572 return dot;
573 }
574
575 public Map<String, Map<String, String>> getReferred() {
576 return referred;
577 }
578
579 /**
580 * Return the set of unreachable code depending on exports and the bundle
581 * activator.
582 *
583 * @return
584 */
585 public Set<String> getUnreachable() {
586 Set<String> unreachable = new HashSet<String>(uses.keySet()); // all
587 for (Iterator<String> r = exports.keySet().iterator(); r.hasNext();) {
588 String packageName = r.next();
589 removeTransitive(packageName, unreachable);
590 }
591 if (activator != null) {
592 String pack = activator.substring(0, activator.lastIndexOf('.'));
593 removeTransitive(pack, unreachable);
594 }
595 return unreachable;
596 }
597
598 public Map<String, Set<String>> getUses() {
599 return uses;
600 }
601
602 /**
603 * Get the version from the manifest, a lot of work!
604 *
605 * @return version or unknown.
606 */
607 public String getVersion() {
608 return getBndInfo("version", "<unknown version>");
609 }
610
611 public long getBndLastModified() {
612 String time = getBndInfo("modified", "0");
613 try {
614 return Long.parseLong(time);
615 } catch (Exception e) {
616 }
617 return 0;
618 }
619
620 public String getBndInfo(String key, String defaultValue) {
621 if (bndInfo == null) {
622 bndInfo = new Properties();
623 try {
624 InputStream in = getClass().getResourceAsStream("bnd.info");
625 if (in != null) {
626 bndInfo.load(in);
627 in.close();
628 }
629 } catch (IOException ioe) {
630 warning("Could not read bnd.info in " + getClass().getPackage()
631 + ioe);
632 }
633 }
634 return bndInfo.getProperty(key, defaultValue);
635 }
636
637 /**
638 * Merge the existing manifest with the instructions.
639 *
640 * @param manifest
641 * The manifest to merge with
642 * @throws IOException
643 */
644 public void mergeManifest(Manifest manifest) throws IOException {
645 if (manifest != null) {
646 Attributes attributes = manifest.getMainAttributes();
647 for (Iterator<Object> i = attributes.keySet().iterator(); i
648 .hasNext();) {
649 Name name = (Name) i.next();
650 String key = name.toString();
651 // Dont want instructions
652 if (key.startsWith("-"))
653 continue;
654
655 if (getProperty(key) == null)
656 setProperty(key, (String) attributes.get(name));
657 }
658 }
659 }
660
661 // public Signer getSigner() {
662 // String sign = getProperty("-sign");
663 // if (sign == null) return null;
664 //
665 // Map parsed = parseHeader(sign);
666 // Signer signer = new Signer();
667 // String password = (String) parsed.get("password");
668 // if (password != null) {
669 // signer.setPassword(password);
670 // }
671 //
672 // String keystore = (String) parsed.get("keystore");
673 // if (keystore != null) {
674 // File f = new File(keystore);
675 // if (!f.isAbsolute()) f = new File(base, keystore);
676 // signer.setKeystore(f);
677 // } else {
678 // error("Signing requires a keystore");
679 // return null;
680 // }
681 //
682 // String alias = (String) parsed.get("alias");
683 // if (alias != null) {
684 // signer.setAlias(alias);
685 // } else {
686 // error("Signing requires an alias for the key");
687 // return null;
688 // }
689 // return signer;
690 // }
691
692 public void setBase(File file) {
693 super.setBase(file);
694 getProperties().put("project.dir", getBase().getAbsolutePath());
695 }
696
697 /**
698 * Set the classpath for this analyzer by file.
699 *
700 * @param classpath
701 * @throws IOException
702 */
703 public void setClasspath(File[] classpath) throws IOException {
704 List<Jar> list = new ArrayList<Jar>();
705 for (int i = 0; i < classpath.length; i++) {
706 if (classpath[i].exists()) {
707 Jar current = new Jar(classpath[i]);
708 list.add(current);
709 } else {
710 error("Missing file on classpath: " + classpath[i]);
711 }
712 }
713 for (Iterator<Jar> i = list.iterator(); i.hasNext();) {
714 addClasspath(i.next());
715 }
716 }
717
718 public void setClasspath(Jar[] classpath) {
719 for (int i = 0; i < classpath.length; i++) {
720 addClasspath(classpath[i]);
721 }
722 }
723
724 public void setClasspath(String[] classpath) {
725 for (int i = 0; i < classpath.length; i++) {
726 Jar jar = getJarFromName(classpath[i], " setting classpath");
727 if (jar != null)
728 addClasspath(jar);
729 }
730 }
731
732 /**
733 * Set the JAR file we are going to work in. This will read the JAR in
734 * memory.
735 *
736 * @param jar
737 * @return
738 * @throws IOException
739 */
740 public Jar setJar(File jar) throws IOException {
741 Jar jarx = new Jar(jar);
742 addClose(jarx);
743 return setJar(jarx);
744 }
745
746 /**
747 * Set the JAR directly we are going to work on.
748 *
749 * @param jar
750 * @return
751 */
752 public Jar setJar(Jar jar) {
753 this.dot = jar;
754 return jar;
755 }
756
757 protected void begin() {
758 super.begin();
759
760 updateModified(getBndLastModified(), "bnd last modified");
761 String doNotCopy = getProperty(DONOTCOPY);
762 if (doNotCopy != null)
763 Analyzer.doNotCopy = Pattern.compile(doNotCopy);
764
765 verifyManifestHeadersCase(getProperties());
766 }
767
768 /**
769 * Check if the given class or interface name is contained in the jar.
770 *
771 * @param interfaceName
772 * @return
773 */
774 boolean checkClass(String interfaceName) {
775 String path = interfaceName.replace('.', '/') + ".class";
776 if (classspace.containsKey(path))
777 return true;
778
779 String pack = interfaceName;
780 int n = pack.lastIndexOf('.');
781 if (n > 0)
782 pack = pack.substring(0, n);
783 else
784 pack = ".";
785
786 return imports.containsKey(pack);
787 }
788
789 /**
790 * Create the resource for a DS component.
791 *
792 * @param list
793 * @param name
794 * @param info
795 * @throws UnsupportedEncodingException
796 */
797 Resource createComponentResource(String name, Map<String, String> info)
798 throws IOException {
799
800 ByteArrayOutputStream out = new ByteArrayOutputStream();
801 PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
802 pw.println("<?xml version='1.0' encoding='utf-8'?>");
803 pw.print("<component name='" + name + "'");
804
805 String factory = info.get(COMPONENT_FACTORY);
806 if (factory != null)
807 pw.print(" factory='" + factory + "'");
808
809 String immediate = info.get(COMPONENT_IMMEDIATE);
810 if (immediate != null)
811 pw.print(" immediate='" + immediate + "'");
812
813 String enabled = info.get(COMPONENT_ENABLED);
814 if (enabled != null)
815 pw.print(" enabled='" + enabled + "'");
816
817 pw.println(">");
818
819 // Allow override of the implementation when people
820 // want to choose their own name
821 String impl = (String) info.get(COMPONENT_IMPLEMENTATION);
822 pw.println(" <implementation class='" + (impl == null ? name : impl)
823 + "'/>");
824
825 String provides = info.get(COMPONENT_PROVIDE);
826 boolean servicefactory = Boolean.getBoolean(info
827 .get(COMPONENT_SERVICEFACTORY)
828 + "");
829 provides(pw, provides, servicefactory);
830 properties(pw, info);
831 reference(info, pw);
832 pw.println("</component>");
833 pw.close();
834 byte[] data = out.toByteArray();
835 out.close();
836 return new EmbeddedResource(data, 0);
837 }
838
839 /**
840 * Try to get a Jar from a file name/path or a url, or in last resort from
841 * the classpath name part of their files.
842 *
843 * @param name
844 * URL or filename relative to the base
845 * @param from
846 * Message identifying the caller for errors
847 * @return null or a Jar with the contents for the name
848 */
849 Jar getJarFromName(String name, String from) {
850 File file = new File(name);
851 if (!file.isAbsolute())
852 file = new File(getBase(), name);
853
854 if (file.exists())
855 try {
856 Jar jar = new Jar(file);
857 addClose(jar);
858 return jar;
859 } catch (Exception e) {
860 error("Exception in parsing jar file for " + from + ": " + name
861 + " " + e);
862 }
863 // It is not a file ...
864 try {
865 // Lets try a URL
866 URL url = new URL(name);
867 Jar jar = new Jar(fileName(url.getPath()));
868 addClose(jar);
869 URLConnection connection = url.openConnection();
870 InputStream in = connection.getInputStream();
871 long lastModified = connection.getLastModified();
872 if (lastModified == 0)
873 // We assume the worst :-(
874 lastModified = System.currentTimeMillis();
875 EmbeddedResource.build(jar, in, lastModified);
876 in.close();
877 return jar;
878 } catch (IOException ee) {
879 // Check if we have files on the classpath
880 // that have the right name, allows us to specify those
881 // names instead of the full path.
882 for (Iterator<Jar> cp = getClasspath().iterator(); cp.hasNext();) {
883 Jar entry = cp.next();
884 if (entry.source != null && entry.source.getName().equals(name)) {
885 return entry;
886 }
887 }
888 // error("Can not find jar file for " + from + ": " + name);
889 }
890 return null;
891 }
892
893 private String fileName(String path) {
894 int n = path.lastIndexOf('/');
895 if (n > 0)
896 return path.substring(n + 1);
897 return path;
898 }
899
900 /**
901 *
902 * @param manifest
903 * @throws Exception
904 */
905 void merge(Manifest result, Manifest old) throws IOException {
906 if (old != null) {
907 for (Iterator<Map.Entry<Object, Object>> e = old
908 .getMainAttributes().entrySet().iterator(); e.hasNext();) {
909 Map.Entry<Object, Object> entry = e.next();
910 Attributes.Name name = (Attributes.Name) entry.getKey();
911 String value = (String) entry.getValue();
912 if (name.toString().equalsIgnoreCase("Created-By"))
913 name = new Attributes.Name("Originally-Created-By");
914 if (!result.getMainAttributes().containsKey(name))
915 result.getMainAttributes().put(name, value);
916 }
917
918 // do not overwrite existing entries
919 Map<String, Attributes> oldEntries = old.getEntries();
920 Map<String, Attributes> newEntries = result.getEntries();
921 for (Iterator<Map.Entry<String, Attributes>> e = oldEntries
922 .entrySet().iterator(); e.hasNext();) {
923 Map.Entry<String, Attributes> entry = e.next();
924 if (!newEntries.containsKey(entry.getKey())) {
925 newEntries.put(entry.getKey(), entry.getValue());
926 }
927 }
928 }
929 }
930
931 /**
932 * Print the Service-Component properties element
933 *
934 * @param pw
935 * @param info
936 */
937 void properties(PrintWriter pw, Map<String, String> info) {
938 Collection<String> properties = split(info.get(COMPONENT_PROPERTIES));
939 for (Iterator<String> p = properties.iterator(); p.hasNext();) {
940 String clause = p.next();
941 int n = clause.indexOf('=');
942 if (n <= 0) {
943 error("Not a valid property in service component: " + clause);
944 } else {
945 String type = null;
946 String name = clause.substring(0, n);
947 if (name.indexOf('@') >= 0) {
948 String parts[] = name.split("@");
949 name = parts[1];
950 type = parts[0];
951 }
952 String value = clause.substring(n + 1).trim();
953 // TODO verify validity of name and value.
954 pw.print("<property name='");
955 pw.print(name);
956 pw.print("'");
957
958 if (type != null) {
959 if (VALID_PROPERTY_TYPES.matcher(type).matches()) {
960 pw.print(" type='");
961 pw.print(type);
962 pw.print("'");
963 } else {
964 warning("Invalid property type '" + type
965 + "' for property " + name);
966 }
967 }
968
969 String parts[] = value.split("\\s*(\\||\\n)\\s*");
970 if (parts.length > 1) {
971 pw.println(">");
972 for (String part : parts) {
973 pw.println(part);
974 }
975 pw.println("</property>");
976 } else {
977 pw.print(" value='");
978 pw.print(parts[0]);
979 pw.print("'/>");
980 }
981 }
982 }
983 }
984
985 /**
986 * @param pw
987 * @param provides
988 */
989 void provides(PrintWriter pw, String provides, boolean servicefactory) {
990 if (provides != null) {
991 if (!servicefactory)
992 pw.println(" <service>");
993 else
994 pw.println(" <service servicefactory='true'>");
995
996 StringTokenizer st = new StringTokenizer(provides, ",");
997 while (st.hasMoreTokens()) {
998 String interfaceName = st.nextToken();
999 pw.println(" <provide interface='" + interfaceName + "'/>");
1000 if (!checkClass(interfaceName))
1001 error("Component definition provides a class that is neither imported nor contained: "
1002 + interfaceName);
1003 }
1004 pw.println(" </service>");
1005 }
1006 }
1007
1008 final static Pattern REFERENCE = Pattern.compile("([^(]+)(\\(.+\\))?");
1009
1010 /**
1011 * @param info
1012 * @param pw
1013 */
1014
1015 void reference(Map<String, String> info, PrintWriter pw) {
1016 Collection<String> dynamic = split(info.get(COMPONENT_DYNAMIC));
1017 Collection<String> optional = split(info.get(COMPONENT_OPTIONAL));
1018 Collection<String> multiple = split(info.get(COMPONENT_MULTIPLE));
1019
1020 for (Iterator<Map.Entry<String, String>> r = info.entrySet().iterator(); r
1021 .hasNext();) {
1022 Map.Entry<String, String> ref = r.next();
1023 String referenceName = (String) ref.getKey();
1024 String target = null;
1025 String interfaceName = (String) ref.getValue();
1026 if (interfaceName == null || interfaceName.length() == 0) {
1027 error("Invalid Interface Name for references in Service Component: "
1028 + referenceName + "=" + interfaceName);
1029 }
1030 char c = interfaceName.charAt(interfaceName.length() - 1);
1031 if ("?+*~".indexOf(c) >= 0) {
1032 if (c == '?' || c == '*' || c == '~')
1033 optional.add(referenceName);
1034 if (c == '+' || c == '*')
1035 multiple.add(referenceName);
1036 if (c == '+' || c == '*' || c == '?')
1037 dynamic.add(referenceName);
1038 interfaceName = interfaceName.substring(0, interfaceName
1039 .length() - 1);
1040 }
1041
1042 // TODO check if the interface is contained or imported
1043
1044 if (referenceName.endsWith(":")) {
1045 if (!SET_COMPONENT_DIRECTIVES.contains(referenceName))
1046 error("Unrecognized directive in Service-Component header: "
1047 + referenceName);
1048 continue;
1049 }
1050
1051 Matcher m = REFERENCE.matcher(interfaceName);
1052 if (m.matches()) {
1053 interfaceName = m.group(1);
1054 target = m.group(2);
1055 }
1056
1057 if (!checkClass(interfaceName))
1058 error("Component definition refers to a class that is neither imported nor contained: "
1059 + interfaceName);
1060
1061 pw.print(" <reference name='" + referenceName + "' interface='"
1062 + interfaceName + "'");
1063
1064 String cardinality = optional.contains(referenceName) ? "0" : "1";
1065 cardinality += "..";
1066 cardinality += multiple.contains(referenceName) ? "n" : "1";
1067 if (!cardinality.equals("1..1"))
1068 pw.print(" cardinality='" + cardinality + "'");
1069
1070 if (Character.isLowerCase(referenceName.charAt(0))) {
1071 String z = referenceName.substring(0, 1).toUpperCase()
1072 + referenceName.substring(1);
1073 pw.print(" bind='set" + z + "'");
1074 // TODO Verify that the methods exist
1075
1076 // TODO ProSyst requires both a bind and unbind :-(
1077 // if ( dynamic.contains(referenceName) )
1078 pw.print(" unbind='unset" + z + "'");
1079 // TODO Verify that the methods exist
1080 }
1081 if (dynamic.contains(referenceName)) {
1082 pw.print(" policy='dynamic'");
1083 }
1084
1085 if (target != null) {
1086 Filter filter = new Filter(target);
1087 if (filter.verify() == null)
1088 pw.print(" target='" + filter.toString() + "'");
1089 else
1090 error("Target for " + referenceName
1091 + " is not a correct filter: " + target + " "
1092 + filter.verify());
1093 }
1094 pw.println("/>");
1095 }
1096 }
1097
1098 String stem(String name) {
1099 int n = name.lastIndexOf('.');
1100 if (n > 0)
1101 return name.substring(0, n);
1102 else
1103 return name;
1104 }
1105
1106 /**
1107 * Bnd is case sensitive for the instructions so we better check people are
1108 * not using an invalid case. We do allow this to set headers that should
1109 * not be processed by us but should be used by the framework.
1110 *
1111 * @param properties
1112 * Properties to verify.
1113 */
1114
1115 void verifyManifestHeadersCase(Properties properties) {
1116 for (Iterator<Object> i = properties.keySet().iterator(); i.hasNext();) {
1117 String header = (String) i.next();
1118 for (int j = 0; j < headers.length; j++) {
1119 if (!headers[j].equals(header)
1120 && headers[j].equalsIgnoreCase(header)) {
1121 warning("Using a standard OSGi header with the wrong case (bnd is case sensitive!), using: "
1122 + header + " and expecting: " + headers[j]);
1123 break;
1124 }
1125 }
1126 }
1127 }
1128
1129 /**
1130 * We will add all exports to the imports unless there is a -noimport
1131 * directive specified on an export. This directive is skipped for the
1132 * manifest.
1133 *
1134 * We also remove any version parameter so that augmentImports can do the
1135 * version policy.
1136 *
1137 */
1138 Map<String, Map<String, String>> addExportsToImports(
1139 Map<String, Map<String, String>> exports) {
1140 Map<String, Map<String, String>> importsFromExports = newHashMap();
1141 for (Map.Entry<String, Map<String, String>> packageEntry : exports
1142 .entrySet()) {
1143 String packageName = packageEntry.getKey();
1144 Map<String, String> parameters = packageEntry.getValue();
1145 String noimport = (String) parameters.get(NO_IMPORT_DIRECTIVE);
1146 if (noimport == null || !noimport.equalsIgnoreCase("true")) {
1147 if (parameters.containsKey("version")) {
1148 parameters = newMap(parameters);
1149 parameters.remove("version");
1150 }
1151 importsFromExports.put(packageName, parameters);
1152 }
1153 }
1154 return importsFromExports;
1155 }
1156
1157 /**
1158 * Create the imports/exports by parsing
1159 *
1160 * @throws IOException
1161 */
1162 void analyzeClasspath() throws IOException {
1163 classpathExports = newHashMap();
1164 for (Iterator<Jar> c = getClasspath().iterator(); c.hasNext();) {
1165 Jar current = c.next();
1166 checkManifest(current);
1167 for (Iterator<String> j = current.getDirectories().keySet()
1168 .iterator(); j.hasNext();) {
1169 String dir = j.next();
1170 Resource resource = current.getResource(dir + "/packageinfo");
1171 if (resource != null) {
1172 InputStream in = resource.openInputStream();
1173 String version = parsePackageInfo(in);
1174 in.close();
1175 setPackageInfo(dir, "version", version);
1176 }
1177 }
1178 }
1179 }
1180
1181 /**
1182 *
1183 * @param jar
1184 */
1185 void checkManifest(Jar jar) {
1186 try {
1187 Manifest m = jar.getManifest();
1188 if (m != null) {
1189 String exportHeader = m.getMainAttributes().getValue(
1190 EXPORT_PACKAGE);
1191 if (exportHeader != null) {
1192 Map<String, Map<String, String>> exported = parseHeader(exportHeader);
1193 if (exported != null)
1194 classpathExports.putAll(exported);
1195 }
1196 }
1197 } catch (Exception e) {
1198 warning("Erroneous Manifest for " + jar + " " + e);
1199 }
1200 }
1201
1202 /**
1203 * Find some more information about imports in manifest and other places.
1204 */
1205 void augmentImports() {
1206 for (String packageName : imports.keySet()) {
1207 setProperty(CURRENT_PACKAGE, packageName);
1208 try {
1209 Map<String, String> importAttributes = imports.get(packageName);
1210 Map<String, String> exporterAttributes = classpathExports
1211 .get(packageName);
1212 if (exporterAttributes == null)
1213 exporterAttributes = exports.get(packageName);
1214
1215 if (exporterAttributes != null) {
1216 augmentVersion(importAttributes, exporterAttributes);
1217 augmentMandatory(importAttributes, exporterAttributes);
1218 if (exporterAttributes.containsKey(IMPORT_DIRECTIVE))
1219 importAttributes.put(IMPORT_DIRECTIVE,
1220 exporterAttributes.get(IMPORT_DIRECTIVE));
1221 }
1222
1223 // Convert any attribute values that have macros.
1224 for (String key : importAttributes.keySet()) {
1225 String value = importAttributes.get(key);
1226 if (value.indexOf('$') >= 0) {
1227 value = getReplacer().process(value);
1228 importAttributes.put(key, value);
1229 }
1230 }
1231
1232 // You can add a remove-attribute: directive with a regular
1233 // expression for attributes that need to be removed. We also
1234 // remove all attributes that have a value of !. This allows
1235 // you to use macros with ${if} to remove values.
1236 String remove = importAttributes
1237 .remove(REMOVE_ATTRIBUTE_DIRECTIVE);
1238 Instruction removeInstr = null;
1239
1240 if (remove != null)
1241 removeInstr = Instruction.getPattern(remove);
1242
1243 for (Iterator<Map.Entry<String, String>> i = importAttributes
1244 .entrySet().iterator(); i.hasNext();) {
1245 Map.Entry<String, String> entry = i.next();
1246 if (entry.getValue().equals("!"))
1247 i.remove();
1248 else if (removeInstr != null
1249 && removeInstr.matches((String) entry.getKey()))
1250 i.remove();
1251 else {
1252 // Not removed ...
1253 }
1254 }
1255
1256 } finally {
1257 unsetProperty(CURRENT_PACKAGE);
1258 }
1259 }
1260 }
1261
1262 /**
1263 * If we use an import with mandatory attributes we better all use them
1264 *
1265 * @param currentAttributes
1266 * @param exporter
1267 */
1268 private void augmentMandatory(Map<String, String> currentAttributes,
1269 Map<String, String> exporter) {
1270 String mandatory = (String) exporter.get("mandatory:");
1271 if (mandatory != null) {
1272 String[] attrs = mandatory.split("\\s*,\\s*");
1273 for (int i = 0; i < attrs.length; i++) {
1274 if (!currentAttributes.containsKey(attrs[i]))
1275 currentAttributes.put(attrs[i], exporter.get(attrs[i]));
1276 }
1277 }
1278 }
1279
1280 /**
1281 * Check if we can augment the version from the exporter.
1282 *
1283 * We allow the version in the import to specify a @ which is replaced with
1284 * the exporter's version.
1285 *
1286 * @param currentAttributes
1287 * @param exporter
1288 */
1289 private void augmentVersion(Map<String, String> currentAttributes,
1290 Map<String, String> exporter) {
1291
1292 String exportVersion = (String) exporter.get("version");
1293 if (exportVersion == null)
1294 exportVersion = (String) exporter.get("specification-version");
1295 if (exportVersion == null)
1296 return;
1297
1298 exportVersion = cleanupVersion(exportVersion);
1299
1300 setProperty("@", exportVersion);
1301
1302 String importRange = currentAttributes.get("version");
1303 if (importRange != null) {
1304 importRange = cleanupVersion(importRange);
1305 importRange = getReplacer().process(importRange);
1306 } else
1307 importRange = getVersionPolicy();
1308
1309 unsetProperty("@");
1310
1311 // See if we can borrow the version
1312 // we mist replace the ${@} with the version we
1313 // found this can be useful if you want a range to start
1314 // with the found version.
1315 currentAttributes.put("version", importRange);
1316 }
1317
1318 /**
1319 * Add the uses clauses
1320 *
1321 * @param exports
1322 * @param uses
1323 * @throws MojoExecutionException
1324 */
1325 void doUses(Map<String, Map<String, String>> exports,
1326 Map<String, Set<String>> uses,
1327 Map<String, Map<String, String>> imports) {
1328 if ("true".equalsIgnoreCase(getProperty(NOUSES)))
1329 return;
1330
1331 for (Iterator<String> i = exports.keySet().iterator(); i.hasNext();) {
1332 String packageName = i.next();
1333 setProperty(CURRENT_PACKAGE, packageName);
1334 try {
1335 Map<String, String> clause = exports.get(packageName);
1336 String override = clause.get(USES_DIRECTIVE);
1337 if (override == null)
1338 override = USES_USES;
1339
1340 Set<String> usedPackages = uses.get(packageName);
1341 if (usedPackages != null) {
1342 // Only do a uses on exported or imported packages
1343 // and uses should also not contain our own package
1344 // name
1345 Set<String> sharedPackages = new HashSet<String>();
1346 sharedPackages.addAll(imports.keySet());
1347 sharedPackages.addAll(exports.keySet());
1348 usedPackages.retainAll(sharedPackages);
1349 usedPackages.remove(packageName);
1350
1351 StringBuffer sb = new StringBuffer();
1352 String del = "";
1353 for (Iterator<String> u = usedPackages.iterator(); u
1354 .hasNext();) {
1355 String usedPackage = u.next();
1356 if (!usedPackage.startsWith("java.")) {
1357 sb.append(del);
1358 sb.append(usedPackage);
1359 del = ",";
1360 }
1361 }
1362 if (override.indexOf('$') >= 0) {
1363 setProperty(CURRENT_USES, sb.toString());
1364 override = getReplacer().process(override);
1365 unsetProperty(CURRENT_USES);
1366 } else
1367 // This is for backward compatibility 0.0.287
1368 // can be deprecated over time
1369 override = override
1370 .replaceAll(USES_USES, sb.toString()).trim();
1371 if (override.endsWith(","))
1372 override = override.substring(0, override.length() - 1);
1373 if (override.startsWith(","))
1374 override = override.substring(1);
1375 if (override.length() > 0) {
1376 clause.put("uses:", override);
1377 }
1378 }
1379 } finally {
1380 unsetProperty(CURRENT_PACKAGE);
1381 }
1382 }
1383 }
1384
1385 /**
1386 * Transitively remove all elemens from unreachable through the uses link.
1387 *
1388 * @param name
1389 * @param unreachable
1390 */
1391 void removeTransitive(String name, Set<String> unreachable) {
1392 if (!unreachable.contains(name))
1393 return;
1394
1395 unreachable.remove(name);
1396
1397 Set<String> ref = uses.get(name);
1398 if (ref != null) {
1399 for (Iterator<String> r = ref.iterator(); r.hasNext();) {
1400 String element = (String) r.next();
1401 removeTransitive(element, unreachable);
1402 }
1403 }
1404 }
1405
1406 /**
1407 * Helper method to set the package info
1408 *
1409 * @param dir
1410 * @param key
1411 * @param value
1412 */
1413 void setPackageInfo(String dir, String key, String value) {
1414 if (value != null) {
1415 String pack = dir.replace('/', '.');
1416 Map<String, String> map = classpathExports.get(pack);
1417 if (map == null) {
1418 map = new HashMap<String, String>();
1419 classpathExports.put(pack, map);
1420 }
1421 map.put(key, value);
1422 }
1423 }
1424
1425 public void close() {
1426 super.close();
1427 if (dot != null)
1428 dot.close();
1429
1430 if (classpath != null)
1431 for (Iterator<Jar> j = classpath.iterator(); j.hasNext();) {
1432 Jar jar = j.next();
1433 jar.close();
1434 }
1435 }
1436
1437 /**
1438 * Findpath looks through the contents of the JAR and finds paths that end
1439 * with the given regular expression
1440 *
1441 * ${findpath (; reg-expr (; replacement)? )? }
1442 *
1443 * @param args
1444 * @return
1445 */
1446 public String _findpath(String args[]) {
1447 return findPath("findpath", args, true);
1448 }
1449
1450 public String _findname(String args[]) {
1451 return findPath("findname", args, false);
1452 }
1453
1454 String findPath(String name, String[] args, boolean fullPathName) {
1455 if (args.length > 3) {
1456 warning("Invalid nr of arguments to " + name + " "
1457 + Arrays.asList(args) + ", syntax: ${" + name
1458 + " (; reg-expr (; replacement)? )? }");
1459 return null;
1460 }
1461
1462 String regexp = ".*";
1463 String replace = null;
1464
1465 switch (args.length) {
1466 case 3:
1467 replace = args[2];
1468 case 2:
1469 regexp = args[1];
1470 }
1471 StringBuffer sb = new StringBuffer();
1472 String del = "";
1473
1474 Pattern expr = Pattern.compile(regexp);
1475 for (Iterator<String> e = dot.getResources().keySet().iterator(); e
1476 .hasNext();) {
1477 String path = e.next();
1478 if (!fullPathName) {
1479 int n = path.lastIndexOf('/');
1480 if (n >= 0) {
1481 path = path.substring(n + 1);
1482 }
1483 }
1484
1485 Matcher m = expr.matcher(path);
1486 if (m.matches()) {
1487 if (replace != null)
1488 path = m.replaceAll(replace);
1489
1490 sb.append(del);
1491 sb.append(path);
1492 del = ", ";
1493 }
1494 }
1495 return sb.toString();
1496 }
1497
1498 public void putAll(Map<String, String> additional, boolean force) {
1499 for (Iterator<Map.Entry<String, String>> i = additional.entrySet()
1500 .iterator(); i.hasNext();) {
1501 Map.Entry<String, String> entry = i.next();
1502 if (force || getProperties().get(entry.getKey()) == null)
1503 setProperty((String) entry.getKey(), (String) entry.getValue());
1504 }
1505 }
1506
1507 boolean firstUse = true;
1508
1509 public List<Jar> getClasspath() {
1510 if (firstUse) {
1511 firstUse = false;
1512 String cp = getProperty(CLASSPATH);
1513 if (cp != null)
1514 for (String s : split(cp)) {
1515 Jar jar = getJarFromName(s, "getting classpath");
1516 if (jar != null)
1517 addClasspath(jar);
1518 }
1519 }
1520 return classpath;
1521 }
1522
1523 public void addClasspath(Jar jar) {
1524 if (isPedantic() && jar.getResources().isEmpty())
1525 warning("There is an empty jar or directory on the classpath: "
1526 + jar.getName());
1527
1528 classpath.add(jar);
1529 }
1530
1531 public void addClasspath(File cp) throws IOException {
1532 if (!cp.exists())
1533 warning("File on classpath that does not exist: " + cp);
1534 Jar jar = new Jar(cp);
1535 addClose(jar);
1536 classpath.add(jar);
1537 }
1538
1539 public void clear() {
1540 classpath.clear();
1541 }
1542
1543 public Jar getTarget() {
1544 return dot;
1545 }
1546
1547 public Map<String, Clazz> analyzeBundleClasspath(Jar dot,
1548 Map<String, Map<String, String>> bundleClasspath,
1549 Map<String, Map<String, String>> contained,
1550 Map<String, Map<String, String>> referred,
1551 Map<String, Set<String>> uses) throws IOException {
1552 Map<String, Clazz> classSpace = new HashMap<String, Clazz>();
1553
1554 if (bundleClasspath.isEmpty()) {
1555 analyzeJar(dot, "", classSpace, contained, referred, uses);
1556 } else {
1557 for (String path : bundleClasspath.keySet()) {
1558 if (path.equals(".")) {
1559 analyzeJar(dot, "", classSpace, contained, referred, uses);
1560 continue;
1561 }
1562 //
1563 // There are 3 cases:
1564 // - embedded JAR file
1565 // - directory
1566 // - error
1567 //
1568
1569 Resource resource = dot.getResource(path);
1570 if (resource != null) {
1571 try {
1572 Jar jar = new Jar(path);
1573 addClose(jar);
1574 EmbeddedResource.build(jar, resource);
1575 analyzeJar(jar, "", classSpace, contained, referred,
1576 uses);
1577 } catch (Exception e) {
1578 warning("Invalid bundle classpath entry: " + path + " "
1579 + e);
1580 }
1581 } else {
1582 if (dot.getDirectories().containsKey(path)) {
1583 analyzeJar(dot, path, classSpace, contained, referred,
1584 uses);
1585 } else {
1586 warning("No sub JAR or directory " + path);
1587 }
1588 }
1589 }
1590 }
1591 return classSpace;
1592 }
1593
1594 /**
1595 * We traverse through all the classes that we can find and calculate the
1596 * contained and referred set and uses. This method ignores the Bundle
1597 * classpath.
1598 *
1599 * @param jar
1600 * @param contained
1601 * @param referred
1602 * @param uses
1603 * @throws IOException
1604 */
1605 private void analyzeJar(Jar jar, String prefix,
1606 Map<String, Clazz> classSpace,
1607 Map<String, Map<String, String>> contained,
1608 Map<String, Map<String, String>> referred,
1609 Map<String, Set<String>> uses) throws IOException {
1610
1611 next: for (String path : jar.getResources().keySet()) {
1612 if (path.startsWith(prefix)) {
1613 String relativePath = path.substring(prefix.length());
1614 String pack = getPackage(relativePath);
1615
1616 if (pack != null && !contained.containsKey(pack)) {
1617 if (!(pack.equals(".") || isMetaData(relativePath))) {
1618
1619 Map<String, String> map = new LinkedHashMap<String, String>();
1620 contained.put(pack, map);
1621 Resource pinfo = jar.getResource(prefix
1622 + pack.replace('.', '/') + "/packageinfo");
1623 if (pinfo != null) {
1624 InputStream in = pinfo.openInputStream();
1625 String version = parsePackageInfo(in);
1626 in.close();
1627 if (version != null)
1628 map.put("version", version);
1629 }
1630 }
1631 }
1632
1633 if (path.endsWith(".class")) {
1634 Resource resource = jar.getResource(path);
1635 Clazz clazz;
1636
1637 try {
1638 InputStream in = resource.openInputStream();
1639 clazz = new Clazz(relativePath, in);
1640 in.close();
1641 } catch (Throwable e) {
1642 error("Invalid class file: " + relativePath, e);
1643 e.printStackTrace();
1644 continue next;
1645 }
1646
1647 String calculatedPath = clazz.getClassName() + ".class";
1648 if (!calculatedPath.equals(relativePath))
1649 error("Class in different directory than declared. Path from class name is "
1650 + calculatedPath
1651 + " but the path in the jar is "
1652 + relativePath
1653 + " from " + jar);
1654
1655 classSpace.put(relativePath, clazz);
1656 referred.putAll(clazz.getReferred());
1657
1658 // Add all the used packages
1659 // to this package
1660 Set<String> t = uses.get(pack);
1661 if (t == null)
1662 uses.put(pack, t = new LinkedHashSet<String>());
1663 t.addAll(clazz.getReferred().keySet());
1664 t.remove(pack);
1665 }
1666 }
1667 }
1668 }
1669
1670 /**
1671 * Clean up version parameters. Other builders use more fuzzy definitions of
1672 * the version syntax. This method cleans up such a version to match an OSGi
1673 * version.
1674 *
1675 * @param VERSION_STRING
1676 * @return
1677 */
1678 static Pattern fuzzyVersion = Pattern
1679 .compile(
1680 "(\\d+)(\\.(\\d+)(\\.(\\d+))?)?([^a-zA-Z0-9](.*))?",
1681 Pattern.DOTALL);
1682 static Pattern fuzzyVersionRange = Pattern
1683 .compile(
1684 "(\\(|\\[)\\s*([-\\da-zA-Z.]+)\\s*,\\s*([-\\da-zA-Z.]+)\\s*(\\]|\\))",
1685 Pattern.DOTALL);
1686 static Pattern fuzzyModifier = Pattern.compile("(\\d+[.-])*(.*)",
1687 Pattern.DOTALL);
1688
1689 static Pattern nummeric = Pattern.compile("\\d*");
1690
1691 static public String cleanupVersion(String version) {
1692 if (Verifier.VERSIONRANGE.matcher(version).matches())
1693 return version;
1694
1695 Matcher m = fuzzyVersionRange.matcher(version);
1696 if (m.matches()) {
1697 String prefix = m.group(1);
1698 String first = m.group(2);
1699 String last = m.group(3);
1700 String suffix = m.group(4);
1701 return prefix + cleanupVersion(first) + "," + cleanupVersion(last)
1702 + suffix;
1703 } else {
1704 m = fuzzyVersion.matcher(version);
1705 if (m.matches()) {
1706 StringBuffer result = new StringBuffer();
1707 String major = m.group(1);
1708 String minor = m.group(3);
1709 String micro = m.group(5);
1710 String qualifier = m.group(7);
1711
1712 if (major != null) {
1713 result.append(major);
1714 if (minor != null) {
1715 result.append(".");
1716 result.append(minor);
1717 if (micro != null) {
1718 result.append(".");
1719 result.append(micro);
1720 if (qualifier != null) {
1721 result.append(".");
1722 cleanupModifier(result, qualifier);
1723 }
1724 } else if (qualifier != null) {
1725 result.append(".0.");
1726 cleanupModifier(result, qualifier);
1727 }
1728 } else if (qualifier != null) {
1729 result.append(".0.0.");
1730 cleanupModifier(result, qualifier);
1731 }
1732 return result.toString();
1733 }
1734 }
1735 }
1736 return version;
1737 }
1738
1739 static void cleanupModifier(StringBuffer result, String modifier) {
1740 Matcher m = fuzzyModifier.matcher(modifier);
1741 if (m.matches())
1742 modifier = m.group(2);
1743
1744 for (int i = 0; i < modifier.length(); i++) {
1745 char c = modifier.charAt(i);
1746 if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')
1747 || (c >= 'A' && c <= 'Z') || c == '_' || c == '-')
1748 result.append(c);
1749 }
1750 }
1751
1752 /**
1753 * Decide if the package is a metadata package.
1754 *
1755 * @param pack
1756 * @return
1757 */
1758 boolean isMetaData(String pack) {
1759 for (int i = 0; i < METAPACKAGES.length; i++) {
1760 if (pack.startsWith(METAPACKAGES[i]))
1761 return true;
1762 }
1763 return false;
1764 }
1765
1766 public String getPackage(String clazz) {
1767 int n = clazz.lastIndexOf('/');
1768 if (n < 0)
1769 return ".";
1770 return clazz.substring(0, n).replace('/', '.');
1771 }
1772
1773 //
1774 // We accept more than correct OSGi versions because in a later
1775 // phase we actually cleanup maven versions. But it is a bit yucky
1776 //
1777 static String parsePackageInfo(InputStream jar) throws IOException {
1778 try {
1779 Properties p = new Properties();
1780 p.load(jar);
1781 jar.close();
1782 if (p.containsKey("version")) {
1783 return p.getProperty("version");
1784 }
1785 } catch (Exception e) {
1786 e.printStackTrace();
1787 }
1788 return null;
1789 }
1790
1791 public String getVersionPolicy() {
1792 return getProperty(VERSIONPOLICY, "${version;==;${@}}");
1793 }
1794
1795 /**
1796 * The extends macro traverses all classes and returns a list of class names
1797 * that extend a base class.
1798 */
1799
1800 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";
1801
1802 public String _classes(String args[]) {
1803 // Macro.verifyCommand(args, _classesHelp, new
1804 // Pattern[]{null,Pattern.compile("(implementing|implements|extending|extends|importing|imports|any)"),
1805 // null}, 3,3);
1806 Set<Clazz> matched = new HashSet<Clazz>(classspace.values());
1807 for (int i = 1; i < args.length; i += 2) {
1808 if (args.length < i + 1)
1809 throw new IllegalArgumentException(
1810 "${classes} macro must have odd number of arguments. "
1811 + _classesHelp);
1812
1813 String typeName = args[i];
1814 Clazz.QUERY type = null;
1815 if (typeName.equals("implementing")
1816 || typeName.equals("implements"))
1817 type = Clazz.QUERY.IMPLEMENTS;
1818 else if (typeName.equals("extending") || typeName.equals("extends"))
1819 type = Clazz.QUERY.EXTENDS;
1820 else if (typeName.equals("importing") || typeName.equals("imports"))
1821 type = Clazz.QUERY.IMPORTS;
1822 else if (typeName.equals("all"))
1823 type = Clazz.QUERY.ANY;
1824 else if (typeName.equals("version"))
1825 type = Clazz.QUERY.VERSION;
1826 else if (typeName.equals("named"))
1827 type = Clazz.QUERY.NAMED;
1828
1829 if (type == null)
1830 throw new IllegalArgumentException(
1831 "${classes} has invalid type: " + typeName + ". "
1832 + _classesHelp);
1833 // The argument is declared as a dotted name but the classes
1834 // use a slashed named. So convert the name before we make it a
1835 // instruction.
1836 String pattern = args[i + 1].replace('.', '/');
1837 Instruction instr = Instruction.getPattern(pattern);
1838
1839 for (Iterator<Clazz> c = matched.iterator(); c.hasNext();) {
1840 Clazz clazz = c.next();
1841 if (!clazz.is(type, instr, classspace))
1842 c.remove();
1843 }
1844 }
1845 if (matched.isEmpty())
1846 return "";
1847
1848 return join(matched);
1849 }
1850
1851 /**
1852 * Get the exporter of a package ...
1853 */
1854
1855 public String _exporters(String args[]) throws Exception {
1856 Macro
1857 .verifyCommand(
1858 args,
1859 "${exporters;<packagename>}, returns the list of jars that export the given package",
1860 null, 2, 2);
1861 StringBuilder sb = new StringBuilder();
1862 String del = "";
1863 String pack = args[1].replace('.', '/');
1864 for (Jar jar : classpath) {
1865 if (jar.getDirectories().containsKey(pack)) {
1866 sb.append(del);
1867 sb.append(jar.getName());
1868 }
1869 }
1870 return sb.toString();
1871 }
1872
1873 public Map<String, Clazz> getClassspace() {
1874 return classspace;
1875 }
1876
1877}