You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by mc...@apache.org on 2009/07/13 12:06:50 UTC

svn commit: r793527 [3/7] - in /felix/trunk/bundleplugin: ./ src/main/java/aQute/ src/main/java/aQute/bnd/ src/main/java/aQute/bnd/build/ src/main/java/aQute/bnd/help/ src/main/java/aQute/bnd/make/ src/main/java/aQute/bnd/service/ src/main/java/aQute/b...

Added: felix/trunk/bundleplugin/src/main/java/aQute/lib/osgi/Analyzer.java
URL: http://svn.apache.org/viewvc/felix/trunk/bundleplugin/src/main/java/aQute/lib/osgi/Analyzer.java?rev=793527&view=auto
==============================================================================
--- felix/trunk/bundleplugin/src/main/java/aQute/lib/osgi/Analyzer.java (added)
+++ felix/trunk/bundleplugin/src/main/java/aQute/lib/osgi/Analyzer.java Mon Jul 13 10:06:47 2009
@@ -0,0 +1,1596 @@
+/* Copyright 2006 aQute SARL 
+ * Licensed under the Apache License, Version 2.0, see http://www.apache.org/licenses/LICENSE-2.0 */
+package aQute.lib.osgi;
+
+/**
+ * This class can calculate the required headers for a (potential) JAR file. It
+ * analyzes a directory or JAR for the packages that are contained and that are
+ * referred to by the bytecodes. The user can the use regular expressions to
+ * define the attributes and directives. The matching is not fully regex for
+ * convenience. A * and ? get a . prefixed and dots are escaped.
+ * 
+ * <pre>
+ *                                                             			*;auto=true				any		
+ *                                                             			org.acme.*;auto=true    org.acme.xyz
+ *                                                             			org.[abc]*;auto=true    org.acme.xyz
+ * </pre>
+ * 
+ * Additional, the package instruction can start with a '=' or a '!'. The '!'
+ * indicates negation. Any matching package is removed. The '=' is literal, the
+ * expression will be copied verbatim and no matching will take place.
+ * 
+ * Any headers in the given properties are used in the output properties.
+ */
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.jar.*;
+import java.util.jar.Attributes.*;
+import java.util.regex.*;
+
+import aQute.bnd.service.*;
+import aQute.lib.filter.*;
+
+public class Analyzer extends Processor {
+
+    static Pattern                         doNotCopy      = Pattern
+                                                                  .compile("CVS|.svn");
+    static String                          version;
+    static Pattern                         versionPattern = Pattern
+                                                                  .compile("(\\d+\\.\\d+)\\.\\d+.*");
+    final Map<String, Map<String, String>> contained      = newHashMap();                            // package
+    final Map<String, Map<String, String>> referred       = newHashMap();                            // package
+    final Map<String, Set<String>>         uses           = newHashMap();                            // package
+    Map<String, Clazz>                     classspace;
+    Map<String, Map<String, String>>       exports;
+    Map<String, Map<String, String>>       imports;
+    Map<String, Map<String, String>>       bundleClasspath;                                          // Bundle
+    final Map<String, Map<String, String>> ignored        = newHashMap();                            // Ignored
+    // packages
+    Jar                                    dot;
+    Map<String, Map<String, String>>       classpathExports;
+
+    String                                 activator;
+
+    final List<Jar>                        classpath      = newList();
+
+    static Properties                      bndInfo;
+
+    boolean                                analyzed;
+    String                                 bsn;
+
+    public Analyzer(Processor parent) {
+        super(parent);
+    }
+
+    public Analyzer() {
+    }
+
+    /**
+     * Specifically for Maven
+     * 
+     * @param properties
+     *            the properties
+     */
+
+    public static Properties getManifest(File dirOrJar) throws IOException {
+        Analyzer analyzer = new Analyzer();
+        analyzer.setJar(dirOrJar);
+        Properties properties = new Properties();
+        properties.put(IMPORT_PACKAGE, "*");
+        properties.put(EXPORT_PACKAGE, "*");
+        analyzer.setProperties(properties);
+        Manifest m = analyzer.calcManifest();
+        Properties result = new Properties();
+        for (Iterator<Object> i = m.getMainAttributes().keySet().iterator(); i
+                .hasNext();) {
+            Attributes.Name name = (Attributes.Name) i.next();
+            result.put(name.toString(), m.getMainAttributes().getValue(name));
+        }
+        return result;
+    }
+
+    /**
+     * Calcualtes the data structures for generating a manifest.
+     * 
+     * @throws IOException
+     */
+    public void analyze() throws IOException {
+        if (!analyzed) {
+            analyzed = true;
+            classpathExports = newHashMap();
+            activator = getProperty(BUNDLE_ACTIVATOR);
+            bundleClasspath = parseHeader(getProperty(BUNDLE_CLASSPATH));
+
+            analyzeClasspath();
+
+            classspace = analyzeBundleClasspath(dot, bundleClasspath,
+                    contained, referred, uses);
+
+            for (AnalyzerPlugin plugin : getPlugins(AnalyzerPlugin.class)) {
+                if (plugin instanceof AnalyzerPlugin) {
+                    AnalyzerPlugin analyzer = (AnalyzerPlugin) plugin;
+                    try {
+                        boolean reanalyze = analyzer.analyzeJar(this);
+                        if (reanalyze)
+                            classspace = analyzeBundleClasspath(dot,
+                                    bundleClasspath, contained, referred, uses);
+                    } catch (Exception e) {
+                        error("Plugin Analyzer " + analyzer
+                                + " throws exception " + e);
+                        e.printStackTrace();
+                    }
+                }
+            }
+
+            if (activator != null) {
+                // Add the package of the activator to the set
+                // of referred classes. This must be done before we remove
+                // contained set.
+                int n = activator.lastIndexOf('.');
+                if (n > 0) {
+                    referred.put(activator.substring(0, n),
+                            new LinkedHashMap<String, String>());
+                }
+            }
+
+            referred.keySet().removeAll(contained.keySet());
+            if (referred.containsKey(".")) {
+                error("The default package '.' is not permitted by the Import-Package syntax. \n"
+                        + " This can be caused by compile errors in Eclipse because Eclipse creates \n"
+                        + "valid class files regardless of compile errors.\n"
+                        + "The following package(s) import from the default package "
+                        + getUsedBy("."));
+            }
+
+            Map<String, Map<String, String>> exportInstructions = parseHeader(getProperty(EXPORT_PACKAGE));
+            Map<String, Map<String, String>> additionalExportInstructions = parseHeader(getProperty(EXPORT_CONTENTS));
+            exportInstructions.putAll(additionalExportInstructions);
+            Map<String, Map<String, String>> importInstructions = parseHeader(getImportPackages());
+            Map<String, Map<String, String>> dynamicImports = parseHeader(getProperty(DYNAMICIMPORT_PACKAGE));
+
+            if (dynamicImports != null) {
+                // Remove any dynamic imports from the referred set.
+                referred.keySet().removeAll(dynamicImports.keySet());
+            }
+
+            Map<String, Map<String, String>> superfluous = newHashMap();
+            // Tricky!
+            for (Iterator<String> i = exportInstructions.keySet().iterator(); i
+                    .hasNext();) {
+                String instr = i.next();
+                if (!instr.startsWith("!"))
+                    superfluous.put(instr, exportInstructions.get(instr));
+            }
+
+            exports = merge("export-package", exportInstructions, contained,
+                    superfluous.keySet(), null);
+
+            // disallow export of default package
+            exports.remove(".");
+
+            for (Iterator<Map.Entry<String, Map<String, String>>> i = superfluous
+                    .entrySet().iterator(); i.hasNext();) {
+                // It is possible to mention metadata directories in the export
+                // explicitly, they are then exported and removed from the
+                // warnings. Note that normally metadata directories are not
+                // exported.
+                Map.Entry<String, Map<String, String>> entry = i.next();
+                String pack = entry.getKey();
+                if (isDuplicate(pack))
+                    i.remove();
+                else if (isMetaData(pack)) {
+                    exports.put(pack, entry.getValue());
+                    i.remove();
+                }
+            }
+
+            if (!superfluous.isEmpty()) {
+                warning("Superfluous export-package instructions: "
+                        + superfluous.keySet());
+            }
+
+            // Add all exports that do not have an -noimport: directive
+            // to the imports.
+            Map<String, Map<String, String>> referredAndExported = newMap(referred);
+            referredAndExported.putAll(addExportsToImports(exports));
+
+            // match the imports to the referred and exported packages,
+            // merge the info for matching packages
+            Set<String> extra = new TreeSet<String>(importInstructions.keySet());
+            imports = merge("import-package", importInstructions,
+                    referredAndExported, extra, ignored);
+
+            // Instructions that have not been used could be superfluous
+            // or if they do not contain wildcards, should be added
+            // as extra imports, the user knows best.
+            for (Iterator<String> i = extra.iterator(); i.hasNext();) {
+                String p = i.next();
+                if (p.startsWith("!") || p.indexOf('*') >= 0
+                        || p.indexOf('?') >= 0 || p.indexOf('[') >= 0) {
+                    if (!isResourceOnly())
+                        warning("Did not find matching referal for " + p);
+                } else {
+                    Map<String, String> map = importInstructions.get(p);
+                    imports.put(p, map);
+                }
+            }
+
+            // See what information we can find to augment the
+            // imports. I.e. look on the classpath
+            augmentImports();
+
+            // Add the uses clause to the exports
+            doUses(exports, uses, imports);
+        }
+    }
+
+    /**
+     * Copy the input collection into an output set but skip names that have
+     * been marked as duplicates or are optional.
+     * 
+     * @param superfluous
+     * @return
+     */
+    Set<Instruction> removeMarkedDuplicates(Collection<Instruction> superfluous) {
+        Set<Instruction> result = new HashSet<Instruction>();
+        for (Iterator<Instruction> i = superfluous.iterator(); i.hasNext();) {
+            Instruction instr = (Instruction) i.next();
+            if (!isDuplicate(instr.getPattern()) && !instr.isOptional())
+                result.add(instr);
+        }
+        return result;
+    }
+
+    /**
+     * Analyzer has an empty default but the builder has a * as default.
+     * 
+     * @return
+     */
+    protected String getImportPackages() {
+        return getProperty(IMPORT_PACKAGE);
+    }
+
+    /**
+     * 
+     * @return
+     */
+    boolean isResourceOnly() {
+        return getProperty(RESOURCEONLY, "false").equalsIgnoreCase("true");
+    }
+
+    /**
+     * Answer the list of packages that use the given package.
+     */
+    Set<String> getUsedBy(String pack) {
+        Set<String> set = newSet();
+        for (Iterator<Map.Entry<String, Set<String>>> i = uses.entrySet()
+                .iterator(); i.hasNext();) {
+            Map.Entry<String, Set<String>> entry = i.next();
+            Set<String> used = entry.getValue();
+            if (used.contains(pack))
+                set.add(entry.getKey());
+        }
+        return set;
+    }
+
+    /**
+     * One of the main workhorses of this class. This will analyze the current
+     * setp and calculate a new manifest according to this setup. This method
+     * will also set the manifest on the main jar dot
+     * 
+     * @return
+     * @throws IOException
+     */
+    public Manifest calcManifest() throws IOException {
+        analyze();
+        Manifest manifest = new Manifest();
+        Attributes main = manifest.getMainAttributes();
+
+        main.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+        main.putValue(BUNDLE_MANIFESTVERSION, "2");
+
+        boolean noExtraHeaders = "true"
+                .equalsIgnoreCase(getProperty(NOEXTRAHEADERS));
+
+        if (!noExtraHeaders) {
+            main.putValue(CREATED_BY, System.getProperty("java.version") + " ("
+                    + System.getProperty("java.vendor") + ")");
+            main.putValue(TOOL, "Bnd-" + getVersion());
+            main.putValue(BND_LASTMODIFIED, "" + System.currentTimeMillis());
+        }
+        String exportHeader = printClauses(exports,
+                "uses:|include:|exclude:|mandatory:|" + IMPORT_DIRECTIVE, true);
+
+        if (exportHeader.length() > 0)
+            main.putValue(EXPORT_PACKAGE, exportHeader);
+        else
+            main.remove(EXPORT_PACKAGE);
+
+        Map<String, Map<String, String>> temp = removeKeys(imports, "java.");
+        if (!temp.isEmpty()) {
+            main.putValue(IMPORT_PACKAGE, printClauses(temp, "resolution:"));
+        } else {
+            main.remove(IMPORT_PACKAGE);
+        }
+
+        temp = newMap(contained);
+        temp.keySet().removeAll(exports.keySet());
+
+        if (!temp.isEmpty())
+            main.putValue(PRIVATE_PACKAGE, printClauses(temp, ""));
+        else
+            main.remove(PRIVATE_PACKAGE);
+
+        if (!ignored.isEmpty()) {
+            main.putValue(IGNORE_PACKAGE, printClauses(ignored, ""));
+        } else {
+            main.remove(IGNORE_PACKAGE);
+        }
+
+        if (bundleClasspath != null && !bundleClasspath.isEmpty())
+            main.putValue(BUNDLE_CLASSPATH, printClauses(bundleClasspath, ""));
+        else
+            main.remove(BUNDLE_CLASSPATH);
+
+
+        for (Enumeration<?> h = getProperties().propertyNames(); h
+                .hasMoreElements();) {
+            String header = (String) h.nextElement();
+            if (header.trim().length() == 0) {
+                warning("Empty property set with value: "
+                        + getProperties().getProperty(header));
+                continue;
+            }
+            if (!Character.isUpperCase(header.charAt(0))) {
+                if (header.charAt(0) == '@')
+                    doNameSection(manifest, header);
+                continue;
+            }
+
+            if (header.equals(BUNDLE_CLASSPATH)
+                    || header.equals(EXPORT_PACKAGE)
+                    || header.equals(IMPORT_PACKAGE))
+                continue;
+
+            if (header.equalsIgnoreCase("Name")) {
+                error("Your bnd file contains a header called 'Name'. This interferes with the manifest name section.");
+                continue;
+            }
+
+            if (Verifier.HEADER_PATTERN.matcher(header).matches()) {
+                String value = getProperty(header);
+                if (value != null && main.getValue(header) == null) {
+                    if (value.trim().length() == 0)
+                        main.remove(header);
+                    else if (value.trim().equals("<<EMPTY>>"))
+                        main.putValue(header, "");
+                    else
+                        main.putValue(header, value);
+                }
+            } else {
+                // TODO should we report?
+            }
+        }
+
+        //
+        // Calculate the bundle symbolic name if it is
+        // not set.
+        // 1. set
+        // 2. name of properties file (must be != bnd.bnd)
+        // 3. name of directory, which is usualy project name
+        //
+        String bsn = getBsn();
+        if (main.getValue(BUNDLE_SYMBOLICNAME) == null) {
+            main.putValue(BUNDLE_SYMBOLICNAME, bsn);
+        }
+
+        //
+        // Use the same name for the bundle name as BSN when
+        // the bundle name is not set
+        //
+        if (main.getValue(BUNDLE_NAME) == null) {
+            main.putValue(BUNDLE_NAME, bsn);
+        }
+
+        if (main.getValue(BUNDLE_VERSION) == null)
+            main.putValue(BUNDLE_VERSION, "0");
+
+        // Copy old values into new manifest, when they
+        // exist in the old one, but not in the new one
+        merge(manifest, dot.getManifest());
+
+        // Remove all the headers mentioned in -removeheaders
+        Map<String, Map<String, String>> removes = parseHeader(getProperty(REMOVE_HEADERS));
+        for (Iterator<String> i = removes.keySet().iterator(); i.hasNext();) {
+            String header = i.next();
+            for (Iterator<Object> j = main.keySet().iterator(); j.hasNext();) {
+                Attributes.Name attr = (Attributes.Name) j.next();
+                if (attr.toString().matches(header)) {
+                    j.remove();
+                    progress("Removing header: " + header);
+                }
+            }
+        }
+
+        dot.setManifest(manifest);
+        return manifest;
+    }
+
+    /**
+     * This method is called when the header starts with a @, signifying a name
+     * section header. The name part is defined by replacing all the @ signs to
+     * a /, removing the first and the last, and using the last part as header
+     * name:
+     * 
+     * <pre>
+     * &#064;org@osgi@service@event@Implementation-Title
+     * </pre>
+     * 
+     * This will be the header Implementation-Title in the
+     * org/osgi/service/event named section.
+     * 
+     * @param manifest
+     * @param header
+     */
+    private void doNameSection(Manifest manifest, String header) {
+        String path = header.replace('@', '/');
+        int n = path.lastIndexOf('/');
+        // Must succeed because we start with @
+        String name = path.substring(n + 1);
+        // Skip first /
+        path = path.substring(1, n);
+        if (name.length() != 0 && path.length() != 0) {
+            Attributes attrs = manifest.getAttributes(path);
+            if (attrs == null) {
+                attrs = new Attributes();
+                manifest.getEntries().put(path, attrs);
+            }
+            attrs.putValue(name, getProperty(header));
+        } else {
+            warning(
+                    "Invalid header (starts with @ but does not seem to be for the Name section): %s",
+                    header);
+        }
+    }
+
+    /**
+     * Clear the key part of a header. I.e. remove everything from the first ';'
+     * 
+     * @param value
+     * @return
+     */
+    public String getBsn() {
+        String value = getProperty(BUNDLE_SYMBOLICNAME);
+        if (value == null) {
+            if (getPropertiesFile() != null)
+                value = getPropertiesFile().getName();
+
+            if (value == null || value.equals("bnd.bnd"))
+                value = getBase().getName();
+            else if (value.endsWith(".bnd"))
+                value = value.substring(0, value.length() - 4);
+        }
+
+        if (value == null)
+            return "untitled";
+
+        int n = value.indexOf(';');
+        if (n > 0)
+            value = value.substring(0, n);
+        return value.trim();
+    }
+
+    public String _bsn(String args[]) {
+        return getBsn();
+    }
+
+    /**
+     * Calculate an export header solely based on the contents of a JAR file
+     * 
+     * @param bundle
+     *            The jar file to analyze
+     * @return
+     */
+    public String calculateExportsFromContents(Jar bundle) {
+        String ddel = "";
+        StringBuffer sb = new StringBuffer();
+        Map<String, Map<String, Resource>> map = bundle.getDirectories();
+        for (Iterator<String> i = map.keySet().iterator(); i.hasNext();) {
+            String directory = (String) i.next();
+            if (directory.equals("META-INF")
+                    || directory.startsWith("META-INF/"))
+                continue;
+            if (directory.equals("OSGI-OPT")
+                    || directory.startsWith("OSGI-OPT/"))
+                continue;
+            if (directory.equals("/"))
+                continue;
+
+            if (directory.endsWith("/"))
+                directory = directory.substring(0, directory.length() - 1);
+
+            directory = directory.replace('/', '.');
+            sb.append(ddel);
+            sb.append(directory);
+            ddel = ",";
+        }
+        return sb.toString();
+    }
+
+
+    public Map<String, Map<String, String>> getBundleClasspath() {
+        return bundleClasspath;
+    }
+
+    public Map<String, Map<String, String>> getContained() {
+        return contained;
+    }
+
+    public Map<String, Map<String, String>> getExports() {
+        return exports;
+    }
+
+    public Map<String, Map<String, String>> getImports() {
+        return imports;
+    }
+
+    public Jar getJar() {
+        return dot;
+    }
+
+    public Map<String, Map<String, String>> getReferred() {
+        return referred;
+    }
+
+    /**
+     * Return the set of unreachable code depending on exports and the bundle
+     * activator.
+     * 
+     * @return
+     */
+    public Set<String> getUnreachable() {
+        Set<String> unreachable = new HashSet<String>(uses.keySet()); // all
+        for (Iterator<String> r = exports.keySet().iterator(); r.hasNext();) {
+            String packageName = r.next();
+            removeTransitive(packageName, unreachable);
+        }
+        if (activator != null) {
+            String pack = activator.substring(0, activator.lastIndexOf('.'));
+            removeTransitive(pack, unreachable);
+        }
+        return unreachable;
+    }
+
+    public Map<String, Set<String>> getUses() {
+        return uses;
+    }
+
+    /**
+     * Get the version from the manifest, a lot of work!
+     * 
+     * @return version or unknown.
+     */
+    public String getVersion() {
+        return getBndInfo("version", "<unknown version>");
+    }
+
+    public long getBndLastModified() {
+        String time = getBndInfo("modified", "0");
+        try {
+            return Long.parseLong(time);
+        } catch (Exception e) {
+        }
+        return 0;
+    }
+
+    public String getBndInfo(String key, String defaultValue) {
+        if (bndInfo == null) {
+            bndInfo = new Properties();
+            try {
+                InputStream in = Analyzer.class.getResourceAsStream("bnd.info");
+                if (in != null) {
+                    bndInfo.load(in);
+                    in.close();
+                }
+            } catch (IOException ioe) {
+                warning("Could not read bnd.info in "
+                        + Analyzer.class.getPackage() + ioe);
+            }
+        }
+        return bndInfo.getProperty(key, defaultValue);
+    }
+
+    /**
+     * Merge the existing manifest with the instructions.
+     * 
+     * @param manifest
+     *            The manifest to merge with
+     * @throws IOException
+     */
+    public void mergeManifest(Manifest manifest) throws IOException {
+        if (manifest != null) {
+            Attributes attributes = manifest.getMainAttributes();
+            for (Iterator<Object> i = attributes.keySet().iterator(); i
+                    .hasNext();) {
+                Name name = (Name) i.next();
+                String key = name.toString();
+                // Dont want instructions
+                if (key.startsWith("-"))
+                    continue;
+
+                if (getProperty(key) == null)
+                    setProperty(key, (String) attributes.get(name));
+            }
+        }
+    }
+
+    public void setBase(File file) {
+        super.setBase(file);
+        getProperties().put("project.dir", getBase().getAbsolutePath());
+    }
+
+    /**
+     * Set the classpath for this analyzer by file.
+     * 
+     * @param classpath
+     * @throws IOException
+     */
+    public void setClasspath(File[] classpath) throws IOException {
+        List<Jar> list = new ArrayList<Jar>();
+        for (int i = 0; i < classpath.length; i++) {
+            if (classpath[i].exists()) {
+                Jar current = new Jar(classpath[i]);
+                list.add(current);
+            } else {
+                error("Missing file on classpath: " + classpath[i]);
+            }
+        }
+        for (Iterator<Jar> i = list.iterator(); i.hasNext();) {
+            addClasspath(i.next());
+        }
+    }
+
+    public void setClasspath(Jar[] classpath) {
+        for (int i = 0; i < classpath.length; i++) {
+            addClasspath(classpath[i]);
+        }
+    }
+
+    public void setClasspath(String[] classpath) {
+        for (int i = 0; i < classpath.length; i++) {
+            Jar jar = getJarFromName(classpath[i], " setting classpath");
+            if (jar != null)
+                addClasspath(jar);
+        }
+    }
+
+    /**
+     * Set the JAR file we are going to work in. This will read the JAR in
+     * memory.
+     * 
+     * @param jar
+     * @return
+     * @throws IOException
+     */
+    public Jar setJar(File jar) throws IOException {
+        Jar jarx = new Jar(jar);
+        addClose(jarx);
+        return setJar(jarx);
+    }
+
+    /**
+     * Set the JAR directly we are going to work on.
+     * 
+     * @param jar
+     * @return
+     */
+    public Jar setJar(Jar jar) {
+        this.dot = jar;
+        return jar;
+    }
+
+    protected void begin() {
+        super.begin();
+
+        updateModified(getBndLastModified(), "bnd last modified");
+        String doNotCopy = getProperty(DONOTCOPY);
+        if (doNotCopy != null)
+            Analyzer.doNotCopy = Pattern.compile(doNotCopy);
+
+        verifyManifestHeadersCase(getProperties());
+    }
+
+    /**
+     * Check if the given class or interface name is contained in the jar.
+     * 
+     * @param interfaceName
+     * @return
+     */
+    public boolean checkClass(String interfaceName) {
+        String path = interfaceName.replace('.', '/') + ".class";
+        if (classspace.containsKey(path))
+            return true;
+
+        String pack = interfaceName;
+        int n = pack.lastIndexOf('.');
+        if (n > 0)
+            pack = pack.substring(0, n);
+        else
+            pack = ".";
+
+        return imports.containsKey(pack);
+    }
+
+    /**
+     * Try to get a Jar from a file name/path or a url, or in last resort from
+     * the classpath name part of their files.
+     * 
+     * @param name
+     *            URL or filename relative to the base
+     * @param from
+     *            Message identifying the caller for errors
+     * @return null or a Jar with the contents for the name
+     */
+    Jar getJarFromName(String name, String from) {
+        File file = new File(name);
+        if (!file.isAbsolute())
+            file = new File(getBase(), name);
+
+        if (file.exists())
+            try {
+                Jar jar = new Jar(file);
+                addClose(jar);
+                return jar;
+            } catch (Exception e) {
+                error("Exception in parsing jar file for " + from + ": " + name
+                        + " " + e);
+            }
+        // It is not a file ...
+        try {
+            // Lets try a URL
+            URL url = new URL(name);
+            Jar jar = new Jar(fileName(url.getPath()));
+            addClose(jar);
+            URLConnection connection = url.openConnection();
+            InputStream in = connection.getInputStream();
+            long lastModified = connection.getLastModified();
+            if (lastModified == 0)
+                // We assume the worst :-(
+                lastModified = System.currentTimeMillis();
+            EmbeddedResource.build(jar, in, lastModified);
+            in.close();
+            return jar;
+        } catch (IOException ee) {
+            // Check if we have files on the classpath
+            // that have the right name, allows us to specify those
+            // names instead of the full path.
+            for (Iterator<Jar> cp = getClasspath().iterator(); cp.hasNext();) {
+                Jar entry = cp.next();
+                if (entry.source != null && entry.source.getName().equals(name)) {
+                    return entry;
+                }
+            }
+            // error("Can not find jar file for " + from + ": " + name);
+        }
+        return null;
+    }
+
+    private String fileName(String path) {
+        int n = path.lastIndexOf('/');
+        if (n > 0)
+            return path.substring(n + 1);
+        return path;
+    }
+
+    /**
+     * 
+     * @param manifest
+     * @throws Exception
+     */
+    void merge(Manifest result, Manifest old) throws IOException {
+        if (old != null) {
+            for (Iterator<Map.Entry<Object, Object>> e = old
+                    .getMainAttributes().entrySet().iterator(); e.hasNext();) {
+                Map.Entry<Object, Object> entry = e.next();
+                Attributes.Name name = (Attributes.Name) entry.getKey();
+                String value = (String) entry.getValue();
+                if (name.toString().equalsIgnoreCase("Created-By"))
+                    name = new Attributes.Name("Originally-Created-By");
+                if (!result.getMainAttributes().containsKey(name))
+                    result.getMainAttributes().put(name, value);
+            }
+
+            // do not overwrite existing entries
+            Map<String, Attributes> oldEntries = old.getEntries();
+            Map<String, Attributes> newEntries = result.getEntries();
+            for (Iterator<Map.Entry<String, Attributes>> e = oldEntries
+                    .entrySet().iterator(); e.hasNext();) {
+                Map.Entry<String, Attributes> entry = e.next();
+                if (!newEntries.containsKey(entry.getKey())) {
+                    newEntries.put(entry.getKey(), entry.getValue());
+                }
+            }
+        }
+    }
+
+    String stem(String name) {
+        int n = name.lastIndexOf('.');
+        if (n > 0)
+            return name.substring(0, n);
+        else
+            return name;
+    }
+
+    /**
+     * Bnd is case sensitive for the instructions so we better check people are
+     * not using an invalid case. We do allow this to set headers that should
+     * not be processed by us but should be used by the framework.
+     * 
+     * @param properties
+     *            Properties to verify.
+     */
+
+    void verifyManifestHeadersCase(Properties properties) {
+        for (Iterator<Object> i = properties.keySet().iterator(); i.hasNext();) {
+            String header = (String) i.next();
+            for (int j = 0; j < headers.length; j++) {
+                if (!headers[j].equals(header)
+                        && headers[j].equalsIgnoreCase(header)) {
+                    warning("Using a standard OSGi header with the wrong case (bnd is case sensitive!), using: "
+                            + header + " and expecting: " + headers[j]);
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * We will add all exports to the imports unless there is a -noimport
+     * directive specified on an export. This directive is skipped for the
+     * manifest.
+     * 
+     * We also remove any version parameter so that augmentImports can do the
+     * version policy.
+     * 
+     */
+    Map<String, Map<String, String>> addExportsToImports(
+            Map<String, Map<String, String>> exports) {
+        Map<String, Map<String, String>> importsFromExports = newHashMap();
+        for (Map.Entry<String, Map<String, String>> packageEntry : exports
+                .entrySet()) {
+            String packageName = packageEntry.getKey();
+            Map<String, String> parameters = packageEntry.getValue();
+            String noimport = (String) parameters.get(NO_IMPORT_DIRECTIVE);
+            if (noimport == null || !noimport.equalsIgnoreCase("true")) {
+                if (parameters.containsKey(VERSION_ATTRIBUTE)) {
+                    parameters = newMap(parameters);
+                    parameters.remove(VERSION_ATTRIBUTE);
+                }
+                importsFromExports.put(packageName, parameters);
+            }
+        }
+        return importsFromExports;
+    }
+
+    /**
+     * Create the imports/exports by parsing
+     * 
+     * @throws IOException
+     */
+    void analyzeClasspath() throws IOException {
+        classpathExports = newHashMap();
+        for (Iterator<Jar> c = getClasspath().iterator(); c.hasNext();) {
+            Jar current = c.next();
+            checkManifest(current);
+            for (Iterator<String> j = current.getDirectories().keySet()
+                    .iterator(); j.hasNext();) {
+                String dir = j.next();
+                Resource resource = current.getResource(dir + "/packageinfo");
+                if (resource != null) {
+                    InputStream in = resource.openInputStream();
+                    String version = parsePackageInfo(in);
+                    in.close();
+                    setPackageInfo(dir, "version", version);
+                }
+            }
+        }
+    }
+
+    /**
+     * 
+     * @param jar
+     */
+    void checkManifest(Jar jar) {
+        try {
+            Manifest m = jar.getManifest();
+            if (m != null) {
+                String exportHeader = m.getMainAttributes().getValue(
+                        EXPORT_PACKAGE);
+                if (exportHeader != null) {
+                    Map<String, Map<String, String>> exported = parseHeader(exportHeader);
+                    if (exported != null)
+                        classpathExports.putAll(exported);
+                }
+            }
+        } catch (Exception e) {
+            warning("Erroneous Manifest for " + jar + " " + e);
+        }
+    }
+
+    /**
+     * Find some more information about imports in manifest and other places.
+     */
+    void augmentImports() {
+        for (String packageName : imports.keySet()) {
+            setProperty(CURRENT_PACKAGE, packageName);
+            try {
+                Map<String, String> importAttributes = imports.get(packageName);
+                Map<String, String> exporterAttributes = classpathExports
+                        .get(packageName);
+                if (exporterAttributes == null)
+                    exporterAttributes = exports.get(packageName);
+
+                if (exporterAttributes != null) {
+                    augmentVersion(importAttributes, exporterAttributes);
+                    augmentMandatory(importAttributes, exporterAttributes);
+                    if (exporterAttributes.containsKey(IMPORT_DIRECTIVE))
+                        importAttributes.put(IMPORT_DIRECTIVE,
+                                exporterAttributes.get(IMPORT_DIRECTIVE));
+                }
+
+                // Convert any attribute values that have macros.
+                for (String key : importAttributes.keySet()) {
+                    String value = importAttributes.get(key);
+                    if (value.indexOf('$') >= 0) {
+                        value = getReplacer().process(value);
+                        importAttributes.put(key, value);
+                    }
+                }
+
+                // You can add a remove-attribute: directive with a regular
+                // expression for attributes that need to be removed. We also
+                // remove all attributes that have a value of !. This allows
+                // you to use macros with ${if} to remove values.
+                String remove = importAttributes
+                        .remove(REMOVE_ATTRIBUTE_DIRECTIVE);
+                Instruction removeInstr = null;
+
+                if (remove != null)
+                    removeInstr = Instruction.getPattern(remove);
+
+                for (Iterator<Map.Entry<String, String>> i = importAttributes
+                        .entrySet().iterator(); i.hasNext();) {
+                    Map.Entry<String, String> entry = i.next();
+                    if (entry.getValue().equals("!"))
+                        i.remove();
+                    else if (removeInstr != null
+                            && removeInstr.matches((String) entry.getKey()))
+                        i.remove();
+                    else {
+                        // Not removed ...
+                    }
+                }
+
+            } finally {
+                unsetProperty(CURRENT_PACKAGE);
+            }
+        }
+    }
+
+    /**
+     * If we use an import with mandatory attributes we better all use them
+     * 
+     * @param currentAttributes
+     * @param exporter
+     */
+    private void augmentMandatory(Map<String, String> currentAttributes,
+            Map<String, String> exporter) {
+        String mandatory = (String) exporter.get("mandatory:");
+        if (mandatory != null) {
+            String[] attrs = mandatory.split("\\s*,\\s*");
+            for (int i = 0; i < attrs.length; i++) {
+                if (!currentAttributes.containsKey(attrs[i]))
+                    currentAttributes.put(attrs[i], exporter.get(attrs[i]));
+            }
+        }
+    }
+
+    /**
+     * Check if we can augment the version from the exporter.
+     * 
+     * We allow the version in the import to specify a @ which is replaced with
+     * the exporter's version.
+     * 
+     * @param currentAttributes
+     * @param exporter
+     */
+    private void augmentVersion(Map<String, String> currentAttributes,
+            Map<String, String> exporter) {
+
+        String exportVersion = (String) exporter.get("version");
+        if (exportVersion == null)
+            exportVersion = (String) exporter.get("specification-version");
+        if (exportVersion == null)
+            return;
+
+        exportVersion = cleanupVersion(exportVersion);
+
+        setProperty("@", exportVersion);
+
+        String importRange = currentAttributes.get("version");
+        if (importRange != null) {
+            importRange = cleanupVersion(importRange);
+            importRange = getReplacer().process(importRange);
+        } else
+            importRange = getVersionPolicy();
+
+        unsetProperty("@");
+
+        // See if we can borrow the version
+        // we mist replace the ${@} with the version we
+        // found this can be useful if you want a range to start
+        // with the found version.
+        currentAttributes.put("version", importRange);
+    }
+
+    /**
+     * Add the uses clauses
+     * 
+     * @param exports
+     * @param uses
+     * @throws MojoExecutionException
+     */
+    void doUses(Map<String, Map<String, String>> exports,
+            Map<String, Set<String>> uses,
+            Map<String, Map<String, String>> imports) {
+        if ("true".equalsIgnoreCase(getProperty(NOUSES)))
+            return;
+
+        for (Iterator<String> i = exports.keySet().iterator(); i.hasNext();) {
+            String packageName = i.next();
+            setProperty(CURRENT_PACKAGE, packageName);
+            try {
+                Map<String, String> clause = exports.get(packageName);
+                String override = clause.get(USES_DIRECTIVE);
+                if (override == null)
+                    override = USES_USES;
+
+                Set<String> usedPackages = uses.get(packageName);
+                if (usedPackages != null) {
+                    // Only do a uses on exported or imported packages
+                    // and uses should also not contain our own package
+                    // name
+                    Set<String> sharedPackages = new HashSet<String>();
+                    sharedPackages.addAll(imports.keySet());
+                    sharedPackages.addAll(exports.keySet());
+                    usedPackages.retainAll(sharedPackages);
+                    usedPackages.remove(packageName);
+
+                    StringBuffer sb = new StringBuffer();
+                    String del = "";
+                    for (Iterator<String> u = usedPackages.iterator(); u
+                            .hasNext();) {
+                        String usedPackage = u.next();
+                        if (!usedPackage.startsWith("java.")) {
+                            sb.append(del);
+                            sb.append(usedPackage);
+                            del = ",";
+                        }
+                    }
+                    if (override.indexOf('$') >= 0) {
+                        setProperty(CURRENT_USES, sb.toString());
+                        override = getReplacer().process(override);
+                        unsetProperty(CURRENT_USES);
+                    } else
+                        // This is for backward compatibility 0.0.287
+                        // can be deprecated over time
+                        override = override
+                                .replaceAll(USES_USES, sb.toString()).trim();
+                    if (override.endsWith(","))
+                        override = override.substring(0, override.length() - 1);
+                    if (override.startsWith(","))
+                        override = override.substring(1);
+                    if (override.length() > 0) {
+                        clause.put("uses:", override);
+                    }
+                }
+            } finally {
+                unsetProperty(CURRENT_PACKAGE);
+            }
+        }
+    }
+
+    /**
+     * Transitively remove all elemens from unreachable through the uses link.
+     * 
+     * @param name
+     * @param unreachable
+     */
+    void removeTransitive(String name, Set<String> unreachable) {
+        if (!unreachable.contains(name))
+            return;
+
+        unreachable.remove(name);
+
+        Set<String> ref = uses.get(name);
+        if (ref != null) {
+            for (Iterator<String> r = ref.iterator(); r.hasNext();) {
+                String element = (String) r.next();
+                removeTransitive(element, unreachable);
+            }
+        }
+    }
+
+    /**
+     * Helper method to set the package info
+     * 
+     * @param dir
+     * @param key
+     * @param value
+     */
+    void setPackageInfo(String dir, String key, String value) {
+        if (value != null) {
+            String pack = dir.replace('/', '.');
+            Map<String, String> map = classpathExports.get(pack);
+            if (map == null) {
+                map = new HashMap<String, String>();
+                classpathExports.put(pack, map);
+            }
+            map.put(key, value);
+        }
+    }
+
+    public void close() {
+        super.close();
+        if (dot != null)
+            dot.close();
+
+        if (classpath != null)
+            for (Iterator<Jar> j = classpath.iterator(); j.hasNext();) {
+                Jar jar = j.next();
+                jar.close();
+            }
+    }
+
+    /**
+     * Findpath looks through the contents of the JAR and finds paths that end
+     * with the given regular expression
+     * 
+     * ${findpath (; reg-expr (; replacement)? )? }
+     * 
+     * @param args
+     * @return
+     */
+    public String _findpath(String args[]) {
+        return findPath("findpath", args, true);
+    }
+
+    public String _findname(String args[]) {
+        return findPath("findname", args, false);
+    }
+
+    String findPath(String name, String[] args, boolean fullPathName) {
+        if (args.length > 3) {
+            warning("Invalid nr of arguments to " + name + " "
+                    + Arrays.asList(args) + ", syntax: ${" + name
+                    + " (; reg-expr (; replacement)? )? }");
+            return null;
+        }
+
+        String regexp = ".*";
+        String replace = null;
+
+        switch (args.length) {
+        case 3:
+            replace = args[2];
+        case 2:
+            regexp = args[1];
+        }
+        StringBuffer sb = new StringBuffer();
+        String del = "";
+
+        Pattern expr = Pattern.compile(regexp);
+        for (Iterator<String> e = dot.getResources().keySet().iterator(); e
+                .hasNext();) {
+            String path = e.next();
+            if (!fullPathName) {
+                int n = path.lastIndexOf('/');
+                if (n >= 0) {
+                    path = path.substring(n + 1);
+                }
+            }
+
+            Matcher m = expr.matcher(path);
+            if (m.matches()) {
+                if (replace != null)
+                    path = m.replaceAll(replace);
+
+                sb.append(del);
+                sb.append(path);
+                del = ", ";
+            }
+        }
+        return sb.toString();
+    }
+
+    public void putAll(Map<String, String> additional, boolean force) {
+        for (Iterator<Map.Entry<String, String>> i = additional.entrySet()
+                .iterator(); i.hasNext();) {
+            Map.Entry<String, String> entry = i.next();
+            if (force || getProperties().get(entry.getKey()) == null)
+                setProperty((String) entry.getKey(), (String) entry.getValue());
+        }
+    }
+
+    boolean firstUse = true;
+
+    public List<Jar> getClasspath() {
+        if (firstUse) {
+            firstUse = false;
+            String cp = getProperty(CLASSPATH);
+            if (cp != null)
+                for (String s : split(cp)) {
+                    Jar jar = getJarFromName(s, "getting classpath");
+                    if (jar != null)
+                        addClasspath(jar);
+                }
+        }
+        return classpath;
+    }
+
+    public void addClasspath(Jar jar) {
+        if (isPedantic() && jar.getResources().isEmpty())
+            warning("There is an empty jar or directory on the classpath: "
+                    + jar.getName());
+
+        classpath.add(jar);
+    }
+
+    public void addClasspath(File cp) throws IOException {
+        if (!cp.exists())
+            warning("File on classpath that does not exist: " + cp);
+        Jar jar = new Jar(cp);
+        addClose(jar);
+        classpath.add(jar);
+    }
+
+    public void clear() {
+        classpath.clear();
+    }
+
+    public Jar getTarget() {
+        return dot;
+    }
+
+    public Map<String, Clazz> analyzeBundleClasspath(Jar dot,
+            Map<String, Map<String, String>> bundleClasspath,
+            Map<String, Map<String, String>> contained,
+            Map<String, Map<String, String>> referred,
+            Map<String, Set<String>> uses) throws IOException {
+        Map<String, Clazz> classSpace = new HashMap<String, Clazz>();
+
+        if (bundleClasspath.isEmpty()) {
+            analyzeJar(dot, "", classSpace, contained, referred, uses);
+        } else {
+            for (String path : bundleClasspath.keySet()) {
+                if (path.equals(".")) {
+                    analyzeJar(dot, "", classSpace, contained, referred, uses);
+                    continue;
+                }
+                //
+                // There are 3 cases:
+                // - embedded JAR file
+                // - directory
+                // - error
+                //
+
+                Resource resource = dot.getResource(path);
+                if (resource != null) {
+                    try {
+                        Jar jar = new Jar(path);
+                        addClose(jar);
+                        EmbeddedResource.build(jar, resource);
+                        analyzeJar(jar, "", classSpace, contained, referred,
+                                uses);
+                    } catch (Exception e) {
+                        warning("Invalid bundle classpath entry: " + path + " "
+                                + e);
+                    }
+                } else {
+                    if (dot.getDirectories().containsKey(path)) {
+                        analyzeJar(dot, path + '/', classSpace, contained,
+                                referred, uses);
+                    } else {
+                        warning("No sub JAR or directory " + path);
+                    }
+                }
+            }
+        }
+        return classSpace;
+    }
+
+    /**
+     * We traverse through all the classes that we can find and calculate the
+     * contained and referred set and uses. This method ignores the Bundle
+     * classpath.
+     * 
+     * @param jar
+     * @param contained
+     * @param referred
+     * @param uses
+     * @throws IOException
+     */
+    private void analyzeJar(Jar jar, String prefix,
+            Map<String, Clazz> classSpace,
+            Map<String, Map<String, String>> contained,
+            Map<String, Map<String, String>> referred,
+            Map<String, Set<String>> uses) throws IOException {
+
+        next: for (String path : jar.getResources().keySet()) {
+            if (path.startsWith(prefix)) {
+                String relativePath = path.substring(prefix.length());
+                String pack = getPackage(relativePath);
+
+                if (pack != null && !contained.containsKey(pack)) {
+                    if (!isMetaData(relativePath)) {
+
+                        Map<String, String> map = new LinkedHashMap<String, String>();
+                        contained.put(pack, map);
+                        Resource pinfo = jar.getResource(prefix
+                                + pack.replace('.', '/') + "/packageinfo");
+                        if (pinfo != null) {
+                            InputStream in = pinfo.openInputStream();
+                            String version = parsePackageInfo(in);
+                            in.close();
+                            if (version != null)
+                                map.put("version", version);
+                        }
+                    }
+                }
+
+                if (path.endsWith(".class")) {
+                    Resource resource = jar.getResource(path);
+                    Clazz clazz;
+
+                    try {
+                        InputStream in = resource.openInputStream();
+                        clazz = new Clazz(relativePath, in);
+                        in.close();
+                    } catch (Throwable e) {
+                        error("Invalid class file: " + relativePath, e);
+                        e.printStackTrace();
+                        continue next;
+                    }
+
+                    String calculatedPath = clazz.getClassName() + ".class";
+                    if (!calculatedPath.equals(relativePath))
+                        error("Class in different directory than declared. Path from class name is "
+                                + calculatedPath
+                                + " but the path in the jar is "
+                                + relativePath
+                                + " from " + jar);
+
+                    classSpace.put(relativePath, clazz);
+                    referred.putAll(clazz.getReferred());
+
+                    // Add all the used packages
+                    // to this package
+                    Set<String> t = uses.get(pack);
+                    if (t == null)
+                        uses.put(pack, t = new LinkedHashSet<String>());
+                    t.addAll(clazz.getReferred().keySet());
+                    t.remove(pack);
+                }
+            }
+        }
+    }
+
+    /**
+     * Clean up version parameters. Other builders use more fuzzy definitions of
+     * the version syntax. This method cleans up such a version to match an OSGi
+     * version.
+     * 
+     * @param VERSION_STRING
+     * @return
+     */
+    static Pattern fuzzyVersion      = Pattern
+                                             .compile(
+                                                     "(\\d+)(\\.(\\d+)(\\.(\\d+))?)?([^a-zA-Z0-9](.*))?",
+                                                     Pattern.DOTALL);
+    static Pattern fuzzyVersionRange = Pattern
+                                             .compile(
+                                                     "(\\(|\\[)\\s*([-\\da-zA-Z.]+)\\s*,\\s*([-\\da-zA-Z.]+)\\s*(\\]|\\))",
+                                                     Pattern.DOTALL);
+    static Pattern fuzzyModifier     = Pattern.compile("(\\d+[.-])*(.*)",
+                                             Pattern.DOTALL);
+
+    static Pattern nummeric          = Pattern.compile("\\d*");
+
+    static public String cleanupVersion(String version) {
+        if (Verifier.VERSIONRANGE.matcher(version).matches())
+            return version;
+
+        Matcher m = fuzzyVersionRange.matcher(version);
+        if (m.matches()) {
+            String prefix = m.group(1);
+            String first = m.group(2);
+            String last = m.group(3);
+            String suffix = m.group(4);
+            return prefix + cleanupVersion(first) + "," + cleanupVersion(last)
+                    + suffix;
+        } else {
+            m = fuzzyVersion.matcher(version);
+            if (m.matches()) {
+                StringBuffer result = new StringBuffer();
+                String major = m.group(1);
+                String minor = m.group(3);
+                String micro = m.group(5);
+                String qualifier = m.group(7);
+
+                if (major != null) {
+                    result.append(major);
+                    if (minor != null) {
+                        result.append(".");
+                        result.append(minor);
+                        if (micro != null) {
+                            result.append(".");
+                            result.append(micro);
+                            if (qualifier != null) {
+                                result.append(".");
+                                cleanupModifier(result, qualifier);
+                            }
+                        } else if (qualifier != null) {
+                            result.append(".0.");
+                            cleanupModifier(result, qualifier);
+                        }
+                    } else if (qualifier != null) {
+                        result.append(".0.0.");
+                        cleanupModifier(result, qualifier);
+                    }
+                    return result.toString();
+                }
+            }
+        }
+        return version;
+    }
+
+    static void cleanupModifier(StringBuffer result, String modifier) {
+        Matcher m = fuzzyModifier.matcher(modifier);
+        if (m.matches())
+            modifier = m.group(2);
+
+        for (int i = 0; i < modifier.length(); i++) {
+            char c = modifier.charAt(i);
+            if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')
+                    || (c >= 'A' && c <= 'Z') || c == '_' || c == '-')
+                result.append(c);
+        }
+    }
+
+    /**
+     * Decide if the package is a metadata package.
+     * 
+     * @param pack
+     * @return
+     */
+    boolean isMetaData(String pack) {
+        for (int i = 0; i < METAPACKAGES.length; i++) {
+            if (pack.startsWith(METAPACKAGES[i]))
+                return true;
+        }
+        return false;
+    }
+
+    public String getPackage(String clazz) {
+        int n = clazz.lastIndexOf('/');
+        if (n < 0)
+            return ".";
+        return clazz.substring(0, n).replace('/', '.');
+    }
+
+    //
+    // We accept more than correct OSGi versions because in a later
+    // phase we actually cleanup maven versions. But it is a bit yucky
+    //
+    static String parsePackageInfo(InputStream jar) throws IOException {
+        try {
+            Properties p = new Properties();
+            p.load(jar);
+            jar.close();
+            if (p.containsKey("version")) {
+                return p.getProperty("version");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public String getVersionPolicy() {
+        return getProperty(VERSIONPOLICY, "${version;==;${@}}");
+    }
+
+    /**
+     * The extends macro traverses all classes and returns a list of class names
+     * that extend a base class.
+     */
+
+    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";
+
+    public String _classes(String args[]) {
+        // Macro.verifyCommand(args, _classesHelp, new
+        // Pattern[]{null,Pattern.compile("(implementing|implements|extending|extends|importing|imports|any)"),
+        // null}, 3,3);
+        Set<Clazz> matched = new HashSet<Clazz>(classspace.values());
+        for (int i = 1; i < args.length; i += 2) {
+            if (args.length < i + 1)
+                throw new IllegalArgumentException(
+                        "${classes} macro must have odd number of arguments. "
+                                + _classesHelp);
+
+            String typeName = args[i];
+            Clazz.QUERY type = null;
+            if (typeName.equals("implementing")
+                    || typeName.equals("implements"))
+                type = Clazz.QUERY.IMPLEMENTS;
+            else if (typeName.equals("extending") || typeName.equals("extends"))
+                type = Clazz.QUERY.EXTENDS;
+            else if (typeName.equals("importing") || typeName.equals("imports"))
+                type = Clazz.QUERY.IMPORTS;
+            else if (typeName.equals("all"))
+                type = Clazz.QUERY.ANY;
+            else if (typeName.equals("version"))
+                type = Clazz.QUERY.VERSION;
+            else if (typeName.equals("named"))
+                type = Clazz.QUERY.NAMED;
+
+            if (type == null)
+                throw new IllegalArgumentException(
+                        "${classes} has invalid type: " + typeName + ". "
+                                + _classesHelp);
+            // The argument is declared as a dotted name but the classes
+            // use a slashed named. So convert the name before we make it a
+            // instruction.
+            String pattern = args[i + 1].replace('.', '/');
+            Instruction instr = Instruction.getPattern(pattern);
+
+            for (Iterator<Clazz> c = matched.iterator(); c.hasNext();) {
+                Clazz clazz = c.next();
+                if (!clazz.is(type, instr, classspace))
+                    c.remove();
+            }
+        }
+        if (matched.isEmpty())
+            return "";
+
+        return join(matched);
+    }
+
+    /**
+     * Get the exporter of a package ...
+     */
+
+    public String _exporters(String args[]) throws Exception {
+        Macro
+                .verifyCommand(
+                        args,
+                        "${exporters;<packagename>}, returns the list of jars that export the given package",
+                        null, 2, 2);
+        StringBuilder sb = new StringBuilder();
+        String del = "";
+        String pack = args[1].replace('.', '/');
+        for (Jar jar : classpath) {
+            if (jar.getDirectories().containsKey(pack)) {
+                sb.append(del);
+                sb.append(jar.getName());
+            }
+        }
+        return sb.toString();
+    }
+
+    public Map<String, Clazz> getClassspace() {
+        return classspace;
+    }
+
+}

Propchange: felix/trunk/bundleplugin/src/main/java/aQute/lib/osgi/Analyzer.java
------------------------------------------------------------------------------
    svn:eol-style = native