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