blob: a529fbc23d23194cedf70500d89cbef81d31bc4e [file] [log] [blame]
Stuart McCullochd00f9712009-07-13 10:06:47 +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(), null);
168
169 // disallow export of default package
170 exports.remove(".");
171
172 for (Iterator<Map.Entry<String, Map<String, String>>> i = superfluous
173 .entrySet().iterator(); i.hasNext();) {
174 // It is possible to mention metadata directories in the export
175 // explicitly, they are then exported and removed from the
176 // warnings. Note that normally metadata directories are not
177 // exported.
178 Map.Entry<String, Map<String, String>> entry = i.next();
179 String pack = entry.getKey();
180 if (isDuplicate(pack))
181 i.remove();
182 else if (isMetaData(pack)) {
183 exports.put(pack, entry.getValue());
184 i.remove();
185 }
186 }
187
188 if (!superfluous.isEmpty()) {
189 warning("Superfluous export-package instructions: "
190 + superfluous.keySet());
191 }
192
193 // Add all exports that do not have an -noimport: directive
194 // to the imports.
195 Map<String, Map<String, String>> referredAndExported = newMap(referred);
196 referredAndExported.putAll(addExportsToImports(exports));
197
198 // match the imports to the referred and exported packages,
199 // merge the info for matching packages
200 Set<String> extra = new TreeSet<String>(importInstructions.keySet());
201 imports = merge("import-package", importInstructions,
202 referredAndExported, extra, ignored);
203
204 // Instructions that have not been used could be superfluous
205 // or if they do not contain wildcards, should be added
206 // as extra imports, the user knows best.
207 for (Iterator<String> i = extra.iterator(); i.hasNext();) {
208 String p = i.next();
209 if (p.startsWith("!") || p.indexOf('*') >= 0
210 || p.indexOf('?') >= 0 || p.indexOf('[') >= 0) {
211 if (!isResourceOnly())
212 warning("Did not find matching referal for " + p);
213 } else {
214 Map<String, String> map = importInstructions.get(p);
215 imports.put(p, map);
216 }
217 }
218
219 // See what information we can find to augment the
220 // imports. I.e. look on the classpath
221 augmentImports();
222
223 // Add the uses clause to the exports
224 doUses(exports, uses, imports);
225 }
226 }
227
228 /**
229 * Copy the input collection into an output set but skip names that have
230 * been marked as duplicates or are optional.
231 *
232 * @param superfluous
233 * @return
234 */
235 Set<Instruction> removeMarkedDuplicates(Collection<Instruction> superfluous) {
236 Set<Instruction> result = new HashSet<Instruction>();
237 for (Iterator<Instruction> i = superfluous.iterator(); i.hasNext();) {
238 Instruction instr = (Instruction) i.next();
239 if (!isDuplicate(instr.getPattern()) && !instr.isOptional())
240 result.add(instr);
241 }
242 return result;
243 }
244
245 /**
246 * Analyzer has an empty default but the builder has a * as default.
247 *
248 * @return
249 */
250 protected String getImportPackages() {
251 return getProperty(IMPORT_PACKAGE);
252 }
253
254 /**
255 *
256 * @return
257 */
258 boolean isResourceOnly() {
259 return getProperty(RESOURCEONLY, "false").equalsIgnoreCase("true");
260 }
261
262 /**
263 * Answer the list of packages that use the given package.
264 */
265 Set<String> getUsedBy(String pack) {
266 Set<String> set = newSet();
267 for (Iterator<Map.Entry<String, Set<String>>> i = uses.entrySet()
268 .iterator(); i.hasNext();) {
269 Map.Entry<String, Set<String>> entry = i.next();
270 Set<String> used = entry.getValue();
271 if (used.contains(pack))
272 set.add(entry.getKey());
273 }
274 return set;
275 }
276
277 /**
278 * One of the main workhorses of this class. This will analyze the current
279 * setp and calculate a new manifest according to this setup. This method
280 * will also set the manifest on the main jar dot
281 *
282 * @return
283 * @throws IOException
284 */
285 public Manifest calcManifest() throws IOException {
286 analyze();
287 Manifest manifest = new Manifest();
288 Attributes main = manifest.getMainAttributes();
289
290 main.put(Attributes.Name.MANIFEST_VERSION, "1.0");
291 main.putValue(BUNDLE_MANIFESTVERSION, "2");
292
293 boolean noExtraHeaders = "true"
294 .equalsIgnoreCase(getProperty(NOEXTRAHEADERS));
295
296 if (!noExtraHeaders) {
297 main.putValue(CREATED_BY, System.getProperty("java.version") + " ("
298 + System.getProperty("java.vendor") + ")");
299 main.putValue(TOOL, "Bnd-" + getVersion());
300 main.putValue(BND_LASTMODIFIED, "" + System.currentTimeMillis());
301 }
302 String exportHeader = printClauses(exports,
303 "uses:|include:|exclude:|mandatory:|" + IMPORT_DIRECTIVE, true);
304
305 if (exportHeader.length() > 0)
306 main.putValue(EXPORT_PACKAGE, exportHeader);
307 else
308 main.remove(EXPORT_PACKAGE);
309
310 Map<String, Map<String, String>> temp = removeKeys(imports, "java.");
311 if (!temp.isEmpty()) {
312 main.putValue(IMPORT_PACKAGE, printClauses(temp, "resolution:"));
313 } else {
314 main.remove(IMPORT_PACKAGE);
315 }
316
317 temp = newMap(contained);
318 temp.keySet().removeAll(exports.keySet());
319
320 if (!temp.isEmpty())
321 main.putValue(PRIVATE_PACKAGE, printClauses(temp, ""));
322 else
323 main.remove(PRIVATE_PACKAGE);
324
325 if (!ignored.isEmpty()) {
326 main.putValue(IGNORE_PACKAGE, printClauses(ignored, ""));
327 } else {
328 main.remove(IGNORE_PACKAGE);
329 }
330
331 if (bundleClasspath != null && !bundleClasspath.isEmpty())
332 main.putValue(BUNDLE_CLASSPATH, printClauses(bundleClasspath, ""));
333 else
334 main.remove(BUNDLE_CLASSPATH);
335
336
337 for (Enumeration<?> h = getProperties().propertyNames(); h
338 .hasMoreElements();) {
339 String header = (String) h.nextElement();
340 if (header.trim().length() == 0) {
341 warning("Empty property set with value: "
342 + getProperties().getProperty(header));
343 continue;
344 }
345 if (!Character.isUpperCase(header.charAt(0))) {
346 if (header.charAt(0) == '@')
347 doNameSection(manifest, header);
348 continue;
349 }
350
351 if (header.equals(BUNDLE_CLASSPATH)
352 || header.equals(EXPORT_PACKAGE)
353 || header.equals(IMPORT_PACKAGE))
354 continue;
355
356 if (header.equalsIgnoreCase("Name")) {
357 error("Your bnd file contains a header called 'Name'. This interferes with the manifest name section.");
358 continue;
359 }
360
361 if (Verifier.HEADER_PATTERN.matcher(header).matches()) {
362 String value = getProperty(header);
363 if (value != null && main.getValue(header) == null) {
364 if (value.trim().length() == 0)
365 main.remove(header);
366 else if (value.trim().equals("<<EMPTY>>"))
367 main.putValue(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 a name
422 * section header. The name part is defined by replacing all the @ signs to
423 * a /, removing the first and the last, and using the last part as header
424 * name:
425 *
426 * <pre>
427 * &#064;org@osgi@service@event@Implementation-Title
428 * </pre>
429 *
430 * This will be the header Implementation-Title in the
431 * org/osgi/service/event named section.
432 *
433 * @param manifest
434 * @param header
435 */
436 private void doNameSection(Manifest manifest, String header) {
437 String path = header.replace('@', '/');
438 int n = path.lastIndexOf('/');
439 // Must succeed because we start with @
440 String name = path.substring(n + 1);
441 // Skip first /
442 path = path.substring(1, n);
443 if (name.length() != 0 && path.length() != 0) {
444 Attributes attrs = manifest.getAttributes(path);
445 if (attrs == null) {
446 attrs = new Attributes();
447 manifest.getEntries().put(path, attrs);
448 }
449 attrs.putValue(name, getProperty(header));
450 } else {
451 warning(
452 "Invalid header (starts with @ but does not seem to be for the Name section): %s",
453 header);
454 }
455 }
456
457 /**
458 * Clear the key part of a header. I.e. remove everything from the first ';'
459 *
460 * @param value
461 * @return
462 */
463 public String getBsn() {
464 String value = getProperty(BUNDLE_SYMBOLICNAME);
465 if (value == null) {
466 if (getPropertiesFile() != null)
467 value = getPropertiesFile().getName();
468
469 if (value == null || value.equals("bnd.bnd"))
470 value = getBase().getName();
471 else if (value.endsWith(".bnd"))
472 value = value.substring(0, value.length() - 4);
473 }
474
475 if (value == null)
476 return "untitled";
477
478 int n = value.indexOf(';');
479 if (n > 0)
480 value = value.substring(0, n);
481 return value.trim();
482 }
483
484 public String _bsn(String args[]) {
485 return getBsn();
486 }
487
488 /**
489 * Calculate an export header solely based on the contents of a JAR file
490 *
491 * @param bundle
492 * The jar file to analyze
493 * @return
494 */
495 public String calculateExportsFromContents(Jar bundle) {
496 String ddel = "";
497 StringBuffer sb = new StringBuffer();
498 Map<String, Map<String, Resource>> map = bundle.getDirectories();
499 for (Iterator<String> i = map.keySet().iterator(); i.hasNext();) {
500 String directory = (String) i.next();
501 if (directory.equals("META-INF")
502 || directory.startsWith("META-INF/"))
503 continue;
504 if (directory.equals("OSGI-OPT")
505 || directory.startsWith("OSGI-OPT/"))
506 continue;
507 if (directory.equals("/"))
508 continue;
509
510 if (directory.endsWith("/"))
511 directory = directory.substring(0, directory.length() - 1);
512
513 directory = directory.replace('/', '.');
514 sb.append(ddel);
515 sb.append(directory);
516 ddel = ",";
517 }
518 return sb.toString();
519 }
520
521
522 public Map<String, Map<String, String>> getBundleClasspath() {
523 return bundleClasspath;
524 }
525
526 public Map<String, Map<String, String>> getContained() {
527 return contained;
528 }
529
530 public Map<String, Map<String, String>> getExports() {
531 return exports;
532 }
533
534 public Map<String, Map<String, String>> getImports() {
535 return imports;
536 }
537
538 public Jar getJar() {
539 return dot;
540 }
541
542 public Map<String, Map<String, String>> getReferred() {
543 return referred;
544 }
545
546 /**
547 * Return the set of unreachable code depending on exports and the bundle
548 * activator.
549 *
550 * @return
551 */
552 public Set<String> getUnreachable() {
553 Set<String> unreachable = new HashSet<String>(uses.keySet()); // all
554 for (Iterator<String> r = exports.keySet().iterator(); r.hasNext();) {
555 String packageName = r.next();
556 removeTransitive(packageName, unreachable);
557 }
558 if (activator != null) {
559 String pack = activator.substring(0, activator.lastIndexOf('.'));
560 removeTransitive(pack, unreachable);
561 }
562 return unreachable;
563 }
564
565 public Map<String, Set<String>> getUses() {
566 return uses;
567 }
568
569 /**
570 * Get the version from the manifest, a lot of work!
571 *
572 * @return version or unknown.
573 */
574 public String getVersion() {
575 return getBndInfo("version", "<unknown version>");
576 }
577
578 public long getBndLastModified() {
579 String time = getBndInfo("modified", "0");
580 try {
581 return Long.parseLong(time);
582 } catch (Exception e) {
583 }
584 return 0;
585 }
586
587 public String getBndInfo(String key, String defaultValue) {
588 if (bndInfo == null) {
589 bndInfo = new Properties();
590 try {
591 InputStream in = Analyzer.class.getResourceAsStream("bnd.info");
592 if (in != null) {
593 bndInfo.load(in);
594 in.close();
595 }
596 } catch (IOException ioe) {
597 warning("Could not read bnd.info in "
598 + Analyzer.class.getPackage() + ioe);
599 }
600 }
601 return bndInfo.getProperty(key, defaultValue);
602 }
603
604 /**
605 * Merge the existing manifest with the instructions.
606 *
607 * @param manifest
608 * The manifest to merge with
609 * @throws IOException
610 */
611 public void mergeManifest(Manifest manifest) throws IOException {
612 if (manifest != null) {
613 Attributes attributes = manifest.getMainAttributes();
614 for (Iterator<Object> i = attributes.keySet().iterator(); i
615 .hasNext();) {
616 Name name = (Name) i.next();
617 String key = name.toString();
618 // Dont want instructions
619 if (key.startsWith("-"))
620 continue;
621
622 if (getProperty(key) == null)
623 setProperty(key, (String) attributes.get(name));
624 }
625 }
626 }
627
628 public void setBase(File file) {
629 super.setBase(file);
630 getProperties().put("project.dir", getBase().getAbsolutePath());
631 }
632
633 /**
634 * Set the classpath for this analyzer by file.
635 *
636 * @param classpath
637 * @throws IOException
638 */
639 public void setClasspath(File[] classpath) throws IOException {
640 List<Jar> list = new ArrayList<Jar>();
641 for (int i = 0; i < classpath.length; i++) {
642 if (classpath[i].exists()) {
643 Jar current = new Jar(classpath[i]);
644 list.add(current);
645 } else {
646 error("Missing file on classpath: " + classpath[i]);
647 }
648 }
649 for (Iterator<Jar> i = list.iterator(); i.hasNext();) {
650 addClasspath(i.next());
651 }
652 }
653
654 public void setClasspath(Jar[] classpath) {
655 for (int i = 0; i < classpath.length; i++) {
656 addClasspath(classpath[i]);
657 }
658 }
659
660 public void setClasspath(String[] classpath) {
661 for (int i = 0; i < classpath.length; i++) {
662 Jar jar = getJarFromName(classpath[i], " setting classpath");
663 if (jar != null)
664 addClasspath(jar);
665 }
666 }
667
668 /**
669 * Set the JAR file we are going to work in. This will read the JAR in
670 * memory.
671 *
672 * @param jar
673 * @return
674 * @throws IOException
675 */
676 public Jar setJar(File jar) throws IOException {
677 Jar jarx = new Jar(jar);
678 addClose(jarx);
679 return setJar(jarx);
680 }
681
682 /**
683 * Set the JAR directly we are going to work on.
684 *
685 * @param jar
686 * @return
687 */
688 public Jar setJar(Jar jar) {
689 this.dot = jar;
690 return jar;
691 }
692
693 protected void begin() {
694 super.begin();
695
696 updateModified(getBndLastModified(), "bnd last modified");
697 String doNotCopy = getProperty(DONOTCOPY);
698 if (doNotCopy != null)
699 Analyzer.doNotCopy = Pattern.compile(doNotCopy);
700
701 verifyManifestHeadersCase(getProperties());
702 }
703
704 /**
705 * Check if the given class or interface name is contained in the jar.
706 *
707 * @param interfaceName
708 * @return
709 */
710 public boolean checkClass(String interfaceName) {
711 String path = interfaceName.replace('.', '/') + ".class";
712 if (classspace.containsKey(path))
713 return true;
714
715 String pack = interfaceName;
716 int n = pack.lastIndexOf('.');
717 if (n > 0)
718 pack = pack.substring(0, n);
719 else
720 pack = ".";
721
722 return imports.containsKey(pack);
723 }
724
725 /**
726 * Try to get a Jar from a file name/path or a url, or in last resort from
727 * the classpath name part of their files.
728 *
729 * @param name
730 * URL or filename relative to the base
731 * @param from
732 * Message identifying the caller for errors
733 * @return null or a Jar with the contents for the name
734 */
735 Jar getJarFromName(String name, String from) {
736 File file = new File(name);
737 if (!file.isAbsolute())
738 file = new File(getBase(), name);
739
740 if (file.exists())
741 try {
742 Jar jar = new Jar(file);
743 addClose(jar);
744 return jar;
745 } catch (Exception e) {
746 error("Exception in parsing jar file for " + from + ": " + name
747 + " " + e);
748 }
749 // It is not a file ...
750 try {
751 // Lets try a URL
752 URL url = new URL(name);
753 Jar jar = new Jar(fileName(url.getPath()));
754 addClose(jar);
755 URLConnection connection = url.openConnection();
756 InputStream in = connection.getInputStream();
757 long lastModified = connection.getLastModified();
758 if (lastModified == 0)
759 // We assume the worst :-(
760 lastModified = System.currentTimeMillis();
761 EmbeddedResource.build(jar, in, lastModified);
762 in.close();
763 return jar;
764 } catch (IOException ee) {
765 // Check if we have files on the classpath
766 // that have the right name, allows us to specify those
767 // names instead of the full path.
768 for (Iterator<Jar> cp = getClasspath().iterator(); cp.hasNext();) {
769 Jar entry = cp.next();
770 if (entry.source != null && entry.source.getName().equals(name)) {
771 return entry;
772 }
773 }
774 // error("Can not find jar file for " + from + ": " + name);
775 }
776 return null;
777 }
778
779 private String fileName(String path) {
780 int n = path.lastIndexOf('/');
781 if (n > 0)
782 return path.substring(n + 1);
783 return path;
784 }
785
786 /**
787 *
788 * @param manifest
789 * @throws Exception
790 */
791 void merge(Manifest result, Manifest old) throws IOException {
792 if (old != null) {
793 for (Iterator<Map.Entry<Object, Object>> e = old
794 .getMainAttributes().entrySet().iterator(); e.hasNext();) {
795 Map.Entry<Object, Object> entry = e.next();
796 Attributes.Name name = (Attributes.Name) entry.getKey();
797 String value = (String) entry.getValue();
798 if (name.toString().equalsIgnoreCase("Created-By"))
799 name = new Attributes.Name("Originally-Created-By");
800 if (!result.getMainAttributes().containsKey(name))
801 result.getMainAttributes().put(name, value);
802 }
803
804 // do not overwrite existing entries
805 Map<String, Attributes> oldEntries = old.getEntries();
806 Map<String, Attributes> newEntries = result.getEntries();
807 for (Iterator<Map.Entry<String, Attributes>> e = oldEntries
808 .entrySet().iterator(); e.hasNext();) {
809 Map.Entry<String, Attributes> entry = e.next();
810 if (!newEntries.containsKey(entry.getKey())) {
811 newEntries.put(entry.getKey(), entry.getValue());
812 }
813 }
814 }
815 }
816
817 String stem(String name) {
818 int n = name.lastIndexOf('.');
819 if (n > 0)
820 return name.substring(0, n);
821 else
822 return name;
823 }
824
825 /**
826 * Bnd is case sensitive for the instructions so we better check people are
827 * not using an invalid case. We do allow this to set headers that should
828 * not be processed by us but should be used by the framework.
829 *
830 * @param properties
831 * Properties to verify.
832 */
833
834 void verifyManifestHeadersCase(Properties properties) {
835 for (Iterator<Object> i = properties.keySet().iterator(); i.hasNext();) {
836 String header = (String) i.next();
837 for (int j = 0; j < headers.length; j++) {
838 if (!headers[j].equals(header)
839 && headers[j].equalsIgnoreCase(header)) {
840 warning("Using a standard OSGi header with the wrong case (bnd is case sensitive!), using: "
841 + header + " and expecting: " + headers[j]);
842 break;
843 }
844 }
845 }
846 }
847
848 /**
849 * We will add all exports to the imports unless there is a -noimport
850 * directive specified on an export. This directive is skipped for the
851 * manifest.
852 *
853 * We also remove any version parameter so that augmentImports can do the
854 * version policy.
855 *
856 */
857 Map<String, Map<String, String>> addExportsToImports(
858 Map<String, Map<String, String>> exports) {
859 Map<String, Map<String, String>> importsFromExports = newHashMap();
860 for (Map.Entry<String, Map<String, String>> packageEntry : exports
861 .entrySet()) {
862 String packageName = packageEntry.getKey();
863 Map<String, String> parameters = packageEntry.getValue();
864 String noimport = (String) parameters.get(NO_IMPORT_DIRECTIVE);
865 if (noimport == null || !noimport.equalsIgnoreCase("true")) {
866 if (parameters.containsKey(VERSION_ATTRIBUTE)) {
867 parameters = newMap(parameters);
868 parameters.remove(VERSION_ATTRIBUTE);
869 }
870 importsFromExports.put(packageName, parameters);
871 }
872 }
873 return importsFromExports;
874 }
875
876 /**
877 * Create the imports/exports by parsing
878 *
879 * @throws IOException
880 */
881 void analyzeClasspath() throws IOException {
882 classpathExports = newHashMap();
883 for (Iterator<Jar> c = getClasspath().iterator(); c.hasNext();) {
884 Jar current = c.next();
885 checkManifest(current);
886 for (Iterator<String> j = current.getDirectories().keySet()
887 .iterator(); j.hasNext();) {
888 String dir = j.next();
889 Resource resource = current.getResource(dir + "/packageinfo");
890 if (resource != null) {
891 InputStream in = resource.openInputStream();
892 String version = parsePackageInfo(in);
893 in.close();
894 setPackageInfo(dir, "version", version);
895 }
896 }
897 }
898 }
899
900 /**
901 *
902 * @param jar
903 */
904 void checkManifest(Jar jar) {
905 try {
906 Manifest m = jar.getManifest();
907 if (m != null) {
908 String exportHeader = m.getMainAttributes().getValue(
909 EXPORT_PACKAGE);
910 if (exportHeader != null) {
911 Map<String, Map<String, String>> exported = parseHeader(exportHeader);
912 if (exported != null)
913 classpathExports.putAll(exported);
914 }
915 }
916 } catch (Exception e) {
917 warning("Erroneous Manifest for " + jar + " " + e);
918 }
919 }
920
921 /**
922 * Find some more information about imports in manifest and other places.
923 */
924 void augmentImports() {
925 for (String packageName : imports.keySet()) {
926 setProperty(CURRENT_PACKAGE, packageName);
927 try {
928 Map<String, String> importAttributes = imports.get(packageName);
929 Map<String, String> exporterAttributes = classpathExports
930 .get(packageName);
931 if (exporterAttributes == null)
932 exporterAttributes = exports.get(packageName);
933
934 if (exporterAttributes != null) {
935 augmentVersion(importAttributes, exporterAttributes);
936 augmentMandatory(importAttributes, exporterAttributes);
937 if (exporterAttributes.containsKey(IMPORT_DIRECTIVE))
938 importAttributes.put(IMPORT_DIRECTIVE,
939 exporterAttributes.get(IMPORT_DIRECTIVE));
940 }
941
942 // Convert any attribute values that have macros.
943 for (String key : importAttributes.keySet()) {
944 String value = importAttributes.get(key);
945 if (value.indexOf('$') >= 0) {
946 value = getReplacer().process(value);
947 importAttributes.put(key, value);
948 }
949 }
950
951 // You can add a remove-attribute: directive with a regular
952 // expression for attributes that need to be removed. We also
953 // remove all attributes that have a value of !. This allows
954 // you to use macros with ${if} to remove values.
955 String remove = importAttributes
956 .remove(REMOVE_ATTRIBUTE_DIRECTIVE);
957 Instruction removeInstr = null;
958
959 if (remove != null)
960 removeInstr = Instruction.getPattern(remove);
961
962 for (Iterator<Map.Entry<String, String>> i = importAttributes
963 .entrySet().iterator(); i.hasNext();) {
964 Map.Entry<String, String> entry = i.next();
965 if (entry.getValue().equals("!"))
966 i.remove();
967 else if (removeInstr != null
968 && removeInstr.matches((String) entry.getKey()))
969 i.remove();
970 else {
971 // Not removed ...
972 }
973 }
974
975 } finally {
976 unsetProperty(CURRENT_PACKAGE);
977 }
978 }
979 }
980
981 /**
982 * If we use an import with mandatory attributes we better all use them
983 *
984 * @param currentAttributes
985 * @param exporter
986 */
987 private void augmentMandatory(Map<String, String> currentAttributes,
988 Map<String, String> exporter) {
989 String mandatory = (String) exporter.get("mandatory:");
990 if (mandatory != null) {
991 String[] attrs = mandatory.split("\\s*,\\s*");
992 for (int i = 0; i < attrs.length; i++) {
993 if (!currentAttributes.containsKey(attrs[i]))
994 currentAttributes.put(attrs[i], exporter.get(attrs[i]));
995 }
996 }
997 }
998
999 /**
1000 * Check if we can augment the version from the exporter.
1001 *
1002 * We allow the version in the import to specify a @ which is replaced with
1003 * the exporter's version.
1004 *
1005 * @param currentAttributes
1006 * @param exporter
1007 */
1008 private void augmentVersion(Map<String, String> currentAttributes,
1009 Map<String, String> exporter) {
1010
1011 String exportVersion = (String) exporter.get("version");
1012 if (exportVersion == null)
1013 exportVersion = (String) exporter.get("specification-version");
1014 if (exportVersion == null)
1015 return;
1016
1017 exportVersion = cleanupVersion(exportVersion);
1018
1019 setProperty("@", exportVersion);
1020
1021 String importRange = currentAttributes.get("version");
1022 if (importRange != null) {
1023 importRange = cleanupVersion(importRange);
1024 importRange = getReplacer().process(importRange);
1025 } else
1026 importRange = getVersionPolicy();
1027
1028 unsetProperty("@");
1029
1030 // See if we can borrow the version
1031 // we mist replace the ${@} with the version we
1032 // found this can be useful if you want a range to start
1033 // with the found version.
1034 currentAttributes.put("version", importRange);
1035 }
1036
1037 /**
1038 * Add the uses clauses
1039 *
1040 * @param exports
1041 * @param uses
1042 * @throws MojoExecutionException
1043 */
1044 void doUses(Map<String, Map<String, String>> exports,
1045 Map<String, Set<String>> uses,
1046 Map<String, Map<String, String>> imports) {
1047 if ("true".equalsIgnoreCase(getProperty(NOUSES)))
1048 return;
1049
1050 for (Iterator<String> i = exports.keySet().iterator(); i.hasNext();) {
1051 String packageName = i.next();
1052 setProperty(CURRENT_PACKAGE, packageName);
1053 try {
1054 Map<String, String> clause = exports.get(packageName);
1055 String override = clause.get(USES_DIRECTIVE);
1056 if (override == null)
1057 override = USES_USES;
1058
1059 Set<String> usedPackages = uses.get(packageName);
1060 if (usedPackages != null) {
1061 // Only do a uses on exported or imported packages
1062 // and uses should also not contain our own package
1063 // name
1064 Set<String> sharedPackages = new HashSet<String>();
1065 sharedPackages.addAll(imports.keySet());
1066 sharedPackages.addAll(exports.keySet());
1067 usedPackages.retainAll(sharedPackages);
1068 usedPackages.remove(packageName);
1069
1070 StringBuffer sb = new StringBuffer();
1071 String del = "";
1072 for (Iterator<String> u = usedPackages.iterator(); u
1073 .hasNext();) {
1074 String usedPackage = u.next();
1075 if (!usedPackage.startsWith("java.")) {
1076 sb.append(del);
1077 sb.append(usedPackage);
1078 del = ",";
1079 }
1080 }
1081 if (override.indexOf('$') >= 0) {
1082 setProperty(CURRENT_USES, sb.toString());
1083 override = getReplacer().process(override);
1084 unsetProperty(CURRENT_USES);
1085 } else
1086 // This is for backward compatibility 0.0.287
1087 // can be deprecated over time
1088 override = override
1089 .replaceAll(USES_USES, sb.toString()).trim();
1090 if (override.endsWith(","))
1091 override = override.substring(0, override.length() - 1);
1092 if (override.startsWith(","))
1093 override = override.substring(1);
1094 if (override.length() > 0) {
1095 clause.put("uses:", override);
1096 }
1097 }
1098 } finally {
1099 unsetProperty(CURRENT_PACKAGE);
1100 }
1101 }
1102 }
1103
1104 /**
1105 * Transitively remove all elemens from unreachable through the uses link.
1106 *
1107 * @param name
1108 * @param unreachable
1109 */
1110 void removeTransitive(String name, Set<String> unreachable) {
1111 if (!unreachable.contains(name))
1112 return;
1113
1114 unreachable.remove(name);
1115
1116 Set<String> ref = uses.get(name);
1117 if (ref != null) {
1118 for (Iterator<String> r = ref.iterator(); r.hasNext();) {
1119 String element = (String) r.next();
1120 removeTransitive(element, unreachable);
1121 }
1122 }
1123 }
1124
1125 /**
1126 * Helper method to set the package info
1127 *
1128 * @param dir
1129 * @param key
1130 * @param value
1131 */
1132 void setPackageInfo(String dir, String key, String value) {
1133 if (value != null) {
1134 String pack = dir.replace('/', '.');
1135 Map<String, String> map = classpathExports.get(pack);
1136 if (map == null) {
1137 map = new HashMap<String, String>();
1138 classpathExports.put(pack, map);
1139 }
1140 map.put(key, value);
1141 }
1142 }
1143
1144 public void close() {
1145 super.close();
1146 if (dot != null)
1147 dot.close();
1148
1149 if (classpath != null)
1150 for (Iterator<Jar> j = classpath.iterator(); j.hasNext();) {
1151 Jar jar = j.next();
1152 jar.close();
1153 }
1154 }
1155
1156 /**
1157 * Findpath looks through the contents of the JAR and finds paths that end
1158 * with the given regular expression
1159 *
1160 * ${findpath (; reg-expr (; replacement)? )? }
1161 *
1162 * @param args
1163 * @return
1164 */
1165 public String _findpath(String args[]) {
1166 return findPath("findpath", args, true);
1167 }
1168
1169 public String _findname(String args[]) {
1170 return findPath("findname", args, false);
1171 }
1172
1173 String findPath(String name, String[] args, boolean fullPathName) {
1174 if (args.length > 3) {
1175 warning("Invalid nr of arguments to " + name + " "
1176 + Arrays.asList(args) + ", syntax: ${" + name
1177 + " (; reg-expr (; replacement)? )? }");
1178 return null;
1179 }
1180
1181 String regexp = ".*";
1182 String replace = null;
1183
1184 switch (args.length) {
1185 case 3:
1186 replace = args[2];
1187 case 2:
1188 regexp = args[1];
1189 }
1190 StringBuffer sb = new StringBuffer();
1191 String del = "";
1192
1193 Pattern expr = Pattern.compile(regexp);
1194 for (Iterator<String> e = dot.getResources().keySet().iterator(); e
1195 .hasNext();) {
1196 String path = e.next();
1197 if (!fullPathName) {
1198 int n = path.lastIndexOf('/');
1199 if (n >= 0) {
1200 path = path.substring(n + 1);
1201 }
1202 }
1203
1204 Matcher m = expr.matcher(path);
1205 if (m.matches()) {
1206 if (replace != null)
1207 path = m.replaceAll(replace);
1208
1209 sb.append(del);
1210 sb.append(path);
1211 del = ", ";
1212 }
1213 }
1214 return sb.toString();
1215 }
1216
1217 public void putAll(Map<String, String> additional, boolean force) {
1218 for (Iterator<Map.Entry<String, String>> i = additional.entrySet()
1219 .iterator(); i.hasNext();) {
1220 Map.Entry<String, String> entry = i.next();
1221 if (force || getProperties().get(entry.getKey()) == null)
1222 setProperty((String) entry.getKey(), (String) entry.getValue());
1223 }
1224 }
1225
1226 boolean firstUse = true;
1227
1228 public List<Jar> getClasspath() {
1229 if (firstUse) {
1230 firstUse = false;
1231 String cp = getProperty(CLASSPATH);
1232 if (cp != null)
1233 for (String s : split(cp)) {
1234 Jar jar = getJarFromName(s, "getting classpath");
1235 if (jar != null)
1236 addClasspath(jar);
1237 }
1238 }
1239 return classpath;
1240 }
1241
1242 public void addClasspath(Jar jar) {
1243 if (isPedantic() && jar.getResources().isEmpty())
1244 warning("There is an empty jar or directory on the classpath: "
1245 + jar.getName());
1246
1247 classpath.add(jar);
1248 }
1249
1250 public void addClasspath(File cp) throws IOException {
1251 if (!cp.exists())
1252 warning("File on classpath that does not exist: " + cp);
1253 Jar jar = new Jar(cp);
1254 addClose(jar);
1255 classpath.add(jar);
1256 }
1257
1258 public void clear() {
1259 classpath.clear();
1260 }
1261
1262 public Jar getTarget() {
1263 return dot;
1264 }
1265
1266 public Map<String, Clazz> analyzeBundleClasspath(Jar dot,
1267 Map<String, Map<String, String>> bundleClasspath,
1268 Map<String, Map<String, String>> contained,
1269 Map<String, Map<String, String>> referred,
1270 Map<String, Set<String>> uses) throws IOException {
1271 Map<String, Clazz> classSpace = new HashMap<String, Clazz>();
1272
1273 if (bundleClasspath.isEmpty()) {
1274 analyzeJar(dot, "", classSpace, contained, referred, uses);
1275 } else {
1276 for (String path : bundleClasspath.keySet()) {
1277 if (path.equals(".")) {
1278 analyzeJar(dot, "", classSpace, contained, referred, uses);
1279 continue;
1280 }
1281 //
1282 // There are 3 cases:
1283 // - embedded JAR file
1284 // - directory
1285 // - error
1286 //
1287
1288 Resource resource = dot.getResource(path);
1289 if (resource != null) {
1290 try {
1291 Jar jar = new Jar(path);
1292 addClose(jar);
1293 EmbeddedResource.build(jar, resource);
1294 analyzeJar(jar, "", classSpace, contained, referred,
1295 uses);
1296 } catch (Exception e) {
1297 warning("Invalid bundle classpath entry: " + path + " "
1298 + e);
1299 }
1300 } else {
1301 if (dot.getDirectories().containsKey(path)) {
1302 analyzeJar(dot, path + '/', classSpace, contained,
1303 referred, uses);
1304 } else {
1305 warning("No sub JAR or directory " + path);
1306 }
1307 }
1308 }
1309 }
1310 return classSpace;
1311 }
1312
1313 /**
1314 * We traverse through all the classes that we can find and calculate the
1315 * contained and referred set and uses. This method ignores the Bundle
1316 * classpath.
1317 *
1318 * @param jar
1319 * @param contained
1320 * @param referred
1321 * @param uses
1322 * @throws IOException
1323 */
1324 private void analyzeJar(Jar jar, String prefix,
1325 Map<String, Clazz> classSpace,
1326 Map<String, Map<String, String>> contained,
1327 Map<String, Map<String, String>> referred,
1328 Map<String, Set<String>> uses) throws IOException {
1329
1330 next: for (String path : jar.getResources().keySet()) {
1331 if (path.startsWith(prefix)) {
1332 String relativePath = path.substring(prefix.length());
1333 String pack = getPackage(relativePath);
1334
1335 if (pack != null && !contained.containsKey(pack)) {
1336 if (!isMetaData(relativePath)) {
1337
1338 Map<String, String> map = new LinkedHashMap<String, String>();
1339 contained.put(pack, map);
1340 Resource pinfo = jar.getResource(prefix
1341 + pack.replace('.', '/') + "/packageinfo");
1342 if (pinfo != null) {
1343 InputStream in = pinfo.openInputStream();
1344 String version = parsePackageInfo(in);
1345 in.close();
1346 if (version != null)
1347 map.put("version", version);
1348 }
1349 }
1350 }
1351
1352 if (path.endsWith(".class")) {
1353 Resource resource = jar.getResource(path);
1354 Clazz clazz;
1355
1356 try {
1357 InputStream in = resource.openInputStream();
1358 clazz = new Clazz(relativePath, in);
1359 in.close();
1360 } catch (Throwable e) {
1361 error("Invalid class file: " + relativePath, e);
1362 e.printStackTrace();
1363 continue next;
1364 }
1365
1366 String calculatedPath = clazz.getClassName() + ".class";
1367 if (!calculatedPath.equals(relativePath))
1368 error("Class in different directory than declared. Path from class name is "
1369 + calculatedPath
1370 + " but the path in the jar is "
1371 + relativePath
1372 + " from " + jar);
1373
1374 classSpace.put(relativePath, clazz);
1375 referred.putAll(clazz.getReferred());
1376
1377 // Add all the used packages
1378 // to this package
1379 Set<String> t = uses.get(pack);
1380 if (t == null)
1381 uses.put(pack, t = new LinkedHashSet<String>());
1382 t.addAll(clazz.getReferred().keySet());
1383 t.remove(pack);
1384 }
1385 }
1386 }
1387 }
1388
1389 /**
1390 * Clean up version parameters. Other builders use more fuzzy definitions of
1391 * the version syntax. This method cleans up such a version to match an OSGi
1392 * version.
1393 *
1394 * @param VERSION_STRING
1395 * @return
1396 */
1397 static Pattern fuzzyVersion = Pattern
1398 .compile(
1399 "(\\d+)(\\.(\\d+)(\\.(\\d+))?)?([^a-zA-Z0-9](.*))?",
1400 Pattern.DOTALL);
1401 static Pattern fuzzyVersionRange = Pattern
1402 .compile(
1403 "(\\(|\\[)\\s*([-\\da-zA-Z.]+)\\s*,\\s*([-\\da-zA-Z.]+)\\s*(\\]|\\))",
1404 Pattern.DOTALL);
1405 static Pattern fuzzyModifier = Pattern.compile("(\\d+[.-])*(.*)",
1406 Pattern.DOTALL);
1407
1408 static Pattern nummeric = Pattern.compile("\\d*");
1409
1410 static public String cleanupVersion(String version) {
1411 if (Verifier.VERSIONRANGE.matcher(version).matches())
1412 return version;
1413
1414 Matcher m = fuzzyVersionRange.matcher(version);
1415 if (m.matches()) {
1416 String prefix = m.group(1);
1417 String first = m.group(2);
1418 String last = m.group(3);
1419 String suffix = m.group(4);
1420 return prefix + cleanupVersion(first) + "," + cleanupVersion(last)
1421 + suffix;
1422 } else {
1423 m = fuzzyVersion.matcher(version);
1424 if (m.matches()) {
1425 StringBuffer result = new StringBuffer();
1426 String major = m.group(1);
1427 String minor = m.group(3);
1428 String micro = m.group(5);
1429 String qualifier = m.group(7);
1430
1431 if (major != null) {
1432 result.append(major);
1433 if (minor != null) {
1434 result.append(".");
1435 result.append(minor);
1436 if (micro != null) {
1437 result.append(".");
1438 result.append(micro);
1439 if (qualifier != null) {
1440 result.append(".");
1441 cleanupModifier(result, qualifier);
1442 }
1443 } else if (qualifier != null) {
1444 result.append(".0.");
1445 cleanupModifier(result, qualifier);
1446 }
1447 } else if (qualifier != null) {
1448 result.append(".0.0.");
1449 cleanupModifier(result, qualifier);
1450 }
1451 return result.toString();
1452 }
1453 }
1454 }
1455 return version;
1456 }
1457
1458 static void cleanupModifier(StringBuffer result, String modifier) {
1459 Matcher m = fuzzyModifier.matcher(modifier);
1460 if (m.matches())
1461 modifier = m.group(2);
1462
1463 for (int i = 0; i < modifier.length(); i++) {
1464 char c = modifier.charAt(i);
1465 if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')
1466 || (c >= 'A' && c <= 'Z') || c == '_' || c == '-')
1467 result.append(c);
1468 }
1469 }
1470
1471 /**
1472 * Decide if the package is a metadata package.
1473 *
1474 * @param pack
1475 * @return
1476 */
1477 boolean isMetaData(String pack) {
1478 for (int i = 0; i < METAPACKAGES.length; i++) {
1479 if (pack.startsWith(METAPACKAGES[i]))
1480 return true;
1481 }
1482 return false;
1483 }
1484
1485 public String getPackage(String clazz) {
1486 int n = clazz.lastIndexOf('/');
1487 if (n < 0)
1488 return ".";
1489 return clazz.substring(0, n).replace('/', '.');
1490 }
1491
1492 //
1493 // We accept more than correct OSGi versions because in a later
1494 // phase we actually cleanup maven versions. But it is a bit yucky
1495 //
1496 static String parsePackageInfo(InputStream jar) throws IOException {
1497 try {
1498 Properties p = new Properties();
1499 p.load(jar);
1500 jar.close();
1501 if (p.containsKey("version")) {
1502 return p.getProperty("version");
1503 }
1504 } catch (Exception e) {
1505 e.printStackTrace();
1506 }
1507 return null;
1508 }
1509
1510 public String getVersionPolicy() {
1511 return getProperty(VERSIONPOLICY, "${version;==;${@}}");
1512 }
1513
1514 /**
1515 * The extends macro traverses all classes and returns a list of class names
1516 * that extend a base class.
1517 */
1518
1519 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";
1520
1521 public String _classes(String args[]) {
1522 // Macro.verifyCommand(args, _classesHelp, new
1523 // Pattern[]{null,Pattern.compile("(implementing|implements|extending|extends|importing|imports|any)"),
1524 // null}, 3,3);
1525 Set<Clazz> matched = new HashSet<Clazz>(classspace.values());
1526 for (int i = 1; i < args.length; i += 2) {
1527 if (args.length < i + 1)
1528 throw new IllegalArgumentException(
1529 "${classes} macro must have odd number of arguments. "
1530 + _classesHelp);
1531
1532 String typeName = args[i];
1533 Clazz.QUERY type = null;
1534 if (typeName.equals("implementing")
1535 || typeName.equals("implements"))
1536 type = Clazz.QUERY.IMPLEMENTS;
1537 else if (typeName.equals("extending") || typeName.equals("extends"))
1538 type = Clazz.QUERY.EXTENDS;
1539 else if (typeName.equals("importing") || typeName.equals("imports"))
1540 type = Clazz.QUERY.IMPORTS;
1541 else if (typeName.equals("all"))
1542 type = Clazz.QUERY.ANY;
1543 else if (typeName.equals("version"))
1544 type = Clazz.QUERY.VERSION;
1545 else if (typeName.equals("named"))
1546 type = Clazz.QUERY.NAMED;
1547
1548 if (type == null)
1549 throw new IllegalArgumentException(
1550 "${classes} has invalid type: " + typeName + ". "
1551 + _classesHelp);
1552 // The argument is declared as a dotted name but the classes
1553 // use a slashed named. So convert the name before we make it a
1554 // instruction.
1555 String pattern = args[i + 1].replace('.', '/');
1556 Instruction instr = Instruction.getPattern(pattern);
1557
1558 for (Iterator<Clazz> c = matched.iterator(); c.hasNext();) {
1559 Clazz clazz = c.next();
1560 if (!clazz.is(type, instr, classspace))
1561 c.remove();
1562 }
1563 }
1564 if (matched.isEmpty())
1565 return "";
1566
1567 return join(matched);
1568 }
1569
1570 /**
1571 * Get the exporter of a package ...
1572 */
1573
1574 public String _exporters(String args[]) throws Exception {
1575 Macro
1576 .verifyCommand(
1577 args,
1578 "${exporters;<packagename>}, returns the list of jars that export the given package",
1579 null, 2, 2);
1580 StringBuilder sb = new StringBuilder();
1581 String del = "";
1582 String pack = args[1].replace('.', '/');
1583 for (Jar jar : classpath) {
1584 if (jar.getDirectories().containsKey(pack)) {
1585 sb.append(del);
1586 sb.append(jar.getName());
1587 }
1588 }
1589 return sb.toString();
1590 }
1591
1592 public Map<String, Clazz> getClassspace() {
1593 return classspace;
1594 }
1595
1596}